C++ 中如何实现深拷贝?
在 C++ 中,当类中包含指针成员或动态分配的资源(如 new 分配的内存、文件句柄等)时,编译器默认生成的浅拷贝(Shallow Copy,只复制指针地址)会导致多个对象指向同一块内存,进而引发重复释放(Double Free)、内存泄漏或数据被意外修改等严重问题。
因此,我们需要实现深拷贝(Deep Copy):为新对象分配独立的内存,并将原对象内存中的数据复制过去。
实现深拷贝的核心是遵循 C++ 的“三法则”(Rule of Three),即如果你需要自定义以下任何一个,你通常需要自定义全部三个:
- 析构函数(释放资源)
- 拷贝构造函数(实现深拷贝)
- 拷贝赋值运算符(实现深拷贝并处理自我赋值)
以下是具体的实现方法和最佳实践:
1. 经典实现方法(手动管理内存)
我们以一个包含 int* 指针的类为例:
#include <iostream>
class DeepCopyDemo {
private:
int* data;
public:
// 1. 构造函数
DeepCopyDemo(int value) {
data = new int(value);
std::cout << "构造函数调用\n";
}
// 2. 析构函数(释放内存)
~DeepCopyDemo() {
delete data;
std::cout << "析构函数调用\n";
}
// 3. 拷贝构造函数(深拷贝)
// 重点:申请新内存,复制数据
DeepCopyDemo(const DeepCopyDemo& other) {
data = new int(*(other.data)); // 解引用并复制值
std::cout << "拷贝构造函数调用(深拷贝)\n";
}
// 4. 拷贝赋值运算符(深拷贝)
DeepCopyDemo& operator=(const DeepCopyDemo& other) {
std::cout << "拷贝赋值运算符调用(深拷贝)\n";
// 步骤1:检查自我赋值(非常重要!)
if (this == &other) {
return *this;
}
// 步骤2:释放原有的内存
delete data;
// 步骤3:申请新内存并复制数据
data = new int(*(other.data));
// 步骤4:返回自身的引用,支持连续赋值 a = b = c
return *this;
}
// 辅助函数
void setValue(int val) { *data = val; }
void print() const { std::cout << "Value: " << *data << ", Address: " << data << "\n"; }
};
int main() {
DeepCopyDemo obj1(10);
DeepCopyDemo obj2 = obj1; // 调用拷贝构造函数
DeepCopyDemo obj3(20);
obj3 = obj1; // 调用拷贝赋值运算符
// 修改 obj1 不会影响 obj2 和 obj3,证明是深拷贝
obj1.setValue(100);
obj1.print();
obj2.print();
obj3.print();
return 0;
}
2. 更优雅的实现:“拷贝与交换”惯用法(Copy-and-Swap Idiom)
上面实现的“拷贝赋值运算符”虽然正确,但如果在 new int 时抛出异常,会导致当前对象的 data 变成野指针。
为了保证强异常安全性,C++ 推荐使用 Copy-and-Swap 惯用法来实现拷贝赋值运算符:
#include <algorithm> // for std::swap
class SafeDeepCopy {
private:
int* data;
public:
SafeDeepCopy(int value) : data(new int(value)) {}
~SafeDeepCopy() { delete data; }
SafeDeepCopy(const SafeDeepCopy& other) : data(new int(*(other.data))) {}
// 友元 swap 函数
friend void swap(SafeDeepCopy& first, SafeDeepCopy& second) noexcept {
// 交换内部成员
std::swap(first.data, second.data);
}
// 拷贝赋值运算符(注意参数是按值传递!这里会隐式调用拷贝构造函数)
SafeDeepCopy& operator=(SafeDeepCopy other) {
// 将当前对象的内容与局部对象 other 进行交换
swap(*this, other);
return *this;
// other 离开作用域时,会自动调用析构函数,释放掉当前对象原有的旧内存
}
};
优点:代码极其简洁,自动处理了自我赋值问题,且保证了强异常安全性(如果在按值传参时内存分配失败,当前对象不会被破坏)。
3. 现代 C++ 最佳实践:零法则(Rule of Zero)
在 Modern C++ (C++11 及以后) 中,手动写 new 和 delete 是极其不推荐的。如果你想避免写繁琐的深拷贝代码,最好的方法是使用标准库提供的资源管理类。
这被称为“零法则”:类的设计应该使其不需要自定义析构函数、拷贝构造函数和赋值运算符。
方案 A:使用 std::vector 或 std::string
STL 容器自带深拷贝语义。如果你把裸指针换成 std::vector,编译器默认生成的拷贝函数就已经是深拷贝了!
#include <vector>
class ModernClass {
private:
std::vector<int> data; // 替代裸指针或动态数组
public:
ModernClass(int value) : data(1, value) {}
// 不需要手动写析构、拷贝构造和赋值运算符!
// 默认的浅拷贝行为对 std::vector 来说就是深拷贝其元素。
};
方案 B:结合智能指针(如果必须用指针)
如果你必须使用指针实现多态或特殊逻辑,并且需要深拷贝,std::unique_ptr 默认禁止拷贝,你需要手动编写拷贝构造函数;如果是 std::shared_ptr,默认拷贝只是增加引用计数(不是深拷贝)。
如果你使用 std::unique_ptr 并需要深拷贝:
#include <memory>
class SmartPointerDeepCopy {
private:
std::unique_ptr<int> data;
public:
SmartPointerDeepCopy(int value) : data(std::make_unique<int>(value)) {}
// 拷贝构造函数(手动实现深拷贝)
SmartPointerDeepCopy(const SmartPointerDeepCopy& other) {
if (other.data) {
data = std::make_unique<int>(*other.data);
}
}
// 拷贝赋值运算符
SmartPointerDeepCopy& operator=(const SmartPointerDeepCopy& other) {
if (this != &other) {
if (other.data) {
data = std::make_unique<int>(*other.data);
} else {
data.reset();
}
}
return *this;
}
// 不需要写析构函数,unique_ptr 会自动释放内存!
};
总结建议
- 首选:使用
std::vector、std::string等 RAII 容器,依靠编译器的默认行为,根本不需要自己写深拷贝(Rule of Zero)。 - 次选:如果必须自己管理资源,必须同时实现析构函数、拷贝构造函数、拷贝赋值运算符(Rule of Three)。
- 进阶:使用 Copy-and-Swap 惯用法编写赋值运算符,以提高代码的异常安全性和鲁棒性。