C++零之法制和三五法则
三之法则 (The Rule of Three) - C++98时代
如果一个类需要自定义以下三者中的任意一个 ,那么它几乎肯定需要全部三个
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
当一个类直接管理裸资源 时(例如,通过new
/delete
管理裸指针,或通过fopen
/fclose
管理文件句柄)
c++
class MyString_RuleOf3 {
private:
char* data_;
public:
MyString_RuleOf3(const char* s = "") {
data_ = new char[strlen(s) + 1];
strcpy(data_, s);
}
// 1. 析构函数 (因为有new,所以必须有delete)
~MyString_RuleOf3() {
delete[] data_;
}
// 2. 拷贝构造函数 (必须深拷贝,否则两个对象指向同一内存)
MyString_RuleOf3(const MyString_RuleOf3& other) {
data_ = new char[strlen(other.data_) + 1];
strcpy(data_, other.data_);
}
// 3. 拷贝赋值运算符 (同上,需要深拷贝并处理自我赋值)
MyString_RuleOf3& operator=(const MyString_RuleOf3& other) {
if (this != &other) {
delete[] data_; // 释放旧资源
data_ = new char[strlen(other.data_) + 1]; // 分配新资源
strcpy(data_, other.data_); // 拷贝内容
}
return *this;
}
};
五之法则 (The Rule of Five) - C++11时代
随着C++11引入移动语义,三之法则扩展为五之法则
法则内容 :如果一个类定义了五个特殊成员函数中的任何一个,它应该把它们全部定义出来(或者将不需要的删除 = delete
)
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数 (Move Constructor)
- 移动赋值运算符 (Move Assignment Operator)
同三之法则,但通过添加移动操作来提升性能,避免对临时对象进行不必要的深拷贝
c++
// 续上例...
// 4. 移动构造函数 (窃取资源,而非拷贝)
MyString_RuleOf5(MyString_RuleOf5&& other) noexcept {
data_ = other.data_; // 直接拿走指针
other.data_ = nullptr; // 将源对象置于安全可析构的状态
}
// 5. 移动赋值运算符
MyString_RuleOf5& operator=(MyString_RuleOf5&& other) noexcept {
if (this != &other) {
delete[] data_; // 释放自己的旧资源
data_ = other.data_; // 窃取源的资源
other.data_ = nullptr;
}
return *this;
}
零之法则 (The Rule of Zero) - 现代C++的理想
法则内容 :类应该只负责业务逻辑,而不应直接管理任何资源。 相反,它应该使用那些本身就遵循"五之法则"的资源管理类 (如std::string
, std::vector
, std::unique_ptr
)来持有资源;这样,一个特殊成员函数都不需要写
核心思想:将资源管理的工作委托给标准库。编译器自动生成的默认特殊成员函数会完美地完成工作,因为它们会自动调用其成员变量对应的函数
- 默认析构函数 会自动调用
std::string
的析构函数(它会释放内存) - 默认拷贝构造函数 会自动调用
std::string
的拷贝构造函数(它会执行深拷贝) - 默认移动构造函数 会自动调用
std::string
的移动构造函数(它会窃取资源)
c++
#include <string>
#include <utility>
class MyString_RuleOf0 {
private:
std::string data_; // 直接使用std::string管理资源
public:
// 构造函数只关心业务逻辑
MyString_RuleOf0(const std::string& s) : data_(s) {}
// 析构函数?不需要写,编译器默认生成。
// 拷贝构造?不需要写,编译器默认生成。
// 拷贝赋值?不需要写,编译器默认生成。
// 移动构造?不需要写,编译器默认生成。
// 移动赋值?不需要写,编译器默认生成。
};
零之法则 是现代C++的首选设计目标。它通过组合和委托,从根本上消除了手动管理资源的需要
数据组合必须直接管理裸资源
通常发生于和C语言库或底层操作系统API交互时。
例如,一个API返回一个需要手动释放的句柄(handle
)和一块需要手动free
的内存(buffer
)
-
创建一个专门的RAII包装类(遵循五之法则)
c++struct C_ApiResource { void* handle; char* buffer; }; C_ApiResource* create_resource(); void destroy_resource(C_ApiResource* res);
-
需要创建一个C++包装类来管理
C_ApiResource
c++// 这个类是底层RAII包装器,它必须遵循"五之法则" class ApiResourceHandle { private: C_ApiResource* resource_ = nullptr; // 直接管理裸资源 public: // 构造函数获取资源 ApiResourceHandle() : resource_(create_resource()) {} // 1. 析构函数:释放资源 ~ApiResourceHandle() { if (resource_) { destroy_resource(resource_); } } // 2. 删除拷贝操作,确保资源所有权唯一 ApiResourceHandle(const ApiResourceHandle&) = delete; ApiResourceHandle& operator=(const ApiResourceHandle&) = delete; // 4. 移动构造函数:转移所有权 ApiResourceHandle(ApiResourceHandle&& other) noexcept : resource_(other.resource_) { other.resource_ = nullptr; } // 5. 移动赋值运算符:转移所有权 ApiResourceHandle& operator=(ApiResourceHandle&& other) noexcept { if (this != &other) { if (resource_) { destroy_resource(resource_); } resource_ = other.resource_; other.resource_ = nullptr; } return *this; } // ... 其他操作资源的接口 ... };
-
在高层级类中使用这个包装类(享受零之法则)
c++class HighLevelProcessor { private: // 使用我们自己封装好的RAII包装类 ApiResourceHandle handle_; public: HighLevelProcessor() {} // 默认构造函数即可 // HighLevelProcessor现在可以遵循"零之法则"了! // 编译器为它生成的特殊成员函数会自动调用ApiResourceHandle对应的版本。 };
自定义数据组合:用标准库组件(std::string
, std::vector
, std::unique_ptr
等)构成,那么直接组合它们,并让所有类都遵循"零之法则"
必须与C库等底层接口打交道,管理裸资源组合:那么就创建一个专门的底层RAII包装类,让这个类遵循"五之法则" 。然后,程序的其余部分就可以使用这个包装类,并继续享受"零之法则"带来的简洁与安全