右值引用

本文最后更新于:1 年前

C++11新特性 仅供面试专用

1.什么是右值引用

  首先介绍下左右值(我发现自己学了好长时间都没有对两者进行区分~)
  对于左值、右值可以简单的以放在等号左右为区分,等号左边的是左值,等号右边的是右值。
  再来详细的介绍下各自的特点:
    左值:
    1)左值可以取地址
    2)左值可以修改
    3)左值可以放在等号左右两边
    右值:
    1)右值不可以取地址
    2)右值不可以直接修改
    3)右值只能放在等号右边
    4)右值往往是没有名称的

  再来举几个实例:
  左值举例:int a; int a = 3;
  上面的a都是左值

  关于右值,C++11将右值分了两类:纯右值、将亡值。从例子上看:
  1)纯右值 int a = 3; 就是指等号右边的常数,上式中的3
  2)将亡值其实就是中间变量的过渡,过渡之后就消亡,可以细分两种
   (1)函数的临时返回值 int a = f(3); f(3)的返回值是右值,副本拷贝给a,然后消失
   (2)表达式 像(x+y),其中(x+y)是右值

  在左右值使用时有三个原则,不能违反原则,否则编译无法通过:
  原则1:右值可以赋给左值,左值不能给右值(左值权限更大)

1
2
3
4
5
int a = 3;  // a是左值,3是右值
int d = a; // d和a都是左值,左值可以赋给左值
int &&d = a; × // 右值引用左值不行
int &&d = 10; // 右值引用右值可以
int &&d = f(10); // 右值引用右值可以

​ 原则2:右值无法修改

1
int a = 10;  // 10是右值常数,无法修改

​ 原则3:编译器允许为左值建立引用,不可以为右值建立引用

1
2
3
int num = 10;
int &b = num; √ // num是左值,可以左值建立引用
int &b = 10; × // 10是右值,不可以右值建立引用

 有一个问题存在:只有左值可以修改,那如果想对右值进行修改怎么办? 解决办法就是——右值引用

  右值引用的语法:&&
  使用右值引用需要注意三个问题:
  1)右值引用必须要进行初始化

1
int && a;  × // 必须初始化

​ 2)不能使用左值进行初始化

1
2
3
int num = 10;
int && a = num; × //不能使用左值进行右值初始化
int && a = 10; √

​ 3)右值引用可以对右值进行修改

1
2
int &&a = 10;    // 这里的a是右值引用,其实是10
a = 100;

可以发现当对右值加上应用后可以修改值也可以修改地址,从功能上升为左值。所以有一种说法:右值引用的本质就是不用拷贝的左值。

2.右值引用的好处?

  先想想引用的目的,传递参数有两种方式:值传递和引用传递。二者相比引用传递的优势就是通过传递地址,来减少一次拷贝。在常规写程序的时候,使用的都是左值引用。左值引用有两个使用场景:函数传参、函数返回值。
  1)函数传参:int f(int &a);
  2)函数返回值:int& f();
  以上两种情况使用的都是引用传递相比于值传递减少了拷贝次数。但有一种情况会出问题:就是返回值是一个临时对象。如下代码:

1
2
3
4
A& f() {
A a;
return a;
}

  当返回对象a的地址时,其实a作为在栈上的临时对象,作用域已经到了,被析构。这样如果外界再对这个地址进行访问时,就会出现问题。这也左值引用的一个弊端,而右值引用的出现就是为了解决这个问题。那右值引用是怎么解决返回的临时变量析构? 当返回值为右值引用时,会把返回的临时变量中的内存居为己用,仍保持了有效性,也避免了拷贝。

3.右值引用的应用

  右值引用的应用场景场景主要有两个:移动语义、完美转发。下面分别介绍一下:
  移动语义
  如果我们把赋值这类操作看作资源转移,那么传统的资源转让是通过拷贝实现的,需要两份空间。而移动语义是通过移动来实现资源转让,只使用一个空间。来看一下移动语义的实现原理:
  首先明确移动语义和右值引用的关系:实现移动语义,就必须使用右值引用。移动语义具体实现是基于移动构造和移动赋值,而移动构造函数和移动赋值函数都需要形参为右值引用类型。
  移动构造和移动赋值负责在不发生拷贝的情况下将资源转移到目标对象名下,
  ps:这里的构造和赋值一般指对象初始化的两种方式:

1
2
3
4
5
// 移动构造
Obj a(b);

// 移动赋值
Obj a = b;

  移动语义避免了拷贝的风险,拷贝有两方面,一方面避免了浅拷贝可能引发的悬空指针的问题,另一方面也避免了深拷贝昂贵的开销

  移动语义避免了拷贝的风险,拷贝有两方面,一方面避免了浅拷贝可能引发的悬空指针的问题,另一方面也避免了深拷贝昂贵的开销

  前面说移动语义只能用右值引用实现,那有的时候就是希望用左值怎么办呢?这里有一个move函数,作用是把左值强制转换成右值引用,然后就能继续使用右值引用的特性。一般move用于对象,因为只有对象才会有各种构造函数,对于基本类型就无效啦。

完美转发
  存在这样一种情况:

1
2
3
void notPerfectForward(int &&i) {
printValue(i); i会被当作左值处理
}

  这个转发过程中,i最开始是右值引用,但再次传递时却变成了左值。失去了右值引用的特性,不是我们的预期。这种情况适合使用完美转发。
  完美转发指函数模板可以将自己的参数完美地转发给内部调用的其他函数。完美指不仅能准确转发参数的值,还能保证转发参数的左右值属性不变。简单点说也就是如果参数是左值引用,转发给下一个函数还是左值引用;如果参数是右值引用,则转发给下一个函数还是右值引用。
  完美转发的实现基于,std::forward,像下面这样:

1
2
3
4
template<typename T>
void PerfectForward(T &&i) {
printValue(std::forward<T>(i)); 这个i会被当作右值处理
}

  右值引用提供了很好的特性,这篇文章只是简单的对右值引用一些常见的概念做了解释,但想要具体使用还远远不够,只有在项目中多多使用,才能融会贯通,加油哇~


右值引用
https://wlpswmt.github.io/2023/03/19/右值引用/
作者
Sivan Zhang
发布于
2023年3月19日
许可协议