深拷贝必须手动实现,因默认拷贝构造函数仅逐字节复制指针地址,导致双释放或数据意外共享;正确做法是为动态资源分配新内存并复制内容,同时遵循rule of five完善特殊成员函数。

深拷贝必须手动实现,编译器生成的默认拷贝构造函数只做浅拷贝——只要类里有指针成员或持有动态资源(比如 new 出来的内存、打开的文件描述符、malloc 的缓冲区),不写自定义拷贝构造函数就必然出问题。
为什么默认拷贝构造函数不够用
编译器生成的拷贝构造函数对每个成员做逐字节复制。如果类中包含 int* 成员,它只会复制指针地址,不会复制指针指向的内容。结果是两个对象的指针指向同一块堆内存,析构时被释放两次,触发 double free 错误;或者一个对象修改了数据,另一个“意外”跟着变。
常见错误现象:
- 程序在析构阶段崩溃,报错类似
free(): double free detected in tcache 2 - 对象 A 修改某字段后,对象 B 的对应字段也变了
- Valgrind 报告
Invalid read/write或use after free
怎么写正确的拷贝构造函数
核心原则:为每个动态资源分配新内存,并把原内容完整复制过去。不是简单地 other.ptr 赋值,而是 new int(*other.ptr) 或 new int[other.size] 后再 std::copy。
实操要点:
- 参数必须是
const MyClass&,避免递归调用和意外修改源对象 - 先分配新资源,再复制内容;若分配失败(抛异常),确保已有资源不泄漏(即遵循 strong exception safety)
- 记得同步复制所有相关成员:比如
data_指针和对应的size_计数器必须一起更新 - 如果类还管理其他资源(如
FILE*、socket fd),也要fopen/dup等重新获取独立句柄
示例片段:
class Buffer {
int* data_;
size_t size_;
public:
Buffer(const Buffer& other) : size_(other.size_) {
data_ = new int[size_]; // 分配新内存
std::copy(other.data_, other.data_ + size_, data_); // 复制内容
}
~Buffer() { delete[] data_; }
};
别忘了赋值运算符重载和移动语义
只写拷贝构造函数还不够。当发生 a = b 时,调用的是 operator=,它默认也是浅拷贝。必须同步实现,且要处理自赋值(a = a)和异常安全。
更关键的是:C++11 后,如果你没显式禁用或定义移动操作,编译器可能因存在用户定义的拷贝构造函数而抑制自动生成移动构造函数。这意味着容器(如 std::vector<buffer></buffer>)扩容时会反复深拷贝,性能暴跌。
建议做法:
- 按 Rule of Five 实现全部五个特殊成员函数:拷贝构造、拷贝赋值、移动构造、移动赋值、析构
- 或直接用
= default显式声明移动操作(前提是资源管理足够简单,比如只含std::unique_ptr) - 若资源不可移动(比如绑定了特定硬件句柄),则显式
= delete移动函数
用智能指针能绕过深拷贝吗
不能“绕过”,但可以改变语义。用 std::shared_ptr<int></int> 本身是共享所有权,拷贝时只是增加引用计数——这是设计上的共享,不是深拷贝。如果你需要的是逻辑上独立的副本(比如图像处理中两个 Image 对象不能互相影响),shared_ptr 不解决问题,仍需在拷贝构造里 new 新内存并复制像素数据。
真正省事的方式是:把裸指针换成 std::vector、std::string 等自带深拷贝语义的类型。它们的拷贝构造函数内部已经做了正确的事,你只需确保自己不直接碰 new/delete。
最容易被忽略的一点:深拷贝的“深度”由你的业务语义决定。比如一个类持有一个 std::vector<:shared_ptr>></:shared_ptr>,你可能希望拷贝时只复制 vector 和 shared_ptr(浅层),也可能需要递归克隆每个 Node 对象(深层)。这个边界得你自己画清楚,编译器可不管你要的是几层。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











