c++异常处理要求throw与catch类型精确匹配,禁止隐式转换和多态转型;析构函数严禁抛异常;异常安全分三级,推荐复制-交换等手法实现强保证;禁用-fno-exceptions除非裸机等特殊场景。

throw 和 catch 必须类型匹配,否则异常会直接终止程序
很多初学者以为 catch (int) 能捕获所有数值型异常,其实不然——C++ 的异常匹配是静态、精确的,不进行隐式转换(比如 short 不会自动转成 int),也不支持多态向上转型(除非 catch 写的是引用或指针)。常见错误现象是:明明抛了 std::runtime_error,却写了 catch (std::exception)(值传递),导致对象被切片,丢失派生类信息。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 总是用
catch (const std::exception& e)捕获标准异常,避免切片和拷贝开销 - 自定义异常类务必继承
std::exception或其子类,并重载what() - 不要依赖
catch (...)做“兜底”——它无法获取异常信息,且掩盖了类型设计问题 - 编译时加
-fexceptions(GCC/Clang 默认开启,但交叉编译或嵌入式环境可能关闭)
析构函数里 throw 会导致 std::terminate 立即调用
C++ 明确禁止在析构函数中抛出未捕获的异常。一旦发生,程序直接调用 std::terminate(默认行为是 abort),没有回溯、没有日志、不可恢复。这不是风格问题,是语言硬性约束。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 析构函数必须是
noexcept(C++11 起默认隐式添加,显式写出来更清晰) - 如果析构中可能出错(比如关闭文件、释放网络连接),把可能失败的操作移到普通成员函数中,由用户显式调用并处理异常
- 实在需要记录错误(如日志),改用
std::cerr或回调,绝不能throw - RAII 对象(如
std::fstream)内部已遵守该规则,放心使用
异常安全有三个等级,强异常安全最难保证但最值得追求
“异常安全”不是非黑即白的概念,而是分层的:基本保证(不泄露资源、对象仍可析构)、强烈保证(操作失败则状态回滚到调用前)、不抛异常保证(如 swap、move 构造)。很多人误以为只要没内存泄漏就算安全,其实逻辑一致性更重要。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 优先用“复制-交换”(copy-and-swap)惯用法实现赋值运算符:先构造新副本,再交换,失败则旧对象不受影响
- 避免在构造函数体中做可能失败的初始化;改用成员初始化列表 +
std::unique_ptr管理堆资源,靠 RAII 自动回滚 - 容器操作(如
std::vector::push_back)只提供基本异常安全,扩容失败时已有元素仍有效,但新元素未插入——别假设它能回滚整个序列 - 第三方库文档若未明确声明异常安全等级,按“基本保证”处理
启用异常会略微增加二进制体积和运行时开销,但现代编译器优化后影响极小
有人因担心性能禁用异常(-fno-exceptions),结果发现 std::optional、std::variant、甚至某些 STL 实现(如 libc++ 的 std::thread)都依赖异常机制,强行关闭会导致链接失败或未定义行为。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 仅在裸机、内核模块、或硬实时系统等明确禁止异常的场景才关掉;普通应用别碰
-fno-exceptions - 异常开销主要在“抛出路径”(stack unwinding),而非
try块本身——没异常时几乎零成本 - 若真遇到 unwind 性能瓶颈(如高频错误路径),说明设计有问题:该用返回码或
std::expected(C++23)的地方用了异常 - 调试时可用
__cxa_throw符号下断点,观察异常实际抛出处
异常安全真正的难点不在语法,而在状态管理边界——你得清楚每一行代码执行后对象处于什么状态,而异常会让控制流跳转得毫无征兆。越是封装好的类,越容易藏匿未考虑异常路径的资源操作。










