c++ printf怎么用_C程序设计的异常处理

本文介绍了在C程序设计中如何处理异常,特别是当多个步骤可能出现错误或存在多层函数嵌套时。讨论了goto语句在错误恢复和跳出多层循环中的应用,并指出其滥用可能导致的混乱。此外,还详细讲解了longjmp函数与setjmp函数的配合使用,用于在函数间进行错误返回。通过实例展示了变量值在longjmp跳转后的变化,并提醒注意编译优化可能带来的影响。
摘要由CSDN通过智能技术生成
大 家新年好,感谢大家对本公众号一如既往地 支持 ,后面争取创作出更加优质的文章。 今天是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申请的资源,再释放步骤一 的资源,最后退出,其他的地方出错也是类似处理。上面是一种代码框架,实际写代码应该根据实际情况来处理异常。我们来看一下效果:

63fab1715ed4949c4b1d05d0605002a6.png

以上就是goto在多个步骤容易出错时的一种处理。这里顺便提一下goto的另外一种应用场景,就是用来跳出多层循环。我们知道跳出循环一般使用break和continue,但是这个只能调出当前循环,不能跳出多层循环,有时候在多层循环里面,一旦条件满足,我们就不需要再执行后面的循环了,使用goto可以解决这个问题。我们来看一下代码:
#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 ,就非常麻烦。代码的执行结果是:

132de0361b2ef884db7458ffb70f249e.png

第一次k=0,正常打印,第二次,k=1,满足条件,跳出循环,执行后面的语句,打印出after goto.当然,问题也快出来了,刚刚是上面跳到了下面,如果我们再从下面跳上去会怎么样?我们打开最后一行的注释,重新编译执行,会发现打印出几百上千行的内容:

2588cc4bf2bbac9b6b936ca9d9edc422.png

代码看起来好像不复杂,就是先跳下去,然后又跳回原来的后面,怎么会打印这么多东西呢?这就是使用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 #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的返回值也很容易确定问题出在哪里。来看一下效果:

3938adb362c0b2965b1873fe54987aa1.png

使用longjmp还有一个问题我们可能也需要关注一下,就是当使用longjmp返回的时候,函数里的那些变量还能保持原来的值吗?我们可以做一个实验来验证这一点:
#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函数,那么这时各变量的值如何?是刚开始赋的初值,还是后面改变后的值呢?我们编译执行一下:

248e51aae9e72c9e562d02c477fdc4e8.png

可以发现使用register声明的变量保持的是初值,而其他变量都是改变后的值。如果编译时进行优化,结果又如何?

a771b65b1d40c305728f427955dd0e5d.png

可以发现除了刚刚的register声明的变量,普通局部变量(自动变量)也没有更新,而是保持了初值,这通常不是我们希望的,我们肯定是希望得到最新的值,这也是因为编译优化带来的问题。所以如果希望避免这个问题,可以加上volatile来修饰。以上就是今天要分享的内容,主要是在C程序中,由多个步骤可能引发的错误,或者是多层嵌套里面可能出现的错误进行处理,还要注意资源的回收等问题。附带讲了乱用goto带来的弊端,以及在函数间跳转与返回时变量的值的改变,程序优化带来的影响等。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值