第十五章 错误处理:程序出问题时的体面做法

← 返回目录

本章目标:异常(try/catch/throw)、noexcept、错误码、 以及现代推荐的 std::expected。搞懂"什么时候用哪套"。

15.1 错误处理的三条路(总览)

方案特点适合场景
返回错误码老 C 风格,容易忘检查系统层
异常 throw强制处理/传播,成本高大规模、库
optional/expected类型里带着"可能失败",显式现代 C++ 首选

现代 C++ 的推荐次序:

  1. 先想 optional / expected(显式、无隐藏成本)
  2. 系统边界(main 附近)用异常
  3. 绝不裸返回 -1 这种魔法值

15.2 异常基础(老代码里到处都是,必须会读)

#include <stdexcept>

double divide(double a, double b) {
    if (b == 0.0) throw std::invalid_argument("除数不能为 0");
    return a / b;
}

int main() {
    try {
        double r = divide(1.0, 0.0);
        std::println("结果 {}", r);
    } catch (const std::invalid_argument& e) {
        std::println("出错:{}", e.what());
    } catch (const std::exception& e) {
        std::println("其他错误:{}", e.what());
    } catch (...) {
        std::println("未知错误");
    }
}

术语:

标准异常类型:

std::runtime_error     运行期错误(最常用)
std::invalid_argument  参数非法
std::out_of_range      越界(vector::at 会抛它)
std::length_error      长度超限
std::bad_alloc         内存分配失败
全部继承自 std::exception(有 .what() 返回描述)

15.3 异常传播:不接就往上抛

异常会沿调用链向上传播,直到有人 catch。 没人接 → 程序崩溃(terminate)。

void a() { b(); }      // a 里没 try
void b() { c(); }
void c() { throw std::runtime_error("坏了"); }
// 异常从 c → b → a → main,main 也没接 → 崩溃

关键性质:传播过程中,**栈上的对象会被正常析构**! 这就是 RAII 和异常能共存的原因:

void f() {
    auto f = std::ofstream("log.txt");  // 打开
    throw std::runtime_error("x");      // 抛异常
    // f 离开作用域 → 文件自动关闭!没有泄漏
}

这个性质叫"栈展开"(stack unwinding), 是 C++ 相比 C 的巨大优势:出错也绝不泄漏资源。

15.4 noexcept:声明"我不会抛异常"

void log() noexcept { ... }   // 承诺绝不抛异常

好处:

  1. 编译器可以优化(不用维护异常状态)
  2. 一旦真的抛了 → 直接 terminate(立刻终止,不静默吞掉)

什么时候标 noexcept?

move 的默认规则:如果你定义了移动构造,记得标 noexcept。 标准库(vector 扩容)会检查 noexcept 决定搬元素用移动还是拷贝。

15.5 异常的性能与争议

因为他们要求"任何一行都不能被中断"

15.6 std::optional 与 std::expected:现代显式错误

optional(可能没有值,C++17):

std::optional<int> find_first_even(const std::vector<int>& v) {
    auto it = std::find_if(v.begin(), v.end(),
                           [](int x){ return x % 2 == 0; });
    if (it == v.end()) return std::nullopt;
    return *it;
}

expected(有值 or 有错误,C++23):

std::expected<double, std::string>
sqrt_checked(double x) {
    if (x < 0) return std::unexpected("负数不能开平方");
    return std::sqrt(x);
}

auto r = sqrt_checked(-4);
if (r) std::println("{}", *r);
else   std::println("失败:{}", r.error());

使用铁律:

15.7 实战决策树(背下来)

  1. 函数可能失败?

├─ 失败原因不重要 → optional ├─ 失败原因重要 → expected ├─ 失败极罕见且语义复杂(如文件损坏)→ 异常 └─ 调用方一定会立刻处理 → expected(显式)

  1. 函数不可能失败 → 不用任何东西 + noexcept
  2. 跨系统边界(main、库接口)→ 捕获并转成错误对象

15.8 防御性编程习惯

本章小结

练习题


  1. 用 expected 写一个解析函数:字符串转 int,失败返回错误描述。
  2. 写一个函数读取文件为 string,用 expected 表达"文件不存在"。
  3. 观察:vector::at 越界会抛什么异常?catch 住打印 what()。
  4. 写一个 noexcept 函数并在里面故意 throw,看程序怎么死。
  5. 把"除法"写成三种版本:错误码版、异常版、expected 版,

感受各自的调用方式差异。