深拷贝必须手动实现,编译器默认只做浅拷贝;含指针成员的类若不显式定义拷贝构造函数和赋值运算符,将导致同一内存被多次释放或use-after-free;应为每个对象独立分配内存并完整复制数据,兼顾分配、复制与异常安全。

深拷贝必须手动实现,编译器默认只做浅拷贝
当你定义了一个含指针成员的类(比如 char* 或 std::unique_ptr),不写拷贝构造函数和赋值运算符,operator= 就会直接复制指针地址——两个对象指向同一块内存。后续任意一方释放内存,另一方再访问就是 use-after-free,程序大概率崩溃或行为未定义。
深拷贝的核心是:为每个对象独立申请新内存,并把原对象的数据完整复制过去。这不能靠编译器自动生成,必须显式编码。
拷贝构造函数里用 new 分配新内存并逐字节复制
这是最典型的深拷贝入口点。关键不是“调用 memcpy”或“写个循环”,而是确保分配、复制、异常安全三者兼顾。
- 如果用
new char[len],记得先检查len是否为 0,避免new char[0]虽合法但容易漏掉边界逻辑 - 复制内容推荐用
std::copy(对 POD 类型)或逐成员赋值(对含构造逻辑的子对象),别直接memcpy,除非你 100% 确认类型是 trivially copyable - 在分配内存后、复制数据前发生异常(比如
std::bad_alloc),要保证对象处于有效状态——通常意味着在构造函数体里做分配和复制,而不是在初始化列表里尝试分配(因为初始化列表失败会导致对象未构造完成)
示例片段:
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
class String {
char* data_;
size_t len_;
public:
String(const String& other) : len_(other.len_) {
data_ = new char[len_ + 1]; // 多留一个给 '\0'
std::copy(other.data_, other.data_ + len_ + 1, data_);
}
};别忘了同步实现 operator=,且必须处理自赋值和异常安全
只写拷贝构造函数不够。用户写 a = b; 时触发的是赋值运算符,它默认也是浅拷贝。而且赋值可能自赋值(a = a;),也可能在中途抛异常,导致对象处于半更新状态。
- 自赋值检查不能只靠
if (this == &other)—— 这能防住a = a,但防不住a = std::move(a)这类移动赋值场景;更稳健的做法是“拷贝-交换”(copy-and-swap)惯用法 - 推荐实现方式:以传值方式接收参数(触发一次深拷贝),然后
swap成员——这样异常安全,且天然处理自赋值 - 交换操作本身应为
noexcept,否则swap可能抛异常,破坏强异常保证
简写示意:
String& operator=(String other) { // 注意:传值,自动触发拷贝构造
swap(*this, other);
return *this;
}
<p>friend void swap(String& a, String& b) noexcept {
using std::swap;
swap(a.data<em>, b.data</em>);
swap(a.len<em>, b.len</em>);
}现代 C++ 更推荐用 std::vector 或 std::string 替代裸指针
手动管理内存的深拷贝容易出错,不是因为逻辑难,而是细节多:长度计算、空指针判断、异常路径清理、const 正确性……这些都可能漏掉。
如果你的类本质是“拥有一段可变长度的数据”,直接用 std::vector<T> 或 std::string,它们内部已正确实现深拷贝语义,且支持移动语义、异常安全、SBO(small buffer optimization)等优化。
- 改用
std::vector<int>后,你根本不需要写拷贝构造函数——编译器生成的默认版本就满足深拷贝需求 - 如果必须保留裸指针接口(比如对接 C API),至少把所有权封装进
std::unique_ptr,它的拷贝被禁用,赋值需显式release()或clone(),能提前暴露设计意图
真正需要手写深拷贝的场景,其实越来越少。多数时候,问题不在于“怎么写深拷贝”,而在于“是否真的需要自己管理内存”。

















