异常是程序在无法按正常路径继续时,向调用链上层报告错误的一种机制。抛出异常后,控制沿栈展开直至匹配的 catch 处理;未捕获异常通常导致 std::terminate。本节说明 try、throw、catch 的基本语法、标准异常层次、与错误码的选择,以及与 RAII 的关系。

基本结构

C++ 异常捕获运行
#include <iostream>
#include <stdexcept>

double divide(double a, double b) {
    if (b == 0.0) {
        throw std::invalid_argument("divisor is zero");
    }
    return a / b;
}

int main() {
    try {
        std::cout << divide(10, 0) << std::endl;
    } catch (const std::invalid_argument& e) {
        std::cerr << e.what() << std::endl;
    }
    return 0;
}

catch 按类型匹配;派生类处理器应写在基类之前,否则派生对象可能被基类 catch 截获。捕获常用 const std::exception& 只读访问 what();捕获非引用会切片,应避免 catch (std::exception e)。

标准异常层次(节选)

类型典型含义
std::logic_error 派生编程错误,如 invalid_argument、out_of_range
std::runtime_error 派生运行期错误,如 overflow_error
std::bad_allocnew 失败

库函数在参数无效时常抛 std::out_of_range(如 vector::at)。自定义类型可继承 std::exception 并实现 what()。

栈展开与 RAII

异常从 work 抛到 main 的 catch 时,中间尚未离开的局部对象会按声明的反序析构,这就叫栈展开。因此 FileGuard、unique_ptr、lock_guard、文件流等 RAII 对象仍会释放资源。若只用裸 new 且未在抛出前 delete,这块内存就会泄漏。下面与析构函数里的 FileGuard 相同:即使 throw 了,也会先打印 close 再进入 catch。

C++ 异常时仍析构运行
#include <iostream>
#include <stdexcept>

class FileGuard {
public:
    FileGuard() { std::cout << "open\n"; }
    ~FileGuard() { std::cout << "close\n"; }
};

void work() {
    FileGuard g;                          // 先 open
    throw std::runtime_error("fail");     // 离开 work 前仍析构 g
}

int main() {
    try {
        work();
    } catch (const std::runtime_error& e) {
        std::cout << e.what() << "\n";
    }
    return 0;
}

输出:

open
close
fail

若把 FileGuard g 换成 int* p = new int(1) 且不 delete,throw 之后 p 指向的内存不会自动释放。日常应使用智能指针或上述包装类。

重新抛出与嵌套

内层可以先记下日志,再用无操作数的 throw; 把同一个异常原样交给外层。这与 throw e; 不同:后者可能切片或换成副本。下面 middle 嵌套在 main 的 try 里,inner 抛出后先被 middle 接到,再重新抛到 main。

C++ 重新抛出运行
#include <iostream>
#include <stdexcept>

void inner() {
    throw std::runtime_error("disk full");
}

void middle() {
    try {
        inner();
    } catch (...) {
        std::cout << "middle: log and rethrow\n";
        throw;  // 原样抛给上层,类型仍是 runtime_error
    }
}

int main() {
    try {
        middle();
    } catch (const std::runtime_error& e) {
        std::cout << "main: " << e.what() << "\n";
    }
    return 0;
}

输出:

middle: log and rethrow
main: disk full

catch (...) 能接到任意类型,但读不到 what(),所以这里只记一句再 throw;。若内层已经处理完毕、不必再往上走,就不要重新抛出。

何时使用异常

  • 适合:无法本地恢复的致命错误、构造函数失败(无法返回错误码)、跨多层传播的错误。
  • 慎用:预期内的控制流、高频路径;可考虑 std::optional 或错误码(optional)。
  • noexcept:承诺不抛异常的函数可标记 noexcept,违反时直接 terminate;移动操作常标记 noexcept 以利容器优化。

与 C 语言返回错误码不同,异常会跳过中间层的显式检查,但代价是控制流不直观;团队应统一边界风格(库接口抛异常还是返回 expected / optional)。

本节说明了异常处理的基本流程。下一节介绍文件流:用 fstream 读写磁盘文件。