// 以下是我个人的理解,如有错误请告知,非常感谢你。 #include <string> #include <iostream> using namespace std; string one("cute"); // non-const lvalue const string two("fluffy"); // const lvalue string three() // non-const rvalue { return "kittens"; } const string four() // const rvalue { return "are an essential part of a healthy diet"; } string& five(string& i) { std::cout << i; i = "haha"; return i; } void six(const string& i) { } void six(string& i) { } // 1,左值,右值针对表达式而言,而不是针对对象。 // 2,表达式是一个或多个操作数通过操作符组合而成,最简单的表达式只包含一个字面值或变量。 // 我认为函数调用也是一个表达式,因为操作符对操作数操作是函数调用。 // 3,每个表达式都会产生一个结果,这个结果可以分成左值和右值,这是一个全集,只有两种可能。 // 4,什么是语句,语句是表达式加分号。 // 5,lvalue 是指那些单一表达式结束之后依然存在的持久对象。例如: obj,*ptr, prt[index], ++x 都是 lvalue。 // 6,上面的话强调一个操作数,一个操作符的表达式返回的结果,在表达式结束之后依然存在的持久对象, // 7,如果说这个表达式结束之后就是一个分号,那么这个表达式的结果,也不会消失。 // 8,另一个培养判断一个表达式是不是 lvalue 的直觉感的方法就是自问一下"我能不能对表达式取址?", // 9,如果能够,那就是一个 lvalue;如果不能,那就是 一个 rvalue。 int main() { 345; // 345这是一个字面值,所以它又是一个表达式,加上一个分号就构成了语句,这些都是合法的。 // 这个字面值是不可以取址的,所以字面值表达式的结果是一个右值。 string i = "hehe"; i; // i是一个变量,所有它也是一个表达式,加上一个分号就构成了语句。 // 这个表达式的值就是对象i,在分号之后继续存在,而且也可以被取址,所以这个表达式是一个左值。 three(); // three()是一个表达式,这个表达式的结果是一个string类型的临时对象,这个对象在分号之后就会消失, // 我不知道它能不能被取址,反正它是右值。 // 不明白如果它是右值,它就是不能被取址么,还是说8和5都是必要条件,都满足才是左值,右值不一定都要满足, // 有一个满足就是右值,还有因为是全集的关系,非左值即是右值。 four(); // four()是一个表达式,这个表达式的结果是一个const string类型的临时对象,在分号之后就会消失, // 所以这是一个const右值。 five(i); // five(i)是一个表达式,当且仅当函数调用返回一个引用的时候,函数调用表达式是一个左值。 // Type&可绑定到non-const lvalue,比如本例(可以用这个引用来读取和修改原来的值), // 但不能绑定到const lvalue,因为那将违背const正确性; // 也不能把它绑定到non-const rvalue,这样做极端危险,你用这个引用来修改临时对象,但临时对象早就不存在了, // 这将导致难以捕捉而令人讨厌的 bug,因此 C++ 明智地禁止这这么做。 five(three()); // Type&绑定到non-const rvalue,这句话不是不行么,vs2008就是能够编译通过且运行没有问题呀,服了。 // 把编译选项改为了/w4,出了warning c4239,MSDN:This type conversion is not allowed by the C++ standard, // but it is permitted here as an extension. This warning is always followed by at least one line of // explanation describing the language rule being violated. // C4239.cpp // compile with: /W4 /c // struct C { // C() {} // }; // void func(void) { // C & rC = C(); // C4239,C()表达式的结果是标准的non-const rvalue,匿名对象,临时变量等等叫法我不知道对不对,但是 // non-const rvalue应该是正确的叫法 // const C & rC2 = C(); // OK // rC2; // } // 表达式的结果实际上有四种,non-const lvalue, non-const rvalue, const lvalue, const rvalue // Type&只可能绑定到non-const lvalue, 不能绑定到const lvalue, const rvalue,显而易见,因为它们是const 而Type&不是。 // 不能绑定到non-const rvalue已经如上作了说明。 // 关于为什么,c++语言认为,定义一个非const的函数,你就是有要修改传入实参的企图,如果你不想 // 修改它,为什么不把函数定义一个接受const对象的函数,即定义为const Type&。而一但你把函数定义为Type&,那么c++语言就认为 // 在二种non-const这一的non-const rvalue就危险了,所以c++压根就禁止了你这么做,因为non-cnost rvalue的生命期的问题。 // 总结Type&只能接受四种类型之一的non-const lvalue。两种情况下常见,一种是直接的定义引用时,一种是five这种情况。 six(i); // 函数six有个重载版本,一个接受const string&, 一个接受string&,当实参为一个non-const的时候, // i比如本例,这时候会调用 // non-const版本,优先匹配,对于const Type&它能接受四种类型中的每一种类型的对象,对于lvalue的两种类型没有什么说的, // 对于rvalue的两种类型,它会先生成一个临时变量,然后让引用关联这个临时变量,而且我认为这不是一个临时变量,而是一个 // 临时的局部变量,因为在分号之后,这个临时变量应该还不在其生命期内的,因为引用还是要有关联的内存的,引用是左值,总 // 要可以取址吧,取到一个临时变量是不可以的,所以我把这种变量叫临时局部变量,说它临时,是因为程序员看不到,说它局部 // 是因为它的生命期应该从定义到函数结尾return这前。这个在c++ primer的书上也是这样讲了一点,和我说的不一样, // double dval = 3.14; // cosnt int& i = dval; // 书中说编译器会把这些代码转换成如下形式的代码 // // double dval = 3.14; // int temp = dval; // const int& i = temp; return 0; }
以下是一些转载
C++ 03 标准 3.10/1 节上说: "每一个表达式要么是一个 lvalue ,要么就是一个 rvalue 。" 应该谨记 lvalue 跟 rvalue 是针对表达式而言的,而不是对象。
lvalue 是指那些单一表达式结束之后依然存在的持久对象。例如: obj,*ptr, prt[index], ++x 都是 lvalue。
rvalue 是指那些表达式结束时(在分号处)就不复存在了的临时对象。例如: 1729 , x + y , std::string("meow") , 和 x++ 都是 rvalue。
注意 ++x 和 x++ 的区别。当我们写 int x = 0; 时, x 是一个 lvalue,因为它代表一个持久对象。 表达式 ++x 也是一个 lvalue,它修改了 x 的值,但还是代表原来那个持久对象。然而,表达式 x++ 却是一个 rvalue,它只是拷贝一份持久对象的初值,再修改持久对象的值,最后返回那份拷贝,那份拷贝是临时对象。 ++x 和 x++ 都递增了 x,但 ++x 返回持久对象本身,而 x++ 返回临时拷贝。这就是为什么 ++x 之所以是一个 lvalue,而 x++ 是一个 rvalue。 lvalue 与 rvalue 之分不在于表达式做了什么,而在于表达式代表了什么(持久对象或临时产物)。
另一个培养判断一个表达式是不是 lvalue 的直觉感的方法就是自问一下"我能不能对表达式取址?",如果能够,那就是一个 lvalue;如果不能,那就是 一个 rvalue。 例如:&obj , &*ptr , &ptr[index] , 和 &++x 都是合法的(即使其中一些例子很蠢),而 &1729 , &(x + y) , &std::string("meow") , 和 &x++ 是不合法的。为什么这个方法凑效?因为取址操作要求它的"操作数必须是一个 lvalue"(见 C++ 03 5.3.1/2)。为什么要有那样的规定?因为对一个持久对象取址是没问题的,但对一个临时对象取址是极端危险的,因为临时对象很快就会被销毁(译注:就像你有一个指向某个对象的指针,那个对象被释放了,但你还在使用那个指针,鬼知道这时候指针指向的是什么东西)。
前面的例子不考虑操作符重载的情况,它只是普通的函数调用语义。"一个函数调用是一个 lvalue 当且仅当它返回一个引用"(见 C++ 03 5.2.2/10)。因此,给定语句 vercor<int> v(10, 1729); , v[0] 是一个 lvalue,因为操作符 []() 返回 int& (且 &v[0] 是合法可用的); 而给定语句 string s("foo");和 string t("bar");,s + t 是一个rvalue,因为操作符 +() 返回 string(而 &(s + t) 也不合法)。
lvalue 和 rvalue 两者都有非常量(modifiable,也就是说non-const)与常量(const )之分。举例来说:
string one("cute");
const string two("fluffy");
string three() { return "kittens"; }
const string four() { return "are an essential part of a healthy diet"; }
one; // modifiable lvalue
two; // const lvalue
three(); // modifiable rvalue
four(); // const rvalue
Type& 可绑定到非常量 lvalue (可以用这个引用来读取和修改原来的值),但不能绑定到 const lvalue,因为那将违背 const 正确性;也不能把它绑定到非常量 rvalue,这样做极端危险,你用这个引用来修改临时对象,但临时对象早就不存在了,这将导致难以捕捉而令人讨厌的 bug,因此 C++ 明智地禁止这这么做。(我要补充一句:VC 有一个邪恶的扩展允许这么蛮干,但如果你编译的时候加上参数 /W4 ,编译器通常会提示警告"邪恶的扩展被激活了")。也不能把它绑定到 const ravlue,因为那会是双倍的糟糕。(细心的读者应该注意到了我在这里并没有谈及模板参数推导)。
const Type& 可以绑定到: 非常量 lvalues, const lvalues,非常量 rvalues 以及 const values。(然后你就可以用这个引用来观察它们)
引用是具名的,因此一个绑定到 rvalue 的引用,它本身是一个 lvalue(没错!是 L)。(因为只有 const 引用可以绑定到 rvalue,所以它是一个 const lvalue)。这让人费解,(不弄清楚的话)到后面会更难以理解,因此我将进一步解释。给定函数 void observe(const string& str), 在 observe()'s 的实现中, str 是一个 const lvalue,在 observe() 返回之前可以对它取址并使用那个地址。这一点即使我们通过传一个 rvalue 参数来调用 observe()也是成立的 ,就像上面的 three() 和 four()。也可以调用 observe("purr"),它构建一个临时 string 并将 str 绑定到那个临时 string。three() 和 foure() 的返回对象是不具名的,因此他们是 rvalue,但是在 observe()中,str 是具名的,所以它是一个 lvalue。正如前面我说的" lvalue 跟 rvalue 是针对表达式而言的,而不是对象"。当然,因为 str 可以被绑定到一个很快会被销毁的临时对象,所以在 observe() 返回之后我们就不应该在任何地方保存这个临时对象的地址。
你有没有对一个绑定到 rvalue 的 const 引用取址过么?当然,你有过!每当你写一个带自赋值检查的拷贝赋值操作符: Foo& operator=(const Foo& other), if( this != &other) { copy struff;}; 或从一个临时变量来拷贝赋值,像: Foo make_foo(); Foo f; f = make_foo(); 的时候,你就做了这样的事情。
这个时候,你可能会问"那么非常量 rvalues 跟 const rvalues 有什么不同呢?我不能将 Type& 绑定到非常量 rvalue 上,也不能通过赋值等操作来修改 rvalue,那我真的可以修改它们?" 问的很好!在 C++ 98/03 中,这两者存在一些细微的差异: non-constrvalues 可以调用 non-const 成员函数。 C++ 不希望你意外地修改临时对象,但直接在non-const rvalues上调用 non-const 成员函数,这样做是很明显的,所以这是被允许的。在 C++ 0x中,答案有了显著的变化,它能用来实现 move 语意。
恭喜!你已经具备了我所谓的"lvalue/rvalue 观",这样你就能够一眼就判断出一个表达式到底是 lvalue 还是 rvalue。再加上你原来对 const 的认识,你就能完全理解为什么给定语句 void mutate(string& ref) 以及前面的变量定义, mutate(one) 是合法的,而 mutate(two), mutate(three()), mutate(four()), mutate("purr") 都是不合法的。如果你是 C++ 98/03 程序员,你已经可以分辨出这些调用中的哪些是合法的,哪些是不合法的;是你的"本能直觉",而不是你的编译器,告诉你 mutate(three()) 是假冒的。你对 lvalue/rvalue 的新认识让你明确地理解为什么 three() 是一个 rvalue,也知道为什么非常量引用不能绑定到右值。知道这些有用么?对语言律师而言,有用,但对普通程序员来说并不见得。毕竟,你如果不理解关于 lvalues 和 rvalues 一切就要领悟这个还隔得远呢。但是重点来了:与 C++ 98/03 相比, C++ 0x 中的 lvalue 和 rvalue 有着更广泛更强劲的含义(尤其是判断表达式是否是 modifiable / const 的 lvalue/rvalue,并据此做些处理)。要有效地使用 C++ 0x,你也需具备对 lvalue/rvalue 的理解。现在万事具备,我们能继续前行了。