文章目录
- 右值引用和移动语义
- 左值和右值
- 左值引用和右值引用
- 完美转发
右值引用和移动语义
左值和右值
-
左值
左值是一个表示数据的表达式(如变量名或解引用的指针)。我们可以获取左值的地址,一般情况下可以对它赋值(对
const
修饰的类型的变量无法后续赋值,但是仍为左值,如const int a = 10;
此处的a是左值)。左值不一定在左边(如int c = a;
)此处的c为左值**//以下的p、b、c、*p、s、s[0]都是左值 //左值:可以取地址 int* p = new int(0); int b = 1; const int c = b; *p = 10; string s("111111"); s[0];
-
右值
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值**(这个不能是左值引用返回)**等等。右值不能取地址。右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边。 右值常常是字面常量或者临时变量
//右值:不能取地址 double x = 1.1, y = 2.2; //以下几个都是常见的右值,常量临时对象,匿名对象 10; x + y; fmin(x, y); string("11111");
右值又分为 纯右值 和 将亡值
纯右值: 内置类型右值将亡值: 类类型的右值引用 (匿名对象等等)
左值引用和右值引用
左值引用是对左值进行引用;右值引用是对右值进行引用。
即左值引用是对左值取别名;右值引用是对右值取别名
-
左值引用
实际上人们常说的引用就是左值引用
- 语法:类型&
//以下的p、b、c、*p、s、s[0]都是左值 //左值:可以取地址 int* p = new int(0); int b = 1; const int c = b; *p = 10; string s("111111"); s[0]; //左值引用给左值取别名 int& r1 = b; int*& r2 = p; int& r3 = *p; string& r4 = s;
- 语法:类型&
-
右值引用
-
语法:类型&&
//右值:不能取地址 double x = 1.1, y = 2.2; //以下几个都是常见的右值,常量临时对象,匿名对象 10; x + y; fmin(x, y); string("11111"); //右值引用给右值取别名 int&& rr1 = 10; double&& rr2 = x+y; double&& rr3 = fmin(x,y);
注意rr1,rr2,rr3是右值引用,但是右值引用本身是左值。即右值被右值引用后,右值引用本身是左值。这才使得后续在移动构造和移动赋值中可以转移对应右值引用的资源。
-
-
左值引用和右值引用的交叉
-
左值引用可以给左值取别名,那么左值引用可以给右值取别名吗?
答案是肯定的。因为在右值引用出现前,已经可以用左值引用引用右值了,即用const
修饰的引用。
如下://右值:不能取地址 double x = 1.1, y = 2.2; //以下几个都是常见的右值,常量临时对象,匿名对象 10; x + y; fmin(x, y); string("11111"); //左值引用引用给右值取别名:不能直接引用,但是const 左值引用可以 const int& rx1 = 10; const double& rx2 = x+y; const double& rx3 = fmin(x, y); const string& rx4 = string("11111");
-
右值引用可以给右值取别名,那么右值引用可以给左值取别名吗?
右值引用给左值取别名,不能直接引用,需要调用一下move函数-
move函数如下
该函数参数是需要一个左值,返回值为一个右值。
该函数的本质就是强制类型转换 -
右值引用给左值取别名
//以下的p、b、c、*p、s、s[0]都是左值 //左值:可以取地址 int* p = new int(0); int b = 1; const int c = b; *p = 10; string s("111111"); s[0]; //右值引用给左值取别名 int&& rrx1 = move(b); int*&& rrx2 = move(p); int&& rrx3 = move(*p); string&& rrx4 = move(s); string&& rrx5 = (string&&)s;
-
-
-
左值引用和右值引用的底层
-
底层的实现和语法层的规定可能是相背离的,因此不要试图用底层实现来佐证语法层。
-
左值引用和右值引用,在语法层面上是没有开空间的!!!
-
以下图为例,来观察左值引用和右值引用的底层
由此处汇编可以看出,实际上左值引用和右值引用在底层上都是开了空间的。对于左值引用,这个空间内存储一个 指向被引用的数据所在的空间。 对于右值引用,这个空间内存储一个 指向被引用的数据所在的空间。 从这里看,左值引用和右值引用在底层实现上几乎没有区别,一个存储的是语法层上左值所在的地址;一个存储的是语法层上右值所在的地址
-
强制类型转换一定产生临时变量
看了左值引用和右值引用的底层,那么在语法层上,想要右值引用引用左值,真的只能通过调用move吗?并不是的,我们只需要产生一个临时变量,然后用右值引用引用该临时变量即可。
什么意思呢?如下int b = 1; int&& rr1 = (int)b; int&& rr2 = (double)b; int&& rr3 = (size_t)b; //等等,都是可以的,强制类型转换产生临时变量,而临时变量是右值,因此可以右值引用。即通过强转使得右值引用 引用 左值。
-
-
-
右值引用的一些应用场景
-
移动构造
移动构造在深拷贝时才有意义
-
移动赋值
移动赋值在深拷贝时才有意义
-
完美转发
由于 模板实例化为右值引用时,右值引用的属性会退化成左值*(即 右值引用本身是左值)*,而我们想要传入的参数原本的属性。完美转发来解决这种场景
-
在模板中,
T&&
表示万能引用,而不表示右值引用
万能引用: 当 传入的数据类型为左值时,T&&为左值引用,实例化为传入数据类型&;当传入数据类型为右值时,T&&为右值引用,实例化为传入数据类型&&。template<class T> T&& Add(T&& a, T&& b) {return a + b; }
-
完美转发
在std中提供了实现 完美转发的函数模板。
完美转发是通过传入的参数的实际类型来进行传递的。
-
应用场景样例
void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; } void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; }// 引用 // 传左值->左值引用 // 传右值->右值引用 template<typename T> void PerfectForward(T&& t) {// 模版实例化是左值引用,保持属性直接传参给Fun// 模版实例化是右值引用,右值引用属性会退化成左值,转换成右值属性再传参给FunFun(forward<T>(t)); } int main() {PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0; }
如果没有使用完美转发,那么当参数传入
PerfectForward
函数后,会在该函数中调用Fun(int&x);
或者Fun(const int& x);
,这是因为 无论t是左值引用还是右值引用,其本身的属性都是左值。
使用完美转发后,那么当参数传入PerfectForward
函数后,会通过原本的数据的参数类型来调用Fun
函数。举个例子:PerfectForward(std::move(b));
函数,调用时,t的类型为const int&&
,t本身是左值,因此Fun(t)
调用的为void Fun(const int& x)
;而Fun(<forward<T>(t));
中forward<T>(t)
是通过自动推导,返回一个 与 传入t的数据的属性相同的数据,这里可以理解为此处的forward<T>(t)
返回了一个std::move(t)
,因为传入t的数据的属性是const 右值,因此这里将t再转发为右值。
-