一、为什么需要三/五法则

在C++中,类的特殊成员函数就像是一个人的身份证,它们决定了对象如何出生、如何复制、如何移动以及如何消亡。这些函数包括:

  • 构造函数(Constructor)
  • 析构函数(Destructor)
  • 拷贝构造函数(Copy Constructor)
  • 拷贝赋值运算符(Copy Assignment Operator)
  • 移动构造函数(Move Constructor)
  • 移动赋值运算符(Move Assignment Operator)

最初,C++只有“三法则”,即如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符,那么通常这三个都需要自定义。后来,C++11引入了移动语义,于是“五法则”应运而生,增加了移动构造函数和移动赋值运算符。

示例:一个简单的类,未遵循三/五法则

class SimpleString {
public:
    SimpleString(const char* str) {
        size = strlen(str);
        data = new char[size + 1];
        strcpy(data, str);
    }
    
    ~SimpleString() {
        delete[] data;
    }
    
private:
    char* data;
    size_t size;
};

这个类看起来没问题,但如果尝试拷贝它,就会导致双重释放(double-free)的问题,因为它没有定义拷贝构造函数和拷贝赋值运算符。

二、三法则详解

三法则的核心思想是:如果一个类需要自定义析构函数,那么它通常也需要自定义拷贝构造函数和拷贝赋值运算符

示例:遵循三法则的类

class RuleOfThreeString {
public:
    // 构造函数
    RuleOfThreeString(const char* str) {
        size = strlen(str);
        data = new char[size + 1];
        strcpy(data, str);
    }
    
    // 析构函数
    ~RuleOfThreeString() {
        delete[] data;
    }
    
    // 拷贝构造函数(深拷贝)
    RuleOfThreeString(const RuleOfThreeString& other) {
        size = other.size;
        data = new char[size + 1];
        strcpy(data, other.data);
    }
    
    // 拷贝赋值运算符(深拷贝)
    RuleOfThreeString& operator=(const RuleOfThreeString& other) {
        if (this != &other) {  // 防止自赋值
            delete[] data;      // 释放原有资源
            size = other.size;
            data = new char[size + 1];
            strcpy(data, other.data);
        }
        return *this;
    }
    
private:
    char* data;
    size_t size;
};

这个类现在可以安全地进行拷贝,因为每次拷贝都会创建新的内存空间,而不是共享同一块内存。

三、五法则详解

C++11引入了移动语义,允许我们更高效地管理资源。于是,五法则在原有的三法则基础上增加了移动构造函数和移动赋值运算符。

示例:遵循五法则的类

class RuleOfFiveString {
public:
    // 构造函数
    RuleOfFiveString(const char* str) {
        size = strlen(str);
        data = new char[size + 1];
        strcpy(data, str);
    }
    
    // 析构函数
    ~RuleOfFiveString() {
        delete[] data;
    }
    
    // 拷贝构造函数(深拷贝)
    RuleOfFiveString(const RuleOfFiveString& other) {
        size = other.size;
        data = new char[size + 1];
        strcpy(data, other.data);
    }
    
    // 拷贝赋值运算符(深拷贝)
    RuleOfFiveString& operator=(const RuleOfFiveString& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new char[size + 1];
            strcpy(data, other.data);
        }
        return *this;
    }
    
    // 移动构造函数(转移资源所有权)
    RuleOfFiveString(RuleOfFiveString&& other) noexcept {
        data = other.data;
        size = other.size;
        other.data = nullptr;  // 确保原对象析构时不会释放资源
        other.size = 0;
    }
    
    // 移动赋值运算符(转移资源所有权)
    RuleOfFiveString& operator=(RuleOfFiveString&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
    
private:
    char* data;
    size_t size;
};

移动语义的关键在于“偷”资源,而不是拷贝。这样可以避免不必要的内存分配和复制,提升性能。

四、应用场景与注意事项

应用场景

  1. 资源管理类:如智能指针、文件句柄、网络连接等,需要手动管理资源的类。
  2. 高性能场景:移动语义可以显著减少不必要的拷贝,提升性能。
  3. STL容器:标准库容器(如std::vector)会利用移动语义优化性能。

技术优缺点

  • 优点
    • 避免资源泄漏(如内存泄漏、文件未关闭)。
    • 提升性能(移动语义减少拷贝开销)。
  • 缺点
    • 代码复杂度增加,需要仔细实现每个特殊成员函数。
    • 容易出错(如忘记处理自赋值、未正确转移资源所有权)。

注意事项

  1. 移动语义必须标记为noexcept:标准库容器(如std::vector)在扩容时会优先使用移动构造函数(如果它是noexcept的)。
  2. 避免自赋值:在拷贝赋值运算符中,必须检查this != &other
  3. 移动后置空:移动操作后,原对象的资源指针应置空,避免析构时重复释放。

五、总结

三/五法则是C++资源管理的核心规则,理解并正确实现这些特殊成员函数是写出健壮、高效C++代码的关键。三法则确保资源的正确拷贝和释放,五法则在此基础上通过移动语义进一步提升性能。在实际开发中,应根据需求选择是否实现移动语义,并始终注意资源管理的正确性和异常安全性。