拷贝构造函数未重写指针成员会导致野指针,因默认浅拷贝使多个对象共享同一堆内存,析构时重复释放引发崩溃;必须显式实现深拷贝及赋值运算符,并注意自赋值和释放顺序。

拷贝构造函数里没重写指针成员,就会出野指针
默认拷贝构造函数只做浅拷贝,new 出来的指针被简单复制,两个对象指向同一块堆内存。析构时先释放一次,后析构的那个对象再访问就崩了——典型野指针。
必须显式定义拷贝构造函数,并对每个指针成员做深拷贝:
class DataHolder {
int* ptr;
public:
DataHolder(int val) : ptr(new int(val)) {}
// 关键:手动分配新内存,拷贝值
DataHolder(const DataHolder& other)
: ptr(new int(*other.ptr)) {} // 不是 ptr = other.ptr
~DataHolder() { delete ptr; }
};- 漏掉拷贝构造函数,或只写了赋值运算符(
operator=),编译器仍会合成默认拷贝构造,照样浅拷贝 -
ptr = new int(*other.ptr)是必须的;写成ptr = other.ptr就退回浅拷贝 - 如果类里有多个指针,每个都得单独
new+ 解引用拷贝,不能偷懒用memcpy
operator= 没处理自赋值 + 没释放旧内存,也会野指针
赋值运算符不是可有可无的补丁,它和拷贝构造函数承担不同场景:一个是初始化(构造时),一个是已有对象被赋值(运行时)。漏掉它,a = b 依然触发浅拷贝。
正确实现要三步:释放旧资源 → 深拷贝新资源 → 处理自赋值:
立即学习“C++免费学习笔记(深入)”;
DataHolder& operator=(const DataHolder& other) {
if (this == &other) return *this; // 自赋值检查
delete ptr; // 先释放当前持有的内存
ptr = new int(*other.ptr); // 再深拷贝
return *this;
}- 不加
if (this == &other),x = x会导致delete ptr后又去解引用other.ptr(此时已为悬垂指针) - 顺序不能错:必须先
delete,再new;反过来可能new失败抛异常,旧内存却没释放,造成泄漏 - 没写
operator=,编译器生成的版本不做delete,直接指针覆盖,旧内存永久泄漏
std::unique_ptr 能自动防野指针,但语义变了
用智能指针不是“更高级的写法”,而是换了一套所有权规则:把“手动管理”变成“编译期约束”。std::unique_ptr 禁止拷贝,只允许移动,天然堵死浅拷贝路径。
class SafeHolder {
std::unique_ptr<int> ptr;
public:
SafeHolder(int val) : ptr(std::make_unique<int>(val)) {}
// 拷贝构造函数被隐式删除,编译不过
// SafeHolder(const SafeHolder&) = delete; // 编译器已干这事
SafeHolder(SafeHolder&&) = default; // 移动可以
};- 启用移动语义后,
a = std::move(b)是安全的,b.ptr自动置空,不会残留野指针 - 想支持拷贝?得自己写拷贝构造并显式
std::make_unique深拷贝,不能依赖默认行为 - 别用
std::shared_ptr图省事——它解决的是共享生命周期问题,不是深拷贝需求;两个shared_ptr指向同一份数据,还是浅语义
含指针成员的类,析构函数里 double free 最难调试
野指针崩溃不一定发生在访问时,常在析构阶段爆发:double free 或 free(): invalid pointer 这类错误信息,基本就是浅拷贝+多次析构的铁证。
- 开启 AddressSanitizer(
-fsanitize=address)能快速定位哪行delete在操作已释放内存 - 不要在析构函数里打日志或调用虚函数——此时虚表可能已被销毁,反而掩盖真正问题
- 所有动态分配的指针,必须确保:构造时
new、拷贝时new、赋值时先delete再new、析构时delete——四点缺一不可
深拷贝不是多写几行代码的事,是让每个对象彻底拥有自己的资源。一旦中间断链,野指针就在那儿等着,而且往往在最意想不到的时机触发。










