浅拷贝是编译器默认的逐字节复制,导致指针成员共享同一堆内存,引发野指针、double free等错误;深拷贝需手动实现拷贝构造函数和operator=,并处理自赋值与资源释放;推荐使用std::vector、std::string等raii类型替代裸指针。

浅拷贝就是默认的,但默认行为往往不是你想要的
编译器自动生成的拷贝构造函数和赋值运算符只做逐字节复制——指针成员被原样复制,两个对象指向同一块堆内存。一旦其中一个析构时 delete 了那块内存,另一个对象再访问就是野指针,程序大概率崩溃或出现难以复现的 double free 或 use-after-free 错误。
常见错误现象:
- 对象还在用,却突然读到垃圾值或触发段错误
- 程序在析构阶段崩溃,堆栈里出现
operator delete相关报错 - 两个不同对象修改同一份数据,彼此“意外联动”
只要类里有 new 出来的资源(比如 int*、char*、std::vector 以外的手动管理内存),就别依赖默认拷贝。
深拷贝必须手动写拷贝构造函数和 operator=
拷贝构造函数负责初始化新对象,operator= 负责已有对象的重新赋值,二者逻辑高度相似,但 operator= 必须处理自赋值(a = a)和旧资源释放。
立即学习“C++免费学习笔记(深入)”;
实操建议:
- 先写拷贝构造函数:分配新内存 → 复制内容 → 更新指针
- 再写
operator=:检查自赋值 → 释放当前资源 → 按拷贝构造逻辑重建 → 返回*this - 如果类里有多个动态成员,每个都要独立申请、独立拷贝,不能共用一个
new
示例(简化版):
class Buffer {
char* data_;
size_t size_;
public:
Buffer(const char* s) : size_(strlen(s)) {
data_ = new char[size_ + 1];
strcpy(data_, s);
}
<pre class='brush:php;toolbar:false;'>// 拷贝构造函数:深拷贝
Buffer(const Buffer& other) : size_(other.size_) {
data_ = new char[size_ + 1];
strcpy(data_, other.data_);
}
// 赋值运算符:深拷贝 + 自赋值防护
Buffer& operator=(const Buffer& other) {
if (this == &other) return *this; // 自赋值检查
delete[] data_; // 先释放旧资源
size_ = other.size_;
data_ = new char[size_ + 1];
strcpy(data_, other.data_);
return *this;
}
~Buffer() { delete[] data_; }};
移动语义能绕过深拷贝,但不等于可以不写拷贝逻辑
C++11 后,如果你写了移动构造函数和移动赋值,编译器可能在临时对象场景下自动调用它们,避免深拷贝开销。但这只是优化路径,不是替代方案。
关键点:
- 移动不会影响拷贝构造函数是否需要存在;用户仍可能显式写
Buffer b = a;,这走的是拷贝,不是移动 - 移动后源对象处于“有效但未指定状态”,不能再安全读取其资源——这点和浅拷贝完全不同
- 如果类支持移动,记得在移动操作里把源对象的指针置为
nullptr,否则析构时会重复delete
比如移动构造函数末尾加一句:other.data_ = nullptr;,否则 other 析构时会误删已被移走的内存。
RAII 和智能指针让深拷贝问题大幅减少
手动管理 new/delete 是深拷贝出错的根源。现代 C++ 更推荐用 std::vector、std::string、std::unique_ptr 等 RAII 类型封装资源。
它们自带正确的拷贝/移动语义:
-
std::vector拷贝是深拷贝,但你不用写一行代码 -
std::unique_ptr默认禁止拷贝,只允许移动,天然防浅拷贝误用 -
std::shared_ptr拷贝是浅拷贝引用计数,但语义安全,不用你操心释放时机
所以,与其花时间调试自己写的拷贝构造函数,不如先问:这个指针能不能换成 std::vector<t></t>?这个 char* 能不能换成 std::string?
真要手管内存时,最容易被忽略的是:析构函数里没判空就 delete[],或者拷贝构造里忘了给新指针初始化,导致后续 strcpy 写入随机地址。










