• C++ 之 移动构造函数


    1、左值和右值

    C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。

    1. 通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象,表达式结束后依然存在的持久化对象,所有的具名变量或者对象都是左值。
    2. 右值是指临时的对象,它们只在当前的语句中有效,他就像是代表了一个由编译器创建的临时内存位置,在访问它之后,该内存就会被回收,该值也不能被访问了。

    具体代码如下 :

    int i = 0;           // i是左值, 0是右值,i在当前表达式结束之后,依然可以被使用,被引用,而0不行,它是临时值,当前表达式结束之后,此表达式中的0值就不存在了
     
    class A {
      public:
        int a;
    };
     A getTemp()
    {
        return A();
    }
    A a = getTemp();      // a是左值  getTemp()的返回值是右值(临时变量)
     
    ((i>0) ? i : j) = 1;   //右值不只是存在在表达式的右边,在此表达式中,0作为右值出现在表达式的左边,并且将值赋值给i或者j。  
     
    const int &a = 1;     //在 C++11 之前,右值是不能被引用的,最大限度就是用常量引用绑定一个右值
     
    T().set().get();      //在这种情况下,右值是可以被修改的,T 是一个类,set 是一个函数为 T 中的一个变量赋值,get 用来取出这个变量的值。在这句中,T() 生成一个临时对象,就是右值,set() 修改了变量的值,也就修改了这个右值。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2、左值引用和右值引用

    (1)左值引用就是最传统的引用 &。
    如下:

    int a = 10;
    int& refA = a; // refA是a的别名, 修改refA就是修改a, a是左值,左移是左值引用
    int& b = 1; //编译错误! 1是右值,不能够使用左值引用
    
    • 1
    • 2
    • 3

    (2)C++ Primer Plus 第6版18.1.9中说到,c++11中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。具体语法如下:

    int&& a = 1; // &&是右值引用的符号,实质上就是将不具名(匿名)变量取了个别名
    int b = 1;
    int && c = b; //编译错误! 不能将一个左值复制给一个右值引用
    
    • 1
    • 2
    • 3

    (3) 右值引用的三个特点:
    a、通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去。

    class A {
      public:
        int a;
    };
    A getTemp()
    {
        return A();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    A && a = getTemp(); //getTemp()的返回值是右值(临时变量)getTemp()返回的右值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),
    //而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量a的生命期一样,只要a还活着,该右值临时变量将会一直存活下去。

    b、右值引用独立于左值和右值。意思是右值引用类型的变量可能是左值也可能是右值。
    从下面的代码中可以看到,T&&表示的值类型不确定,可能是左值又可能是右值,这就是右值引用的一个特点。

    template<typename T>
    void f(T&& t){}
     
    f(10); //t是右值
     
    int x = 10;
    f(x); //t是左值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    c、T&& t在发生自动类型推断的时候,它是未定的引用类型(universal references),如果被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化。

    template<typename T>
    void f(T&& t){}
    f(10); //t是右值   //这里发生自动类型推断
     
    int x = 10;
    f(x); //t是左值    //发生自动类型推断
     
    template<typename T>
    class Test {
        Test(Test&& rhs); //这里不会发生类型推断,因为已经是确定的Tset &&类型的
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3、const左值引用

    从上面的例子可以看到,
    左值引用只能绑定左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。但是,const左值引用却是个奇葩,它可以算是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。

    const int & a = 1; //常量左值引用绑定 右值, 不会报错
     
    class A {
      public:
        int a;
    };
    A getTemp()
    {
        return A();
    }
    const A & a = getTemp();   //不会报错 而 A& a 会报错
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    总结如下(其中T是一个类型):
    左值引用, 使用 T&, 只能绑定左值
    右值引用, 使用 T&&, 只能绑定右值
    常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值
    已命名的右值引用,编译器会认为是个左值

    4、移动构造函数和移动赋值函数

    首先,移动构造函数是一个构造函数,他是用来构造一个对象的,和拷贝构造函数、构造函数等价。但与默认构造函数不同,编译器不提供默认移动构造函数。移动构造函数和移动赋值函数所执行的是同样的操作,只不过情况不一样,一种是直接构造一个对象,另一种是利用“=”运算符把一个对象赋值给另一个对象的时候。
    移动构造函数和移动赋值函数与拷贝构造函数所执行的作用的是一样的,都是通过一个对象去构造一个新对象。但有时候我们会遇到这样一种情况,我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a掌握的内存资源仍然存在(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a对象掌握的内存空间?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷。
    总的来说,就是类中有指针类型的成员变量时,当遇到对象构造对象时,需要使用拷贝构造函数中的深拷贝的方式把该指针成员赋值,这种深拷贝的方法会重新在堆上分配成员指针分配内存,当出现多次上述对象a初始化对象b之后,对象a不在存在的情况是,程序就会在堆上分配多次内存,大大降低程序运行效率,所以移动构造函数就将即将放弃的对象的内存空间直接给新对象使用,就能避免许多临时对象的创建,也能避免程序多次在堆上申请空间,从而大大的提高了执行效率。他的原型如下:

     A & operator=(A &&); //右值引用
      //正确的移动构造函数的写法
      A(A && a):p(a.p)
      {
            std::cout<<"move c"<<std::endl;
            //如果调用A的移动构造函数,则打印 "move c"
            a.p=nullptr;     //移动构造函数的灵魂在与使临时对象的指针为空,从而占据临时对象的内存空间,在临时对象析构的时候也不会造成指针悬挂。
      }
      //正确的移动赋值函数写法
      A & operator=(A && a){
        if(&a == this)
          return *this;
          p=a.p;
          a.p=nullptr;
          return *this;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    对于移动构造函数,需要以下几点需要注意:
    参数(右值)的符号必须是右值引用符号,即“&&”,因为右值是即将析构的对象,将它的内存空间变废为用,可以提高效率。
    参数(右值)不可以是常量,因为我们需要修改右值。
    参数(右值)的资源链接和标记必须修改。否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。
    移动构造函数的原理如下图:
    在这里插入图片描述

  • 相关阅读:
    数字藏品值得探究,依然是广阔的大海播
    OJ题目【栈和队列】
    chatgpt赋能python:Python请求头——让你的网络请求更有效率
    【Docker 基础教程】Docker命令运行原理及一些简单的操作命令
    GPIO端口之AFIO的完全映射与部分映射的理解
    【WebLogic】WebLogic 2023年7月补丁导致JVM崩溃的解决方案
    主动发现系统稳定性缺陷:混沌工程
    使用html画一个键盘
    2022.11.17 HDU-4911 Inversion
    Python 模拟超市收银抹零行为
  • 原文地址:https://blog.csdn.net/bin_bujiangjiu/article/details/128077540