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 <iostream>
#include <string>

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;
}

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

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

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

移动构造实现

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

#include <iostream>
#include <string>

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;
}

结果:
在这里插入图片描述
解释:
  上面的程序中,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");

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

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;
}

在这里插入图片描述
对比移动构造实现一节的例程运行结果发现,非匿名对象 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

  • 156
    点赞
  • 500
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 22
    评论
移动构造函数C++11引入的一个新特性,通过移动构造函数,可以将一个对象的资源所有权转移到另一个对象,避免了不必要的资源拷贝,提高了程序的运行效率。 移动构造函数的定义如下: ```cpp class ClassA { public: ClassA(ClassA&& other) noexcept { // 移动构造函数的实现 } }; ``` 其中,`ClassA&&`表示移动构造函数的参数为右值引用。移动构造函数通常使用`noexcept`关键字进行修饰,表示该函数不会抛出异常。 移动构造函数的实现需要将另一个对象的资源转移到当前对象,并将另一个对象的资源置为无效。常见的实现方式是通过移动构造函数中的`std::move()`函数来完成。例如,对于一个包含动态分配内存的类: ```cpp class MyClass { public: MyClass(int n) : data(new int[n]), size(n) {} MyClass(MyClass&& other) noexcept : data(other.data), size(other.size) { other.data = nullptr; other.size = 0; } ~MyClass() { delete[] data; } private: int* data; int size; }; ``` 在移动构造函数中,将另一个对象的指针和大小赋值给当前对象,并将另一个对象的指针置为nullptr,大小置为0,表示另一个对象的资源已经被移动到了当前对象。 使用移动构造函数可以大大提高程序的性能,特别是在涉及到大量的资源拷贝时。移动构造函数可以通过`std::move()`函数进行调用,例如: ```cpp MyClass a(10); MyClass b(std::move(a)); // 调用移动构造函数 ``` 在这个例子中,对象a的资源被移动到了对象b中,对象a的资源已经无效了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吾爱技术圈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值