本章目标:异常(try/catch/throw)、noexcept、错误码、 以及现代推荐的 std::expected。搞懂"什么时候用哪套"。
| 方案 | 特点 | 适合场景 |
|---|---|---|
| 返回错误码 | 老 C 风格,容易忘检查 | 系统层 |
| 异常 throw | 强制处理/传播,成本高 | 大规模、库 |
| optional/expected | 类型里带着"可能失败",显式 | 现代 C++ 首选 |
现代 C++ 的推荐次序:
#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() 返回描述)
异常会沿调用链向上传播,直到有人 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 的巨大优势:出错也绝不泄漏资源。
void log() noexcept { ... } // 承诺绝不抛异常
好处:
什么时候标 noexcept?
move 的默认规则:如果你定义了移动构造,记得标 noexcept。 标准库(vector 扩容)会检查 noexcept 决定搬元素用移动还是拷贝。
因为他们要求"任何一行都不能被中断"
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());
使用铁律:
├─ 失败原因不重要 → optional ├─ 失败原因重要 → expected ├─ 失败极罕见且语义复杂(如文件损坏)→ 异常 └─ 调用方一定会立刻处理 → expected(显式)
练习题
感受各自的调用方式差异。