异常处理

原理

栈展开(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_面向对象(一)类与对象基础 — 构造/析构期间异常行为的底层原理。