1、C语言异常处理机制为您的 C程序添加异常处理1、什么是异常异常一般指的是程序运行期(Run-Time)发生的非正常情况。异常一般是不可预测的,如:内存不足、打开文件失败、范围溢出等。UNIX 使用信号给出异常,并当发生异常时转跳到信号处理过程进行异常处理。DOS 下的信号对比 UNIX系统而言相对较少。C标准库提供两个特殊的函数:setjmp() 及 longjmp(),这两个函数是结构化异常的基础,正是利用这两个函数的特性来实现异常。所以,异常的处理过程可以描述为这样:首先设置一个跳转点(setjmp() 函数可以实现这一功能),然后在其后的代码中任意地方调用 longjmp() 跳转回这个
2、跳转点上,以此来实现当发生异常时,转到处理异常的程序上,在其后的介绍中将介绍如何实现。setjmp() 为跳转返回保存现场并为异常提供处理程序,longjmp() 则进行跳转(抛出异常),setjmp() 与 longjmp() 可以在函数间进行跳转,这就像一个全局的 goto 语句,可以跨函数跳转。举个例子,程序在 main() 函数内使用 setjmp() 设置跳转,并调用另一函数 A,函数 A内调用 B,B 抛出异常(调用 longjmp() 函数),则程序直接跳回到 main() 函数内使用 setjmp() 的地方返回,并且返回一个值。2、jmp_buf 异常结构使用 setjmp(
3、) 及 longjmp() 函数前,需要先认识一下 jmp_buf 异常结构。jmp_buf 将使用在 setjmp() 函数中,用于保存当前程序现场(保存当前需要用到的寄存器的值),jmp_buf 结构在 setjmp.h 文件内声明:typedef structunsigned j_sp; / 堆栈指针寄存器unsigned j_ss; / 堆栈段unsigned j_flag; / 标志寄存器unsigned j_cs; / 代码段unsigned j_ip; / 指令指针寄存器unsigned j_bp; / 基址指针unsigned j_di; / 目的指针unsigned j_es
4、; / 附加段unsigned j_si; / 源变址unsigned j_ds; / 数据段 jmp_buf;jmp_buf 结构存放了程序当前寄存器的值,以确保使用 longjmp() 后可以跳回到该执行点上继续执行。3、setjmp() 与 longjmp() 函数详细说明setjmp() 与 longjmp() 函数原型如下:void _Cdecl longjmp(jmp_buf jmpb, int retval);int _Cdecl setjmp(jmp_buf jmpb);_Cdecl 声明函数的参数使用标准 C的进栈方式(由右向左)压栈,_Cdecl 是 C语言的一种调用约定,
5、除此以外,PASCAL 也是调用约定之一。C 标准调用约定(_Cdecl)所声明的函数不自动清除堆栈,这一事务由调用者自行负责这也是 C可以支持不固定个数的参数的原因。此外,这一调用约定将在函数名前添加一个下划线字符,如某一函数声明为:int cdecl DoSomething(void);编译时将自动为 DoSomething 加上下划线前缀,即函数名变为: _DoSomething。setjmp() 与 longjmp() 函数都使用了 jmp_buf 结构作为形参,它们的调用关系是这样的:首先调用 setjmp() 函数来初始化 jmp_buf 结构变量 jmpb,将当前CPU中的大部分
6、影响到程序执行的寄存器的值存入 jmpb,为 longjmp() 函数提供跳转,setjmp() 函数是一个有趣的函数,它能返回两次,它应该是所有库函数中唯一一个能返回两次的函数,第一次是初始化时,返回零,第二次遇到 longjmp() 函数调用后,longjmp() 函数使 setjmp() 函数发生第二次返回,返回值由 longjmp() 的第二个参数给出(整型,这时不应该再返回零)。在使用 setjmp() 初始化 jmpb 后,可以其后的程序中任意地方使用 longjmp() 函数跳转会 setjmp() 函数的位置,longjmp() 的第一个参数便是 setjmp() 初始化的 j
7、mpb,若想跳转回刚才设置的 setjmp() 处,则 longjmp() 函数的第一个参数是 setjmp() 所初始化的 jmpb 这个异常,这也说明一件事,即 jmpb 这个异常,一般需要定义为全局变量,否则,若是局部变量,当跨函数调用时就几乎无法使用(除非每次遇到函数调用都将 jmpb 以参数传递,然而明显地,是不值得这样做的);longjmp() 函数的第二个参数是传给 setjmp() 的第二次返回值,这在介绍 setjmp() 函数时已经介绍过。下面是 setjmp() 函数与 longjmp() 函数的一个示意图:通过示意图可以看出 setjmp() 函数与 longjmp()
8、 函数的关系,如何跳转。4、异常处理过程先来对比(参考)一下 C+ 的异常处理,C+ 在语言层上便添加了异常处理机制,使用 try 块来包含那些可能出现错误的代码,你可以在 try 块代码中抛出异常,C+ 使用 throw 来抛出异常。抛出异常后,将转到异常处理程序中执行,C+ 使用 catch 块来包含那些处理异常的代码,catch 块可以接收不同类型的异常。需要说明的是,throw 一般不在 try 块内的代码中抛出异常,try 块内的代码调用了别的函数,如函数 A,函数 A 又调用了函数 B,throw 可以在函数 B中抛出异常,或者更深的函数调用层,无论如何,只要有异常抛出,程序将转到
9、 catch 处执行。C中如何实现,或者明确地说是模拟这一功能?下面介绍的是一些简单的方法。现在假设 longjmp() 第二个值为 1,即 setjmp() 第二次将返回 1。我们使用一组简单的宏来替代 setjmp() 和 longjmp() 以便使用:首先定义一个全局的异常:jmp_buf Jump_Buffer;因为 setjmp() 第一次调用初始化后返回 0,第二次返回非 0,可以这样定义一个宏使得它功能接近于 C+ 的 try。#define try if(!setjmp(Jump_Buffer)当 setjmp() 函数第一次 0 时,取非为真,则执行 try 块内的代码,如:
10、tryTest();当因为调用 longjmp() 抛出异常而导致 setjmp() 第二次返回时(程序将会转到 setjmp() 函数处返回,这时,这时应该执行的是异常处理代码。longjmp() 使 setjmp() 函数返回非 0,if(!setjmp(JumpBuffer) 中将值取非则为假,是以,异常处理放在其后应该使用一个 else:#define catch else如此看起来便跟 C+ 相似了,setjmp() 函数的第二次返回导致 if() 中表达式值为假,刚好使 catch 块得以执行,如:tryTest();catchputs(“Error“);实现如 C+ 的 thro
11、w 语句,事实上以宏替换 longjmp(jmp_buf, int) 的调用:#define throw longjmp(Jump_Buffer, 1)下面的例程解释如何使用这些宏:/* 输入一个整型数,如果大于 100,则以异常抛出 */i nclude “stdio.h“i nclude “conio.h“i nclude “setjmp.h“jmp_buf Jump_Buffer;#define try if(!setjmp(Jump_Buffer)#define catch else#define throw longjmp(Jump_Buffer, 1)int Test(int T)
12、;int Test_T(int T);int Test(int T)if(T 100) throw;elseputs(“OK.“);return;int Test_T(int T)Test(T);return;int main(void)int T;tryputs(“Input a value:“);scanf(“%d“, T+;Test_T(T);catchputs(“Input Error!“);getch();return 0;当遇到 throw 抛出异常,立即转跳到 setjmp 处执行,屏弃了与之无相关的枝节(函数的返回及 throw 其后的代码)main setjmp() - Te
13、st_T - Test throw 当输入一个大于 100的整数,throw 导致异常抛出,使用 1 返回到 setjmp() 函数处,宏 try 使 if(!setjmp(Jump_Buffer) 不成立,执行 catch 块,catch 块是十分简单的 else 分支语句关键词的别称。这一组宏完成了对 setjmp() 及 longjmp() 两个函数的封装,使程序具备简单的异常处理功能。然而,遗憾的是这组宏不具备嵌套的能力;当这组宏应用到嵌套异常,只能响应最后一组异常宏,并且无法抛出异常类型,至少它连一个常量整型都无法抛出,这是因为 jmp_buf 全局只能存放一个 jmpb 异常结构。
14、以下是对上面叙述的简单的图示:虽然这组宏无法嵌套使用,然而抛出一个常量整型是有可能的(甚至是一个结构 struct),更改成如下一组宏,便可抛出一个常量整型,并且可以在 catch 处以 catch(Value) 的方式处理异常。jmp_buf Jump_Buffer;int TValue;#define try if( !( TValue = setjmp(Jump_Buffer) ) )#define catch(Val) elseif(TValue = Val)/* throw 抛出的值不应该等于 0,因为这会导致无法执行 try后面的 catch块而继续执行形成了死循环*/#defin
15、e throw(Val) longjmp(Jump_Buffer, Val)下面的例程演示了这组宏:/* 输入一个整型数值,若大于 100 以异常抛出一个常量 20 否则以异常抛出一个常量 20 */i nclude “stdio.h“i nclude “conio.h“i nclude “setjmp.h“jmp_buf Jump_Buffer;int TValue;#define try if( !( TValue = setjmp(Jump_Buffer) ) )#define catch(Val) else if(TValue = Val)#define catch_all else
16、/*它只能是放置在最后的一个异常处理块 */#define throw(Val) longjmp(Jump_Buffer, Val)int Test(int T);int Test_T(int T);int Test(int T)if(T 100)throw(20); /*只是演示,20 这个常量值并无特别意义*/throw(10); /* catch_all 块将处理这个异常*/return;int Test_T(int T)Test(T);return;int main(void)int T;tryputs(“Input a value:“);scanf(“%d“, T+;Test_T(T);catch(20)puts(“Input Error!(Code: 20)“);catch_allputs(“Unknown error!“);getch();return 0;正是因为 jmp_buf 全局只能存放一个 jmpb 结构,使得只有最后一组宏可以响应异常;这是无法嵌套异常的原因,要实现多重嵌套可以建立一个全局堆栈来维护一组 jmpb 结构,将在日后给出实现,若感兴趣请自行实现,请将实现给我一份以作参考。这里给出的只是 C异常处理的简单实现,若要完善的异常处理,这需要更多的手段(如有部分异常需要由信号捕抓及处理)。