最近一段时间写C++代码,总是被其中的左值/右值以及std::move等东西困扰,打算用文章记录下我学习这部分知识的过程,做个总结,给自己以后做参考;这边文章首先介绍下C++中的左值/右值以及左值引用和右值引用。
左值与右值
直观来说,在一个表达式中,出现在等号左边的表达式是左值,出现在等号右边的表达式是右值;注意,这里强调是表达式而不是变量。左值既可以出现在等号的左边也可以出现在等号的右边,而右值一定只能出现在等号的右边。一个表达式如果不是左值,一定是右值。
本质上说,左值是可以被寻址的值,即左值一定对应内存中的一块区域,而右值既可以是内存中的值,也可以是寄存器中的一个临时变量。一个表达式被用作左值时,使用的是它的地址,而被用作右值时,使用的是它的内容。
如何准确的判断一个表达式是不是左值呢?
- 如果这个表达式可以出现在等号的左边,一定是左值;
- 如果可以对一个表达式使用&符号去地址,它一定是左值;
- 如果它“有名字”,则一定是左值;
int a = b+c ; // a是左值,因为我们可以对a取地址 &a; b+c是右值,因为我们不能对它取地址 &(b+c)编译会报错
在cpp11中,右值又可以进一步分为纯右值(pure rvalue)和将亡值(expiring value);其中纯右值指的是临时变量或者不和变量关联的字面量;其中临时变量包括非引用类型的函数返回值,比如 int f()的返回值,和表达式的结果,比如(a+b)结果就是一个临时变量;字面量比如10,“abc”这些。将亡值是cpp11新增的,适合右值引用相关的概念,包括返回右值引用T&&的函数的返回值,std::move的返回值。
左值引用和右值引用
引用
引用在使用上是变量的一个别名,对引用变量的任何操作都可以理解为对原变量的操作;引用和指针的主要区别如下:
- 引用不能为空,引用任何时候都要指向一块合法的内存;指针可以为空;
- 引用一旦初始化完成,就不能再指向另一个变量;指针可以;
- 引用必须在创建的时候初始化,指针可以在任何时候初始化;
那么什么是左值引用和右值引用呢?
左值引用就是对一个左值进行引用的类型,右值引用就是对一个右值进行引用的类型。左值引用和右值引用都属于引用类型,都必须在声明时对其进行初始化,其原因是引用类型本身并不持有所引用变量的内存,所以必须在初始化时制定要绑定的变量;左值引用是对具名变量的引用,右值引用是对不具名变量(匿名变量)的引用。
形如 const T&
的常量左值引用是个万金油的引用类型,其可以引用非常量左值,常量左值和右值。而形如T&
的非常量左值引用只能接受非常量左值对其进行初始化。
int &a = 2; # 非常量左值引用绑定到右值,编译失败
int b = 2; # 非常量左值变量
const int &c = b; # 常量左值引用绑定到非常量左值,编译通过
const int d = 2; # 常量左值
const int &e = c; # 常量左值引用绑定到常量左值,编译通过
const int &b =2; # 常量左值引用绑定到右值,编程通过
右值引用通常不能绑定任何左值,如果要绑定左值,需要使用std::move()将左值转换为右值;
int a;
int&& r1 = a; //非法,a是左值;
int&& r2 = std::move(a); //合法,通过std::move()将左值转换为右值;
下面来看一个例子:
void foo(int& a) {
cout << "lvalue reference " << a << endl;
}
void foo(int&& a ) {
cout << "rvalue reference " << a << endl;
}
int main() {
int a = 0;
foo(a);
foo(1);
}
输出:
lvalue reference 0
rvalue reference 1
上面的demo重载了foo函数,使其既可以接受左值引用,也可以接受右值引用。从结果可以看出,变量是作为左值处理的,而字面常量(临时对象)是作为右值处理的。
但是如果临时对象通过一个接受右值的函数传递给另一个函数,就会变成左值,因为这个变量在传递的过程中变成了具名变量:
void foo(int& a) {
cout << "lvalue reference " << a << endl;
}
void foo(int&& a ) {
cout << "rvalue reference " << a << endl;
}
void f(int&& a) {
foo(a);
}
int main() {
f(1);
}
输出:
lvalue reference 1