三法则、五法则与零法则
本课属于 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) {}
// 不需要析构函数、复制函数或移动函数!
};遵循这些规则可以避免双重删除、内存泄漏和悬空指针等错误。这些错误会在某些操作已定义、但其他操作缺失时出现。
挑战
简单我们来构建一个遵循“五法则”的 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;
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: C++ 在线编译器