什么是C++中的三法则?通过示例来解析
如果你曾经用C++编写过管理动态内存的类(例如包含new或malloc),那么你很可能遇到过三法则(Rule of Three)。这是该语言最基本的原则之一,可以防止内存泄漏、双重释放和未定义行为。让我们通过实际示例来剖析它,这样你不仅能记住规则,还能理解它为什么有效。
想象一下:你编写了一个在构造函数中分配内存的类。编译器会友好地为你生成拷贝构造函数和赋值运算符。但它是浅层(shallow copy)地完成的。然后问题就开始了。三法则指出:如果你的类显式定义了析构函数、拷贝构造函数或赋值运算符,那么很可能你需要定义所有这三个。为什么?因为这是一个信号,表明你的类管理着某种资源(内存、文件、网络连接),并且拷贝必须是深层的。
问题:浅拷贝(shallow copy)
让我们看一个典型的例子。类String,它在动态内存中存储C风格字符串:
class String {private: char* data; size_t size;public: // 构造函数 String(const char* str) { size = strlen(str); data = new char[size + 1]; strcpy(data, str); }
// 析构函数 ~String() { delete[] data; }
// 拷贝构造函数 — 由编译器生成(浅拷贝) // 赋值运算符 — 由编译器生成(浅拷贝)
void print() const { std::cout << data << std::endl; }};一切看起来都很好,直到我们尝试复制对象:
int main() { String s1("Hello"); String s2 = s1; // 调用拷贝构造函数(由编译器生成) s1.print(); s2.print(); // 程序会因double free错误而崩溃! return 0;}发生了什么?编译器生成了一个拷贝构造函数,它只是复制了指针data。现在s1.data和s2.data指向同一个内存块。当对象离开作用域时,它们的析构函数被调用。第一个析构函数释放内存,第二个析构函数试图再次释放它——双重释放。程序崩溃或行为不可预测。
解决方案:三法则的实际应用
为了解决这种情况,需要实现所有三个特殊的成员函数。这就是三法则。让我们编写类的正确版本:
class String {private: char* data; size_t size;public: // 构造函数 String(const char* str) { size = strlen(str); data = new char[size + 1]; strcpy(data, str); }
// 析构函数 ~String() { delete[] data; }
// 拷贝构造函数 — 深拷贝 String(const String& other) { size = other.size; data = new char[size + 1]; strcpy(data, other.data); }
// 赋值运算符 — 深拷贝 String& operator=(const String& other) { if (this == &other) return *this; // 防止自赋值
delete[] data; // 释放旧资源
size = other.size; data = new char[size + 1]; strcpy(data, other.data);
return *this; }
void print() const { std::cout << data << std::endl; }};现在拷贝工作正常:每个对象拥有自己的内存块。赋值也正常