异常处理(进阶)
复习
- 语言运行时:提供异常等隐藏服务
- 函数调用与运行栈:调用栈的结构
- 控制流:程序的执行路径
本章为进阶内容,零基础读者可以跳过,不影响后续阅读。
TL;DR
- 异常让错误可以跨多层调用向上传播
- 抛出异常时,运行时要“展开栈”,找到能处理它的地方
- 运行时通常维护一张异常表,记录哪里能处理什么
- 展开过程中还要正确执行清理工作
正文
程序出错时,错误可能发生在很深的调用里,而真正能处理它的代码却在很外层。用返回值一层层往上传递,太累。异常(exception)就是为了解决这件事:让错误“跳”过多层调用,直接送到能处理它的地方。
抛出与捕获
异常机制有两个关键动作:
- 抛出(throw):在出错处抛出一个异常
- 捕获(catch):在某处处理匹配的异常
当异常被抛出,运行时会沿着调用栈从内到外查找:哪个函数能处理这种异常?找到后,控制权就转到那里。
展开栈
这个“查找并转移”的过程,叫栈展开(stack unwinding):把中间那些“不负责处理”的函数栈帧,一层层弹掉。
但栈帧不能随便一弹了事。因为那些被跳过的函数,可能有需要清理的东西:
- 打开的文件要关闭
- 申请的资源要释放
- 对象要析构
所以展开过程中,运行时必须一边退栈,一边把每一层的清理工作做掉。“跳过”不是“扔掉”,该做的收尾还得做。
怎么知道哪里能处理
运行时凭什么知道“哪一层能处理这个异常”?常见做法是维护一张异常表:记录某段代码范围对应哪个处理入口。抛出异常时,就查这张表,找到匹配的处理者。
这种基于表格的方式,好处是正常执行时几乎没有额外开销——平时不必检查什么,只有真正抛异常时,才去查表。这也算一种“把开销留给异常路径、让正常路径更快”的设计取舍。
用异常处理错误,代码更清晰;而这份清晰,背后是一整套栈展开与异常表的机制。
思考题 1
异常处理为什么需要“展开栈”?
思考题 2
抛出异常时,为什么还要做“清理”工作?
小结
知识点
- 异常让错误跨多层调用向上传播
- 抛出时沿调用栈查找能处理它的地方
- 查找过程需要展开栈,逐层退栈
- 展开时要执行各层的清理,并借助异常表定位处理者
参考资料
- Wikipedia(zh):异常处理:跨调用传播错误的机制
- Wikipedia(zh):调用栈:栈展开所依赖的结构
思考题答案(仅供参考)
思考题 1
因为异常可能从很深的调用处抛出,而处理者在外层。要让控制权转移到处理者,就必须把中间那些不处理它的函数栈帧逐层弹出,也就是展开栈,才能回到能处理它的那一层。
思考题 2
因为被跳过的那些函数可能持有需要释放的资源,比如打开的文件、申请的内存、需要析构的对象。若不清理,这些资源就会泄漏。所以展开时每一步都要把该做的收尾做完,做到“跳过但不遗漏”。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪