C++ 构造函数漫谈(二)

三五零法则

我们知道,编译器会为类自动生成几个特别的成员函数:构造函数、复制构造函数、复制赋值运算符、析构函数。后三者比较特殊,我们在下面会频繁提到。

三法则

若一个类需要用户显式定义 析构函数、复制构造函数、复制赋值运算符 中的一个,那么这三个函数都需要显式定义 。如果用户显式定义了其中一个,另外两个还是会被编译器隐式定义,这种混杂的情况容易生产无法预期的错误。
如果一个类中有非基本数据类型或者非类类型的成员(如指针、文件描述符等),则这一法则表现的更为明显:隐式析构函数无法对这种成员进行有效的释放,隐式复制构造函数和隐式复制赋值运算符无法进行深拷贝。

五法则

C++11有的移动语义,所以三法则又扩展出了五法则。因为用户定义析构函数、复制构造函数或复制赋值运算符的存在阻止移动构造函数和移动赋值运算符的隐式定义,所以任何想要移动语义的类必须声明全部五个特殊成员函数:

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
class rule_of_five
{
char* cstring; // 以裸指针为动态分配内存的句柄
public:
rule_of_five(const char* s = "")
: cstring(nullptr)
{
if (s) {
std::size_t n = std::strlen(s) + 1;
cstring = new char[n]; // 分配
std::memcpy(cstring, s, n); // 填充
}
}

~rule_of_five()
{
delete[] cstring; // 解分配
}

rule_of_five(const rule_of_five& other) // 复制构造函数
: rule_of_five(other.cstring)
{}

rule_of_five(rule_of_five&& other) noexcept // 移动构造函数
: cstring(std::exchange(other.cstring, nullptr))
{}

rule_of_five& operator=(const rule_of_five& other) // 复制赋值
{
return *this = rule_of_five(other);
}

rule_of_five& operator=(rule_of_five&& other) noexcept // 移动赋值
{
std::swap(cstring, other.cstring);
return *this;
}

// 另一种方法是用以下函数替代两个赋值运算符
// rule_of_five& operator=(rule_of_five other) noexcept
// {
// std::swap(cstring, other.cstring);
// return *this;
// }
};

与三之法则不同的是,不提供移动构造函数和移动赋值运算符通常不是错误,但会导致失去优化机会。

零法则

如前所述,如果我们不需要显式定义三个特殊成员函数的中任何一个,那么其它两个也不要显式地定义。