应使用 const 引用捕获异常(如 catch (const std::exception& e)),避免值传递导致拷贝开销、切片或二次异常;析构函数禁止抛异常;throw 应使用标准或自定义异常类而非裸字符串;catch 块须记录日志、重新抛出或转换错误,不可静默吞没。

catch 里别用值传递 std::exception
值传递会触发拷贝,而异常对象可能很大,或者拷贝构造函数抛出新异常——这会让程序直接调用 std::terminate。更糟的是,如果派生类重写了 what(),值传递会切片(slicing),丢失具体类型信息。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 一律用 const 引用:
catch (const std::exception& e) - 想捕获所有 C++ 异常?用
catch (const std::exception& e)而不是catch (...)——后者不带类型信息,连e.what()都调不了 - 如果必须处理非
std::exception类型(比如int或原始指针),单独写一个catch (int),但要清楚这是反模式,只应在封装 C API 时临时妥协
析构函数里禁止抛异常(noexcept 是默认契约)
析构函数抛异常是 C++ 的雷区:如果栈展开过程中另一个异常正在传播,而此时又抛出新异常,std::terminate 立刻触发。C++11 起,所有析构函数隐式为 noexcept(true),显式抛异常会编译失败(除非你手动加 noexcept(false),但别这么干)。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 在析构函数里做清理时,把可能抛异常的操作包进
try-catch,并吞掉或记录错误,绝不让异常逃出函数体 - 资源管理优先用 RAII(如
std::unique_ptr、std::fstream),它们的析构函数都保证不抛异常 - 如果某类资源释放确实可能失败(比如网络 socket 关闭失败),设计成“可检查失败”而非“抛异常”,例如提供
close()成员函数并返回bool或std::error_code
throw 表达式后面别跟裸字符串字面量
throw "oops"; 看起来简单,但它抛的是 const char*,不是 std::exception 子类。这意味着:无法用 catch (const std::exception&) 捕获;没有 what();不能携带额外上下文(比如错误码、文件名、行号)。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 统一用标准异常类:
throw std::runtime_error("failed to open file: " + filename); - 需要结构化错误信息?自定义异常类,继承
std::runtime_error,并在构造函数里存入std::error_code或上下文字段 - 避免
throw std::exception()这种空异常——它没消息,调试时只能靠堆栈,毫无意义
不要在 catch 块里“吃掉”异常却不留痕迹
写 catch (...) { } 或 catch (const std::exception&) { return false; } 看似简化逻辑,实际等于把错误静默丢进黑洞。上游调用方完全不知道发生了什么,日志里也没线索,问题只能靠现象倒推。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 每个
catch至少做三件事之一:重新抛出(throw;)、记录日志(用e.what()和当前上下文)、转成上层能理解的错误返回(比如返回std::nullopt或std::expected) - 日志里别只记
e.what(),加上函数名、文件、行号(可用__func__、__FILE__、__LINE__) - 如果真要静默处理(极少数情况,比如 UI 层忽略某个已知可恢复的 IO 错误),注释必须写清“为什么可以忽略”和“后果是什么”
异常安全最难的部分不是语法,而是判断哪些代码路径必须强异常安全(比如容器插入中途失败不能破坏原有状态),以及什么时候该用异常、什么时候该用返回值。这两者的边界,往往比 try 和 catch 的括号还难画准。









