Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

异常处理(进阶)

复习

  • 语言运行时:提供异常等隐藏服务
  • 函数调用与运行栈:调用栈的结构
  • 控制流:程序的执行路径

本章为进阶内容,零基础读者可以跳过,不影响后续阅读。

TL;DR

  • 异常让错误可以跨多层调用向上传播
  • 抛出异常时,运行时要“展开栈”,找到能处理它的地方
  • 运行时通常维护一张异常表,记录哪里能处理什么
  • 展开过程中还要正确执行清理工作

正文

  程序出错时,错误可能发生在很深的调用里,而真正能处理它的代码却在很外层。用返回值一层层往上传递,太累。异常(exception)就是为了解决这件事:让错误“跳”过多层调用,直接送到能处理它的地方。

抛出与捕获

  异常机制有两个关键动作:

  • 抛出(throw):在出错处抛出一个异常
  • 捕获(catch):在某处处理匹配的异常

  当异常被抛出,运行时会沿着调用栈从内到外查找:哪个函数能处理这种异常?找到后,控制权就转到那里。

展开栈

  这个“查找并转移”的过程,叫栈展开(stack unwinding):把中间那些“不负责处理”的函数栈帧,一层层弹掉。

  但栈帧不能随便一弹了事。因为那些被跳过的函数,可能有需要清理的东西:

  • 打开的文件要关闭
  • 申请的资源要释放
  • 对象要析构

  所以展开过程中,运行时必须一边退栈,一边把每一层的清理工作做掉。“跳过”不是“扔掉”,该做的收尾还得做。

怎么知道哪里能处理

  运行时凭什么知道“哪一层能处理这个异常”?常见做法是维护一张异常表:记录某段代码范围对应哪个处理入口。抛出异常时,就查这张表,找到匹配的处理者。

  这种基于表格的方式,好处是正常执行时几乎没有额外开销——平时不必检查什么,只有真正抛异常时,才去查表。这也算一种“把开销留给异常路径、让正常路径更快”的设计取舍。

  用异常处理错误,代码更清晰;而这份清晰,背后是一整套栈展开与异常表的机制。

思考题 1

  异常处理为什么需要“展开栈”?

思考题 2

  抛出异常时,为什么还要做“清理”工作?

小结

知识点

  • 异常让错误跨多层调用向上传播
  • 抛出时沿调用栈查找能处理它的地方
  • 查找过程需要展开栈,逐层退栈
  • 展开时要执行各层的清理,并借助异常表定位处理者

参考资料

  1. Wikipedia(zh):异常处理:跨调用传播错误的机制
  2. Wikipedia(zh):调用栈:栈展开所依赖的结构

思考题答案(仅供参考)

思考题 1

  因为异常可能从很深的调用处抛出,而处理者在外层。要让控制权转移到处理者,就必须把中间那些不处理它的函数栈帧逐层弹出,也就是展开栈,才能回到能处理它的那一层。

思考题 2

  因为被跳过的那些函数可能持有需要释放的资源,比如打开的文件、申请的内存、需要析构的对象。若不清理,这些资源就会泄漏。所以展开时每一步都要把该做的收尾做完,做到“跳过但不遗漏”。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪