• C++ 移动构造函数详解


    引言

    移动构造函数是什么?
    移动构造是C++11标准中提供的一种新的构造方法。

    先举个生活例子,你有一本书,你不想看,但我很想看,那么我有哪些方法可以让我能看这本书?有两种做法,一种是你直接把书交给我,另一种是我去买一些稿纸来,然后照着你这本书一字一句抄到稿纸上。

    显然,第二种方法很浪费时间,但这正是有些深拷贝构造函数的做法,而移动构造函数便能像第一种做法一样省时,第一种做法在 C++ 中叫做完美转发。

    在C++11之前,如果要将源对象的状态转移到目标对象只能通过复制。
    而现在在某些情况下,我们没有必要复制对象,只需要移动它们。

    C++11引入移动语义:
    源对象资源的控制权全部交给目标对象。

    复制构造和移动构造对比

    复制构造是这样的:

    在对象被复制后临时对象和复制构造的对象各自占有不同的同样大小的堆内存,就是一个副本。
    从下图中可以看到,临时对象和新建对象a申请的堆内存同时存在。
    在这里插入图片描述

    移动构造是这样的:

    就是让这个临时对象它原本控制的内存的空间转移给构造出来的对象,这样就相当于把它移动过去了。
    从下图中可以看到,原本由临时对象申请的堆内存,由新建对象a接管,临时对象不再指向该堆内存。
    在这里插入图片描述

    改进的拷贝构造

    设想一种情况,我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么我们可以对指针进行浅复制,这样就避免了新的空间的分配,大大降低了构造的成本。

    但是我们知道,指针的浅层复制是非常危险的,浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了(不明白可参考深拷贝和浅拷贝问题详解)。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间(同时也是b->value指向的空间),注意,即使没有判断NULL的语句,直接delete null也是不会发生什么事的。

    #include 
    #include 
    
    using namespace std;
    
    class Integer {
    private:
        int* m_ptr;
    public:
    	Integer(int value)
            : m_ptr(new int(value)) {
            cout << "Call Integer(int value)有参" << endl;
        }
        //参数为常量左值引用的深拷贝构造函数,不改变 source.ptr_ 的值
        Integer(const Integer& source)
            : m_ptr(new int(*source.m_ptr)) {
            cout << "Call Integer(const Integer& source)拷贝" << endl;
        }
        //参数为左值引用的浅拷贝构造函数,转移堆内存资源所有权,改变 source.m_ptr的值 为nullptr
        Integer(Integer& source)
          : m_ptr(source.m_ptr) {
            source.m_ptr= nullptr;
            cout << "Call Integer(Integer& source)" << endl;
        }
        ~Integer() {
            cout << "Call ~Integer()析构" << endl;
            delete m_ptr;
        }
    
        int GetValue(void) { return *m_ptr; }
    };
    
    Integer getNum()
    {
        Integer a(100);
        return a;
    }
    int main(int argc, char const* argv[]) {
        Integer a(getNum()); 
        cout << "a=" << a.GetValue() << endl;
        cout << "-----------------" << endl;
        Integer temp(10000);
        Integer b(temp);
        cout << "b=" << b.GetValue() << endl;
        cout << "-----------------" << endl;
    
        return 0;
    }
    
    • 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

    结果:
    在这里插入图片描述

    在程序中,参数为常量左值引用的浅拷贝构造函数的做法相当于前面说的的移动构造。

    由运行结果可以看出,当同时存在参数类型为常量左值引用Integer(const Integer& source)和左值引用Integer(Integer& source)的拷贝构造函数时,getNum()返回的临时对象(右值)只能选择前者,非匿名对象 temp (左值)可以选择后者也可以选择前者,系统选择后者是因为该情况后者比前者好。为什么getNum()返回的临时对象(右值)只能选择前者?这是因为常量左值引用可以接受左值、右值、常量左值、常量右值,而左值引用只能接受左值。因此,对于右值,参数为任何类型左值引用的深拷贝构造函数Integer(Integer& source)无法实现完美转发。还有一种办法——右值引用。看下一节。

    移动构造实现

    移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。这意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只有当用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。移动构造函数的例子如下:

    #include 
    #include 
    
    using namespace std;
    
    class Integer {
    private:
        int* m_ptr;
    public:
    	Integer(int value)
            : m_ptr(new int(value)) {
            cout << "Call Integer(int value)有参" << endl;
        }
        
        Integer(const Integer& source)
            : m_ptr(new int(*source.m_ptr)) {
            cout << "Call Integer(const Integer& source)拷贝" << endl;
        }
    
    	Integer(Integer&& source)
    	  : m_ptr(source.m_ptr) {
    	    source.m_ptr= nullptr;
    	    cout << "Call Integer(Integer&& source)移动" << endl;
    	}
        
        ~Integer() {
            cout << "Call ~Integer()析构" << endl;
            delete m_ptr;
        }
    
        int GetValue(void) { return *m_ptr; }
    };
    Integer getNum()
    {
        Integer a(100);
        return a;
    }
    int main(int argc, char const* argv[]) {
        Integer a(getNum()); 
        cout << "a=" << a.GetValue() << endl;
        cout << "-----------------" << endl;
        Integer temp(10000);
        Integer b(temp);
        cout << "b=" << b.GetValue() << endl;
        cout << "-----------------" << endl;
    
        return 0;
    }
    
    • 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

    结果:
    在这里插入图片描述
    解释:
      上面的程序中,getNum()函数中需要返回的是一个局部变量,因此它此时就是一个临时变量,因为在函数结束后它就消亡了,对应的其动态内存也会被析构掉,所以系统在执行return函数之前,需要再生成一个临时对象将a中的数据内容返回到被调的主函数中,此处自然就有两种解决方法:1、调用复制构造函数进行备份;2、使用移动构造函数把即将消亡的且仍需要用到的这部分内存的所有权进行转移,手动延长它的生命周期。

    显然,前者需要深拷贝操作依次复制全部数据,而后者只需要“变更所有权”即可。

    上面的运行结果中第一次析构就是return a; 这个临时对象在转移完内存所用权之后就析构了。

    此处更需要说明的是:遇到这种情况时,编译器会很智能帮你选择类内合适的构造函数去执行,如果没有移动构造函数,它只能默认的选择复制构造函数,而同时存在移动构造函数和复制构造函数则自然会优先选择移动构造函数。
    比如上述程序如果只注释掉移动构造函数而其他不变,运行后结果如下:
    原来调用了移动构造函数的地方变成了拷贝构造。
    在这里插入图片描述

    注:移动构造的&&是右值引用,而getNum()函数返回的临时变量是右值

    【思考】
    1、移动构造函数的第一个参数必须是自身类型的右值引用(不需要const,为啥?右值使用const没有意义),若存在额外的参数,任何额外的参数都必须有默认实参

    2、看移动构造函数体里面,我们发现参数指针所指向的对象转给了当前正在被构造的指针后,接着就把参数里面的指针置为空指针(source.m_ptr= nullptr;),对象里面的指针置为空指针后,将来析构函数析构该指针(delete m_ptr;)时,是delete一个空指针,不发生任何事情,这就是一个移动构造函数。

    3、有个疑问希望有识之士解答:匿名变量也是右值,为什么上面的程序换成 Integer a(Integer(100)); 后运行却不会调用移动构造函数?
    我怀疑是VS有什么优化机制,但我看了优化是关了的。

    移动构造优点

    移动构造函数是c++11的新特性,移动构造函数传入的参数是一个右值 用&&标出。

    首先讲讲拷贝构造函数:拷贝构造函数是先将传入的参数对象进行一次深拷贝,再传给新对象。这就会有一次拷贝对象的开销,拷贝的内存越大越耗费时间,并且进行了深拷贝,就需要给对象分配地址空间。而移动构造函数就是为了解决这个拷贝开销而产生的。

    移动构造函数首先将传递参数的内存地址空间接管,然后将内部所有指针设置为nullptr,并且在原地址上进行新对象的构造,最后调用原对象的的析构函数,这样做既不会产生额外的拷贝开销,也不会给新对象分配内存空间。即提高程序的执行效率,节省内存消耗。

    左值、右值、左值引用、右值引用

    何为左值?能用取址符号 & 取出地址的皆为左值,剩下的都是右值。

    而且,匿名变量一律属于右值。

    int i = 1; // i 是左值,1 是右值
    
    int GetZero {
        int zero = 0return zero;
    }
    //j 是左值,GetZero() 是右值,因为返回值存在于寄存器中
    int j = GetZero();
    
    //s 是左值,string("no name") 是匿名变量,是右值
    string s = string("no name");
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这一块的东西不在这里深入了,要专门开一篇。

    std::move

    std::move() 能把左值强制转换为右值。

    移动构造实现一节的例程我们把语句 Integer b(temp); 改为 Integer b(std::move(temp)); 后,运行结果如下。

    int main(int argc, char const* argv[]) {
        Integer a(getNum()); 
        cout << "a=" << a.GetValue() << endl;
        cout << "-----------------" << endl;
        Integer temp(10000);
        Integer b(std::move(temp));
        cout << "b=" << b.GetValue() << endl;
        cout << "-----------------" << endl;
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述
    对比移动构造实现一节的例程运行结果发现,非匿名对象 temp (左值)在加了std::move之后强制转为右值也能做 只接收右值的移动拷贝函数 的参数了,因此编译器在这里调用了移动拷贝函数。

    从“b=10000”的上一行可以看出,std::move() 确实把左值 temp 转换为右值。

    总结

    【重点】
    1、移动构造是C++11标准中提供的一种新的构造方法
    2、移动构造接管源对象,既不会产生额外的拷贝开销,也不会给新对象分配内存空间。提高程序的执行效率,节省内存消耗。
    3、移动构造函数的第一个参数必须是自身类型的右值引用

    我们回过头再看开始举的例子,
    你有一本书,(对应一个对象A)
    你不想看,(这个对象A不需要再使用了)
    但我很想看,(需要新建一个一样的对象B)
    那么我有哪些方法可以让我能看这本书?
    有两种做法,(两种做法其实对应的就是拷贝构造函数和移动构造函数)
    一种是你直接把书交给我,(对应移动构造函数,资源发生了转移)
    另一种是我去买一些稿纸来,(买一些稿纸意味着重新申请一块资源)
    然后照着你这本书一字一句抄到稿纸上。(把原来的对象A拷贝到对象B,这时存在两个内容一样的对象,但对象A用不到了就浪费了)

    这个例子用于体现拷贝构造函数和移动构造函数的不同点非常契合。

    【参考鸣谢】
    https://blog.csdn.net/zyq11223/article/details/48766515
    https://www.cnblogs.com/liwe1004/p/14806288.html
    http://t.zoukankan.com/Joezzz-p-9707250.html
    https://blog.51cto.com/u_15006953/2552184
    https://blog.csdn.net/weixin_36725931/article/details/85218924 【优质】
    https://blog.csdn.net/TABE_/article/details/122203569

  • 相关阅读:
    常见排序算法
    【JAVA-Day04】Java关键字和示例:深入了解常用关键字的用法
    甲方安全建设思考-3
    HyperLynx(十五)多板仿真
    python调用git出错:ImportError: Failed to initialize: Bad git executable.
    docker 安装 mysql (单体架构)
    ThreadLocal线程变量
    高校教务系统登录页面JS分析——重庆交通大学
    只需 6 步,你就可以搭建一个云原生操作系统原型
    FEDformer
  • 原文地址:https://blog.csdn.net/weixin_44788542/article/details/126284429