本节主要讲解移动语义的含义以及实现它的方式。
C++ 98/03 标准中,想用其他对象初始化一个同类的新对象,只能借助拷贝构造函数。(原理是为新对象复制一份和其他对象一模一样的数据)
#include<iostream>
using namespace std;
class Demo{
public:
Demo():num(new int(10))
{
cout<<"construct"<<endl;
}
//拷贝构造函数
Demo(const Demo &d):num(new int(*d.num))
{
cout<<"copy construct"<<endl;
}
~Demo()
{
cout<<"class destruct"<<endl;
}
private:
int *num;
};
Demo get_demo()
{
return Demo();
}
int main()
{
Demo a = get_demo();
return 0;
}
/*
construct! <-- 执行 Demo()
copy construct! <-- 执行 return Demo()
class destruct! <-- 销毁 Demo() 产生的匿名对象
copy construct! <-- 执行 a = get_demo()
class destruct! <-- 销毁 get_demo() 返回的临时对象
class destruct! <-- 销毁 a
*/
- 利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次拷贝(而且是深拷贝)操作。
- 对少量堆空间的临时对象来说可以接受,但如果临时对象中的指针成员申请了大量的堆空间,那么 2 次深拷贝操作势必会影响 a 对象初始化的执行效率。
问题:当类中包含指针类型的成员变量。使用它来初始化同类对象时,怎样才能避免深拷贝导致的效率问题呢?
C++11 标准引入了解决方案,该标准中引入了右值引用的语法,借助它可以实现移动语义。
C++移动构造函数(移动语义的具体实现)
移动语义:以移动而非深拷贝的方式初始化含有指针成员的类对象。即将临时对象拥有的内存资源“据为己有”。
移动构造函数操作步骤:
- ① 将临时对象的指针成员浅拷贝。
- ② 将临时对象的指针成员置为nullptr。
#include<iostream>
using namespace std;
class Demo{
public:
Demo():num(new int(10))
{
cout<<"construct"<<endl;
}
//拷贝构造函数
Demo(const Demo &d):num(new int(*d.num))
{
cout<<"copy construct"<<endl;
}
//移动构造函数
Demo(Demo &&d):num(d.num) //临时对象指针成员浅拷贝
{
d.num = nullptr; //临时对象的指针成员置为nullptr
cout<<"move construct"<<endl;
}
~Demo()
{
cout<<"class destruct"<<endl;
}
private:
int *num;
};
Demo get_demo()
{
return Demo(); // Demo()调用construct, 返回值为临时对象调用move construct, Demo()调用class destruct
}
int main()
{
Demo a = get_demo(); // 给a初始化调用move construct,销毁get_demo()临时对象调用class destruct,a超过作用域调用class destruct
return 0;
}
/*
construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!
*/
- 当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。
总结:
- 当用户利用右值初始化类对象时,会调用移动构造函数;
- 使用左值(非右值)初始化类对象时,会调用拷贝构造函数。
如果想左值对象初始化同类对象时也通过 移动构造函数 完成呢?
- 可以使用C++11的
std::move()
函数,它可以将左值强制转换成对应的右值,由此可以使用移动构造函数。