浅拷贝是编译器自动生成的逐字节复制,导致指针成员共享同一内存,引发double free等问题;深拷贝需手动实现拷贝构造函数,为指针重新分配并复制数据;移动语义可避免开销,优先用于临时对象。

浅拷贝就是默认的拷贝行为,但会出问题
当你没写拷贝构造函数时,C++ 编译器自动生成一个——它逐字节复制对象内存,也就是浅拷贝。对只含基本类型或不带资源指针的类,这没问题;但一旦类里有 int*、char* 或其他动态分配的资源,浅拷贝会让两个对象指向同一块堆内存。
常见错误现象:double free or corruption、程序崩溃、值被意外覆盖。比如一个 String 类用 char* 存字符串,浅拷贝后两个对象析构时都调用 delete[] ptr,第二次 delete 就崩了。
- 浅拷贝本质是位拷贝(bitwise copy),不调用任何构造函数
- 所有成员变量按声明顺序依次复制,包括指针值本身(而非它指向的内容)
- 只要类中没有显式定义拷贝构造函数,就一定发生浅拷贝
深拷贝必须手动实现拷贝构造函数
深拷贝的核心是:为每个指针成员重新分配内存,并把原对象的数据完整复制过去。这意味着你必须自己写拷贝构造函数,且通常要配合重载赋值运算符 operator= 和析构函数(三法则/五法则)。
示例关键逻辑:
立即学习“C++免费学习笔记(深入)”;
class String {
char* data;
size_t len;
public:
String(const char* s) : len(s ? strlen(s) : 0) {
data = new char[len + 1];
if (s) strcpy(data, s);
}
// 深拷贝构造函数
String(const String& other) : len(other.len) {
data = new char[len + 1]; // 新分配
if (other.data) strcpy(data, other.data); // 复制内容
}
~String() { delete[] data; }};
- 必须检查源对象指针是否为空,避免
strcpy(nullptr) - 分配内存失败时应抛异常(如
std::bad_alloc),否则可能造成部分初始化对象 - 若类中有多个指针成员,每个都要单独 new + memcpy / copy
拷贝构造函数被调用的 4 种典型场景
很多人以为只在 String s2 = s1; 时触发,其实还有更隐蔽的调用点,容易漏掉深拷贝逻辑导致 bug。
- 用一个已存在对象初始化新对象:
String s2(s1);或String s2 = s1; - 函数传参时以值传递方式接收对象:
void func(String s) { ... }→ 调用拷贝构造创建形参 - 函数返回局部对象(非引用):
String create() { return String("hi"); }→ 可能触发(即使有 RVO 优化,语义上仍要求可拷贝) - 用对象初始化容器元素(如
std::vector)→ 拷贝构造被调用 10 次v(10, s1);
这些地方一旦用了浅拷贝,默认行为不会报错,但后续析构或修改会暴露问题。
现代 C++ 更推荐移动语义替代深拷贝
深拷贝开销大,尤其数据量大时。C++11 后,如果对象临时生成或明确不再需要原值,应该优先用移动构造函数把资源“偷过来”,而不是复制。
例如补充移动构造函数:
String(String&& other) noexcept : data(other.data), len(other.len) {
other.data = nullptr;
other.len = 0;
}- 移动构造函数参数是右值引用
T&&,且最好加noexcept - 移动后原对象必须处于有效但未指定状态(比如指针置空),确保析构安全
- 容器扩容、
std::make_unique、函数返回临时对象等场景,编译器会优先选移动而非拷贝
真正难的不是写深拷贝,而是判断什么时候该深拷贝、什么时候该移动、什么时候根本不该拷贝(改用智能指针或值语义封装)。资源管理边界模糊时,bug 往往藏在析构和异常路径里。










