为了更好的研究现代C++的一些高级特性(C++11中的右值引用,移动语义,完美转发等),理解左值和右值是前提。

什么是左值和右值

首先,让我们远离任何形式的定义,清空你的脑袋。

在C++中,左值是指向特定内存地址的值,而右值不指向任何内存地址。

通常情况下,右值是临时且生命周期短暂的,而左值的生命周期更长且通常以变量的形式存在。

让我们来看一些例子。

1
int x = 666;

在上面的代码中,666是一个右值,它是一个整数(字面量),没有特定的内存地址(程序运行时存储在临时寄存器上)。

666被赋值给了变量x,变量是可寻址的,有特定的内存地址,所以X是一个左值。

在C++中,赋值的左操作数必须为左值。

对于左值x,我们可以这样做:

1
int* y= &x;

通过操作符&,我们可以取得x的内存地址,将它赋值给y。这也是完全合法的,赋值的左操作数是一个左值(变量),右操作数是一个右值(它由对左值取地址(&)产生)。

我们不可以这样做:

1
2
int y;
666 = y; // error

你可能会说,当然不可以。但底层的原因是字面量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
2
3
4
5
int setValue()
{
return 6;
}
setValue() = 3; // error

显然,setValue() 返回是一个右值(一个临时的整数 6),它不能被当作左操作数。

那么,如果函数返回值是一个左值呢?

1
2
3
4
5
6
int global = 100;
int& setGlobal()
{
return global;
}
setGlobal() = 400; // OK

setGlobal()返回是一个引用(引用是指向现有内存地址的),所以它能够被赋值。注意这里的&, 它不是取地址的操作符,它是类型定义的一部分。

当你需要重载某些操作符,可能会需要返回左值。

左值到右值的转换

左值可能会被转换成右值。例如:

1
2
3
int x = 1;
int y = 3;
int z = x + y; // ok

xy是两个左值,但是加法操作符+需要的是右值,这里发生了左值到右值的隐式转换。许多其他操作符也一样,例如-, *, /

左值引用

那么相反的,右值可以被转换成左值吗?不能!这不是一个技术限制,是特定的编程语言设计成这样的而已。

在C++中,如果你这样做:

1
2
3
int y = 10;
int& yref = y;
yref++; // y is now 11

我们定义了一个int&类型的变量yref, 一个指向y的引用。这就是左值引用。现在,我们通过yref来改变y的值啦。

我们还知道,引用必须指向一个已经存在的内存地址(例如左值),在这里,y是确定存在的,所以这样写没有问题。

那么,如果我们直接将10赋值给一个引用,可以吗?

1
int& yref = 10; // will it work?

不行!引用必须指向已经存在的内存地址,而字面量10并没有特定的内存地址,它是一个右值。

同样地,下面的代码也不能正常工作。

1
2
3
4
5
6
7
8
9
10
void fnc(int& x)
{
}
int main()
{
fnc(10); // Nope!
// This works instead:
// int x = 10;
// fnc(x);
}

在注释掉的能正常工作的代码中,我们用变量x来存储右值10,然后再传递给函数fnc.

如果我们只是想传递一个整数给函数,是不是非常不方便?

常量左值引用

通常上面的例子,我们知道不能将右值赋值给左值引用。例如int& ref = 10

但是加上常量限定,居然就可以了:

1
const int& ref = 10; // OK!

下面的例子也同样可以:

1
2
3
4
5
6
7
void fnc(const int& x)
{
}
int main()
{
fnc(10); // OK!
}

右值没有特定的内存地址,随时可能失效,取它的引用没有意义;而 用const限定解决了“右值可能被修改”的问题。再次强调,这并不是技术上的限制,只是C++的选择。

在开发过程中,我们推荐传输入参数使用常量引用(const T &),它能避免不必要的拷贝和临时对象的构造

实际上,编译器为我们创建了一个隐藏的变量来存储字面常量,然后再把它赋值给引用。就像下面这样:

1
2
3
4
5
6
// the following...
const int& ref = 10;

// ... would translate to:
int __internal_unique_name = 10;
const int& ref = __internal_unique_name;

结语

理解左值和右值能帮助你了解C++的内部工作原理,在C++11中,引入了右值引用移动语义。也就是,右值也可以被修改了!!

我将在后面的篇幅中介绍它们。

Reference