左值右值
左值和右值: 一个亲和的定义
左值指向一个内存地址, 右值什么都不指向.
通常右值生存周期很短, 左值的要长一些.
这样想会很有意思,就是把左值比作一个容器, 而右值是容器内的东西.
int x = 666;
666
是一个右值,一个数字(严格来说是一个文字常量)没有特定的内存地址,除了程序运行时的一些临时寄存器. 这个数字赋给了变量x
. 一个变量是有特定的内存地址的, 所以他是一个左值. c++声明, 一个赋值语句要求一个左值作为左操作数, 所以这个例子这是完全合法的.
有了一个左值x
. 我们就可以这样做
iny* y = &x;
这里我们通过取地址运算符&
获取x的内存地址并且把它放进y
中. 它接受左值参数并生成右值, 这又是一个完全合法的操作: 在赋值语句的左边, 我们有一个左值(变量), 在右边有一个取地址运算符产生的右值.
但是我们不能像下面这样做:
int y;
666 = y;
这很明显. 但是真正的技术原因是666
作为一个文字常量,一个右值, 是没有一个特定的内存地址的, 所以我们无法把y赋值给任何地方.
运行的化GCC会告诉我们
error: lvalue required as left operand of assignment
# 左值要求作为赋值语句的左操作数
他说的是对的, 赋值语句的左操作数通常要求的是左值,而在我们的程序中, 我们使用了一个右值666
.
我们也不能这样做
int* y = &666;
GCC提供了如下:
error: lvalue required as unary '&' operand`
# 左值要求作为一元运算符`&`的操作数.
他又是对的, 取地址操作符&
希望的是一个左值作为参数, 因为只有一个左值才有&
操作符需要操作的地址.
函数返回左值和右值
我们知道一个赋值语句的左操作数必须是一个左值. 因此, 像下面的这个函数肯定会抛出lvalue required as left operand of assignment
, 左值要求作为一个赋值语句的左操作数
int setValue()
{
return 0;
}
setValue() = 3;
很清楚,setValue()
返回的是一个右值(临时数字6
), 不能作为一个赋值语句的左操作数, 现在如果一个函数返回的是左值会发生什么呢?
int global = 100;
int& setGlobal()
{
return global;
}
setGlobal() = 400;
这是合法的,因为setGlobal
返回的是一个引用. 一个引用是指向现有内存地址(变量global)的东西, 所以它能够被赋值. 注意这里的&
: 这不是取地址运算符, 它定义了这个函数的返回类型(一个引用)
从函数中返回左值的能力看起来相当模糊, 但是当你做一些高级的事情, 比如实现一些运算符的重载却是很有用的. 在以后的章节中会有更多内容.
左值到右值的转换
一个左值实可以转换为右值的: 这是完全合法的, 并且经常发生. 让我们以+
运算符作为例子. 根据c++规范, 它拿两个右值
作为参数并返回一个右值
.
int x = 1;
int y = 3;
int z = x + y;
等一下: x
和y
都是左值, 但是加法运算符需要的不是两个右值吗? 答案很简单,x
和y
经历了隐式的左值到右值的转换. 很多其他的操作符也执行这样的转换, 如减法,加法和除法.
左值引用
右值能够被转化为左值吗? 不能. 它不是一种技术上的局限,不过编程语言就是这样设计的
int y = 10;
int &yref = y;
yref++; // y is 11 now
你可以说yref
是int&
类型的: 一个指向y
的引用. 他被称为左值引用. 现在你能够通过引用类型yref
来改变y
的值.
我们知道一个引用必须指向一个在内存中已经存在的对象, 也就是一个左值, 在这里y
的确是已经存在了的, 所以代码能够正确运行.
现在,如果我简化这个过程, 尝试直接将10
赋值给我们的引用, 没有对象去保存他
int& yref = 10; // 这样可以吗?
在右边我们有一个临时的量, 一个需要被存在左值某个位置中的右值.
在左边我们有一个必须指向已经存在对象的引用(左值).
但是10
作为一个数值的常数, 换句话说, 没有一个特定的内存地址, 再换句话说,一个右值, 这就与引用的思想发生冲突了.
如果你这样想, 禁止从右值转化为左值, 一个数字常数(右值)应该变成左值以便被引用. 如果我们允许这样,那么你就能够通过引用改变数字常数的值, 完全没有任何意义不是吗?更重要的是,如果一旦数值消失, 引用将指向什么呢?
下面的例子也是同样的道理
void fnc(int& x)
{}
int main()
{
fuc(10); //这样是错的
// 下面是对的
// int x = 10;
// fnc(x);
}
我们通过一个临时的右值(10
)传递给一个需要引用类型参数的函数, 但是我们不能将右值转化为左值.有一个解决办法就是创建一个临时变量来存储这个右值,然后我们把这个临时变量传递进去,非常方便.
上面的编译时会报错
error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
意思是说引用不是const, 也就是常量, 但是根据语言规范,允许将一个const的左值绑定到右值, 所以下面的片段是对的
const int& ref = 10;
当然下面的也是对的
void fnc(const int& x)
{}
int main()
{
fnc(10);
}
背后的原理很简单, 文字常量10
是volatile类型的,不知道什么时候就过期了, 所以引用他是没有意义的, 让我们将引用本身设置为const的, 这样它指向的内容就不能改变了,然后就解决了修改右值的问题. 同样的, 这不是技术的局限,而是c++工作人员选择的避免愚蠢问题的方式.
这使得非常常见的c++的习惯用法, 函数中参数通过接收常量引用来接受值成为可能,就像我们前面的代码中写的那样,避免了不必要的临时对象的拷贝和构造.
其实在背后, 编译器会为你创建一个隐藏的变量(一个左值)用来存储原始的文字常量,并且这个绑定这个隐藏变量与你的引用,. 这和我们上面手动的做法基本相似,例如:
//这样写
const int& ref = 10;
//编译器为你做:
int __hidden_val = 10;
const int& ref = __hidden_val;