C++中的三/五/零法则解析

C++中的三/五/零法则解析

技术背景

在C++中,用户自定义类型的变量遵循值语义,这意味着对象在各种上下文中会被隐式复制。理解“复制对象”的实际含义对于编写正确、高效的代码至关重要。当类管理资源时,如动态分配的内存、文件句柄或互斥锁,复制对象的默认行为(成员逐个复制)可能会导致问题,如内存泄漏、悬空指针和未定义行为。因此,需要明确处理复制构造函数、复制赋值运算符和析构函数。

实现步骤

特殊成员函数的隐式定义

当没有显式声明复制构造函数、复制赋值运算符和析构函数时,编译器会隐式定义它们。例如,对于一个简单的person类:

1
2
3
4
5
6
7
8
9
10
11
12
#include <string>

class person
{
std::string name;
int age;

public:
person(const std::string& name, int age) : name(name), age(age)
{
}
};

编译器隐式定义的特殊成员函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 复制构造函数
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. 复制赋值运算符
person& operator=(const person& that)
{
name = that.name;
age = that.age;
return *this;
}

// 3. 析构函数
~person()
{
}

管理资源时的显式定义

当类管理资源时,如动态分配的内存,需要显式定义复制构造函数和复制赋值运算符,以实现深拷贝。例如:

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
#include <cstring>

class person
{
char* name;
int age;

public:
// 构造函数获取资源
person(const char* the_name, int the_age)
{
name = new char[strlen(the_name) + 1];
strcpy(name, the_name);
age = the_age;
}

// 析构函数释放资源
~person()
{
delete[] name;
}

// 复制构造函数
person(const person& that)
{
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}

// 复制赋值运算符
person& operator=(const person& that)
{
if (this != &that)
{
delete[] name;
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
return *this;
}
};

异常安全的复制赋值运算符

为了处理内存分配失败的异常情况,可以引入局部变量并重新排序语句:

1
2
3
4
5
6
7
8
9
person& operator=(const person& that)
{
char* local_name = new char[strlen(that.name) + 1];
strcpy(local_name, that.name);
delete[] name;
name = local_name;
age = that.age;
return *this;
}

禁止对象复制

对于一些不能或不应该复制的资源,如文件句柄或互斥锁,可以将复制构造函数和复制赋值运算符声明为private或使用C++11的delete关键字:

1
2
3
4
5
6
7
class person
{
// ...
private:
person(const person& that) = delete;
person& operator=(const person& that) = delete;
};

核心代码

以下是一个完整的示例,展示了三法则的应用:

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
#include <iostream>
#include <cstring>

class person
{
char* name;
int age;

public:
// 构造函数
person(const char* the_name, int the_age)
{
name = new char[strlen(the_name) + 1];
strcpy(name, the_name);
age = the_age;
}

// 析构函数
~person()
{
delete[] name;
}

// 复制构造函数
person(const person& that)
{
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}

// 复制赋值运算符
person& operator=(const person& that)
{
if (this != &that)
{
delete[] name;
char* local_name = new char[strlen(that.name) + 1];
strcpy(local_name, that.name);
name = local_name;
age = that.age;
}
return *this;
}

void print() const
{
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};

int main()
{
person a("John", 30);
person b(a);
person c("Jane", 25);
c = a;

a.print();
b.print();
c.print();

return 0;
}

最佳实践

  • 尽量使用标准库提供的类,如std::string,它们已经正确处理了资源管理,避免使用原始指针成员。
  • 当类管理资源时,遵循三法则,显式声明和定义复制构造函数、复制赋值运算符和析构函数。
  • 在实现复制赋值运算符时,考虑异常安全性,避免对象处于无效状态。
  • 如果不需要对象复制,明确禁止复制操作,防止潜在的错误。

常见问题

成员逐个复制的问题

默认的成员逐个复制可能会导致多个对象共享同一个资源,如指针,从而引发悬空指针和内存泄漏问题。解决方法是实现深拷贝,为每个对象分配独立的资源。

自赋值问题

在复制赋值运算符中,需要检查自赋值情况(x = x),避免删除正在使用的资源。

异常安全问题

内存分配可能会抛出异常,导致对象处于无效状态。可以使用局部变量和重新排序语句来提高异常安全性,或者使用复制交换惯用法。

C++11的影响

从C++11开始,引入了移动构造函数和移动赋值运算符,形成了五法则。在需要处理资源管理的类中,也应该考虑实现这两个函数,以提高性能。同时,三法则也可以扩展为零/三/五法则,即如果类不需要管理资源,可以不声明任何特殊成员函数。


C++中的三/五/零法则解析
https://119291.xyz/posts/2025-05-14.c-plus-plus-rule-of-three-five-zero-explanation/
作者
ww
发布于
2025年5月14日
许可协议