异常处理

原理

栈展开(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 中给出用户友好的提示。