必须显式定义拷贝构造函数和赋值运算符,或禁用拷贝(=delete),因默认浅拷贝会导致资源重复释放;移动操作须声明noexcept,否则vector扩容仍触发浅拷贝崩溃。

拷贝构造函数没写,std::vector 里存对象就崩
默认生成的拷贝构造函数只做成员逐字节复制,如果类里有指针或句柄(比如 int*、FILE*、std::mutex),浅拷贝会让两个对象共享同一块资源。析构时重复释放,double free 或 use-after-free 就来了。
典型场景:类里手动 new 了一块内存,又没重写拷贝构造函数和赋值运算符;或者把这种对象塞进 std::vector,一扩容就触发拷贝——崩溃往往发生在 vector 重新分配内存后旧对象析构时。
- 只要类里有裸指针、系统资源句柄、或任何需要“独占语义”的成员,就必须显式定义拷贝构造函数和
operator= - 更稳妥的做法是直接禁用拷贝:
= delete,改用移动语义或智能指针管理资源 - 别依赖编译器自动生成的拷贝行为,哪怕当前看起来“没出错”——它只是还没触发双重析构
std::unique_ptr 能自动防浅拷贝,但别误传引用
std::unique_ptr 的设计目标就是禁止拷贝、只允许移动,天然规避浅拷贝陷阱。但它不是万能护身符:如果你把 std::unique_ptr 成员通过引用传给函数,而函数内部又做了隐式拷贝(比如放进容器、返回局部变量),问题照样出现。
常见错误现象:std::unique_ptr 在函数返回后变空,或者运行时报 std::bad_weak_ptr(误混用了 std::shared_ptr 和 weak_ptr)。
立即学习“C++免费学习笔记(深入)”;
- 确认所有权转移路径:用
std::move()显式移交,别指望编译器猜你意图 - 避免在类中混合裸指针和
std::unique_ptr——统一资源管理方式,否则边界容易模糊 - 如果必须共享所有权,用
std::shared_ptr,但要小心循环引用;此时std::weak_ptr是解环关键
移动构造函数写了,但忘了 noexcept,std::vector 扩容仍可能拷贝
std::vector 在扩容时优先尝试移动元素,但前提是移动操作被标记为 noexcept。如果移动构造函数或移动赋值运算符没加 noexcept,标准库会退回到调用拷贝构造函数——这时候,如果拷贝构造函数没写或仍是浅拷贝,问题就重现了。
表现就是:程序在 vector 插入大量对象时突然崩溃,堆栈里看到两次析构同一指针,但你明明写了移动构造函数。
- 所有移动操作都应声明为
noexcept,除非你明确需要异常传播且接受性能代价 - 检查编译器警告:Clang/GCC 开启
-Wpessimizing-move可提示“本可移动却被降级为拷贝” - 用
static_assert(std::is_nothrow_move_constructible_v<myclass>)</myclass>在编译期验证
RAII 不是银弹,析构函数里抛异常会直接终止程序
RAII 的核心是“资源获取即初始化”,但很多人忽略:C++ 规定析构函数默认是 noexcept,如果在其中抛出未捕获的异常,会调用 std::terminate——程序静默退出,连堆栈都不打。
尤其容易踩坑的是文件关闭、网络连接断开、锁释放等操作,它们可能失败,但你不能在析构函数里用 throw 处理。
- 析构函数里所有可能失败的操作,必须用
try/catch(...)吞掉异常,或记录日志后忽略 - 不要在析构函数里调用可能抛异常的第三方函数,除非你已确认其
noexcept声明 - 资源清理逻辑复杂时,考虑把关键步骤拆到普通成员函数中,由用户显式调用并处理错误









