当前位置:   article > 正文

C++左值右值、左值引用&右值引用&&、移动语义move_左值、右值引用和move语义

左值、右值引用和move语义

目录

1.什么是左值、右值

2.什么是左值引用&、右值引用&&

2.1左值引用&

2.2右值引用&&

2.3对左右值引用本质的讨论 

2.3.1右值引用有办法指向左值吗? 

2.3.2左值引用、右值引用本身是左值还是右值? 

2.4 右值引用使用场景

2.4.1浅拷贝重复释放

2.4.2深拷贝构造函数

2.4.3移动构造函数 

2.5移动(move )语义 

 Demo

2.6 forward 完美转发 

2.7 emplace_back 减少内存拷贝和移动


1.什么是左值、右值

可以从2个角度判断:

  1. 左值可以取地址、位于等号左边;
  2. 而右值没法取地址,位于等号右边。

int a = 6;

  1. a可以通过 & 取地址,位于等号左边,所以a是左值。
  2. 6位于等号右边,6没法通过 & 取地址,所以6是个右值。

再举个复杂点的例子:

  1. struct A {
  2. A(int a = 0) {
  3. a_ = a;
  4. }
  5. int a_;
  6. };
  7. A a = A();
  1. 同样的,a可以通过 & 取地址,位于等号左边,所以a是左值。
  2. A()是个临时值,没法通过 & 取地址,位于等号右边,所以A()是个右值。

可见左右值的概念很清晰,有地址的变量就是左值,没有地址的字面值、临时值就是右值。

2.什么是左值引用&、右值引用&&

引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝。

2.1左值引用&

左值引用:能指向左值,不能指向右值的就是左值引用:

  1. int a = 5;
  2. int &ref_a = a; // 左值引用指向左值,编译通过
  3. int &ref_a = 5; // 左值引用指向了右值,会编译失败

引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。

但是,const左值引用是可以指向右值的:

const int &ref_a = 5; // 编译通过

const左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用 const & 作为函数参数的原因之一,如 std::vector 的 push_back :

void push_back (const value_type& val);

如果没有 const , vec.push_back(5) 这样的代码就无法编译通过。

2.2右值引用&&

再看下右值引用,右值引用的标志是 && ,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值:

  1. int &&ref_a_right = 5; // ok
  2. int a = 5;
  3. int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
  4. ref_a_right = 6; // 右值引用的用途:可以修改右值

        右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。

2.3对左右值引用本质的讨论 

2.3.1右值引用有办法指向左值吗? 

有办法,使用 std::move :

  1. int a = 5; // a是个左值
  2. int &ref_a_left = a; // 左值引用指向左值
  3. int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向
  4. cout << a; // 打印结果:5

在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值了?

并不是,打印出a的值仍然是5。


std::move 是一个非常有迷惑性的函数:
1.不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量;
2.但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换: static_cast<T&&>(lvalue) 。 所以,单纯的std::move(xxx)不会有性能提升


同样的,右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值: 

  1. int &&ref_a = 5;
  2. ref_a = 6;
  3. //等同于以下代码:
  4. int temp = 5;
  5. int &&ref_a = std::move(temp);
  6. ref_a = 6;

2.3.2左值引用、右值引用本身是左值还是右值? 

被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。仔细看下边代码: 

  1. #include <iostream>
  2. using namespace std;
  3. void change(int&& right_value) {
  4. right_value = 8;
  5. }
  6. int main() {
  7. int a = 5; // a是个左值
  8. int &ref_a_left = a; // ref_a_left是个左值引用
  9. int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
  10. change(a); // 编译不过,a是左值,change参数要求右值
  11. change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
  12. change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
  13. change(std::move(a)); // 编译通过
  14. change(std::move(ref_a_right)); // 编译通过
  15. change(std::move(ref_a_left)); // 编译通过
  16. change(5); // 当然可以直接接右值,编译通过
  17. // 打印这三个左值的地址,都是一样的
  18. cout << &a << ' ';
  19. cout << &ref_a_left << ' ';
  20. cout << &ref_a_right;
  21. }

        看完后你可能有个问题,std::move会返回一个右值引用 int && ,它是左值还是右值呢? 从表达式 int&&ref = std::move(a) 来看,右值引用 ref 指向的必须是右值,所以move返回的 int && 是个右值。
        所以右值引用既可能是左值,又可能是右值吗? 确实如此:右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。或者说:作为函数返回值的 && 是右值,直接声明出来的 && 是左值。 这同样也符合前面章节对左值,
        右值的判定方式:其实引用和普通变量是一样的, int &&ref = std::move(a) 和 int a = 5 没有什么区别,等号左边就是左值,右边就是右值。 

最后,从上述分析中我们得到如下结论:
1. 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。 

  1. #include <iostream>
  2. using namespace std;
  3. void f1(const int& n) {
  4. n += 1; // 编译失败,const左值引用不能修改指向变量
  5. }
  6. void f2(int && n) {
  7. n += 1; // ok
  8. }
  9. int main() {
  10. f1(5);
  11. f2(5);
  12. }

2.4 右值引用使用场景

右值引用优化性能,避免深拷贝

2.4.1浅拷贝重复释放

        对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:

  1. #include <iostream>
  2. using namespace std;
  3. class A {
  4. public:
  5. A() :m_ptr(new int(0)) {
  6. cout << "constructor A" << endl;
  7. }
  8. ~A() {
  9. cout << "destructor A, m_ptr:" << m_ptr << endl;
  10. delete m_ptr;
  11. m_ptr = nullptr;
  12. }
  13. private:
  14. int* m_ptr;
  15. };
  16. // 为了避免返回值优化,此函数故意这样写
  17. A Get(bool flag) {
  18. A a;
  19. A b;
  20. cout << "ready return" << endl;
  21. if (flag) {
  22. return a;
  23. } else {
  24. return b;
  25. }
  26. }
  27. int main() {
  28. {
  29. A a = Get(false); // 运行报错
  30. }
  31. cout << "main finish" << endl;
  32. return 0;
  33. }

2.4.2深拷贝构造函数

        在上面的代码中,默认构造函数是浅拷贝,main函数的 a 和Get函数的 b 会指向同一个指针 m_ptr,在析构的时候会导致重复删除该指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码: 

  1. #include <iostream>
  2. using namespace std;
  3. class A {
  4. public:
  5. A() :m_ptr(new int(0)) {
  6. cout << "constructor A" << endl;
  7. }
  8. A(const A& a) :m_ptr(new int(*a.m_ptr)) {
  9. cout << "copy constructor A" << endl;
  10. }
  11. ~A() {
  12. cout << "destructor A, m_ptr:" << m_ptr << endl;
  13. delete m_ptr;
  14. m_ptr = nullptr;
  15. }
  16. private:
  17. int* m_ptr;
  18. };
  19. // 为了避免返回值优化,此函数故意这样写
  20. A Get(bool flag) {
  21. A a;
  22. A b;
  23. cout << "ready return" << endl;
  24. if (flag) {
  25. return a;
  26. } else {
  27. return b;
  28. }
  29. }
  30. int main() {
  31. {
  32. A a = Get(false); // 运行报错
  33. }
  34. cout << "main finish" << endl;
  35. return 0;
  36. }

2.4.3移动构造函数 

        这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。

 看下面的代码:

  1. #include <iostream>
  2. using namespace std;
  3. class A {
  4. public:
  5. A() :m_ptr(new int(0)) {
  6. cout << "constructor A" << endl;
  7. }
  8. A(const A& a) :m_ptr(new int(*a.m_ptr)) {
  9. cout << "copy constructor A" << endl;
  10. }
  11. // 移动构造函数,可以浅拷贝
  12. A(A&& a) :m_ptr(a.m_ptr) {
  13. a.m_ptr = nullptr; // 为防止a析构时delete data,提前置空其m_ptr
  14. cout << "move constructor A" << endl;
  15. }
  16. ~A() {
  17. cout << "destructor A, m_ptr:" << m_ptr << endl;
  18. delete m_ptr;
  19. m_ptr = nullptr;
  20. }
  21. private:
  22. int* m_ptr;
  23. };
  24. // 为了避免返回值优化,此函数故意这样写
  25. A Get(bool flag) {
  26. A a;
  27. A b;
  28. cout << "ready return" << endl;
  29. if (flag) {
  30. return a;
  31. } else {
  32. return b;
  33. }
  34. }
  35. int main() {
  36. {
  37. A a = Get(false); // 运行报错
  38. }
  39. cout << "main finish" << endl;
  40. return 0;
  41. }

上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move 语义),右值引用的一个重要目的是用来支持移动语义的。

        移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。       

2.5移动(move )语义 

        移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。

        move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要move语义起作用,核心在于需要对应类型的构造函数支持。  

 Demo

  1. #include <iostream>
  2. #include <vector>
  3. #include <cstdio>
  4. #include <cstdlib>
  5. #include <string.h>
  6. using namespace std;
  7. class MyString {
  8. private:
  9. char* m_data;
  10. size_t m_len;
  11. void copy_data(const char *s) {
  12. m_data = new char[m_len+1];
  13. memcpy(m_data, s, m_len);
  14. m_data[m_len] = '\0';
  15. }
  16. public:
  17. //构造函数
  18. MyString() {
  19. m_data = NULL;
  20. m_len = 0;
  21. }
  22. //构造函数
  23. MyString(const char* p) {
  24. m_len = strlen (p);
  25. copy_data(p);
  26. }
  27. //拷贝构造函数
  28. MyString(const MyString& str) {
  29. m_len = str.m_len;
  30. copy_data(str.m_data);
  31. std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;
  32. }
  33. //重载=
  34. MyString& operator=(const MyString& str) {
  35. if (this != &str) {
  36. m_len = str.m_len;
  37. copy_data(str.m_data);
  38. }
  39. std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl;
  40. return *this;
  41. }
  42. //移动构造函数
  43. MyString(MyString&& str) {
  44. std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;
  45. m_len = str.m_len;
  46. m_data = str.m_data; //避免了不必要的拷贝
  47. str.m_len = 0;
  48. str.m_data = NULL;
  49. }
  50. //重载=
  51. MyString& operator=(MyString&& str) {
  52. std::cout << "Move Assignment is called! source: " << str.m_data << std::endl;
  53. if (this != &str) {
  54. m_len = str.m_len;
  55. m_data = str.m_data; //避免了不必要的拷贝
  56. str.m_len = 0;
  57. str.m_data = NULL;
  58. }
  59. return *this;
  60. }
  61. virtual ~MyString() {
  62. if (m_data) free(m_data);
  63. }
  64. };
  65. int main() {
  66. MyString a;
  67. a = MyString("Hello"); // Move Assignment 因为a已经创建了,所以走=,而不是构造函数
  68. MyString b = a; // Copy Constructor
  69. MyString c = std::move(a); // Move Constructor std::move将左值转为右值
  70. std::vector<MyString> vec;
  71. vec.push_back(MyString("World")); // Move Constructor
  72. return 0;
  73. }

         有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。

2.6 forward 完美转发 

2.7 emplace_back 减少内存拷贝和移动

 推荐一个不错的学习网站 C/C++后台高级服务器https://ke.qq.com/course/417774?flowToken=1010783

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/640037
推荐阅读
相关标签
  

闽ICP备14008679号