前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C程序设计的异常处理

C程序设计的异常处理

作者头像
飞哥
发布2021-01-18 09:55:08
7140
发布2021-01-18 09:55:08
举报
文章被收录于专栏:电子技术研习社

大家新年好,感谢大家对本公众号一如既往地支持,后面争取创作出更加优质的文章。今天是2021年的第一篇文章,分享一下在C程序设计当中对异常的处理。主要是介绍一下goto和longjmp函数的使用。

在写程序的时候,有些地方很容易出错,当然这种出错不是说那种你写错了,而是说比如硬件的初始化失败了,或者资源暂时不可用等等导致函数返回异常。这种错是难以避免的,而且通常是非致命的,只要多尝试几次可能就可以了。比如之前我们写过网络编程,要建立网络通信,我们需要调用socket,bind,listen等等一系列函数,每个函数都有可能会出错。

但是你的程序怎么知道该怎么处理呢?程序出错了显然是不能继续往下执行的,但是立即终止也不合适,因为这种错是非致命的,那么我们应该怎么去设计一个比较健壮的程序呢?今天介绍的可以当做是一种思路。

一、使用goto

说到goto,可能很多人的第一反应是不要用,但是问他为什么他可能讲不出来,因为是别人告诉他的。goto真的不能用吗?当然不是,最有力的证明就是Linux内核里面就有大量的goto语句。实际上,只要用的适当,还是非常好用的,当然我并不是说程序里面goto满天飞。

下面举例说明goto的应用场景:

有时候我们完成一件事情要分为很多个步骤,每个步骤里面还可能占用一些资源,然而这些步骤很容易出错,如果其中某个步骤出错了,就不能继续下一个步骤,也不能立即终止程序,因为这样会使资源得不到释放。那么使用goto就可以调出程序并且对资源进行回收。

来看一段代码:

代码语言:javascript
复制
#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可以解决这个问题。

我们来看一下代码:

代码语言:javascript
复制
#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函数配合使用。

代码语言:javascript
复制
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

先在程序容易出错的地方使用setjmp,定义一个入口,等到后面代码真的出错之后使用longjmp跳转到setjmp处。setjmp直接调用返回0,若从longjmp返回,则为非0.

举个例子:

代码语言:javascript
复制
#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返回的时候,函数里的那些变量还能保持原来的值吗?我们可以做一个实验来验证这一点:

代码语言:javascript
复制
#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带来的弊端,以及在函数间跳转与返回时变量的值的改变,程序优化带来的影响等。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 电子技术研习社 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档