构造函数资源获取失败必须抛异常,因无返回值且不能留半构造对象;析构函数须声明noexcept,避免栈展开时二次异常终止程序;swap和赋值应提供noexcept强保证;异常传播需设边界,仅在关键入口捕获并决策。

构造函数里资源获取失败必须用异常
构造函数没有返回值,一旦初始化中途出错(比如fopen失败、new抛std::bad_alloc),你无法返回错误码,也不能留下一个“半构造”的对象——它可能持有部分资源、指针未初始化、不变量被破坏。此时唯一安全的做法是抛异常,触发栈展开,让已构造的成员自动析构。
常见错误是手动管理裸指针或文件句柄后,在构造失败时只delete或fclose一部分资源,却忘了其他成员;或者试图用goto error清理,但漏掉某个成员的析构逻辑。
- 正确做法:用
std::unique_ptr、std::fstream等RAII类型封装资源,它们的构造函数若失败就直接抛,析构函数保证释放 - 避免在构造函数里调用可能抛异常的自定义函数,除非你已为整个构造过程设计好回滚路径
- 如果必须做多步初始化,考虑拆成两阶段:先构造轻量对象,再用
init()方法(返回std::expected或抛异常)完成重操作
析构函数必须声明noexcept
析构函数里抛异常是C++的硬性雷区。当栈展开过程中,另一个异常正在传播,此时析构函数再抛异常,程序会直接调用std::terminate退出——连日志都来不及打。
典型误用包括:在~FileHandler()里调用fclose后不检查返回值,或在~DatabaseConnection()里执行rollback()并把网络错误转为异常。
立即学习“C++免费学习笔记(深入)”;
- 所有析构函数默认隐式
noexcept(true),显式写noexcept更清晰 - 若析构中需处理可能失败的操作(如
close()系统调用),必须用try/catch(...)吞掉异常,或记录日志但绝不向外抛 - 不要依赖“这个操作肯定不会失败”——磁盘满、权限变化、内核OOM都可能让
close失败
swap和赋值运算符要提供noexcept强保证
容器(如std::vector)扩容、std::sort重排元素时,会大量调用swap。如果swap可能抛异常,整个操作就退化为基本保证,甚至导致数据损坏。
例如std::vector<t></t>在resize时,若T的swap没标记noexcept,它只能逐个移动元素并做异常回滚,性能差且逻辑复杂;反之则可直接交换指针,原子完成。
- 用
friend void swap(X& a, X& b) noexcept实现非成员swap,内部只交换noexcept成员(如原始指针、int、std::unique_ptr) - 赋值运算符优先用copy-and-swap惯用法:
X temp(other); swap(*this, temp);,前提是swap是noexcept - 别给
swap加日志或锁——这些操作本身就不该出现在交换逻辑里
跨函数调用链的异常传播要设边界
不是每个函数都要catch异常。盲目捕获再忽略(catch(...) { /* silence */ })或只打日志不重抛,会让错误静默丢失,状态不一致。
真正需要catch的地方极少:通常是模块入口(如线程主函数)、RPC handler、或明确要降级的场景(如缓存加载失败切到DB)。
- 业务逻辑层尽量不
catch,让异常向上传播到有上下文做决策的位置(比如“重试”还是“告警”) - 若必须
catch,按引用捕获具体类型:catch (const std::runtime_error& e),避免catch (std::exception e)造成切片 - 第三方库调用(如
libcurl)若文档未说明是否抛异常,统一用extern "C"封装或noexceptwrapper隔离
异常安全最难的部分,从来不是语法怎么写,而是判断“这个状态变更到底能不能被中断”。一次std::vector::push_back看似简单,背后涉及内存分配、元素拷贝、size更新三步——任意一步失败,你得确保vector要么完整插入,要么完全不变。这种粒度的思考,没法靠工具自动补全。


















