C++零之法制和三五法则

C++零之法制和三五法则

三之法则 (The Rule of Three) - C++98时代

如果一个类需要自定义以下三者中的任意一个 ,那么它几乎肯定需要全部三个

  1. 析构函数
  2. 拷贝构造函数
  3. 拷贝赋值运算符

当一个类直接管理裸资源 时(例如,通过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

  1. 析构函数
  2. 拷贝构造函数
  3. 拷贝赋值运算符
  4. 移动构造函数 (Move Constructor)
  5. 移动赋值运算符 (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)来持有资源;这样,一个特殊成员函数都不需要写

核心思想:将资源管理的工作委托给标准库。编译器自动生成的默认特殊成员函数会完美地完成工作,因为它们会自动调用其成员变量对应的函数

  1. 默认析构函数 会自动调用std::string的析构函数(它会释放内存)
  2. 默认拷贝构造函数 会自动调用std::string的拷贝构造函数(它会执行深拷贝)
  3. 默认移动构造函数 会自动调用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

  1. 创建一个专门的RAII包装类(遵循五之法则)

    c++ 复制代码
    struct C_ApiResource { void* handle; char* buffer; };
    C_ApiResource* create_resource();
    void destroy_resource(C_ApiResource* res);
  2. 需要创建一个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;
        }
    
        // ... 其他操作资源的接口 ...
    };
  3. 在高层级类中使用这个包装类(享受零之法则)

    c++ 复制代码
    class HighLevelProcessor {
    private:
        // 使用我们自己封装好的RAII包装类
        ApiResourceHandle handle_; 
    
    public:
        HighLevelProcessor() {} // 默认构造函数即可
    
        // HighLevelProcessor现在可以遵循"零之法则"了!
        // 编译器为它生成的特殊成员函数会自动调用ApiResourceHandle对应的版本。
    };

自定义数据组合:用标准库组件(std::string, std::vector, std::unique_ptr等)构成,那么直接组合它们,并让所有类都遵循"零之法则"

必须与C库等底层接口打交道,管理裸资源组合:那么就创建一个专门的底层RAII包装类,让这个类遵循"五之法则" 。然后,程序的其余部分就可以使用这个包装类,并继续享受"零之法则"带来的简洁与安全

相关推荐
李日灐33 分钟前
手搓简单 string 库:了解C++ 字符串底层
开发语言·c++
Elias不吃糖42 分钟前
LeetCode每日一练(3)
c++·算法·leetcode
小龙报1 小时前
《算法通关指南数据结构和算法篇(2)--- 链表专题》
c语言·数据结构·c++·算法·链表·学习方法·visual studio
mjhcsp1 小时前
C++ 动态规划(Dynamic Programming)详解:从理论到实战
c++·动态规划·1024程序员节
随意起个昵称1 小时前
【二分】洛谷P2920,P2985做题小记
c++·算法
望眼欲穿的程序猿2 小时前
Win系统Vscode+CoNan+Cmake实现调试与构建
c语言·c++·后端
lzh200409192 小时前
【C++STL】List详解
开发语言·c++
luoyayun3612 小时前
Qt/C++ 线程池TaskPool与 Worker 框架实践
c++·qt·线程池·taskpool
喵个咪3 小时前
ASIO 定时器完全指南:类型解析、API 用法与实战示例
c++·后端
phdsky3 小时前
【设计模式】抽象工厂模式
c++·设计模式·抽象工厂模式