三五法则

本文讲解c++中的三五法则。

Posted by CongYu on August 2, 2020

三五法则

三五法则(Rule of Three/Five/Zero)是C++中关于资源管理的重要设计准则,它指导我们如何正确处理类的特殊成员函数。

三法则 (Rule of Three)

如果一个类需要自定义以下三个特殊成员函数中的任何一个,那么它通常需要全部三个:

  1. 析构函数 (~ClassName())
  2. 拷贝构造函数 (ClassName(const ClassName& other))
  3. 拷贝赋值运算符 (ClassName& operator=(const ClassName& other))

原因:

当类管理资源(如动态内存、文件句柄、网络连接等)时,默认的浅拷贝行为会导致问题:

  • 双重释放(double free)
  • 悬挂指针(dangling pointer)
  • 资源泄漏

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class MyString {
private:
    char* data;
    size_t length;

public:
    // 构造函数
    MyString(const char* str) {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }
    
    // 1. 析构函数
    ~MyString() {
        delete[] data;
    }
    
    // 2. 拷贝构造函数
    MyString(const MyString& other) {
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
    }
    
    // 3. 拷贝赋值运算符
    MyString& operator=(const MyString& other) {
        if (this != &other) {  // 自赋值检查
            delete[] data;     // 释放原有资源
            
            length = other.length;
            data = new char[length + 1];
            strcpy(data, other.data);
        }
        return *this;
    }
};

五法则 (Rule of Five) - C++11+

C++11引入移动语义后,扩展为五法则,增加了:

  1. 移动构造函数 (ClassName(ClassName&& other))
  2. 移动赋值运算符 (ClassName& operator=(ClassName&& other))

移动语义的优势

  • 避免不必要的深拷贝
  • 提升性能,特别是对于临时对象

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyString {
    // ... 前面的代码 ...
    
    // 4. 移动构造函数
    MyString(MyString&& other) noexcept 
        : data(other.data), length(other.length) {
        other.data = nullptr;  // 转移资源所有权
        other.length = 0;
    }
    
    // 5. 移动赋值运算符
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] data;  // 释放原有资源
            
            data = other.data;
            length = other.length;
            
            other.data = nullptr;  // 转移资源所有权
            other.length = 0;
        }
        return *this;
    }
};

零法则 (Rule of Zero)

最佳实践:如无必要,尽可能避免自定义这些特殊成员函数,让编译器自动生成,或使用RAII技术。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <memory>
#include <string>

class BetterString {
private:
    std::unique_ptr<char[]> data;  // 自动管理内存
    size_t length;

public:
    BetterString(const char* str) 
        : length(strlen(str)), data(std::make_unique<char[]>(length + 1)) {
        strcpy(data.get(), str);
    }
    
    // 不需要自定义析构函数、拷贝/移动函数
    // 编译器会自动生成正确的实现
};

// 或者更简单的方案
class SimpleString {
private:
    std::string data;  // std::string已经正确实现了资源管理

public:
    SimpleString(const char* str) : data(str) {}
    // 完全不需要自定义特殊成员函数
};

总结

情况 描述 建议
零法则 不管理资源 使用标准库容器/智能指针
三法则 管理资源(C++98/03) 实现析构、拷贝构造、拷贝赋值
五法则 管理资源(C++11+) 实现析构、拷贝构造、拷贝赋值,并额外实现移动构造、移动赋值
  1. 优先使用零法则 - 使用RAII和标准库
  2. 避免裸指针 - 使用智能指针管理资源
  3. 移动语义优化 - 为性能关键的类实现移动操作
  4. 异常安全 - 确保在异常情况下资源正确释放