9.3 如何实现异常处理
很多初学者在此之前可能根本没有使用或听说过C语言的异常处理,印象中只有C++和Java等编程语言才有这些内容,C语言怎么会有异常处理呢?当然,对于一般性的学习或应付考试等,几乎不会提及C语言的异常处理。那么,到底什么是异常处理,C语言又是如何实现异常处理的呢?接下来就讲解C语言异常处理的一种典型的实现方法:以setjmp函数和longjmp函数实现异常处理。
首先来了解什么是异常处理。异常是一个在程序执行期间发生的事件,它中断正在执行的程序的正常指令流,而异常处理提供了处理程序运行时出现的任何意外或异常情况的方法。
下面先看setjmp函数和longjmp函数。
setjmp函数的原型如下:
int(jmp_buf env);
打开源代码会发现在setjmp函数中涉及很多寄存器的操作,如EBP、EBX、EDI、ESI、ESP、EIP等,在此就不一一例举了,其作用是在调用setjmp函数的过程中保存程序当前运行时的堆栈环境。保存这些堆栈环境有什么用呢?先来看longjmp函数。
longjmp函数的原型如下:
void longjmp(jmp_buf env,int value);
setjmp函数的功能是保存程序执行时候的堆栈环境。在longjmp()函数中也有一个jmp_buf类型的env变量,这是为了保证接下来调用longjmp时根据这个曾经保存的变量来恢复先前的环境,并且当前的程序控制流会因此返回到最初调用setjmp函数时的程序执行点。此时,在接下来的控制流的历程中,所能访问的所有变量都包含了调用longjmp函数时所拥有的变量。仅通过文字讲解可能有点抽象,还是通过一段代码来进行分析。
include<stdio.h>
include<setjmp.h>
int main()
{
double a,b;
printf("请输入被除数:");
scanf("%lf",&a);
printf("请输入除数:");
if(setjmp(buf)==0)
{
scanf("%lf",&b);
if(0==b)
longjmp(buf,1);
printf("相除的结果为:%f\n",a/b);
}
else
printf("出现错误除数为0\n");
return 0;
}
运行结果:
请输入被除数:23
请输入除数:0
出现错误除数为0
接着上面的内容继续讲解,在一开始我们并没有具体交代setjmp函数和longjmp函数的返回值和参数的具体含义。两个函数中的env变量保存的是调用setjmp函数时当前运行程序的堆栈信息,而longjmp函数的调用就是根据调用setjmp函数时的堆栈信息返回到最初调用setjmp函数的地方,其中的第二个参数就是此刻setjmp函数的返回值。值得注意的是,在调用longjmp函数之后,setjmp函数返回的值必须是非0值,如果longjmp传送的value参数值为0,那么setjmp的返回值实际上是1。一开始调用setjmp函数的时候,它的返回值为0,之后再调用longjmp函数的时候,通过设置longjmp函数的第二个参数来设定它的返回值。
现在分析上边的代码,在main函数中,最初调用setjmp函数时把当前的环境信息保存在buf中,函数返回0,然后向下运行,输入0。由if语句可知,b的值为0时调用longjmp函数,具体方式为“longjmp(buf,1);”。通过上面的讲解,我们知道第一个参数的作用是得到最初调用setjmp函数时的环境信息,以便在使用longjmp函数的时候能够正确返回到setjmp函数最初的调用处,而后面的参数表示的是返回到setjmp函数时的返回值。在此返回1,所以执行else部分的语句。
分析完上面的代码,读者应该知道这两个进行异常处理的函数的使用方法。值得注意的是,在函数setjmp与longjmp结合使用时,必须遵循严格的先后执行顺序,先调用setjmp函数,再调用longjmp函数,以恢复到先前被保存的"程序执行点"。否则,如果在调用setjmp之前执行longjmp函数,将导致程序的执行流变得不确定,可能导致程序崩溃,进而退出执行。
上面实现的异常处理并没有体现出setjmp函数和longjmp函数异常处理的强大之处,这种本地跳转也可以采用前面讲解的goto语句来实现。接下来通过setjmp函数和longjmp函数来实现非本地跳转,看下面的代码。
include<stdio.h>
include<setjmp.h>
jmp_buf buf;
void exception(void)
{
longjmp(buf,1);
}
int main()
{
double a,b;
printf("请输入被除数:");
scanf("%lf",&a);
printf("请输入除数:");
if(setjmp(buf)==0)
{
scanf("%lf",&b);
if(0==b)
exception();
printf("相除的结果为:%f\n",a/b);
}
else
printf("出现错误除数为0\n");
return 0;
}
运行结果:
请输入被除数:12
请输入除数:0
出现错误除数为0
这里对前面的代码做了适当的修改,修改后代码的功能为通过setjmp函数和longjmp函数实现非本地跳转,再来看下面的一段代码。
include<stdio.h>
include<setjmp.h>
include<stdlib.h>
include<string.h>
jmp_buf buf;
int sum(int a[],int n)
{
int i,total;
if(n<=0)
longjmp(buf,1);
total=0;
for(i=0;i<n;i++)
total+=a[i];
return total;
}
float average(int score[],int n)
{
int total,i;
float avg;
total=0;
for(i=0;i<n;i++)
{
if(score[i]<0)
longjmp(buf,2);
total+=score[i];
}
avg=total/n;
return avg;
}
int main()
{
int value,total;
char str[50];
int a[3]={1,2,3},score[3]={21,-56,25};
float avg;
value=setjmp(buf);
if(value==0)
{
total=sum(a,3);
avg=average(score,3);
}
switch(value)
{
case 1:
strcpy(str,"error:传递给sum()函数的参数n为负");
break;
case 2:
strcpy(str,"error:传递给average()函数的学生的成绩为负数");
break;
default:
strcpy(str,"error:其他错误值");
break;
}
printf("%s返回值为:%d\n",str,value);
return 0;
}
运行结果:
error:传递给average()函数的学生的成绩为负数 返回值为:2
上面的代码用setjmp函数和longjmp函数对多个函数进行了异常处理,如果在调用函数时出现了错误,那么就返回不同的数值,根据返回的数值打印输出不同的信息。
为了能够对异常处理有更加深入的认识,接下来再看看signal函数的使用。
头文件:#include<signal.h>。
功能:设置某一信号的对应动作。
函数原型:
void(signal(int signum,void(handler)(int)))(int);
注意 第一个参数signum指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
如果读者是第一次接触上面的函数,可能有些不知道如何着手,一时间难以理解这个函数到底有什么作用。在讲解函数signal之前先来看它的另外一种表示方法。
typedef void(*sig_t)(int);
sig_t signal(int signum,sig_t handler);
将上面的函数原型拆分为如上两行代码。第一行代码定义了一个函数指针,其含有一个int型参数,无返回值;在第二行代码中,signal函数的返回值是一个函数指针,与第一行定义的类型相同,第二个参数也是一个函数指针,signal的返回值就是第二个函数指针指向的函数地址。接下来就用一段代码来模拟该函数的实现方法。
include<stdio.h>
include<stdlib.h>
typedef void(*pfun)();
pfun signal_call(int a,pfun fun);
pfun signal_call(int a,pfun fun)
{
return fun;
}
void func()
{
printf("hello world!\n");
}
int main()
{
pfun p=func;
signal_call(1,p)();
return 0;
}
运行结果:
hello world!
现在分析上面的代码,先采用前面的定义形式实现如下两行代码:
typedef void(*pfun)();
pfun signal_call(int a,pfun fun);
在接下来的main函数中定义了一个函数指针p,使其指向func函数,接下来通过一句代码“signal_call(1,p)();”实现func函数调用,这到底是怎么实现的呢?这里进行一下分析,前面的signal_call(1,p)返回的是一个函数指针,其实在代码中返回的就是p,所以“signal_call(1,p)();”就可以变形为p()。看到这种形式,我们就可以很清楚地看出,调用的就是代码中的func函数了。到这里,读者就明白了signal函数的实现方法。接下来再看一段MSCN中的例程——使用signal捕捉除数为0时的异常代码,我们对其进行了适当的修改。
include<stdio.h>
include<signal.h>
include<setjmp.h>
include<stdlib.h>
include<float.h>
include<string.h>
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;
}
没有发生浮点异常的运行结果为:
请输入被除数:123
请输入除数:3
a/b=41
发生异常时候不会被执行的语句
发生浮点异常的运行结果为:
请输入被除数:45
请输入除数:0
发生浮点计算异常
现在来分析上面的运行结果,先看“_control87(0,_MCW_EM);”这句代码,很多读者可能对这句代码比较陌生,它的功能是开启所有的浮点计算异常。在通常情况下,浮点计算异常是被屏蔽掉的,为了能够使接下来的signal能够捕捉到浮点计算异常,要将其开启。接下来通过signal(SIGFPE,handler)来绑定一个浮点计算异常处理函数,当发生异常时,就调用handler()函数来处理。紧接着通过“ret=setjmp(buf);”保存程序运行的环境信息,以便接下来调用longjmp函数时能够根据保存的信息返回该程序先前setjmp函数的执行点。对比两次的运行结果可以发现,如果发现异常,接下来的打印语句"printf("发生异常时候不会被执行的语句\n");"是不会被执行的,而且直接跳转到绑定的handler函数开始执行。当然,在此仅列举一些简单的代码教读者学会使用setjmp函数和longjmp函数来实现异常处理,读者完全可以在此基础上编写出复杂的异常处理。