0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

goto和longjmp函数的使用

CHANBAEK 来源:嵌入式大本营 作者:小小飞飞哥 2023-05-23 15:50 次阅读

分享一下在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申请的资源,再释放步骤一 的资源,最后退出,其他的地方出错也是类似处理。上面是一种代码框架,实际写代码应该根据实际情况来处理异常。

我们来看一下效果:

图片

以上就是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 ,就非常麻烦。

代码的执行结果是:

图片

第一次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 
#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的返回值也很容易确定问题出在哪里。

来看一下效果:

图片

使用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函数,那么这时各变量的值如何?是刚开始赋的初值,还是后面改变后的值呢?

我们编译执行一下:

图片

可以发现使用register声明的变量保持的是初值,而其他变量都是改变后的值。

如果编译时进行优化,结果又如何?

图片

可以发现除了刚刚的register声明的变量,普通局部变量(自动变量)也没有更新,而是保持了初值,这通常不是我们希望的,我们肯定是希望得到最新的值,这也是因为编译优化带来的问题。所以如果希望避免这个问题,可以加上volatile来修饰。

以上就是今天要分享的内容,主要是在C程序中,由多个步骤可能引发的错误,或者是多层嵌套里面可能出现的错误进行处理,还要注意资源的回收等问题。附带讲了乱用goto带来的弊端,以及在函数间跳转与返回时变量的值的改变,程序优化带来的影响等。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 函数
    +关注

    关注

    3

    文章

    3911

    浏览量

    61313
  • 代码
    +关注

    关注

    30

    文章

    4557

    浏览量

    66826
  • 程序设计
    +关注

    关注

    3

    文章

    258

    浏览量

    30212
  • C程序
    +关注

    关注

    4

    文章

    253

    浏览量

    35750
收藏 人收藏

    评论

    相关推荐

    MDK中不能使用Goto Definition Goto Reference的解决方法

    变量定义的类型及定义的位置,我们仅需选中该变量然后右击鼠标,在弹出的菜单中选中Goto Definition即可自动打开定义该变量的源文件及其所在位置;如果我们需要查找某个函数申明的原型,我们仅需要选中
    发表于 08-02 10:11

    MDK中不能使用Goto Definition Goto Reference的解决方法

    Definition即可自动打开定义该变量的源文件及其所在位置。www.11hero.com如果我们需要查找某个函数申明的原型,我们仅需要选中该函数名然后右击鼠标,在弹出的菜单中选中Goto
    发表于 12-11 21:38

    关于linux中非局部跳转的简单使用

    也是有局限性的,那就是goto语句是一个实现局部跳转的关键字,也就是只能在一个函数中进行跳转,它是无法在不同的函数中实现跳转的。那么如何实现在不同的函数中进行非局部的跳转呢?而setj
    发表于 10-24 16:42

    关于Linux中非局部跳转的简单使用

    代码逻辑容易混乱,大大降低了代码的可读性和可维护性。而且即使使用goto语句进行跳转也是有局限性的,那就是goto语句是一个实现局部跳转的关键字,也就是只能在一个函数中进行跳转,它是无法在不同的
    发表于 05-16 11:40

    如何实现setjmp和longjmp的跳转

    想实现这么一个功能,我在定时中断中判断一个输入点P1.0,当P1.0接收到高电平时进入跳转,转到一个函数。下面代码void Timer1_Interrupt()interrupt 3{ TH1=xx
    发表于 07-22 01:32

    【微信精选】为什么在C语言中,goto这么不受待见?

    控制。该类语句允许程序员对当前代码行断行,而直接进入另一个不同的代码段。列表1为简单的示例。编程语言终究开始引入了函数的概念,即允许程序对代码进行断行。如果已经完成,不再使用goto语句来表示代码
    发表于 09-11 07:30

    setjmp、longjmp原理探究及实现

    setjmp、longjmp原理探究及实现一、原理1、实验2、猜想二、实现三、调用四、总结一、原理C语言中包含头文件即可实现跨函数跳转,通常用于异常处理,在运行代码出现异常时可以自动跳转到调用
    发表于 01-25 07:08

    浅谈C语言goto语句的用法

    冒号(:)。语句标号起标识语句的作用,与goto 语句配合使用。执行goto语句后,程序将跳转到该标号处并执行其后的语句。另外标号必须与goto语句同处于一个函数中,但可以不在一个循环
    发表于 05-06 09:16

    Proteus之goto语句的应用

    Proteus之goto语句的应用,很好的Proteus资料,快来学习吧。
    发表于 04-18 14:49 0次下载

    C语言中的goto语句怎么用?为什么反对使用

    关于C语言的goto语句存在很多争议,很多书籍都建议“谨慎使用,或者根本不用”。这里先不做过多的讨论,存在即合理,既然是C语言中的一个知识点,我们还是有必要学会使用。先看一些goto如何用:
    的头像 发表于 01-24 17:38 6051次阅读
    C语言中的<b class='flag-5'>goto</b>语句怎么用?为什么反对使用

    C语言中到底应不应该使用goto语句

    关于C语言的goto语句存在很多争议,很多书籍都建议“谨慎使用,或者根本不用”。这里先不做过多的讨论,存在即合理,既然是C语言中的一个知识点,我们还是有必要学会使用。先看一些goto如何用:
    的头像 发表于 01-16 09:12 6531次阅读

    goto的优缺点

    很多书籍都会把goto当成反面教材使用,认为如果使用不当,将会造成很多意想不到的问题。但goto作为C语言的一部分,存在即合理,goto有它的缺点,也有它的优点。 缺点: 很容易把逻辑弄乱,增加理解
    的头像 发表于 09-29 14:27 3715次阅读

    Cortex-M单片机中 setjmp、longjmp原理探究及实现

    setjmp、longjmp原理探究及实现一、原理1、实验2、猜想二、实现三、调用四、总结一、原理C语言中包含头文件 <setjump.h> 即可实现跨函数跳转,通常
    发表于 12-01 11:36 15次下载
    Cortex-M单片机中 setjmp、<b class='flag-5'>longjmp</b>原理探究及实现

    SCL用GOTO语句执行程序跳转

    GOTO语句能够执行程序跳转。此引起立即跳转到指定标号,为此而到同块中不同的语句。
    的头像 发表于 02-01 09:22 1962次阅读

    深入探讨嵌入式C编程的goto语句

    什么是goto语句? goto 语句被称为 C 语言中的跳转语句。 用于无条件跳转到其他标签。它将控制权转移到程序的其他部分。 goto 语句一般很少使用,因为它使程序的可读性和复杂性变得更差。
    发表于 01-21 10:41 315次阅读
    深入探讨嵌入式C编程的<b class='flag-5'>goto</b>语句