1. 说一下理解的右值引用

1> 说明右值引用的概念

右值引用其实就是以引用传递的方式使用右值,表现为使用&& 修饰的变量;
那么什么是右值?

2> 延申右值的概念

要说明右值引用,首先需要说明一下什么是右值:(参照 c++ primer 471页)

  • 右值,就是在内存没有确定存储地址、没有变量名,表达式结束就会销毁的值。 也分为非常量右值和常量右值;右值一般是一个常量或者表达式求值过程中产生的临时对象

与之相对应的还有左值:

  • 左值,就是在内存中有确定的存储地址、有变量名,表达式结束依然存在的值;分为非常量左值和常量左值;

左值引用举例说明:

  1. int a=10; //非常量左值(有确定存储地址,也有变量名)
  2. const int a1=10; //常量左值(有确定存储地址,也有变量名)
  3. const int a2=20; //常量左值(有确定存储地址,也有变量名)
  4. //非常量左值引用
  5. int &b1=a; //正确,a是一个非常量左值,可以被非常量左值引用绑定
  6. int &b2=a1; //错误,a1是一个常量左值,不可以被非常量左值引用绑定
  7. int &b3=10; //错误,10是一个非常量右值,不可以被非常量左值引用绑定
  8. int &b4=a1+a2; //错误,(a1+a2)是一个常量右值,不可以被非常量左值引用绑定
  9. //常量左值引用
  10. const int &c1=a; //正确,a是一个非常量左值,可以被非常量右值引用绑定
  11. const int &c2=a1; //正确,a1是一个常量左值,可以被非常量右值引用绑定
  12. const int &c3=a+a1; //正确,(a+a1)是一个非常量右值,可以被常量右值引用绑定
  13. const int &c4=a1+a2; //正确,(a1+a2)是一个常量右值,可以被非常量右值引用绑定

(此处不对左值与右值做深入剖析)

3> 延申右值引用的引入原因

追溯到 c98/03 标准,其中已经有引用这个概念,使用 & 表示,但是该引用方式有一个缺陷,即正常情况下,只能操作 c++ 左值,无法对右值添加引用;举例:

  1. int num = 20;
  2. int &lReference1 = num; // 正确
  3. int &lReference2 = 20; // 错误

(c++98 标准下)编译器允许为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。

但是,其支持使用常量左值引用操作右值:

  1. int num = 20;
  2. const int &lReference1 = num;
  3. const int &lReference2 = 20; // 正确

我们知道右值一般是没有名称的,要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改操作(例如移动语义),显然左值引用的方式行不通。

因此 c++11标准引入了右值引用,能够操作右值;

4> 延申右值引用的应用场景:函数传参 + 移动语义

右值引用的应用场景,主要是支持移动语义和完美转发;

1> 函数传参

举个例子,有时候函数传参需要传递一个类对象:

  1. class TestClass
  2. {
  3. public:
  4. TestClass(std::string str): m_str(str)
  5. { printf("construct TestClass: %s\n", str.c_str()); }
  6. TestClass(const TestClass& t)
  7. { printf("copy TestClass\n"); }
  8. ~TestClass()
  9. { printf("destruct TestClass\n"); }
  10. std::string m_str;
  11. };
  12. void print(TestClass && t) { printf("print %s\n", t.m_str.c_str()); }
  13. void print1(TestClass t) { printf("print1 %s\n", t.m_str.c_str()); }
  14. void main()
  15. {
  16. TestClass t("c++");
  17. print1(t); // 传递左值的方式会触发拷贝构造函数, 多一次拷贝
  18. }

上面代码的输出如下:

construct TestClass: c++ copy TestClass print1 destruct TestClass destruct TestClass

即传递左值的方式触发了一次拷贝构造函数,我们将主函数代码修改如下:

  1. print(std::move(t)); // 传递右值引用的方式不会触发拷贝构造函数

输出如下:

construct TestClass: c++ print c++ destruct TestClass

可以看出将右值引用作为函数实参传递能够避免多余的拷贝;

2> 移动语义

其实就是实现类的移动构造函数。举个例子,先实现如下未添加移动构造函数的类:

  1. class WithoutMoveConstructClass
  2. {
  3. public:
  4. WithoutMoveConstructClass()
  5. { printf("construct WithoutMoveConstructClass \n"); }
  6. WithoutMoveConstructClass(const WithoutMoveConstructClass& w)
  7. { printf("copy WithoutMoveConstructClass \n"); }
  8. ~WithoutMoveConstructClass()
  9. { printf("destruct WithoutMoveConstructClass \n"); }
  10. };
  11. WithoutMoveConstructClass get_wclass() { return WithoutMoveConstructClass(); }
  12. int main()
  13. {
  14. WithoutMoveConstructClass w = get_wclass();
  15. }

在未经优化的条件下编译通过,这样会打印日志:

  1. construct WithoutMoveConstructClass < -- 执行 WithoutMoveConstructClass()
  2. copy WithoutMoveConstructClass < -- 执行 return WithoutMoveConstructClass()
  3. destruct WithoutMoveConstructClass < -- 销毁 WithoutMoveConstructClass() 产生的匿名对象
  4. copy WithoutMoveConstructClass < -- 执行 w = get_wclass()
  5. destruct WithoutMoveConstructClass < -- 销毁 get_wclass() 返回的临时对象
  6. destruct WithoutMoveConstructClass < -- 销毁 w

整个初始化的流程包含以下几个阶段:

  • 1> 执行 get_wclass() 函数体,调用 WithoutMoveConstructClass 类默认构造函数生成一个匿名对象;
  • 2> 执行 return WithoutMoveConstructClass(),调用拷贝构造函数复制一份之前生成的匿名对象,将其作为函数返回值;函数体执行完毕前,匿名对象会被析构销毁
  • 3> 执行 w = get_wclass();,再次调用拷贝构造函数,将之前拷贝得到的临时对象复制给 w,(该行代码执行完毕,get_wclass() 函数返回的对象会被析构)
  • 4> 程序执行结束前,会自行调用 WithoutMoveConstructClass 类的析构函数销毁 w

若代码编译选项不执行优化,上述代码中, return WithoutMoveConstructClass() 会因为创建临时对象而调用拷贝构造函数;

下面添加移动构造函数:

  1. MoveConstructClass(MoveConstructClass&& t)
  2. { printf("move MoveConstructClass\n"); }

然后再次运行代码:

  1. construct MoveConstructClass
  2. move MoveConstructClass
  3. destruct MoveConstructClass
  4. move MoveConstructClass
  5. destruct MoveConstructClass
  6. destruct MoveConstructClass

可以发现,两次操作均触发了移动构造函数;

总结:当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。

移动语义,即移动构造函数的方式,能够省去不必要的拷贝,大大提升程序的性能。

引申 深拷贝与浅拷贝

3> 完美转发

实际开发中使用较少,

参考:右值引用的使用场景牛不才的博客-CSDN博客右值引用的使用场景