异常处理
原理
栈展开(Stack Unwinding)
当 throw 执行时,运行时系统沿调用链向上追溯,逐层销毁各栈帧中的局部对象(调用其析构函数),直到找到匹配的 catch 块。若追溯到 main() 仍未找到,则调用 std::terminate() 终止程序。
main() → funcA() → funcB() → funcC()
→ throw!
→ ~funcC() 局部对象析构
→ ~funcB() 局部对象析构
→ funcA() 中匹配 catch
```cpp
### 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
```cpp
throw std::runtime_error("错误信息"); // 抛出异常(推荐抛出 std::exception 派生类)
try {
// 可能抛出异常的代码
} catch (const std::runtime_error& e) {
// 按引用捕获
} catch (const std::exception& e) {
// 先派生后基类
} catch (...) {
// 捕获所有(无法获取异常对象)
throw; // 重新抛出原始异常(保留类型)
}
```cpp
### 标准异常层次
```cpp
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
```asm
### 自定义异常
```cpp
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; }
};
```cpp
### noexcept(C++11)
```cpp
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
```cpp
> 析构函数默认 `noexcept(true)`。移动构造/赋值标记 `noexcept` 可提升 `std::vector` 扩容性能(避免回退到拷贝构造)。
### 异常安全保证级别
| 级别 | 含义 | 典型实现 |
|---|---|---|
| 不抛出保证 | 函数绝无异常 | 析构函数、`noexcept` 函数 |
| 强保证 | 要么完全成功,要么完全不变 | copy-and-swap 惯用法 |
| 基本保证 | 对象仍可析构,但内容可能已改变 | `std::vector::push_back` |
---
## 实践
**练习**:无专属练习题。异常处理在算法竞赛中不常用,但在工程中至关重要——建议在项目实践中练习:写一个文件读取函数,文件不存在时抛出自定义异常并在上层 catch 中给出用户友好的提示。