Attention:this blog is a translation of https://www.internalpointers.com/post/understanding-meaning-lvalues-and-rvalues-c ,which is posted by @internalpoiners.
一、前言
一直以来,我都对C++中左值(lvalue)和右值(lvalue)的概念模糊不清。我认为是时候好好理解他们了,因为这些概念随着C++语言的进化变得越来越重要。
二、左值和右值——一个友好的定义
首先,让我们避开那些正式的定义。在C++中,一个左值是指向一个指定内存的东西。另一方面,右值就是不指向任何地方的东西。通常来说,右值是暂时和短命的,而左值则活的很久,因为他们以变量的形式(variable)存在。我们可以将左值看作为容器(container)而将右值看做容器中的事物。如果容器消失了,容器中的事物也就自然就无法存在了。
让我们现在来看一些例子:
int x = 666; //ok
在这里,666
是一个右值。一个数字(从技术角度来说他是一个字面常量(literal constant))没有指定的内存地址,当然在程序运行时一些临时的寄存器除外。在该例中,666
被赋值(assign)给x
,x
是一个变量。一个变量有着具体(specific)的内存位置,所以他是一个左值。C++中声明一个赋值(assignment)需要一个左值作为它的左操作数(left operand):这完全合法。
对于左值x
,你可以做像这样的操作:
int* y = &x; //ok
在这里我通过取地址操作符&
获取了x
的内存地址并且把它放进了y
。&
操作符需要一个左值并且产生了一个右值,这也是另一个完全合法的操作:在赋值操作符的左边我们有一个左值(一个变量),在右边我们使用取地址操作符产生的右值。
然而,我们不能这样写:
int y;
666 = y; //error!
可能上面的结论是显而易见的,但是从技术上来说是因为666
是一个字面常量也就是一个右值,它没有一个具体的内存位置(memory location),所以我们会把y
分配到一个不存在的地方。
下面是GCC给出的变异错误提示:
error: lvalue required as left operand of assignment
赋值的左操作数需要一个左值,这里我们使用了一个右值666
。
我们也不能这样做:
int* y = &666;// error~
GCC给出了以下错误提示:
error: lvalue required as unary '&' operand`
&
操作符需要一个左值作为操作数,因为只有一个左值才拥有地址。
三、返回左值和右值的函数
我们知道一个赋值的左操作数必须是一个左值,因此下面的这个函数肯定会抛出错误:lvalue required as left operand of assignment
int setValue()
{
return 6;
}
// ... somewhere in main() ...
setValue() = 3; // error!
错误原因很清楚:setValue()
返回了一个右值(一个临时值6
),他不能作为一个赋值的左操作数。现在,我们看看如果函数返回一个左值,这样的赋值会发生什么变化。看下面的代码片段(snippet):
int global = 100;
int& setGlobal()
{
return global;
}
// ... somewhere in main() ...
setGlobal() = 400; // OK
该程序可以运行,因为在这里setGlobal()
返回一个引用(reference),跟之前的setValue()
不同。一个引用是指向一个已经存在的内存位置(global
变量)的东西,因此它是一个左值,所以它能被赋值。注意这里的&
:它不是取地址操作符,他定义了返回的类型(一个引用)。
可以从函数返回左值看上去有些隐晦,它在你做一些进阶的编程例如实现一些操作符的重载(implementing overload operators)时会很有作用,这些知识会在未来的章节中讲述。
四、左值到右值的转换
一个左值可以被转换(convert)为右值,这完全合法且经常发生。让我们先用+
操作符作为一个例子,根据C++的规范(specification),它使用两个右值作为参数并返回一个右值(译者按:可以将操作符理解为一个函数)。
让我们看下面的代码片段:
int x = 1;
int y = 3;
int z = x + y; // ok
等一下,x
和y
是左值,但是加法操作符需要右值作为参数:发生了什么?答案很简单:x
和y
经历了一个隐式(implicit)的左值到右值(lvalue-to-rvalue)的转换。许多其他的操作符也有同样的转换——减法、加法、除法等等。
五、左值引用
相反呢?一个右值可以被转化为左值吗?不可以,它不是技术所限,而是C++编程语言就是那样设计的。
在C++中,当你做这样的事:
int y = 10;
int& yref = y;
yref++; // y is now 11
这里将yref
声明为类型int&
:一个对y
的引用,它被称作左值引用(lvalue reference)。现在你可以开心地通过该引用改变y
的值了。
我们知道,一个引用必须只想一个具体的内存位置中的一个已经存在的对象,即一个左值。这里y
确实存在,所以代码运行完美。
现在,如果我缩短整个过程,尝试将10
直接赋值给我的引用,并且没有任何对象持有该引用,将会发生什么?
int& yref = 10; // will it work?
在右边我们有一个临时值,一个需要被存储在一个左值中的右值。在左边我们有一个引用(一个左值),他应该指向一个已经存在的对象。但是10
是一个数字常量(numeric constant),也就是一个左值,将它赋给引用与引用所表述的精神冲突。
如果你仔细想想,那就是被禁止的从右值到左值的转换。一个volitile
的数字常量(右值)如果想要被引用,需要先变成一个左值。如果那被允许,你就可以通过它的引用来改变数字常量的值。相当没有意义,不是吗?更重要的是,一旦这些值不再存在这些引用该指向哪里呢?
下面的代码片段同样会发生错误,原因跟刚才的一样:
void fnc(int& x)
{
}
int main()
{
fnc(10); // Nope!
// This works instead:
// int x = 10;
// fnc(x);
}
我将一个临时值10
传入了一个需要引用作为参数的函数中,产生了将右值转换为左值的错误。这里有一个解决方法(workaround),创造一个临时的变量来存储右值,然后将变量传入函数中(就像注释中写的那样)。将一个数字传入一个函数确实不太方便。
六、常量左值引用
先看看GCC对于之前两个代码片段给出的错误提示:
error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
GCC认为引用不是const
的,即一个常量。根据C++规范,你可以将一个const
的左值绑定到一个右值上,所以下面的代码可以成功运行:
const int& ref = 10; // OK!
当然,下面的也是:
void fnc(const int& x)
{
}
int main()
{
fnc(10); // OK!
}
背后的道理是相当直接的,字面常量10
是volatile
的并且会很快失效(expire),所以给他一个引用是没什么意义的。如果我们让引用本身变成常量引用,那样的话该引用指向的值就不能被改变了。现在右值被修改的问题被很好地解决了。同样,这不是一个技术限制,而是C ++人员为避免愚蠢麻烦所作的选择。
应用:C++中经常通过常量引用来将值传入函数中,这避免了不必要的临时对象的创建和拷贝。
编译器会为你创建一个隐藏的变量(即一个左值)来存储初始的字面常量,然后将隐藏的变量绑定到你的引用上去。那跟我之前的一组代码片段中手动完成的是一码事,例如:
// the following...
const int& ref = 10;
// ... would translate to:
int __internal_unique_name = 10;
const int& ref = __internal_unique_name;
现在你的引用指向了真实存在的事物(知道它走出作用域外)并且你可以正常使用它,出克改变他指向的值。
const int& ref = 10;
std::cout << ref << "\n"; // OK!
std::cout << ++ref << "\n"; // error: increment of read-only reference ‘ref’
七、结论
理解左值和右值的含义让我弄清楚了几个C++内在的工作方式。C++11进一步推动了右值的限定,引入了右值引用(rvalue reference)和移动(move semantics)的概念。这些将在下一篇文章中介绍。