• c++11——右值引用


    1. 左值和右值

        左值是表达式结束之后仍然存在的持久化对象,而右值是指表达式结束时就不再存在的临时对象。 
        c++11中,右值分为两种类型:将亡值(xvalue, expiring value),另一个是纯右值(prvalue, pure rvalue). 非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等都是纯右值;将亡值是c++11新增的、与右值引用相关的表达式,比如,将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值。

    2. 左值引用

        左值引用是对左值进行引用的类型,分为常量左值引用和非常量左值引用。其中,常量左值引用可以引用常量左值、非常量左值、常量右值、非常量右值;而非常量左值引用只能引用非常量左值

        int x = 1;
        const int y = 2;
        int& p1 = x;
        int& p2 = y; //出错,非常量左值引用无法引用常量
        const int& p3 = x;
        const int& p4 = y;
    
    3. 右值引用

        c++11增加了右值引用类型,实现对一个右值进行引用,标记为T&&. 因为右值不具名,因此只能通过引用的方式找到它。

    右值引用延长右值的生命期 
        左值引用和右值引用必须在声明的时候立即初始化,因为引用本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期变得和右值引用类型变量的生命期一样长,只要该变量还活着,该右值临时量将会一直存活下去。

    利用右值引用延长右值生命期,可以避免一些临时对象的构造和析构,从而提高性能。比如:

    class A{
    public:
        A(){
            cout << "construct..." << endl;
        }
        ~A(){
            cout << "Destruct..." << endl;
        }
        A(const A&a a){
            cout << "Copy construct..." << endl;
        }
    private:
    };
    A GetA(){
        return A();
    }
    int main(){
        A a = GetA();
        return 0;
    }
    

    以上代码,如果禁止编译器自动进行RVO优化,完全尊造c++的语法规则,则程序的输出为 
    输出1

        其中,GetA()函数内部的A()函数生成一个内部的对象obj1时调用 构造函数; 在函数返回时,临时对象obj2通过拷贝构造函数拷贝了该内部对象的内容;函数返回之后,内部对象obj1调用析构函数销毁;然后 A a = obj2,通过调用拷贝构造函数,a拷贝obj2;a赋值结束之后,obj2 调用析构函数销毁;最后程序结束时,a调用析构函数销毁。

        这整个过程中,在调用函数GetA时会构造和析构临时对象obj2,因此造成不必要的浪费。在c++98/03中可以通过const A& a = GetA()来将临时对象obj2赋值给一个常量左值引用a,延长了该临时对象的生命期;而在c++11中,也可以通过右值引用来延长函数返回时的临时对象(右值)的生命期,而不是在a = GetA()一结束就销毁。 

        其中,GetA()函数内部的A()函数生成一个内部的对象obj1时调用 构造函数; 在函数返回时,临时对象obj2通过拷贝构造函数拷贝了该内部对象的内容;函数返回之后,内部对象obj1调用析构函数销毁;然后 const A& a = obj2或者 A&& a = obj2,此时都是对引用进行初始化,没有对象的构造和析构;当程序结束时,obj2(也就是a)进行析构。

      
      
     

    4. T&&的赋值

    (1)左值和右值是独立于他们的类别的,右值引用类型可能是左值也可能是右值 
    (2)auto&& 或者函数参数类型自动推导的T&&是一个未定的引用类型,被称为universal reference,它可能是左值引用类型也可能是右值引用类型,取决于初始化的值类型。 
    (3)所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用叠加都为左值引用。当T&&为模板参数时,输入左值,它将会变成左值引用,而输入右值时则变为具名的右值引用。 
    (4)编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。

    4.1 左值和右值是独立于他们的类别的,右值引用类型可能是左值也可能是右值 
        int && a = xxxx;中a本身的类型为右值引用,但它是一个具名的变量,为左值

        int &&var1 = 10; //var1为右值引用
        auto&& var2 = var1; //var2此时为一个universial reference,但是由于var1本身是一个左值,因此var2 为左值引用
        int w1, w2;
        auto&& v1 = w1; //左值引用
        decltype(w1)&& v2 = w2; //int &&v2 = w2; //此时对一个右值引用初始化为一个左值,出错!!
    

    4.2 auto&& 或者函数参数类型自动推导的T&&是一个未定的引用类型,被称为universal reference, 
    它可能是左值引用类型也可能是右值引用类型,取决于初始化的值类型。

        auto&& a = 10; //a直接由一个右值初始化,则a为一个右值引用类型
        int x = 20;
        auto&& b = x; //b由一个左值进行初始化,则b为一个左值引用类型
        template<typename T>
        void func(T&& a){
        cout << a << endl;
        }
        ....
        func(10); //被一个右值初始化,a为右值引用类型, a类型为 int&&
        int x = 10;
        func(x); //左值引用类型, a为int& !!!!!
        void f(std:vector<T>&& param); //这里需要注意,这里既有推导类型T,又有确定类型vector。。。。在实际
        //应用时,调用该函数之前,Vector<T>中的推断类型T已经确定了,所以到调用该f函数的时候就没有类型推
        //则 param为右值引用
        template<typenmae T>
        void f(const T&& param){ //这里虽然有类型推导,但是由于带有const限定,则仍然为右值引用
        }
    

    即右值操作符&&只在 auto && / T&& ,且不带cv限定符的时候,才需要推断具体为左值还是右值引用,否则一律为右值引用。

    4.3 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用叠加都为左值引用。 
    当T&&为模板参数时,输入左值,它将会变成左值引用,而输入右值时则变为具名的右值引用。

    引用折叠 
        由于存在T&&这种未定引用类型,当它作为参数时,有可能被一个左值引用或者右值引用的参数初始化,这时经过类型推导的T&&类型,相比右值引用(&&)会发生类型的变化,这种变化成为引用折叠。

        typedef const int T;
        typedef T& TR;
        TR v
    
    TR的定义v的定义v的实际类型
    T& TR v T&
    T& TR& v T&
    T& TR&& v T&
    T&& TR v T&&
    T&& TR& v T&
    T&& TR&& v T&&

    从而,可以看出 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用叠加都为左值引用。

    4.4 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值

        void print(int& i){
            cout << "lvalue " << i << endl;
        }
        void print(int&& i){
            cout << "rvalue " << i << endl;
        }
        void forward(int&& i){
            print(i);
        }
        forward(10); //10为右值,进入forward之后,10变为i,i为一个变量,变为左值。
        //因此,输出 "lvalue " << i << endl;
    

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

        对于含有堆内存的类,需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,将会导致堆内存的重复删除。

    class A{
    public:
        A(): m_ptr(new int(0)){};
        ~A(){
            delete m_ptr;
        }
    private:
        int *m_ptr;
    };
    A Get(bool flag){
        A a;
        A b;
        if (flag)
            return a;
        else
            return b;
    }
    int main(){
        A a = Get(false);   //默认的拷贝构造函数,只是简单的将m_ptr进行赋值
        //在 Get函数内部的b被析构的时候delete m_ptr, 当程序结束的时候a析构也delete m_ptr,二者m_ptr相同。造成内存的重复析构
        return 0;
    }
    

        而如果为类的拷贝构造函数提供了深拷贝,则在程序产生临时对象的时候会出现大量的内存拷贝,降低性能。此时可以使用移动构造函数进行改进。

    class A{
    public:
        A(): m_ptr(new int(0)){};
        A(const A& a):m_ptr(new int(*a.m_ptr)){}; //深拷贝的拷贝构造函数
        A(A&& a):m_ptr(a.m_ptr){    //移动构造函数
            a.m_ptr = NULL;
        }
        
        ~A(){
            delete m_ptr;
        }
    private:
        int *m_ptr;
    };
    A Get(bool flag){
        A a;
        A b;
        if (flag)
            return a;
        else
            return b;
    }
    int main(){
        A a = Get(true); //调用移动构造函数
        A b = a;         //调用拷贝构造函数
        return 0;
    }
    

        使用移动构造函数,其参数是一个右值引用类型的参数 A&&, 没有深拷贝,只有浅拷贝,避免了对临时对象的深拷贝,提高了性能。这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数;否则,则会选择拷贝构造函数 
        在拷贝的源对象为临时的时候,调用移动构造函数,该函数将原来临时对象的资源移动到了拷贝的目的对象,并且将源对象的资源赋空。则之后,源对象被析构,资源已经被转移到目的对象。

        除了使用移动构造补充拷贝构造,还可以使用移动赋值操作符代替拷贝赋值操作符。

    class A{
    public:
        A(): m_ptr(new int(0)){};
        A(const A& a):m_ptr(new int(*a.m_ptr)){}; //深拷贝的拷贝构造函数
        A(A&& a):m_ptr(a.m_ptr){    //移动构造函数
            a.m_ptr = NULL;
        }
        A& operator=(const A& a){
            m_ptr = new int(*a.m_ptr);
        }
        A& operator=(A&& a){
            m_ptr = a.m_ptr;
            a.m_ptr = NULL;
        }
        ~A(){
            delete m_ptr;
        }
    private:
        int *m_ptr;
    };
    A Get(bool flag){
        A a;
        A b;
        if (flag)
            return a;
        else
            return b;
    }
    int main(){
        A a = Get(true); //调用移动构造函数
        A b = a;         //调用拷贝构造函数
        
        a = Get(false); //调用移动赋值操作符
        a = b;  //调用拷贝赋值操作符
        return 0;
    }
    

        上面添加了move版本的构造函数和赋值函数,对原来的类产生了一些影响: 如果提供了move版本的构造函数,则不会生成默认的构造函数。另外,编译器永远不会自动生成move版本的构造函数和赋值函数,他们需要手动显式的添加。 
        当添加了move版本的构造函数和赋值函数的重载形式后,某一个函数调用应当使用哪一个重载版本呢?下面是按照判决的优先级列出的3条规则: 
    (1)常量值只能绑定到常量引用上,不能绑定到非常量引用上 
    (2)左值优先绑定到左值引用上,右值优先绑定到右值引用上 
    (3)非常量值优先绑定到非常量引用上

     

    c++类的拷贝构造函数和赋值操作符:

    class A{
    public:
      A(int x){
        x_ = x;
    
      };
      A(const A& a){
        x_ = a.x_;
      }
         A& operator=  (const A& a){
        x_ = a.x_;
      }
    private:
      int x_;
    };
    A  getA(int x){
      return A(x);
    } ;
    A a = getA(1);  //拷贝构造函数
    A b = getA(2); //拷贝构造函数 
    b = a;    //赋值操作符
    

    拷贝构造函数是在构造类的对象的时候调用的,即 A a = getA(1); 这句新建了一个类A的对象a,调用拷贝构造函数。而 赋值操作符是对一个已经存在的对象进行重新赋值, 不重新生成对象。

    参考

    http://www.cnblogs.com/hujian/archive/2012/02/13/2348621.html

    +
  • 相关阅读:
    屏幕录像专家V7.5(完美破解版,无水印)下载
    常用前端插件推荐
    C#编写QQ找茬外挂
    wp-content-index文章目录插件使用效果调整
    C#读取Word文档内容代码
    js获取当前url地址及参数
    http状态码对应表
    应用程序利用回调向表现层实时推送数据
    解除网页右键限制和开启网页编辑状态的js代码
    IDEA中隐藏.iml文件
  • 原文地址:https://www.cnblogs.com/gtarcoder/p/4805606.html
Copyright © 2020-2023  润新知