异常 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(...) 兜底。