异常处理
原理
栈展开(Stack Unwinding)
当 throw 执行时,运行时系统沿调用链向上追溯,逐层销毁各栈帧中的局部对象(调用其析构函数),直到找到匹配的 catch 块。若追溯到 main() 仍未找到,则调用 std::terminate() 终止程序。
main() → funcA() → funcB() → funcC()
→ throw!
→ ~funcC() 局部对象析构
→ ~funcB() 局部对象析构
→ funcA() 中匹配 catch
RAII 与异常的绑定关系
C++ 没有 finally 子句——因为在 RAII 模型下不需要它。栈展开过程中,所有成功构造的局部对象(智能指针、文件流、互斥锁等 RAII 对象)的析构函数都会被自动调用。这意味着 try-catch 块内不需要手动清理代码——构造即获取资源,析构即释放资源。
零开销异常模型(Itanium ABI)
GCC/Clang 采用 Itanium C++ ABI 的零开销异常模型:
- 正常路径(不抛异常):代码执行零额外开销。不维护额外的运行时记录,不进行额外的条件检查
- 异常路径(抛出异常):开销很大——运行时需要遍历异常表(
.eh_frame段),进行类型匹配,调用大量析构函数
实现依赖 LSDA(Language-Specific Data Area)表和 DWARF 调试信息进行栈回溯。
异常表(Exception Table)
编译器在编译时生成异常表,存储在目标文件的 .eh_frame 段中。异常表包含:
- 每个函数中可能抛出异常的代码区域
- 每个
try块对应的catch块地址 - 需要进行栈展开时局部对象的析构函数地址
catch 的类型匹配
catch 按照先派生后基类的顺序进行类型匹配。错误地将基类 catch 放在派生类 catch 之前,会导致派生类异常被基类截获(编译器通常对此发出警告)。catch(...) 可捕获所有类型,但无法获取异常对象。
构造/析构函数中的异常
构造函数抛出异常时,已初始化的成员和基类子对象会被正确析构(栈展开),但对象本身不被视为已构造完成。析构函数默认 noexcept——在析构函数中抛出异常几乎总是导致 std::terminate()。
构造函数的 try 块(function try block)可捕获初始化列表中的异常,但异常会被自动重新抛出——对象构造仍然失败。
语法
throw / try / catch
throw std::runtime_error("错误信息"); // 抛出异常(推荐抛出 std::exception 派生类)
try {
// 可能抛出异常的代码
} catch (const std::runtime_error& e) {
// 按引用捕获
} catch (const std::exception& e) {
// 先派生后基类
} catch (...) {
// 捕获所有(无法获取异常对象)
throw; // 重新抛出原始异常(保留类型)
}标准异常层次
std::exception
├── std::logic_error (程序逻辑错误)
│ ├── std::invalid_argument
│ ├── std::out_of_range
│ └── std::length_error
├── std::runtime_error (运行时错误)
│ ├── std::overflow_error
│ ├── std::underflow_error
│ └── std::range_error
├── std::bad_alloc (new 失败)
├── std::bad_cast (dynamic_cast 引用版本失败)
└── std::bad_typeid
自定义异常
class MyException : public std::runtime_error {
int errorCode;
public:
MyException(int code, const std::string& msg)
: std::runtime_error(msg), errorCode(code) {}
int getErrorCode() const { return errorCode; }
};noexcept(C++11)
void noThrowFunc() noexcept { /* 保证不抛异常 */ }
// 如果 noexcept 函数抛出异常 → std::terminate()
// 条件 noexcept:只有移动操作不抛异常时才标记
template<typename T>
void swap(T& a, T& b) noexcept(
std::is_nothrow_move_constructible_v<T> &&
std::is_nothrow_move_assignable_v<T>);
// noexcept 运算符:编译期检测
static_assert(noexcept(1 + 2)); // true析构函数默认
noexcept(true)。移动构造/赋值标记noexcept可提升std::vector扩容性能(避免回退到拷贝构造)。
异常安全保证级别
| 级别 | 含义 | 典型实现 |
|---|---|---|
| 不抛出保证 | 函数绝无异常 | 析构函数、noexcept 函数 |
| 强保证 | 要么完全成功,要么完全不变 | copy-and-swap 惯用法 |
| 基本保证 | 对象仍可析构,但内容可能已改变 | std::vector::push_back |
实践
力扣题目:无专属练习题。异常处理在算法竞赛中不常用,但在工程中至关重要——建议在项目实践中练习:写一个文件读取函数,文件不存在时抛出自定义异常并在上层 catch 中给出用户友好的提示。
AI 自检:要求 AI 解释以下代码中 delete 是否能被正确执行——需要从栈展开角度回答:
class Resource { /* ... */ };
void f() {
Resource* p = new Resource();
throw std::runtime_error("error");
delete p; // 是否执行?
}
// 如何改写为 RAII 风格?建议先阅读:04_动态内存 — RAII 与智能指针的异常安全保证;05_面向对象(一)类与对象基础 — 构造/析构期间异常行为的底层原理。