正确写法是参数为const T&、初始化列表中初始化所有成员、函数体内手动深拷贝资源且不调用operator=;必须深拷贝的场景包括裸指针指向堆内存、持有文件/socket句柄等独占资源。

拷贝构造函数怎么写才不会出错
默认生成的拷贝构造函数只做成员逐个赋值(即浅拷贝),只要类里没指针、没动态分配内存、没文件句柄或类似资源,它就足够安全。但一旦有 int*、std::vector 以外的自定义资源管理逻辑,就必须自己写。
正确写法核心就三点:参数必须是 const T&,函数体里手动复制所有资源,且不能调用 operator=(那是赋值,不是构造):
class Buffer {
char* data_;
size_t size_;
public:
Buffer(const Buffer& other) : data_(nullptr), size_(other.size_) {
if (other.data_) {
data_ = new char[size_];
std::memcpy(data_, other.data_, size_);
}
}
};- 漏掉初始化列表里的
data_(nullptr),后续 if 判断可能读未初始化内存 - 把参数写成
Buffer other(值传递)会无限递归调用拷贝构造函数 - 在函数体内用
*this = other;是错的——这是赋值操作,不是构造,而且可能触发未定义行为
哪些场景必须深拷贝
深拷贝的本质是「每个对象独占一份资源」。只要两个对象共享同一块堆内存、同一个文件描述符、同一个 socket 句柄,就存在资源竞争或提前释放风险。
-
char*、void*指向堆内存(比如用new或malloc分配的) - 持有
FILE*、int fd(Linux 文件描述符)、HANDLE(Windows)等系统资源 - 内部缓存了某个全局结构体的引用或指针,且该结构体生命周期不由当前类控制
- 使用了
std::shared_ptr以外的裸指针管理对象图(比如树节点之间用Node*连接)
反例:用 std::string、std::vector、std::shared_ptr 管理资源时,通常不用手写深拷贝——它们自己已经实现了正确的拷贝语义。
立即学习“C++免费学习笔记(深入)”;
浅拷贝不是 bug,但容易被误用
浅拷贝本身完全合法,C++ 默认就是它。问题出在「你以为它安全,其实不安全」。常见错误现象包括:
- 程序运行一阵后崩溃,报
double free or corruption—— 两个对象析构时都delete同一块内存 - 一个对象修改了数据,另一个对象的值也变了 —— 它们指向同一片内存,但本意是各自独立
- 移动后原对象仍被使用,而你没禁用拷贝(比如没把拷贝构造函数设为
= delete)
注意:即使写了深拷贝,也要同步检查 operator= 和析构函数,三者必须语义一致(Rule of Three)。C++11 起更推荐直接用 = default 或 = delete 显式声明,避免隐式生成带来的歧义。
现代 C++ 里怎么绕过手写深拷贝
不是所有情况都得自己 new/delete。多数时候,改用 RAII 容器就能让编译器继续用默认拷贝构造函数,且语义正确。
- 把
char* data_换成std::vector<char> data_,拷贝自动深拷 - 把
FILE*封装进自定义 RAII 类(如FileHandle),并在其拷贝构造函数里执行fopen+fseek复制内容,而非共享句柄 - 明确不需要拷贝?直接删掉:
Buffer(const Buffer&) = delete; - 只允许移动?写
Buffer(Buffer&&) = default;并删掉拷贝(或反过来)
最易被忽略的一点:深拷贝成本可能很高(比如几百 MB 的 buffer),而你根本没意识到——因为没测过拷贝耗时,也没加日志。上线后突然发现某次 vector resize 触发大量对象拷贝,CPU 直接拉满。










