理解C++中的左值和右值
为了更好的研究现代C++的一些高级特性(C++11中的右值引用,移动语义,完美转发等),理解左值和右值是前提。
什么是左值和右值
首先,让我们远离任何形式的定义,清空你的脑袋。
在C++中,左值是指向特定内存地址的值,而右值不指向任何内存地址。
通常情况下,右值是临时且生命周期短暂的,而左值的生命周期更长且通常以变量的形式存在。
让我们来看一些例子。
1 | int x = 666; |
在上面的代码中,666
是一个右值,它是一个整数(字面量),没有特定的内存地址(程序运行时存储在临时寄存器上)。
666
被赋值给了变量x
,变量是可寻址的,有特定的内存地址,所以X
是一个左值。
在C++中,赋值的左操作数必须为左值。
对于左值x
,我们可以这样做:
1 | int* y= &x; |
通过操作符&
,我们可以取得x
的内存地址,将它赋值给y
。这也是完全合法的,赋值的左操作数是一个左值(变量),右操作数是一个右值(它由对左值取地址(&)产生)。
我们不可以这样做:
1 | int y; |
你可能会说,当然不可以。但底层的原因是字面量666
是一个右值,它没有特定的内存地址,我们无法将y赋值给它。
如果你使用GCC编译器,会得到下面的错误信息:
1 | error: lvalue required as left operand of assignment |
同样地,我们也不能这样做:
1 | int* y = &666; // error |
GCC编译器会告诉你
1 | error: lvalue required as unary ‘&’ operand |
是的,我们只能对左值进行取地址的操作(&
),因为只有左值才能被寻址。
函数的返回值是左值/右值
我们知道赋值的左操作数必须是一个左值,所以下面的代码会出现编译错误“左操作数必须为左值”。
1 | int setValue() |
显然,setValue()
返回是一个右值(一个临时的整数 6),它不能被当作左操作数。
那么,如果函数返回值是一个左值呢?
1 | int global = 100; |
setGlobal()
返回是一个引用(引用是指向现有内存地址的),所以它能够被赋值。注意这里的&
, 它不是取地址的操作符,它是类型定义的一部分。
当你需要重载某些操作符,可能会需要返回左值。
左值到右值的转换
左值可能会被转换成右值。例如:
1 | int x = 1; |
x
和y
是两个左值,但是加法操作符+
需要的是右值,这里发生了左值到右值的隐式转换。许多其他操作符也一样,例如-
, *
, /
。
左值引用
那么相反的,右值可以被转换成左值吗?不能!这不是一个技术限制,是特定的编程语言设计成这样的而已。
在C++中,如果你这样做:
1 | int y = 10; |
我们定义了一个int&
类型的变量yref
, 一个指向y
的引用。这就是左值引用。现在,我们通过yref
来改变y
的值啦。
我们还知道,引用必须指向一个已经存在的内存地址(例如左值),在这里,y
是确定存在的,所以这样写没有问题。
那么,如果我们直接将10
赋值给一个引用,可以吗?
1 | int& yref = 10; // will it work? |
不行!引用必须指向已经存在的内存地址,而字面量10
并没有特定的内存地址,它是一个右值。
同样地,下面的代码也不能正常工作。
1 | void fnc(int& x) |
在注释掉的能正常工作的代码中,我们用变量x
来存储右值10
,然后再传递给函数fnc
.
如果我们只是想传递一个整数给函数,是不是非常不方便?
常量左值引用
通常上面的例子,我们知道不能将右值赋值给左值引用。例如int& ref = 10
。
但是加上常量限定,居然就可以了:
1 | const int& ref = 10; // OK! |
下面的例子也同样可以:
1 | void fnc(const int& x) |
右值没有特定的内存地址,随时可能失效,取它的引用没有意义;而 用const
限定解决了“右值可能被修改”的问题。再次强调,这并不是技术上的限制,只是C++的选择。
在开发过程中,我们推荐传输入参数使用常量引用(const T &
),它能避免不必要的拷贝和临时对象的构造。
实际上,编译器为我们创建了一个隐藏的变量来存储字面常量,然后再把它赋值给引用。就像下面这样:
1 | // the following... |
结语
理解左值和右值能帮助你了解C++的内部工作原理,在C++11中,引入了右值引用和移动语义。也就是,右值也可以被修改了!!
我将在后面的篇幅中介绍它们。