一直以来对左值和右值的理解局限于等号的左边是左值,等号的右边是右值;而最近在读一些开源代码时,时常遇到&,&&,std::move,std::forward等,简单bing一下,发现这是C++11 引入的新概念。
左值&右值
左值是指向一个指定内存的东西。另一方面,右值就是不指向任何地方的东西;左值则活的很久,因为他们以变量的形式存在,右值表示一个本应没有名称的临时对象。我们来看些例子:
int a = 123;
上面表达式中a是一个变量,它有具体的内存位置存放着,所以它是一个左值;而右边的123是一个数字,它没有特定的内存,只是程序运行时的一个临时值被赋给a变量。C++中声明一个赋值(assignment)需要一个左值作为它的左操作数(left operand):这完全合法。对于上面的左值a,还可以进行其他操作:
int *b = &a;
int c = a + 1;
上面表达式1通过取地址操作&符将a的内存地址赋值给了变量b(也是一个左值,有内存空间);取地址操作&需要一个左值,并且产生了一个右值(临时值,无内存空间);这也是另一个完全合法的操作:在赋值操作符的左边我们有一个左值(一个变量),在右边我们使用取地址操作符产生的右值。表达式2也类似,c是一个左值,有内存空间,而a+1是一个右值,无内存空间,是程序运行过程中才产生的。而像下面这样的操作则是不允许的:
int x = 0;
123 = x; // error
上面的结论是显而易见的,从技术上来说是因为123是一个字面常量也就是一个右值,它没有一个具体的内存位置,所以这会把x分配到一个不存在的地方导致错误。下面的操作也是不允许的:
int *x = &123;
我们都知道取地址操作符&需要一个左值作为操作数,因为只有一个左值才拥有内存地址。一个右值常量并没有内存地址,所以取地址失败。
返回左值右值的函数
先看下面的一段程序:
int getValue() {
return 123;
}
getValue() = 456; // error
我们都知道赋值的左操作数必须是左值,因此,上面的错误原因很明显:getValue()返回了一个右值,一个临时值123,他不能作为一个赋值的左操作数。再看下面这段程序:
int global = 123;
int& getGlobal() {
return global;
}
getGlobal() = 456; // ok
该程序可以运行,因为在这里 getGlobal()返回一个引用(reference),跟之前的 getValue()不同。一个引用是指向一个已经存在的内存位置(global变量)的东西,因此它是一个左值,所以它能被赋值。注意这里的&:它不是取地址操作符,他定义了返回的类型(一个引用)。
左右值转换
左值转换为右值,这是合法且经常发生的,看如下程序:
int x = 1;
int y = 2;
int z = x + y;
上面代码中x和y都是左值,但是经过加法操作后,x和y经历了一个隐式的左值到右值的转换,很多其他的操作符也有同样的转换——减法,加法,乘法等等。
左值引用
我们日常说的引用,通常指的是左值引用。引用是C++语法做的优化,引用的本质还是靠指针来实现的。引用相当于变量的别名。声明引用的时候必须初始化,且一旦绑定,不可把引用绑定到其他对象;即引用必须初始化,不能对引用重定义;左值引用的基本语法:type &引用名 = 左值表达式;我们直接看下面的代码:
int y = 1;
int& yref = x;
yref++;
这里将yref声明为类型int&:一个对y的引用,它被称作左值引用。现在你可以开心地通过该引用改变y的值了。我们知道,一个引用必须指向一个具体的内存位置中的一个已经存在的对象,即一个左值。这里y确实存在,所以代码运行完美。现在,如果我缩短整个过程,尝试将10直接赋值给我的引用,并且没有任何对象持有该引用,将会发生什么?
int& yref = 10;
在右边我们有一个临时值,一个需要被存储在一个左值中的右值。在左边我们有一个引用(一个左值),他应该指向一个已经存在的对象。但是10
是一个数字常量,没有内存地址,属于一个右值,这与引用的精神不符。因此上面这表达式是错误的。
如果你仔细想想,这就是从右值到左值的转换,是不被允许的。比如,一个volitile
的数字常量(右值)如果想要被引用,需要先变成一个左值。如果那被允许,你就可以通过它的引用来改变数字常量的值。常量都能被改变,世界要大乱了啊。
下面的代码片段同样会发生错误,原因跟刚才的一样:
void fnc(int& x)
{
}
int main()
{
fnc(10); // error!
// This works instead:
// int x = 10;
// fnc(x);
}
常量左值引用
右值并不能取到地址,所以左值引用不能绑定到右值,但是常量左值引用为什么就可以呢?其中的数据在内存中的存储状况是怎么样的一个情况?
先看看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
的并且会很快失效,所以给他一个引用是没什么意义的。如果我们让引用本身变成常量引用,那样的话该引用指向的值就不能被改变了。现在右值被修改的问题被很好地解决了。同样,这不是一个技术限制,而是C++人员为避免愚蠢麻烦所作的选择。
❝
应用:C++中经常通过常量引用来将值传入函数中,这避免了不必要的临时对象的创建和拷贝。
❞
编译器会为你创建一个隐藏的变量(即一个左值)来存储初始的字面常量,然后将隐藏的变量绑定到你的引用上去。那跟我之前的一组代码片段中手动完成的是一码事,例如:
// the following...
const int& ref = 10;
// ... would translate to:
int __internal_unique_name = 10;
const int& ref = __internal_unique_name;
理解左值和右值的含义让我弄清楚了几个C++内在的工作方式。C++11进一步推动了右值的限定,引入了右值引用(rvalue reference)和移动(move semantics)的概念。
参考
understanding-meaning-lvalues-and-rvalues-c