如何在含继承与虚函数的类中应用C++五大法则(Rule of Five)
首先得明确:五大法则的核心是当类持有动态资源(比如你这里的C风格数组、FILE指针、数据库句柄)时,必须手动控制资源的拷贝、移动和销毁逻辑,以此避免内存泄漏、二次释放、浅拷贝野指针这类经典问题。既然你的类没有继承和多态,我们不用操心虚析构的事,只聚焦资源管理本身就行。
先假设你的基础类是这样的(典型的C数组管理场景):
class DynamicArray { private: int* m_data; // C风格动态数组指针 size_t m_size; // 数组元素个数 public: // 普通构造函数:分配C数组 explicit DynamicArray(size_t size) : m_size(size), m_data(new int[size]) {} // 这里原来没有五大法则的函数,会导致浅拷贝问题 };
下面一步步实现五大法则的五个核心函数:
1. 析构函数:释放资源
这是最基础的一步,必须手动实现来释放动态分配的C数组,否则会造成内存泄漏:
~DynamicArray() { delete[] m_data; // 注意用delete[],对应new[]分配的数组 m_data = nullptr; // 可选但推荐:置空避免野指针问题 }
2. 拷贝构造函数:深拷贝资源
如果不手动实现,编译器会生成默认的浅拷贝构造函数,导致两个DynamicArray对象共享同一个m_data指针——当其中一个对象析构时,数组被释放,另一个对象再访问就会触发未定义行为。
我们需要实现深拷贝:创建一个新的数组,把原对象的元素逐个复制过来:
// 拷贝构造:用const引用接收原对象 DynamicArray(const DynamicArray& other) : m_size(other.m_size), m_data(new int[other.m_size]) { // 复制数组元素 for (size_t i = 0; i < m_size; ++i) { m_data[i] = other.m_data[i]; } }
3. 拷贝赋值运算符:安全的深拷贝赋值
默认的拷贝赋值同样会做浅拷贝,存在共享资源的风险。这里推荐用swap idiom实现,既保证异常安全(内存分配失败时原对象状态不会被破坏),又代码简洁:
// 拷贝赋值:返回引用支持链式赋值 DynamicArray& operator=(const DynamicArray& other) { // 先创建临时对象完成深拷贝 DynamicArray temp(other); // 交换临时对象和当前对象的资源 swap(*this, temp); // 临时对象析构时会自动释放当前对象原来的旧资源 return *this; } // 辅助swap函数(通常作为类的友元) friend void swap(DynamicArray& first, DynamicArray& second) noexcept { using std::swap; swap(first.m_data, second.m_data); swap(first.m_size, second.m_size); }
4. 移动构造函数:转移资源所有权
当对象是右值(比如临时对象、std::move后的对象)时,我们不需要做耗时的深拷贝,直接把资源从原对象转移到新对象即可,效率极高。
移动构造的核心是:接管原对象的资源,然后把原对象置为“空”状态(避免原对象析构时释放已经转移的资源):
// 移动构造:用右值引用接收原对象,标记noexcept保证异常安全 DynamicArray(DynamicArray&& other) noexcept : m_data(nullptr), m_size(0) { swap(*this, other); }
也可以直接手动赋值实现,效果一样:
DynamicArray(DynamicArray&& other) noexcept : m_data(other.m_data), m_size(other.m_size) { other.m_data = nullptr; other.m_size = 0; }
加上noexcept很重要:标准库容器会优先选择标记为noexcept的移动构造函数,避免异常安全问题。
5. 移动赋值运算符:转移资源的赋值
和移动构造逻辑类似,把右值对象的资源转移过来,同时保证原对象能安全析构:
// 移动赋值:返回引用,标记noexcept DynamicArray& operator=(DynamicArray&& other) noexcept { swap(*this, other); return *this; }
如果担心自赋值,也可以手动实现更严谨的版本:
DynamicArray& operator=(DynamicArray&& other) noexcept { if (this != &other) { // 避免自赋值 delete[] m_data; // 先释放当前对象的旧资源 // 接管原对象的资源 m_data = other.m_data; m_size = other.m_size; // 置空原对象 other.m_data = nullptr; other.m_size = 0; } return *this; }
完整的类代码
把上面的部分整合起来,就是完整的实现:
#include <algorithm> // 用于std::swap class DynamicArray { private: int* m_data; size_t m_size; // 友元swap函数 friend void swap(DynamicArray& first, DynamicArray& second) noexcept { using std::swap; swap(first.m_data, second.m_data); swap(first.m_size, second.m_size); } public: // 普通构造函数 explicit DynamicArray(size_t size) : m_size(size), m_data(new int[size]) {} // 析构函数 ~DynamicArray() { delete[] m_data; m_data = nullptr; } // 拷贝构造函数 DynamicArray(const DynamicArray& other) : m_size(other.m_size), m_data(new int[other.m_size]) { for (size_t i = 0; i < m_size; ++i) { m_data[i] = other.m_data[i]; } } // 拷贝赋值运算符 DynamicArray& operator=(const DynamicArray& other) { DynamicArray temp(other); swap(*this, temp); return *this; } // 移动构造函数 DynamicArray(DynamicArray&& other) noexcept : m_data(nullptr), m_size(0) { swap(*this, other); } // 移动赋值运算符 DynamicArray& operator=(DynamicArray&& other) noexcept { swap(*this, other); return *this; } // 可选:添加访问接口 int& operator[](size_t index) { return m_data[index]; } const int& operator[](size_t index) const { return m_data[index]; } size_t size() const { return m_size; } };
关键注意点
- 对于C数组,必须用
delete[]释放,对应new[]的分配,不能用delete,否则会触发未定义行为。 - 移动操作一定要把原对象的资源指针置空,避免原对象析构时重复释放资源。
- 拷贝操作必须是深拷贝,绝对不能共享资源。
- 如果你的动态资源是FILE指针、数据库句柄,逻辑完全通用:析构时调用
fclose/db_close,拷贝时创建新的句柄(比如复制文件连接、新建数据库连接),移动时转移句柄并把原句柄置为无效状态。
内容的提问来源于stack exchange,提问作者code

