Menu
CoddyTech

三法则、五法则与零法则

本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 25 课,共 104 课。

当你的类管理动态内存之类的资源时,你已经学过需要自定义析构函数、复制构造函数和移动构造函数。但有一条指导原则可以帮助你决定要实现哪些特殊成员函数:三法则、五法则和零法则。

三法则指出:如果你定义了这三者中的任何一个,就应该定义全部三个:

  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值运算符

五法则将这一原则扩展到现代 C++,增加了移动操作:

  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值运算符
  • 移动构造函数
  • 移动赋值运算符
class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}
    
    ~Buffer() { delete[] data; }                          // 1. 析构函数
    Buffer(const Buffer& other);                          // 2. 拷贝构造函数
    Buffer& operator=(const Buffer& other);               // 3. 拷贝赋值
    Buffer(Buffer&& other) noexcept;                      // 4. 移动构造函数
    Buffer& operator=(Buffer&& other) noexcept;           // 5. 移动赋值
};

这两种赋值运算符遵循同一种模式:防止自赋值,释放旧资源,接管新资源,然后返回 *this:

Buffer& Buffer::operator=(const Buffer& other) {   // 拷贝赋值
    if (this != &other) {
        delete[] data;
        size = other.size;
        data = new int[size];
        std::copy(other.data, other.data + size, data);
    }
    return *this;
}

Buffer& Buffer::operator=(Buffer&& other) noexcept {   // 移动赋值
    if (this != &other) {
        delete[] data;
        data = other.data;
        size = other.size;
        other.data = nullptr;
        other.size = 0;
    }
    return *this;
}

零规则(Rule of Zero)是最简单的方法:如果你的类不直接管理资源,就不要定义这些函数。让编译器生成它们,或者使用能帮你处理资源的智能指针和标准容器。

class Player {
    std::string name;              // std::string 会自行管理内存
    std::vector<int> scores;       // std::vector 会自行管理资源
public:
    Player(std::string n) : name(n) {}
    // 不需要析构函数、复制函数或移动函数!
};

遵循这些规则可以避免双重删除、内存泄漏和悬空指针等错误。这些错误会在某些操作已定义、但其他操作缺失时出现。

challenge icon

挑战

简单

我们来构建一个遵循“五法则”的 TextBuffer 类:实现全部五个特殊成员函数,以妥善管理动态分配的字符数据。这将展示复制和移动操作如何协同工作,构建一个可靠的资源管理类。

你需要创建两个文件来组织代码:

  • TextBuffer.h:定义一个 TextBuffer 类,用动态分配的字符数组存储文本。这个类需要包含:
    • 私有成员:一个名为 data 的 char* 指针,用于存储文本内容;以及一个 size_t length,用于存储字符串长度(不包括空终止符)
    • 一个带参数的构造函数,接收一个 C 字符串(const char*),分配内存、复制内容,并打印 "TextBuffer created: <text>"
    • 一个析构函数,用于释放内存(如果指针不为空),并打印 "TextBuffer destroyed"
    • 一个复制构造函数,用于执行深拷贝,并打印 "TextBuffer copied"
    • 一个复制赋值运算符,用于处理自赋值、清理现有数据、执行深拷贝,并打印 "TextBuffer copy-assigned"。返回 *this
    • 一个移动构造函数(标记为 noexcept),用于转移所有权并打印 "TextBuffer moved"。让源对象保持有效的空状态
    • 一个移动赋值运算符(标记为 noexcept),用于处理自赋值、清理现有数据、转移所有权,并打印 "TextBuffer move-assigned"。返回 *this
    • 一个 getText() 方法,用于返回存储的文本(如果 data 为空,则返回空字符串 "")
    • 一个 getLength() 方法,用于返回长度
  • main.cpp:演示全部五个特殊成员函数的实际用法。从输入中读取一个字符串,然后:
    • 创建一个名为 original 的 TextBuffer,并将输入文本传给它
    • 使用 original 通过复制构造函数创建 copied
    • 创建 another,并将文本 "Temporary" 传给它
    • 使用复制赋值:another = original
    • 使用 std::move() 从 original 移动构造出 moved
    • 创建 target,并将文本 "Target" 传给它
    • 使用移动赋值:target = std::move(copied)
    • 打印 "--- Final State ---"
    • 为每个缓冲区打印 "original: <text> (length: <len>)":original、copied、moved、another、target

移动操作完成后,源对象(original 和 copied)应显示空文本,长度为 0;目标对象则应持有转移过来的数据。这展示了“五法则”的实际效果:五个函数协同工作,确保资源得到安全管理。

添加 <cstring>,以使用 strlen 和 strcpy 等字符串函数;添加 <utility>,以使用 std::move()。

亲手试试

#include <iostream>
#include <string>
#include <utility>
#include "TextBuffer.h"

using namespace std;

int main() {
    string input;
    getline(cin, input);

    // TODO: 用输入文本创建一个名为 'original' 的 TextBuffer

    // TODO: 使用复制构造函数,从 'original' 创建 'copied'

    // TODO: 用文本 "Temporary" 创建 'another'

    // TODO: 使用复制赋值:another = original

    // TODO: 使用 std::move(),通过移动构造从 'original' 创建 'moved'

    // TODO: 用文本 "Target" 创建 'target'

    // TODO: 使用移动赋值:target = std::move(copied)

    // TODO: 打印 "--- Final State ---"

    // TODO: 按以下格式打印每个缓冲区的状态:
    // "original: <text> (length: <len>)"
    // 依次打印:original、copied、moved、another、target

    return 0;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器