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
| person(const person& that) : name(that.name), age(that.age) { }
person& operator=(const person& that) { name = that.name; age = that.age; return *this; }
~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开始,引入了移动构造函数和移动赋值运算符,形成了五法则。在需要处理资源管理的类中,也应该考虑实现这两个函数,以提高性能。同时,三法则也可以扩展为零/三/五法则,即如果类不需要管理资源,可以不声明任何特殊成员函数。