C++ 异常处理入门(try / catch / throw)
程序运行中会出现各种「特殊情况」,导致后续逻辑无法正常执行:文件打不开、内存不足、网络超时、除数为零……这类情况统称为异常。本文介绍 C++ 异常处理的三个关键字、catch 的匹配顺序,以及重新抛出的用法。
一、三个关键字
| 关键字 | 作用 |
|---|---|
| try | 包裹一段「可能出问题」的代码,检查其中产生的异常 |
| catch | 捕获并处理 try 中产生的异常 |
| throw | 在 try 块或它调用的函数里,抛出异常 |
配合使用的基本形态:
1 | try { |
try 块里只要有一处 throw 被执行,后面的语句就不再执行,控制流立刻跳到匹配的 catch。
二、第一个例子:除零异常
1 |
|
输出:
1 | 异常:Divided by zero! |
几点值得注意:
catch (runtime_error& e)里的&是引用,这是推荐写法。用引用可以避免一次对象拷贝,也能正确捕获派生类异常(值传递会发生「对象切片」,丢失派生类信息)。e.what()是std::exception提供的虚函数,返回构造异常时传入的描述字符串。- 除零在 C++ 里并不会自动抛异常(整数除零是未定义行为),所以这里必须手动判断、手动
throw。
三、可以抛出任意类型
C++ 允许把各种类型的变量、对象、指针作为异常抛出,catch 里写对应的类型即可:
1 |
|
1 |
|
不过实践中不建议抛基本类型或裸字符串,原因有两个:
- 无法携带更多信息(错误码、上下文等);
- 容易和普通类型混淆,
catch的语义不清晰。
更好的做法是抛出继承自 std::exception 的类对象,或自定义一个异常类,这样能统一用 e.what() 取描述、统一被 catch (exception&) 兜底。
四、多个 catch 与匹配顺序
一个 try 后面可以跟多个 catch,它们自上而下依次尝试匹配,第一个匹配成功的会被执行,后面的全部跳过。
因此顺序至关重要:派生层次越深的(越专用的)越放前面,越通用的越放后面。 如果把通用异常放在前面,它会把后面的专用异常全都拦住,专用分支永远得不到执行。
先看一下标准库异常的继承关系,这解释了为什么顺序会出错:
graph TD
E["std::exception"] --> L["std::logic_error"]
E --> R["std::runtime_error"]
L --> I["std::invalid_argument"]
L --> O["std::out_of_range"]
R --> V["std::overflow_error"]
logic_error 和 runtime_error 是兄弟关系(都直接继承 exception),彼此不构成父子。所以正确的顺序是:
1 | try { |
反例(错误写法):
1 | // 错误:logic_error 永远匹配不到,是死代码 |
编译器通常不会对这种情况报错,属于典型的「能编译、逻辑错」的 bug,写的时候要特别留意。
五、catch(...):捕获所有异常
对于不知道具体类型的异常,可以用 catch (...) 兜底:
1 | catch (...) { |
它可以匹配任何类型的异常且不需要写出类型,一般放在所有 catch 的最后。
⚠️ 但要注意:既然不知道异常类型,也就拿不到 e.what(),无法给出有意义的错误信息。所以不要无脑用它吃掉所有异常——如果一段代码根本没能力处理这个异常,正确做法是让它继续向上传播,交给能处理的调用方,或者干脆让程序终止并报错。悄悄吞掉异常会让问题被掩盖,后面排查起来非常痛苦。
catch (...) 的合理用途通常只有两个:在最外层做兜底日志(记录后重新抛出),以及资源清理。
六、重新抛出:throw;
如果 catch 里做了必要的清理,但自己处理不了这个异常,可以用不带操作数的 throw; 把它原样抛给上层:
1 | void func() { |
要注意 throw; 和 throw e; 的区别:
1 | catch (exception& e) { |
throw e; 会以静态类型 exception 复制出一个新异常,原本的派生类型信息(以及附加数据)全部丢失。重新抛出永远用裸 throw;。
七、小结
- 异常用于处理「导致后续逻辑无法继续」的意外情况,三个关键字是
try/catch/throw; try中抛出异常后,其后的语句不再执行,控制流直接跳到匹配的catch;catch的参数推荐用引用(异常类型& e),避免拷贝和对象切片;- 可以抛任意类型,但推荐抛继承自
std::exception的类对象,便于统一用e.what()取信息; - 多个
catch自上而下匹配,专用异常在前、通用异常在后,否则专用分支成为死代码; catch (...)能捕获所有类型,但不要用它默默吞掉异常——处理不了就让它向上传播;- 重新抛出用裸
throw;,用throw e;会丢失派生类信息。
