默认拷贝构造函数对原始指针仅做浅拷贝,导致双释放;必须实现深拷贝、赋值运算符及rule of five,并注意参数为const引用、元数据同步与异常安全。

含有原始指针的类默认拷贝构造会引发 double-free
编译器生成的默认拷贝构造函数只做逐成员复制,对 int* 这类指针成员,它复制的是地址值,不是所指内容。结果是两个对象的指针指向同一块堆内存,析构时各自调用 delete 或 delete[],第二次释放直接触发未定义行为——常见报错如 free(): double free detected in tcache 2。
这不是小概率事件:只要类里有 new 出来的资源(比如 char* 缓冲区、int* 数组),且没写自定义拷贝构造,就必然踩中这个坑。
- 典型场景:类封装动态数组(如
Buffer、StringWrapper)、文件句柄、socket fd 等需要手动释放的资源 - 崩溃往往延迟发生:可能在函数返回、容器扩容、作用域结束时才暴露,调试难度大
- Valgrind 或 AddressSanitizer 会明确提示
use-after-free或double-free
拷贝构造函数参数必须是 const 引用
如果把参数写成 MyClass(MyClass other)(传值),编译器会尝试用拷贝构造初始化形参 other,而该过程又需要调用拷贝构造……无限递归,最终栈溢出。这是语法硬性限制,不是风格建议。
正确写法只能是 MyClass(const MyClass& other):
-
const防止误改源对象,也允许传入临时对象或字面量(如func(get_temp_obj())) - 引用避免额外拷贝开销,尤其对大对象
- 若漏掉
const,某些调用(如传临时对象)会编译失败
深拷贝不只是 new + copy,还要同步管理配套状态
光复制指针内容不够。原始指针常伴随长度、容量、标志位等元数据,这些必须一并复制,否则逻辑错乱。
例如一个管理动态数组的类:
class Buffer {
int* data_;
size_t size_;
bool is_sorted_;
public:
Buffer(const Buffer& other)
: size_(other.size_), is_sorted_(other.is_sorted_) {
data_ = new int[size_]; // 分配新内存
std::copy(other.data_, other.data_ + size_, data_); // 复制内容
}
};
- 如果只复制
data_,忘了复制size_,后续std::copy可能越界 - 如果只复制
size_,忘了复制is_sorted_,排序状态就丢失了 - 异常安全要考虑:若
new抛异常,不能让已有资源泄漏(但本例无已有资源,所以相对简单)
写了拷贝构造,别忘了 operator= 和 Rule of Five
只实现拷贝构造函数远远不够。当发生 a = b 时,调用的是赋值运算符,它默认也是浅拷贝。更隐蔽的是:C++11 后,一旦你声明了拷贝构造,编译器通常不会自动生成移动构造函数,导致 std::vector<buffer></buffer> 扩容时反复深拷贝,性能暴跌。
- 必须同步实现
operator=,并处理自赋值(a = a)和异常安全 - 强烈建议按 Rule of Five 补全:拷贝构造、拷贝赋值、移动构造、移动赋值、析构
- 如果类不打算被拷贝,显式删除:
MyClass(const MyClass&) = delete;
最易被忽略的一点:很多开发者以为“我只用 std::vector 存对象,不用拷贝”,但 push_back、resize、sort 都可能触发拷贝或移动——只要类里有原始指针,就必须直面所有权问题。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











