不是必须手动实现深拷贝,但含裸指针等非RAII资源时默认浅拷贝会导致double-free;std::string/vector等已内置深拷贝;赋值运算符推荐复制-交换法,避免自我赋值问题。

拷贝构造函数必须手动实现深拷贝吗
不是“必须”,但只要类里有指针成员(尤其是指向堆内存的 int*、char* 或 std::vector 以外的自定义资源),默认拷贝构造函数只会做浅拷贝——两个对象共享同一块内存,后续析构时会 double-free,程序大概率崩溃。
典型错误现象:double free or corruption、Segmentation fault (core dumped),尤其在函数返回局部对象或容器扩容时高频触发。
- 如果所有成员都是栈对象或标准智能指针(如
std::unique_ptr),编译器生成的拷贝构造函数已自动完成深拷贝语义 - 如果含裸指针、文件句柄、socket 描述符等非 RAII 资源,必须手动实现
- 注意:
std::string和std::vector自身已管理堆内存,它们的拷贝是深拷贝,无需额外处理
赋值运算符重载的正确写法(含自我赋值保护)
赋值运算符比拷贝构造更易出错:它要先释放旧资源,再复制新资源。若没处理自我赋值(a = a),释放后就访问了野指针。
推荐采用「复制-交换」(copy-and-swap)惯用法,天然规避自我赋值和异常安全问题:
立即学习“C++免费学习笔记(深入)”;
class MyClass {
int* data_;
public:
MyClass& operator=(MyClass other) { // 注意:参数按值传递(触发拷贝构造)
swap(*this, other); // 交换 this 和 other 的资源
return *this;
}
friend void swap(MyClass& a, MyClass& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
}
};- 不要写
if (this == &other) return *this;—— 这只是补救,掩盖了设计缺陷 - 参数必须是值传递(
MyClass other),不是const MyClass&,否则无法移动/交换 - 交换函数需声明为
friend并标记noexcept,确保std::swap可调用
拷贝构造函数里最容易漏掉的三件事
即使写了拷贝构造,仍常因细节导致未定义行为:
- 忘了为新分配的内存初始化内容(比如
new int[size]不等于new int[size](),后者才零初始化) - 拷贝长度信息不一致:比如只拷了
size_,但没按它来分配/复制实际数据,或没更新capacity_ - 异常安全性缺失:若在拷贝中途(如 new 失败)抛出异常,当前对象可能处于半构造状态——建议在构造函数体外用
try/catch或直接使用std::vector替代裸数组
示例错误写法:data_ = new int[other.size_]; memcpy(data_, other.data_, other.size_ * sizeof(int)); —— 如果 other.data_ 是 nullptr 就崩了;正确做法应先判空。
现代 C++ 中哪些情况可以彻底避开手写拷贝控制
如果你能控制类的设计,优先用 RAII 和标准类型封装资源,就能让编译器替你干活:
- 用
std::unique_ptr替代T*数组:移动语义可用,拷贝被禁用(符合语义) - 用
std::shared_ptr管理共享资源:拷贝即增加引用计数,天然线程安全 - 用
std::string_view替代const char*参数:避免所有权争议 - 把裸资源封装进独立的小类(如
FileHandle),并在其内部实现正确的拷贝/移动逻辑,主类只需依赖它
手写拷贝构造和赋值运算符不是“高级技巧”,而是资源管理失控的信号灯。真正难的不是语法,是判断“这个类是否该支持拷贝”——很多场景下,删掉拷贝函数、只留移动,反而更清晰、更安全。









