大
家新年好,感谢大家对本公众号一如既往地
支持
,后面争取创作出更加优质的文章。
今天是2021
年的第一篇文章,
分享一下在C程序设计当中对异常的处理。主要是介绍一下goto和longjmp函数的使用。
在写程序的时候,有些地方很容易出错,当然这种出错不是说那种你写错了,而是说比如硬件的初始化失败了,或者资源暂时不可用等等导致函数返回异常。这种错是难以避免的,而且通常是非致命的,只要多尝试几次可能就可以了。比如之前我们写过网络编程,要建立网络通信,我们需要调用socket,bind,listen等等一系列函数,每个函数都有可能会出错。
但是你的程序怎么知道该怎么处理呢?程序出错了显然是不能继续往下执行的,但是立即终止也不合适,因为这种错是非致命的,那么我们应该怎么去设计一个比较健壮的程序呢?今天介绍的可以当做是一种思路。
一、使用goto
说到goto,可能很多人的第一反应是不要用,但是问他为什么他可能讲不出来,因为是别人告诉他的。goto真的不能用吗?当然不是,最有力的证明就是Linux内核里面就有大量的goto语句。实际上,只要用的适当,还是非常好用的,当然我并不是说程序里面goto满天飞。
下面举例说明goto的应用场景:
有时候我们完成一件事情要分为很多个步骤,每个步骤里面还可能占用一些资源,然而这些步骤很容易出错,如果其中某个步骤出错了,就不能继续下一个步骤,也不能立即终止程序,因为这样会使资源得不到释放。那么使用goto就可以调出程序并且对资源进行回收。
来看一段代码:
#include #include char *p1=NULL,*p2=NULL,*p3=NULL;int step1(void);int step2(void);int step3(void);int main(int argc,char* argv[]){ if(step1()<0) { goto error1; } if(step2()<0) { goto error2; } if(step3()<0) { goto error3; }error3: printf("释放步骤3的资源\n"); free(p3);error2: printf("释放步骤2的资源\n"); free(p2);error1: printf("释放步骤1的资源\n"); free(p1); exit(0);}int step1(void){ p1=(char*)malloc(10); return 0;}int step2(void){ p2=(char*)malloc(10); return 0;}int step3(void){ p3=(char*)malloc(10); return -1;}
在这段代码里面,假设完成一件事情一共有三个步骤,每个步骤里面都维护了一个指针变量(资源),假设步骤一和步骤二都是正常的,步骤三出了问题,返回一个错误的值,如果我们接收到步骤三的错误返回值之后立即终止程序,那么步骤一和步骤二里申请的资源就得不到释放,比如这里的指针会造成内存泄漏,显然不是我们希望看到的。但是使用上面的这种结构,如果在步骤二出错了,它会跳转到error2这里先释放步骤2申请的资源,再释放步骤一 的资源,最后退出,其他的地方出错也是类似处理。上面是一种代码框架,实际写代码应该根据实际情况来处理异常。我们来看一下效果:
#include int main(void){ for(int i=0;i<2;i++) { for(int j=0;j<2;j++) { for(int k=0;k<2;k++) { if(k==1) { goto lable; }lable2: printf("i=%d,j=%d,k=%d\n",i,j,k); } } }lable: printf("after goto \n");// goto lable2;}
在这里有三层循环嵌套,一旦条件满足,就通过goto跳出整个循环体,执行后面的代码。如果使用break ,就非常麻烦。代码的执行结果是:
int setjmp(jmp_buf env);void longjmp(jmp_buf env, int val);
先在程序容易出错的地方使用setjmp,定义一个入口,等到后面代码真的出错之后使用longjmp跳转到setjmp处。setjmp直接调用返回0,若从longjmp返回,则为非0.举个例子:
#include #include jmp_buf jmpbuffer;int fun1(void);int fun2(void);int fun3(void);int main(int argc,char* argv[]){ printf("这里是主函数\n"); if(setjmp(jmpbuffer)!=0) { printf("Error\n"); } fun1(); //假设fun1是一个容易出错的函数,出错后将返回上一步,然后再重新执行。 printf("这里是主函数调用fun1之后\n"); return 0;}int fun1(void){ printf("这里是fun1\n"); fun2();}int fun2(void){ printf("这里是fun2\n"); fun3();}int fun3(void){ static int i=0; printf("这里是fun3\n"); if(i++==0) { longjmp(jmpbuffer,1); //跳转回main函数 } return -1;}
在这里,主函数调用了fun1函数,而fun1调用fun2,fun2又调用fun3.这种多层嵌套里面,每一层都可能出错。如果我们希望里面任何一层出错了,就返回main函数,那么用longjmp就可以实现。对上面程序进行解释:当第一次执行setjmp时,由于是直接调用,所以返回0,接着调用我们的功能函数fun1,假设fun3里面出错了,那么就会通过longjmp跳转到setjmp处,同时携带一个返回值1,那么这时就会执行if语句进行错误处理,接着再执行fun1,也许此时就全部正常了,一直执行到最后。(这是很正常的现象,正如开头说的,像硬件初始化,申请资源等都可能不是一次成功的,需要重复多次)。而且在多个地方都可以使用longjmp,携带不同的返回值,这样根据setjmp的返回值也很容易确定问题出在哪里。来看一下效果:
#include #include #include static void f1(int,int,int,int);static void f2(void);static jmp_buf jmpbuffer;static int global;int main(int argc,char* argv[]){ int autoval; register int regival; volatile int volaval; static int staval; global=1;autoval=2;regival=3;volaval=4;staval=5; if(setjmp(jmpbuffer)!=0) { printf("after longjmp:\n"); printf("global=%d,autoval=%d,regival=%d,volaval=%d,staval=%d\n", \ global,autoval,regival,volaval,staval); exit(0); } global=10;autoval=20;regival=30;volaval=40;staval=50; f1(autoval,regival,volaval,staval); exit(0);}static void f1(int a,int b,int c,int d){ printf("in f1():\n"); printf("global=%d,autoval=%d,regival=%d,volaval=%d,staval=%d\n", \ global,a,b,c,d); f2();}static void f2(void){ longjmp(jmpbuffer,1);}
这里我们定义了很多种不同的变量,先对变量赋一个初值,然后改变变量的值,接着调用f1,在f1里打印各变量的值,f1再调用f2,f2使用longjmp跳转回main函数,那么这时各变量的值如何?是刚开始赋的初值,还是后面改变后的值呢?我们编译执行一下: