浅拷贝仅复制指针导致共享内存,深拷贝需手动分配并复制数据;拷贝构造函数在对象初始化、值传递参数、返回局部对象时调用;必须同时重载拷贝构造函数和operator=以避免行为不一致,并注意自赋值、异常安全及元信息同步。
默认的拷贝构造函数和赋值运算符执行的是浅拷贝——它把对象里的每个字节原样复制过去。如果类里有 char*、int* 或其他裸指针成员,浅拷贝后两个对象会指向同一块堆内存。一旦其中一个析构时调用 delete,另一方再访问就是野指针;若两次析构,还会触发 double free 错误。
深拷贝必须在拷贝构造函数和 operator= 中手动分配新内存,并把原始数据逐字节或按逻辑复制过去。这是资源管理的基本守则,不写就会出问题。
它不是只在 A b = a; 这种写法里触发。以下场景都会调用:
A b(a); 或 A b = a;
void func(A x) { ... } 调用时 func(a)
A create() { A x; return x; }
注意:现代编译器普遍启用 RVO/NRVO 优化,可能跳过拷贝构造。但逻辑上仍需正确实现,否则关掉优化或换编译器就崩。
只写拷贝构造函数而忽略赋值运算符,或反过来,会导致行为不一致。比如:
A a; A b; b = a; // 调用 operator=,若没重载,就是浅拷贝 A c = a; // 调用拷贝构造函数,若重载了,是深拷贝
这种
不对称极易引发隐性 bug。标准做法是遵循“三法则”(C++11 后为“五法则”):只要写了析构函数、拷贝构造函数、拷贝赋值运算符中的任一个,另外几个通常也得自己写。
常见疏漏点:
operator= 忘记处理自赋值:a = a; 会导致先 delete 再访问已释放内存new 失败未检查,抛出异常后对象处于半构造状态size_t len)一并复制,导致新对象元信息错乱它们内部已经实现了正确的深拷贝语义。如果你的类里原来用 char* 存字符串,换成 std::string;用 int* + size_t 管理数组,换成 std::vector,那么默认生成的拷贝构造函数就能安全工作。
但这不等于可以忽视原理——当涉及文件句柄、socket、shared memory 等系统资源时,依然要手动管理。而且,有些老项目或嵌入式环境禁用 STL,这时深拷贝逻辑逃不掉。
真正容易被忽略的是:即使用了 std::vector,若类中还有裸指针成员(比如缓存用的 float* m_cache),那依然得自己写深拷贝。别以为加了 STL 就万事大吉。