-
前置知识请参考:
- C++入门(一): 引用(左值引用)
- C++入门(二): 引用(右值引用)
移动构造函数
拷贝构造函数: C++ 98/03 标准定义,作用是用其它对象初始化一个同类的新对象。
需要注意:1.实现原理:为新对象复制一份和其它对象一模一样的数据。2.当类中拥有指针类型的成员变量时,拷贝构造函数中需要以深拷贝的方式复制该指针成员(否则一旦多个对象中的指针成员指向同一块堆空间,这些对象析构时就会对该空间释放多次,这是不允许的)
拷贝构造函数的缺点
然而旧标准中的这种方式,涉及临时变量的产生、销毁以及发生的拷贝操作,对于仅申请少量堆空间的临时对象来说,效率可以接受,但如果临时对象中的指针成员申请了大量的堆空间,那么深拷贝操作势必会影响对象初始化的执行效率
比如:
#include <iostream>
using namespace std;
class demo {
public:
demo() : num(new int(0)) { cout << "construct!" << endl; }
demo(const demo &d) : num(new int(*d.num + 10)) {
cout << "copy construct!" << endl;
}
~demo() { cout << "class destruct!" << *num << endl; }
private:
int *num;
};
demo get_demo() { return demo(); }
int main() {
demo a = demo();
return 0;
}
g++ demo.cpp -fno-elide-constructors
执行结果
#$ ./main
construct! <-- 执行 demo()
copy construct! <-- 执行拷贝构造函数
class destruct!0 <-- 销毁临时对象demo()
class destruct!10 <-- 销毁对象a
如果不加 -fno-elide-constructors 编译选项,编译器会对编译结果进行优化,执行结果为:
construct! <-- 执行 demo()
class destruct!0 <-- 销毁临时对象demo()
程序执行结果分析:
- 执行 demo();调用 demo 类的默认构造函数生成一个临时对象;
- 执行 a = demo() 语句,调用一次拷贝构造函数,将之前临时对象复制给 a(此行代码执行完毕,demo() 生成的临时对象会被析构);
- 程序执行结束前,调用 demo 类的析构函数销毁 a。
利用拷贝构造函数实现对 a 对象的初始化时,进行了 1 次深拷贝操作。
移动语义的引入
那么当类中包含指针类型的成员变量,使用其它对象来初始化同类对象时,怎样才能避免深拷贝导致的效率问题呢?
C++11 标准给出了解决方案:引入了右值引用的语法,借助它可以实现移动语义。而移动构造函数是实现移动语义的一种具体方式。
移动语义: 以移动而非深拷贝的方式初始化含有指针成员的类对象。移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
以demo()程序为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用 demo() 返回临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 NULL),这样就完成了 a.num 的初始化。
程序执行过程中产生的临时对象,往往只用于传递数据,并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
移动构造函数
#include <iostream>
using namespace std;
class demo {
public:
demo() : num(new int(0)) { cout << "construct!" << endl; }
demo(const demo &d) : num(new int(*d.num)) {
cout << "copy construct!" << endl;
}
demo(demo &&d) : num(d.num) { //添加移动构造函数
d.num = NULL;
*num = 10;
cout << "move construct!" << endl;
}
~demo() {
if (num == NULL) {
cout << "class destruct!, 销毁临时对象" << endl;
} else {
cout << "class destruct!, 销毁对象,num = " << *num << endl;
}
}
private:
int *num;
};
int main() {
demo a = demo();
return 0;
}
执行结果:
#$ ./main
construct!
move construct!
class destruct!, 销毁临时对象
class destruct!, 销毁对象,num = 10
说明:
- 添加移动构造函数:使用右值引用形式的参数,在构造函数内 num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num;
- 使用临时对象初始化 a 对象的过程中产生的拷贝过程使用移动构造函数完成
- 当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
- 实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数。当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数
使用左值,调用移动构造函数初始化对象
默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化
C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数
move()
本意为 “移动”,但该函数并不能移动任何数据,它的功能很简单,是将某个左值强制转化为右值。
move( arg )
其中,arg 表示指定的左值对象。该函数会返回 arg 对象的右值形式。
例子1
#include <iostream>
using namespace std;
class movedemo {
public:
movedemo() : num(new int(0)) { cout << "construct!" << endl; }
movedemo(const movedemo &d) : num(new int(*d.num)) { //拷贝构造函数
*num = 10;
cout << "copy construct!" << endl;
}
movedemo(movedemo &&d) : num(d.num) { //移动构造函数
d.num = NULL;
cout << "move construct!" << endl;
}
public: //这里应该是 private,使用 public 是为了更方便说明问题
int *num;
int b = 20;
};
int main() {
movedemo demo;
cout << "demo2:\n";
movedemo demo2 = demo;
cout << *demo.num << endl; //可以执行
cout << *demo2.num << endl; //可以执行
cout << "demo3:\n";
movedemo demo3 = std::move(demo);
//此时 demo.num = NULL,因此下面代码会报运行时错误
cout << *demo.num << endl;
return 0;
}
执行结果:
#$ ./main
construct!
demo2:
copy construct!
0
10
demo3:
move construct!
Segmentation fault (core dumped)
说明:
- demo 对象作为左值,直接用于初始化 demo2 对象,其底层调用的是拷贝构造函数
- 通过调用 move() 函数可以得到 demo 对象的右值形式,用其初始化 demo3 对象,编译器会优先调用移动构造函数
- 调用拷贝构造函数,并不影响 demo 对象,但如果调用移动构造函数,由于函数内部会重置 demo.num 指针的指向为 NULL,所以程序中第 30 行代码会导致程序运行时发生错误
例子2
#include <iostream>
using namespace std;
class first {
public:
first() : num(new int(0)) { cout << "construct!" << endl; }
first(first &&d) : num(d.num) { //移动构造函数
d.num = NULL;
cout << "first move construct!" << endl;
}
public: //这里应该是 private,使用 public 是为了更方便说明问题
int *num;
};
class second {
public:
second() : fir() {}
second(second &&sec)
: fir(move(sec.fir)) { //1.0 用 first 类的移动构造函数初始化 fir
cout << "second move construct" << endl;
}
public: //这里也应该是 private,使用 public 是为了更方便说明问题
first fir;
};
int main() {
second oth;
second oth2 = move(oth); //2.0
// cout << *oth.fir.num << endl; //程序报运行时错误
return 0;
}
执行结果
#$ ./main
construct!
first move construct!
second move construct
- 2.0:由于 oth 为左值,如果想调用移动构造函数为 oth2 初始化,需先利用 move() 函数生成一个 oth 的右值版本;
- 1.0:oth 对象内部还包含一个 first 类对象,对于 oth.fir 来说,其也是一个左值,所以在初始化 oth.fir 时,还需要再调用一次 move() 函数。