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

三/五/零法则不是语法强制要求,而是编译器不会替你检查但出错后极难调试的资源管理铁律——只要你写了 new、fopen、pthread_mutex_init 这类手动资源获取操作,就必须按规则显式定义对应特殊成员函数。
什么时候必须写析构函数?
只要类里有需要「释放」的资源(堆内存、文件描述符、锁、GPU显存等),就必须写用户定义的析构函数。编译器生成的默认析构函数只调用成员的析构函数,不做 delete 或 close()。
常见错误现象:
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::vector、std::unique_ptr、std::shared_ptr、std::string),让它们自动处理生命周期。此时你不需要写任何特殊成员函数——编译器合成的版本就完全正确。
立即学习“C++免费学习笔记(深入)”;
使用场景:
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; 仍属于「用户声明了拷贝构造函数」,会抑制移动函数的自动生成,必须配套补全移动操作。










