浅拷贝是C++默认行为,仅复制指针地址而非内容,导致双删、野指针等问题;含裸指针或动态资源的类必须手动实现深拷贝:分配新内存→复制数据→更新指针。

浅拷贝就是默认的,但多数时候它会出问题
当你没写拷贝构造函数时,C++ 会自动生成一个——它逐字节复制对象内存,对指针成员只复制地址,不复制指向的内容。这意味着两个对象的指针变量指向同一块堆内存。一旦其中一个析构时 delete 了这块内存,另一个再访问就是野指针;若都试图 delete,直接触发 double free 错误。
常见错误现象:double free or corruption、segmentation fault、程序在析构阶段崩溃,且只在对象被拷贝(如传值返回、容器插入)后才出现。
- 只要类里有裸指针(
int*、char*等)、或管理了动态资源(文件句柄、socket、GPU buffer),就必须自己写拷贝构造函数 -
std::string、std::vector这类标准容器内部已实现深拷贝,它们的成员不用额外处理 - 编译器不会警告你“你该写深拷贝”,它只安静地给你一个危险的默认行为
深拷贝必须手动分配新内存并复制内容
核心动作就三步:申请新内存 → 复制原数据 → 更新指针成员。别漏掉任何一步,尤其不能只 new 不 memcpy 或赋值。
假设有个类管理一个动态数组:
立即学习“C++免费学习笔记(深入)”;
class Buffer {
int* data_;
size_t size_;
public:
Buffer(const Buffer& other) : size_(other.size_) {
data_ = new int[size_]; // ① 分配新内存
for (size_t i = 0; i < size_; ++i) {
data_[i] = other.data_[i]; // ② 逐元素复制(不是 memcpy!除非确定 POD)
}
}
};
- 如果用
memcpy(data_, other.data_, size_ * sizeof(int)),对int没问题;但对含虚函数、非 POD 类型(比如含std::string的结构体),必须用循环或std::copy - 别在拷贝构造函数里调用
operator=—— 它依赖已构造完成的对象,而此时当前对象还没构造完 - 记得同步更新所有相关成员:比如
size_、capacity_、引用计数等,漏掉一个就可能造成越界或逻辑错乱
拷贝构造函数和 operator= 必须一起改,否则行为不一致
只改拷贝构造函数,不改赋值运算符,会导致“同一个类,两种拷贝逻辑”:初始化时深拷,赋值时却浅拷——这是最隐蔽的坑之一。
典型错误场景:把对象先声明,再用 = 赋值;或者 vector 扩容时触发移动/拷贝混合操作。
- 赋值运算符要先检查自赋值:
if (this == &other) return *this;,否则delete data_后再读other.data_就崩了 - 赋值时要先释放旧资源,再分配新资源,顺序反了会内存泄漏
- C++11 后推荐用“拷贝-交换”惯用法(copy-and-swap),能自动处理自赋值和异常安全,但要注意临时对象开销
现代 C++ 更推荐用 RAII 和智能指针绕过手动深拷贝
如果你发现自己反复在写深拷贝逻辑,大概率是设计上可以优化:把资源管理从裸指针移到 std::unique_ptr 或 std::shared_ptr,很多问题就消失了。
例如:
class Buffer {
std::unique_ptr<int[]> data_;
size_t size_;
public:
Buffer(const Buffer& other) : size_(other.size_),
data_(std::make_unique<int[]>(other.size_)) {
std::copy(other.data_.get(), other.data_.get() + size_, data_.get());
}
};
-
std::unique_ptr的拷贝构造默认是删除的(禁用),所以你仍需手动实现——但它帮你管好了delete,不会忘 -
std::shared_ptr拷贝是共享引用计数,本质是浅拷贝语义,但行为安全;适合读多写少、需共享所有权的场景 - RAII 不是银弹:如果业务逻辑确实要求物理隔离(比如两个 Buffer 必须互不影响),那深拷贝仍是唯一选择,只是实现更可靠了
真正容易被忽略的是:深拷贝成本本身。一次拷贝几百 MB 的图像数据?要考虑是否真需要——有时传 const Buffer& 或用 move 语义更合理。











