前言

自 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

用一个表格来展示它们之间的关系

对象不存在 对象存在
左值 拷贝构造 拷贝赋值
右值 移动构造 移动赋值

参考