异常 try/catch
抛出、捕获、栈展开与异常安全
商用
◎学完你会
- 用 throw / try / catch 抛出并捕获异常;
- 理解异常处理时的栈展开与析构调用;
- 知道异常安全分级,避免资源泄漏。
1抛出异常,逐层捕获
throw 抛出后,try/catch 按类型匹配捕获;没有 catch 就一路展开到 main 并终止。
f():分母为 0 → throw runtime_error
g():调用 f()(不处理)
main:try 调 g(),catch 捕获
2关键命令
| 写法 | 含义 |
throw std::runtime_error("…"); | 抛出异常(带错误消息) |
try { … } | 包住可能抛异常的代码 |
catch (const std::exception& e) | 按类型捕获;e.what() 取消息 |
catch (...) | 捕获任意异常(兜底) |
✅ 按引用捕获(const T&)可避免切片,也能捕获到继承体系里的派生异常类型。
3栈展开:沿途析构都要执行
抛出异常后,从抛出点沿调用栈往回走(栈展开),沿途所有已构造的局部对象都会被析构,直到被 catch 接住。这正是 RAII 能在异常下自动释放资源的原因(呼应 28.4)。
void f(){
std::ifstream in("a.txt"); // 局部对象,离开作用域(含异常)自动析构
throw std::runtime_error("boom");
// 栈展开时 in 的析构仍会被调用 → 文件句柄关闭,不泄漏
}
try { f(); } catch(const std::exception& e){ cout << e.what(); }
注意:异常安全分基本/强/不抛三级。析构函数、swap、noexcept 函数里不能抛异常(否则 terminate)。持有裸 new 的内存若中途抛异常会泄漏 → 用 RAII/智能指针。
!易错点
① 忘记捕获,异常一路展开到 main 未处理——程序调用 std::terminate 直接终止。
② 按值捕获导致切片——捕获派生异常会切掉子类部分;用 const T&。
③ 在析构里抛异常——析构应 noexcept,抛异常会 terminate。
④ 裸 new 后抛异常泄漏——中途 throw 前没释放 → 用智能指针 / RAII 兜底。
?跨学科:异常,像"问题上报与逐级处理"
throw 像一线员工遇到问题上报:不就地糊弄,而是把问题甩给上层;catch 像各级主管:本层能处理的就处理,处理不了的继续往上报(栈展开)。
上报途中,"临时工位"(局部对象)会被收摊(自动析构)——就像 RAII:无论出没出事,离开岗位都要收尾,不把烂摊子留给下一层。这比"用返回值层层传错误码"(每层都得检查)更省心。
✎练一练
捕获异常应按引用写 catch (const std::exception& e),主要是为了?
按引用捕获可避免切片,也能捕获派生异常类型(如 runtime_error 继承自 exception)。
抛出异常、栈展开过程中,沿途已构造的局部对象会怎样?
栈展开时沿途局部对象都会析构,RAII 资源被自动释放,这正是异常下不泄漏的原因。
抛出的异常一路没有被任何 catch 接住,最终会发生什么?
未捕获的异常会触发 std::terminate,程序终止。故顶层应有 catch(...) 兜底。