当然估计在大学出于一般的性的学习考试之类的话老师几乎是不会提及C语言的异常处理的,那么到底什么是异常处理?C语言中又该如何来实现异常处理呢?那么我们今天就讲解一种典型的实现C语言异常处理的方法,以setjmp()函数和longjmp()函数实现的异常处理,我尽可能的把它们是怎样实现异常处理方法讲解清楚,希望接下来的内容对你有所帮助,让你学到一些新的东西。
首先我们来了解下异常处理,异常是一个在程序执行期间发生的事件,它中断正在执行的程序的正常的指令流,而我们的异常处理功能提供了处理程序运行时出现的任何意外或异常情况的方法。
接下来我们先看看setjmp()函数和longjmp()函数实现C语言异常处理。
setjmp()函数原型:
int ( jmp_buf env );
如果我们打开源代码会发现在setjmp()函数中涉及到很多的寄存器的操作,如Ebp、Ebx、Edi、Esi、Esp、 Eip等等,在此就不一一例举了,我们无非是想向读者说明一个问题,那就是在调用setjmp()函数的过程中保存程序的当前运行时的堆栈环境,保存这些堆栈环境有什么用呢?接下来我们看看longjmp()函数。
longjmp()函数原型:
void longjmp( jmp_buf env, int value );
刚刚上面的函数功能是保存程序执行时候的堆栈环境,我们发现在longjmp()函数里也有一个jmp_buf类型的env变量,这其实是为了保证接下来调用longjmp时,会根据这个曾经保存的变量来恢复先前的环境,并且当前的程序控制流,会因此而返回到最初调用setjmp()函数时的程序执行点。此时,在接下来的控制流的例程中,所能访问的所有的变量,包含了longjmp函数调用时所拥有的变量。我们就这样说读者可能就得有点抽象了,那我们还是来看看一段代码后再来分析吧,在此特地给出了一个简单的代码,由易到难的来分析。
[cpp] view plaincopy#include
#include
jmp_buf buf;
void error_code(void)
{
longjmp(buf,1);
}
int main()
{
double a,b;
printf("请输入被除数:");
scanf("%lf",&a);
printf("请输入除数:");
if(setjmp(buf)==0)
{
scanf("%lf",&b);
if(0==b)
error_code();
printf("相除的结果为:%f\n",a/b);
}
else
printf("出现错误除数为0\n");
return 0;
}
运行结果为:
[cpp] view plaincopy请输入被除数:12
请输入除数:0
出现错误除数为0
Press any key to continue
看了上面的运行结果,现在我们接着上面的讲,在一开始的部分我们并没有具体的交代setjmp()函数和longjmp()函数的返回值和参数的具体含义。两个函数中的env变量保存的是调用setjmp()函数的时候当前运行程序的堆栈信息,而longjmp()函数的调用就是根据在调用setjmp()函数的时候的堆栈信息返回到最初调用setjmp()函数的地方,而其中的第二个参数就是此刻setjmp()函数的返回值,但是值得注意的就是调用longjmp()函数之后setjmp函数返回的值必须是非零值,如果longjmp传送的value参数值为0,那么实际上setjmp返回的值是1。一开始我们调用setjmp()函数的时候,它的返回值为0,之后再调用longjmp()函数的时候,通过设定longjmp()函数的第二个参数来设定它的返回值。
现在我们来分析上边的代码,在main()函数中,我们最初调用setjmp()函数的时候,把当前的环境信息保存在了buf中,函数返回0,然后往下运行,我们输入0。通过if语句发现b的值为0那么就调用error_code()函数来进行处理,在该函数中我们使用了longjmp()函数,其使用方式为longjmp(buf,1);,通过上面的讲解,我们知道第一个参数的作用是用来得到最初调用setjmp()函数是的环境信息,以便在使用longjmp()函数的时候能够正确的返回到setjmp()函数最初的调用处,而后面的参数表示的返回到setjmp()函数的时候的返回值。我们在此返回1,所以执行else部分的语句。
分析完了上面的代码,读者应该都知道了两个函数的使用方法,值得注意的地方就是我们在setjmp与longjmp结合使用时,它们必须有严格的先后执行顺序,先调用setjmp函数,之后再调用longjmp函数,以恢复到先前被保存的“程序执行点”。否则,假如在setjmp调用之前,执行longjmp函数,将导致程序的执行流变的不可猜测,很轻易导致程序崩溃而退出。为了加深读者的对于两个函数参数的使用,我们看看下面的代码:
[cpp] view plaincopy#include
#include
#include
#include
jmp_buf buf;
void func1()
{
longjmp(buf,1);
}
void func2()
{
longjmp(buf,2);
}
void func3()
{
longjmp(buf,3);
}
int main( void )
{
int value;
char str[50];
value = setjmp( buf );
if( value == 0 )
{
func1();
}
switch( value )
{
case 1:
strcpy( str, "func1 return value" );
break;
case 2:
strcpy( str, "func2 return value" );
break;
case 3:
strcpy( str, "func3 return value" );
break;
default:
strcpy( str, "Other error value" );
break;
}
printf("%s:%d\n",str,value);
if(1==value)
{
func2();
}
if(2==value)
{
func3();
}
return 0;
}
运行结果为:
[cpp] view plaincopyfunc1 return value:1
func2 return value:2
func3 return value:3
Press any key to continue
看看运行结果,我们分析下代码,在每个函数中我们调用longjmp()函数,通过设置第二个参数为不同的值来改变setjmp()函数的返回值,然后我们通过判断value值来打印出是那个函数的返回值,我们在此例举这个简单的代码是要大家加深对于这两个函数的参数的使用情况。如果我们在上面的代码中稍作修改,在setjmp()函数的调用之前调用longjmp()函数,我们发现此时没有任何的输出,程序直接崩溃掉退出了。
接下来我们来看看一个函数的使用,如果对于这个函数不理解的读者,可以多看几次我给出的模拟该函数的实现代码。
头文件: #include
功能:设置某一信号的对应动作
函数原型:void (*signal(int signum,void(* handler)(int)))(int);
注意:第一个参数signum指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
如果读者是第一场接触上面的函数的话可能有些不知道该如何着手,一时间有些难以理解,不知道到底是什么意思。别急,我们现在来逐一分析它到底是什么意思,我们在讲解之前再来看看它的另外一种表示方法。
typedef void(*sig_t) ( int );
sig_t signal(int signum,sig_t handler);
把上面的函数原型拆分为了如上两行代码,现在我们分析下上面的两行代码。
第一行代码定义了一个函数指针(注:如果有对函数指针知识点不熟悉的读者可以去阅读我之前写的那篇文章《C语言的那些小秘密之函数指针》),其类型为含有一个int型参数,无返回值;
第二行代码中,signal函数的返回值是一个函数指针,与第一行我们定义的类型相同,第二个参数也为一个函数指针,其实signal的返回值就是第二个函数指针指向的函数地址。这样说可能有不少读者都有些懵的感觉,还是老方法,代码最有说服力,我们还是为读者模拟下signal的实现方式,呈现出一段代码来分析下。
[cpp] view plaincopy#include
#include
typedef void (*pfun) ();
pfun signal_call(int a,pfun fdsa);
pfun signal_call(int a,pfun fdsa)
{
return fdsa;
}
void func()
{
printf("hello world!!!\n");
}
int main()
{
pfun p = func;
signal_call(1,p)();
return 0;
}
运行结果为:
[cpp] view plaincopyhello world!!!
Press any key to continue
现在我们来分析下上面的代码,我们采用上面的定义形式实现了如下两行代码:
typedef void (*pfun) ();
pfun signal_call(int a,pfun fdsa);
在接下来的main()函数中我们定义了一个函数指针p,使其指向了 func()函数,接下来我们使用了一句 signal_call(1,p)();代码,实现了func函数调用,那么这到底是怎么实现的呢?那么我们来分析下,前面的signal_call(1,p)返回的是一个函数指针,在代码中我们发现其实返回的就是p,所以signal_call(1,p)();就可以变形为p(),看到这种形式我们这就可以很清楚的看出,它调用的就是我们代码中的func()函数了。现在读者明白了signal()函数的实现方法,接下来我们来看看一段使用signal捕捉除数为0时候的异常代码。
[cpp] view plaincopy#include
#include
#include
#include
#include
#include
jmp_buf buf;
int err;
void handler( int num )
{
err = num;
printf( "发生浮点计算异常\n");
longjmp( buf, 1);
}
int main( void )
{
double a, b;
char str[20];
int ret;
_control87( 0, _MCW_EM );
if( signal( SIGFPE, handler ) == SIG_ERR )
{
printf("绑定失败\n" );
abort();
}
ret = setjmp( buf );
if(0 == ret )
{
printf("请输入被除数:");
scanf("%lf",&a);
printf("请输入除数:");
scanf("%lf",&b);
printf( "a / b = %4.3g\n", a/b);
printf("发生异常时候不会被执行的语句\n");
}
return 0;
}
没有发生异常时候的运行结果:
[cpp] view plaincopy请输入被除数:123
请输入除数:3
a / b = 41
发生异常时候不会被执行的语句
Press any key to continue
发生异常时候的运行结果:
[cpp] view plaincopy请输入被除数:12
请输入除数:0
发生浮点计算异常
Press any key to continue
现在来分析下上面的运行结果,先看看_control87( 0, _MCW_EM );这句,可能很多读者对于这代码比较陌生,它的功能是开启所有的浮点计算异常,通常情况下浮点计算异常是被屏蔽掉的,我们为了能够使得接下来的signal能够捕捉到浮点计算异常,所以要将其开启。在往下看我们通过signal( SIGFPE, handler )来绑定了一个浮点计算异常处理函数,如果发生异常时,那么就调用handler()函数来处理。接下来通过ret = setjmp( buf );保存程序运行的环境信息,以便接下来的调用longjmp()函数能够根据这个保存的信息返回该程序先前setjmp()函数的执行点。同时我们对比两次运行的结果发现如果发现异常的时候接下来的打印语句“printf("发生异常时候不会被执行的语句\n");”是不会被执行的,直接跳转到我们绑定的handler()函数执行了,当然我们在此仅仅是例举一些简单的代码教会读者学会使用setjmp()函数和longjmp()函数来实现异常处理,读者完全可以在此基础上编写出复杂的异常处理。
到这儿C语言的异常处理部分就结束了,由于本人水平有限,博客中的不妥或错误之处在所难免,殷切希望读者批评指正。同时也欢迎读者共同探讨相关的内容,如果乐意交流的话请留下你宝贵的意见。