C++中的“三/五/零法则”是什么?(管理类资源拷贝的经验准则)

三/五/零法则是资源管理铁律:有手动资源获取(如new、fopen)就必须显式定义对应特殊成员函数;需释放资源(堆内存、文件描述符等)必须写析构函数;C++11后需补全移动构造与移动赋值以避免浅拷贝问题;零法则即用RAII类型(如vector、unique_ptr)委托管理,免写任何特殊函数。

三/五/零法则不是语法强制要求,而是编译器不会替你检查但出错后极难调试的资源管理铁律——只要你写了 newfopenpthread_mutex_init 这类手动资源获取操作,就必须按规则显式定义对应特殊成员函数。

什么时候必须写析构函数?

只要类里有需要「释放」的资源(堆内存、文件描述符、锁、GPU显存等),就必须写用户定义的析构函数。编译器生成的默认析构函数只调用成员的析构函数,不做 deleteclose()

常见错误现象:

class Buffer {
    char* data_;
public:
    Buffer(size_t n) { data_ = new char[n]; }
    // ❌ 没有 ~Buffer() → 内存泄漏
};
即使后续加了拷贝构造函数,漏掉析构函数仍会导致每次拷贝后原对象销毁时资源未释放。

为什么“三法则”变成“五法则”?

C++11 引入移动语义后,仅靠拷贝构造函数和拷贝赋值运算符不足以安全处理资源转移。若类支持移动(比如内部有 std::unique_ptr 或自己管理裸指针),还必须明确定义移动构造函数和移动赋值运算符,否则:

  • 编译器可能合成默认移动函数 → 对指针执行浅拷贝,导致双重 delete
  • 或干脆不合成(因存在用户定义的拷贝函数)→ 移动操作退化为拷贝,性能受损且逻辑错乱

实操建议:

class Buffer {
    char* data_;
    size_t size_;
public:
    Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr; // 防止被析构时二次释放
        other.size_ = 0;
    }
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }
    // ⚠️ 若只定义了上述两个,却没禁用拷贝(= delete),使用者仍可能意外触发拷贝,引发悬空指针
}

“零法则”怎么落地?

零法则本质是规避法则:把资源管理委托给标准库类型(如 std::vectorstd::unique_ptrstd::shared_ptrstd::string),让它们自动处理生命周期。此时你不需要写任何特殊成员函数——编译器合成的版本就完全正确。

使用场景:

class Buffer {
    std::vector data_; // ✅ 不需要自定义析构/拷贝/移动函数
public:
    Buffer(size_t n) : data_(n) {}
    // 所有资源管理由 vector 完成,安全、简洁、无泄漏风险
};
注意:

如果类里混用裸指针和其他 RAII 类型,零法则即失效,必须回归三/五法则。

容易被忽略的关键点

规则生效与否取决于「是否有资源需要管理」,而不是「有没有指针成员」。例如:

class Logger {
    FILE* file_; // ❌ 有裸 FILE* → 必须遵守五法则
    std::string name_; // ✅ string 自己管内存 → 不增加额外负担
};
同时,= default= delete 是显式声明的一部分——写 Buffer(const Buffer&) = default; 仍属于「用户声明了拷贝构造函数」,会抑制移动函数的自动生成,必须配套补全移动操作。