浅拷贝是编译器生成的默认拷贝构造函数,仅逐成员赋值,对指针复制地址而非内容,导致双析构或数据干扰;深拷贝需手动实现拷贝构造函数和赋值运算符,分配新内存并复制数据,并遵循三/五法则;现代C++推荐用vector、string、unique_ptr等RAII容器替代裸指针。

浅拷贝就是默认拷贝构造函数干的事
当你没写拷贝构造函数时,C++ 自动生成的版本只做成员逐个赋值。对内置类型(int、double)没问题,但对指针成员,它复制的是地址值,不是所指内容——两个对象的指针指向同一块堆内存。
典型后果:~A() 析构两次同一块内存,触发 double free or corruption 错误;或一个对象修改了数据,另一个跟着变,违背封装预期。
示例:
class A {
public:
int* data;
A(int x) { data = new int(x); }
// 没写拷贝构造 → 编译器生成浅拷贝版本
~A() { delete data; }
};此时 A a(10); A b = a; 后,a.data == b.data 为 true,危险已埋下。
立即学习“C++免费学习笔记(深入)”;
深拷贝必须手动实现拷贝构造函数和赋值运算符
核心动作:为每个指针成员在堆上新分配内存,并把原对象的数据复制过去。同时要配套重载 operator=,否则使用 = 赋值时仍会触发浅拷贝(编译器生成的默认赋值运算符也是逐成员赋值)。
关键点:
- 拷贝构造函数参数必须是
const A&,避免递归调用 - 分配失败时抛异常(如
std::bad_alloc),不能留空指针或部分初始化状态 - 赋值运算符需自赋值检查:
if (this == &other) return *this; - 旧资源必须先释放再分配新资源,顺序错会导致内存泄漏或悬空指针
简写示例:
class A {
public:
int* data;
A(int x) : data(new int(x)) {}
A(const A& other) : data(new int(*other.data)) {} // 深拷贝
A& operator=(const A& other) {
if (this == &other) return *this;
delete data;
data = new int(*other.data);
return *this;
}
~A() { delete data; }
};现代 C++ 更推荐用 RAII 容器替代裸指针
手动管理深拷贝容易出错,且难以处理复杂嵌套结构。用 std::vector、std::string、std::unique_ptr 等容器后,编译器生成的拷贝构造函数自动变成“语义深拷贝”——因为这些类型自己已经实现了正确的拷贝逻辑。
比如:
class B {
std::vector items;
std::string name;
public:
B(std::initializer_list il) : items(il) {}
// 不用写拷贝构造、赋值、析构 —— 默认行为完全正确
}; 这时 B b1{1,2,3}; B b2 = b1; 中 b2.items 是独立副本,b2.name 也是独立副本,安全且简洁。
若必须用动态资源,优先选 std::unique_ptr 或 std::shared_ptr,它们的拷贝/移动语义清晰,不需手写深拷贝逻辑。
移动语义出现后,深拷贝场景其实变少了
很多原本需要深拷贝的场合(比如函数返回临时对象、容器扩容时元素搬迁),现在由移动构造函数接管。移动操作只是转移指针所有权,不复制数据,性能更高。
所以实际编码中:
- 有裸指针成员?先问能不能换成
std::vector或智能指针 - 必须手写拷贝?务必同步实现拷贝构造 +
operator=+ 析构(三法则),C++11 后建议补上移动版本(五法则) - 调试时怀疑拷贝问题?加日志输出指针地址:
std::cout ,看是否重复
真正需要深拷贝的类,往往意味着资源所有权明确、不可共享,这种设计本身就要格外小心生命周期交叉问题。










