layout | title | date | categories |
---|---|---|---|
post |
异常处理实现 |
2018-10-10 20:00:05 +0800 |
系统 |
现在较高级的编程语言都有异常处理机制(try-catch-finally),下面探讨一下异常处理实现方法,本文说说 C 语言的异常处理机制。没错,C 语言也有异常处理机制。
C++ 中的异常处理机制(并没有 finally 区块):
try {
throw exception();
} catch (const exception &e) {
// exception handler
} catch(const other_exception &e) {
// exception handler
}
下面介绍如何使用 C 代码实现类似异常处理机制。
我们都知道 C 中支持 goto 语句,但是 goto 只能够在本方法内实现跳转,因为跨方法的 goto 需要保存、恢复寄存器、栈指针、PC 等信息。C 中还有两个函数 setjmp / longjmp,他们可以支持跨函数的跳转,setjmp 调用一次,可返回多次,由于降低程序的可读性,setjmp / goto 都不被推荐使用。
先看一段小 demo:
#include <setjmp.h>
#include <stdio.h>
void fn();
jmp_buf A;
int main() {
switch (setjmp(A)) {
case 0:
// first call normally return 0
fn();
break;
case 1:
// exception 1
printf("1\n");
break;
case 2:
// exception 2
printf("2\n");
break;
default:
// default handler
printf("default\n");
break;
}
{
// finally
printf("finally\n");
}
return 0;
}
void fn() {
int input;
scanf("%d", &input);
longjmp(A, input);
}
setjump 将当前栈的信息保存(save stack context,信息包括当前栈指针、PC)。在第一次调用 setjump(A)
时,将当前栈信息保存在 A 中,然后返回 0。下次调用 longjmp(A, input)
,将会跳回到 setjmp(A)
代码处,setjump(A) 返回 input,如果 input == 0,则返回 1,只允许第一次返回 0。
假如在 fn() 函数中,遇到异常情况,则调用 longjmp 调转到具体的异常处理函数中,需要给每一个异常定义一个编号(1\2\3)。如果没异常发生,不调用 longjmp,让 fn 正常返回,那么就不会触发异常处理。
相当于我们自己实现了 try-catch,注意,C++ 并不需要支持 finally 语句,因为 finally 语句块一定会被执行的,在真实编码中 finally 多用于资源的释放,C++ 的资源管理机制是 RAII,也就是通过构造哈数和析构函数来管理资源,在当前 scope 结束时,会调用对象的析构函数,可以理解为 C++ 在代码编译中进行了埋点。
setjmp 尽量只用作 switch 语句的选择,不然会产生令人费解的行为,比如以下代码:
#include <setjmp.h>
#include <stdio.h>
void fn();
jmp_buf A;
int main() {
int ret = setjmp(A);
printf("%d\n", ret);
fn();
return 0;
}
void fn() {
int input;
scanf("%d", &input);
longjmp(A, input);
}
这段代码会是一个无限循环,即使没有显示的 while / for 循环语句,会在标准输出流输出标准输入流的内容。同样 goto 也可以实现循环,goto 相当于无条件循环,映射为汇编语言的 jmp 指令,其实 while / for / if 编译为汇编代码后都会被转化为跳转指令。
编码时有异常跨越函数捕获的需求,在函数 B 中抛出的异常,在函数 A 中捕获,或异常一直到 main 函数没有被捕获就会导致整个程序退出,这又是如何实现的呢?
每一类异常定义唯一的 id 标识,每一个 try 块都对应一个 jmp_buf,每一个 catch 块都对应一个 switch 中的一个 case,如果所有 case 都没命中,那么跳转到 default,default 继续往上抛出异常。在函数的调用栈中维护一个有异常处理(try-catch)的块的 jmp_buf,那么 default 只需要找到栈顶 jmp_buf,然后调用 longjmp(stack_top_jmp_buf, exception_id)
即可甩锅给调用栈中更上层的 try-catch 块处理。
在 C++ 编程语言中尽可能使用 try-catch 语言级别的异常处理机制代替 setjmp / longjmp,因为 C++ 对象有析构函数,调用 longjmp 会导致析构函数无法被调用,会造成资源泄露。如果存在上述情况,编译器不允许这种行为发生,会报错(不信试试)。
了解高级语言中的异常处理机制对真实开发帮助不大,但是对于了解整个系统很有帮助,就像学习 Java 的同学会去了解 JVM 的垃圾回收机制,知其所以然。