• C++ 新特性 | C++ 11 | 移动语义与右值引用


    一、移动语义与右值引用

    1、左值与左值引用

    左值是一个表示数据的表达式,程序可以获取其地址。左值可以出现在赋值语句的左边,也可以出现在赋值语句的右边。左值引用就是对左值的引用,例如:

    int a = 20;   // a 左值
    const int b = 10;  // b 左值
    int c = b;  // c 左值
    int &r = a;  // r 左值引用
    
    • 1
    • 2
    • 3
    • 4

    左值一般有下面这些,如下:

    • 函数名和变量名
    • 返回左值引用的函数调用
    • 前置自增自减表达式++i、–i
    • 由赋值表达式或赋值运算符连接的表达式(a=b, a += b等)
    • 解引用表达式*p

    2、右值与右值引用

    右值即可出现在赋值表达式右边,但不能获取其地址。右值包括字面常量(C风格字符串除外,它表示的是地址)、x + y表达式、以及返回值的函数(条件是该函数返回的不是引用)。C++11新增了右值引用,这是使用&&表示的,右值引用就是对右值的引用,例如:

    int getValue(){
        return 50;
    }
     
    int main(){
        int x = 10;
        int y = 20;
        int &&r1 = 30;  //字面常量是右值
        int &&r2 = x + y;  //表达式是右值
        int &&r3 = getValue();  //函数返回的是int类型的值
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    右值一般有下面这些,如下:

    • 除字符串字面值外的字面值
    • 返回非引用类型的函数调用
    • 后置自增自减表达式i++、i–
    • 算术表达式(a+b, a*b, a&&b, a==b等)
    • 取地址表达式等(&a)

    简单来说,左值是指可以取地址的、具有持久性的对象,而右值是指不能取地址的、临时生成的对象。

    注意:右值引用本身是左值,右值引用本身有名字且可以取地址

    3、将亡值

    将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。例如:

    
    class A {
        xxx;
    };
    A a;
    auto c = std::move(a); // c是将亡值
    auto d = static_cast<A&&>(a); // d是将亡值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    4、移动语义

    讲移动语义之前一定要先搞清楚浅拷贝深拷贝。移动语义,可以理解为转移所有权,深拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,就是转移资源的所有权,通过C++11新增的移动语义可以省去很多拷贝负担。下面实现一个自定义字符串类MyString,MyString内部管理一个C语言的char *数组,这个时候一般都需要实现拷贝构造函数和拷贝赋值函数,实现深拷贝,例如:

    #include 
    #include 
    #include 
    using namespace std;
    
    class MyString
    {
    public:
        static size_t CCtor; //统计调用拷贝构造函数的次数
    
    public:
        // 构造函数
       MyString(const char* cstr=0){
           if (cstr) {
              m_data = new char[strlen(cstr)+1];
              strcpy(m_data, cstr);
           }
       }
    
       // 拷贝构造函数
       MyString(const MyString& str) {
           CCtor ++;
           m_data = new char[ strlen(str.m_data) + 1 ];
           strcpy(m_data, str.m_data);
       }
    
       // 赋值运算符
       MyString& operator=(const MyString& str){
           if (this == &str)
              return *this;
    
           delete[] m_data;
           m_data = new char[ strlen(str.m_data) + 1 ];
           strcpy(m_data, str.m_data);
           return *this;
       }
    
       ~MyString() {
           delete[] m_data;
       }
    
       char* get_c_str() const { return m_data; }
    private:
       char* m_data;
    };
    
    size_t MyString::CCtor = 0;
    
    int main()
    {
        vector<MyString> vecStr;
        vecStr.reserve(1000);
        for(int i = 0; i < 1000; i++){
            vecStr.push_back(MyString("hello"));
        }
        cout << MyString::CCtor << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    输出结果:

    1000
    
    Process returned 0 (0x0)   execution time : 0.052 s
    Press any key to continue.
    
    • 1
    • 2
    • 3
    • 4

    代码看起来挺不错,却发现执行了1000次拷贝构造函数,如果MyString(“hello”)构造出来的字符串本来就很长,构造一遍就很耗时了,最后却还要拷贝一遍,而MyString(“hello”)只是临时对象,拷贝完就没什么用了,这就造成了没有意义的资源申请和释放操作,如果能够直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间,而C++11新增加的移动语义就能够做到这一点。要实现移动语义就必须增加两个函数:移动构造函数移动赋值运算符

    #include 
    #include 
    #include 
    using namespace std;
    
    class MyString
    {
    public:
        static size_t CCtor; //统计调用拷贝构造函数的次数
        static size_t MCtor; //统计调用移动构造函数的次数
        static size_t CAsgn; //统计调用拷贝赋值函数的次数
        static size_t MAsgn; //统计调用移动赋值函数的次数
    
    public:
        // 构造函数
       MyString(const char* cstr=0){
           if (cstr) {
              m_data = new char[strlen(cstr)+1];
              strcpy(m_data, cstr);
           }
       }
    
       // 拷贝构造函数
       MyString(const MyString& str) {
           CCtor ++;
           m_data = new char[ strlen(str.m_data) + 1 ];
           strcpy(m_data, str.m_data);
       }
    
       // 移动构造函数
       MyString(MyString&& str) noexcept
           :m_data(str.m_data) {
           MCtor ++;
           str.m_data = nullptr;
       }
    
       // 重载拷贝赋值运算符
       MyString& operator=(const MyString& str){
           CAsgn ++;
           if (this == &str)
              return *this;
    
           delete[] m_data;
           m_data = new char[ strlen(str.m_data) + 1 ];
           strcpy(m_data, str.m_data);
           return *this;
       }
    
       // 重载移动拷贝赋值运算符
       MyString& operator=(MyString&& str) noexcept{
           MAsgn ++;
           if (this == &str)
              return *this;
    
           delete[] m_data;
           m_data = str.m_data;
           str.m_data = nullptr;
           return *this;
       }
    
       ~MyString() {
           delete[] m_data;
       }
    
       char* get_c_str() const { return m_data; }
    private:
       char* m_data;
    };
    
    size_t MyString::CCtor = 0;
    size_t MyString::MCtor = 0;
    size_t MyString::CAsgn = 0;
    size_t MyString::MAsgn = 0;
    
    int main()
    {
        vector<MyString> vecStr;
        vecStr.reserve(1000);
        for(int i = 0; i < 1000; i++){
            vecStr.push_back(MyString("hello"));
        }
        cout << "CCtor = " << MyString::CCtor << endl;
        cout << "MCtor = " << MyString::MCtor << endl;
        cout << "CAsgn = " << MyString::CAsgn << endl;
        cout << "MAsgn = " << MyString::MAsgn << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86

    输出结果:

    CCtor = 0
    MCtor = 1000
    CAsgn = 0
    MAsgn = 0
    
    Process returned 0 (0x0)   execution time : 0.268 s
    Press any key to continue.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以看到,移动构造函数拷贝构造函数的区别是,拷贝构造的参数是const MyString& str,是常量左值引用,而移动构造的参数是MyString&& str,是右值引用,而MyString(“hello”)是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,通过移动构造函数实现了资源转移。

    使用过程要注意的问题:

    • 如果没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因
    • c++11中的所有容器都实现了move语义,move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝

    5、std::move

    前面已经看到使用std::move是为了实现移动语义,下面看下std::move的实现原理,源码如下:

    /**
     *  @brief  Convert a value to an rvalue.
     *  @param  __t  A thing of arbitrary type.
     *  @return The parameter cast to an rvalue-reference to allow moving it.
    */
    template <typename T>
    typename remove_reference<T>::type&& move(T&& t)
    {
    	return static_cast<typename remove_reference<T>::type&&>(t);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    某种意义上来说,std::move(lvalue) 就约等于 static_cast(lvalue),即将左值强制转换为右值。而 std::move 中封装了一个类型提取器 std::remove_reference 来方便使用。

    6、remove_reference

    std::remove_reference是一个类型提取器,实现的功能比较简单,通过模版提取出最底层类型,提取出来的底层类型保存在std::remove_reference::type,源码如下:

    template<typename _Tp>
      struct remove_reference
      { typedef _Tp   type; };
    
    template<typename _Tp>
      struct remove_reference<_Tp&>
      { typedef _Tp   type; };
    
    template<typename _Tp>
      struct remove_reference<_Tp&&>
      { typedef _Tp   type; };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    示例:通过下面的代码可以看出,通过类型提取器,可以从int &int && 中分离出最底层的int

    #include 
    #include 
    using namespace std;
    
    int main() {
        int i = 10;
        int &ref = i;
    
        std::remove_reference<decltype((i))>::type a = 10;           // 左值
        std::remove_reference<decltype(200)>::type b = 10;           // 右值
        std::remove_reference<decltype((ref))>::type c = 10;         // 左值引用
        std::remove_reference<decltype(std::move(i))>::type d = 10;  // 右值引用
    
        std::cout << std::is_same<int, std::remove_reference<decltype((i))>::type>::value << std::endl;
        std::cout << std::is_same<int, std::remove_reference<decltype(200)>::type>::value << std::endl;
        std::cout << std::is_same<int, std::remove_reference<decltype((ref))>::type>::value << std::endl;
        std::cout << std::is_same<int, std::remove_reference<decltype(std::move(i))>::type>::value << std::endl;
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输出结果

    1
    1
    1
    1
    
    • 1
    • 2
    • 3
    • 4
  • 相关阅读:
    Hadoop 2.x和Hadoop 3.x比较
    面试官:你知道 Java 中的回调机制吗?
    【Linux】进程概念与进程状态
    san.js源码解读之工具(util)篇——extend函数
    高等数学(第七版)同济大学 习题3-7 个人解答
    关于在学习 opengl 时遇到的 bug:在 glBegin 和 glEnd 中间使用 glLineWidth 的问题
    水电远程抄表:现代智能生活的创新实践
    您的报告生成器可以动态执行此操作吗?ViewPro可以
    GBase8s系统表介绍(七)
    【Java List与数组】List<T>数组和数组List<T>的区别(124)
  • 原文地址:https://blog.csdn.net/cloud323/article/details/132701889