大家新年好,感谢大家对本公众号一如既往地支持,后面争取创作出更加优质的文章。今天是2021年的第一篇文章,分享一下在C程序设计当中对异常的处理。主要是介绍一下goto和longjmp函数的使用。
在写程序的时候,有些地方很容易出错,当然这种出错不是说那种你写错了,而是说比如硬件的初始化失败了,或者资源暂时不可用等等导致函数返回异常。这种错是难以避免的,而且通常是非致命的,只要多尝试几次可能就可以了。比如之前我们写过网络编程,要建立网络通信,我们需要调用socket,bind,listen等等一系列函数,每个函数都有可能会出错。
但是你的程序怎么知道该怎么处理呢?程序出错了显然是不能继续往下执行的,但是立即终止也不合适,因为这种错是非致命的,那么我们应该怎么去设计一个比较健壮的程序呢?今天介绍的可以当做是一种思路。
一、使用goto
说到goto,可能很多人的第一反应是不要用,但是问他为什么他可能讲不出来,因为是别人告诉他的。goto真的不能用吗?当然不是,最有力的证明就是Linux内核里面就有大量的goto语句。实际上,只要用的适当,还是非常好用的,当然我并不是说程序里面goto满天飞。
下面举例说明goto的应用场景:
有时候我们完成一件事情要分为很多个步骤,每个步骤里面还可能占用一些资源,然而这些步骤很容易出错,如果其中某个步骤出错了,就不能继续下一个步骤,也不能立即终止程序,因为这样会使资源得不到释放。那么使用goto就可以调出程序并且对资源进行回收。
来看一段代码:
#include <stdio.h>
#include <stdlib.h>
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申请的资源,再释放步骤一 的资源,最后退出,其他的地方出错也是类似处理。上面是一种代码框架,实际写代码应该根据实际情况来处理异常。
我们来看一下效果:
以上就是goto在多个步骤容易出错时的一种处理。这里顺便提一下goto的另外一种应用场景,就是用来跳出多层循环。我们知道跳出循环一般使用break和continue,但是这个只能调出当前循环,不能跳出多层循环,有时候在多层循环里面,一旦条件满足,我们就不需要再执行后面的循环了,使用goto可以解决这个问题。
我们来看一下代码:
#include <stdio.h>
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 ,就非常麻烦。
代码的执行结果是:
第一次k=0,正常打印,第二次,k=1,满足条件,跳出循环,执行后面的语句,打印出after goto.
当然,问题也快出来了,刚刚是上面跳到了下面,如果我们再从下面跳上去会怎么样?我们打开最后一行的注释,重新编译执行,会发现打印出几百上千行的内容:
代码看起来好像不复杂,就是先跳下去,然后又跳回原来的后面,怎么会打印这么多东西呢?这就是使用goto不当带来的害处。这种交叉式地跳来跳去会使得程序结构非常混乱,混乱到我也懒得去分析。
二、使用longjmp
刚刚讲了goto的异常处理,但是goto有一个局限性,就是goto只能在一个函数内进行跳转,不能跨越函数。
如果一个函数里嵌套了多个函数调用,而里层的函数出了错,希望跳转到上一层或上几层的函数,该怎么办?显然,goto是做不到的。这时可以使用longjmp函数。longjmp函数和setjmp函数配合使用。
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
先在程序容易出错的地方使用setjmp,定义一个入口,等到后面代码真的出错之后使用longjmp跳转到setjmp处。setjmp直接调用返回0,若从longjmp返回,则为非0.
举个例子:
#include <stdio.h>
#include <setjmp.h>
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的返回值也很容易确定问题出在哪里。
来看一下效果:
使用longjmp还有一个问题我们可能也需要关注一下,就是当使用longjmp返回的时候,函数里的那些变量还能保持原来的值吗?我们可以做一个实验来验证这一点:
#include <stdio.h>
#include <setjmp.h>
#include <stdlib.h>
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函数,那么这时各变量的值如何?是刚开始赋的初值,还是后面改变后的值呢?
我们编译执行一下:
可以发现使用register声明的变量保持的是初值,而其他变量都是改变后的值。
如果编译时进行优化,结果又如何?
可以发现除了刚刚的register声明的变量,普通局部变量(自动变量)也没有更新,而是保持了初值,这通常不是我们希望的,我们肯定是希望得到最新的值,这也是因为编译优化带来的问题。所以如果希望避免这个问题,可以加上volatile来修饰。
以上就是今天要分享的内容,主要是在C程序中,由多个步骤可能引发的错误,或者是多层嵌套里面可能出现的错误进行处理,还要注意资源的回收等问题。附带讲了乱用goto带来的弊端,以及在函数间跳转与返回时变量的值的改变,程序优化带来的影响等。