前言
自 C++11 以来,引入了移动构造函数和移动赋值函数,使得在构造对象的时候可以减少调用次数,以提高性能。
所以 C++的构造函数从 3 个变成了 5 个,分别是构造函数、拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值函数。
它们非常相似,放在一起容易搞混,于是总结一下,便有了此文,希望能够对大家有所帮助。
先来看下面的这个例子
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
#include<iostream>
using namespace std;
class Test{
public:
Test() {
cout << "construct" << endl;
}
Test(const Test& test) {
cout << "Copy Construct" << endl;
}
Test& operator=(const Test& test) {
cout << "Copy Assignment Operator" << endl;
return *this;
}
Test(Test&& test) {
cout << "Move construct" << endl;
}
Test& operator=(Test&& test) {
cout << "Move Assignment Operator" << endl;
return *this;
}
};
Test GenerateTest() {
return Test();
}
void PassByValue(Test test) {
}
Test&& MoveTest(Test& test) {
return move(test);
}
int main(int argc, char *argv[])
{
cout << "----------------1------------------" << endl;
Test t1;
cout << "----------------2------------------" << endl;
Test t2(t1);
cout << "----------------3------------------" << endl;
Test t3 = t1;
cout << "----------------4------------------" << endl;
Test t4 = Test();
cout << "----------------5------------------" << endl;
Test t5 = Test(t1);
cout << "----------------6------------------" << endl;
t5 = t2;
cout << "----------------7------------------" << endl;
Test t7 = move(t1);
cout << "----------------8------------------" << endl;
t7 = move(t2);
cout << "----------------9------------------" << endl;
Test t8 = GenerateTest();
cout << "----------------10------------------" << endl;
Test t9 = MoveTest(t1);
cout << "----------------11------------------" << endl;
Test&& t10 = MoveTest(t1);
cout << "----------------12------------------" << endl;
t9 = Test();
cout << "----------------13------------------" << endl;
PassByValue(t9);
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
|
----------------1------------------
construct
----------------2------------------
Copy Construct
----------------3------------------
Copy Construct
----------------4------------------
construct
----------------5------------------
Copy Construct
----------------6------------------
Copy Assignment Operator
----------------7------------------
Move construct
----------------8------------------
Move Assignment Operator
----------------9------------------
construct
----------------10------------------
Move construct
----------------11------------------
----------------12------------------
construct
Move Assignment Operator
----------------13------------------
Copy Construct
|
以下结果是关闭了编译器优化后的输出
在编译的时候加上 -fno-elide-constructors
来关闭优化
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
|
----------------1------------------
construct
----------------2------------------
Copy Construct
----------------3------------------
Copy Construct
----------------4------------------
construct
Move construct
----------------5------------------
Copy Construct
Move construct
----------------6------------------
Copy Assignment Operator
----------------7------------------
Move construct
----------------8------------------
Move Assignment Operator
----------------9------------------
construct
Move construct
Move construct
----------------10------------------
Move construct
----------------11------------------
----------------12------------------
construct
Move Assignment Operator
----------------13------------------
Copy Construct
|
分析
我们把开启优化和关闭优化放一起对比看
序号 |
开启优化 |
关闭优化 |
分析 |
1 |
construct |
construct |
调用构造方法,没什么好说的 |
2 |
Copy Construct |
Copy Construct |
使用 t1 初始化 t2,创建新对象,调用拷贝构造函数 |
3 |
Copy Construct |
Copy Construct |
这里就有点迷惑了,t3 是个新对象,所以也是调用拷贝构造函数,而不是拷贝赋值函数 |
4 |
construct |
construct |
开启优化后只需要一次构造 |
|
|
Move construct |
关闭优化还需要一次移动构造函数,因为 Test() 是个右值 |
5 |
Copy Construct |
Copy Construct |
开启优化后只需要一次拷贝构造 |
|
|
Move construct |
关闭优化和上一个一样,由于是右值还需要调用一次移动构造 |
6 |
Copy Assignment Operator |
Copy Assignment Operator |
由于 t5 已经存在了,所以调用拷贝赋值函数 |
7 |
Move construct |
Move construct |
把 t1 转换成右值,调用移动构造函数 |
8 |
Move Assignment Operator |
Move Assignment Operator |
由于 t7 已经存在,所以调用移动赋值函数 |
9 |
construct |
construct |
开启优化只需要一次构造 |
|
|
Move construct |
关闭优化,需要调用两次移动构造,一次是 return 返回给一个临时对象 |
|
|
Move construct |
另一次是把临时对象赋值给 t8,临时对象是右值,所以都调用移动构造函数 |
10 |
Move construct |
Move construct |
使用 move 把 test 转为右值,调用移动构造函数 |
11 |
|
|
只是进行右值绑定,没有创建对象或更新对象,不会调用构造函数 |
12 |
construct |
construct |
t9 是个已经存在的对象,而 Test()是个右值,所以先调用构造函数 |
|
Move Assignment Operator |
Move Assignment Operator |
然后调用移动构造函数 |
13 |
Copy Construct |
Copy Construct |
以传值的方式给形参,调用拷贝构造函数 |
总结
这些构造函数老是把人搞得晕头转向,过几天不看就会忘记搞混,我的记忆方法是抓住两个关键,一个是等号左边的对象,另一个是等号右边的对象。
等号左边的对象决定的是构造还是赋值,也就是如果左边的对象是不存在的就要构造一个新的对象,调用拷贝/移动 构造 。如果对象是存在就是要更新对象的值,也就是要调用拷贝/移动 赋值
等号右边的对象决定的是移动还是拷贝,也就是如果右边的对象是一个右值那么就会调用 移动 构造/赋值。 如果右边的对象不是一个右值,那么表示这个对象可能还需要使用,不能移走,所以要调用 拷贝 构造/赋值。
还有一种情况是没有 =
的,这个就很好判断了。无非也就构造和拷贝构造,如果只有一个对象,那就不存在拷贝不拷贝的,肯定是构造,对应上文中的 1
。如果有两个对象,那肯定是拷贝构造,不存在赋值的情况,对应上文中的 2
。
用一个表格来展示它们之间的关系
|
对象不存在 |
对象存在 |
左值 |
拷贝构造 |
拷贝赋值 |
右值 |
移动构造 |
移动赋值 |
参考