在C中,goto语句是不能跨越函数的,而执行这样跳转功能的是函数setjmp和longjmp。这两个函数对于处理发生在深层嵌套函数调用中的出错情况是非常有用的。
setjmp和longjmp函数也称为非局部goto,非局部指的是,这不是由普通C语言goto语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的某一函数中。
#include <setjmp.h> int setjmp( jmp_buf env ); 返回值:若直接调用则返回0,若从longjmp调用返回则返回非0值 void longjmp( jmp_buf env, int val );
图7-4 调用cmd_add后的各个栈帧(函数调用关系为:main先调用do_line,do_line又调用cmd_add)
在希望返回到的位置调用setjmp,假定此位置在main函数中,因为我们是直接调用该函数,所以其返回值为0。setjmp参数env的类型是一个特殊类型jmp_buf。这一数据类型是某种形式的数组,其中存放:在调用longjmp时能用来恢复栈状态的所有信息。因为需要在另一个函数中引用env变量,所以规范的处理方式是将env变量定义为全局变量。
当检查到一个错误时,例如在cmd_add函数中,则以两个参数调用longjmp函数。第一个就是在调用setjmp时所用的env;第二个参数是具有非0值的val,它将成为从setjmp处返回的值。使用第二个参数的原因是对于一个setjmp可以有多个longjmp。例如,可以在cmd_add中以val为1调用longjmp,也可在do_line中以val为2调用longjmp。在setjmp的返回值就会是1或2,通过测试返回值就可以判断造成返回的longjmp是在cmd_add还是在do_line中。
我们假定在main中调用了setjmp(jmpbuffer),在cmd_add中调用了longjmp(jmpbuffer, 1)为例进行后续说明。
... #include <setjmp.h> ... jmp_buf jmpbuffer; int main(void) { ... if( setjmp(jmpbuffer) != 0 ) printf("error"); ... } ... void cmd_add(void) { ... if( ... ) /* an error has occurred */ longjmp( jmpbuffer, 1 ); ... }
执行main时,调用setjmp,它将所需的信息记入变量jmpbuffer中并返回0。然后调用do_line,它又调用cmd_add,假定在其中检查到一个错误。在cmd_add中调用longjmp之前,栈的形式如图7-4所示。但是longjmp使栈反绕(rewind to)到执行main函数时的情况,也就是抛弃了cmd_add和do_line的栈帧(见图7-5)。调用longjmp造成main中setjmp的返回,但是,这一次的返回值是1(longjmp的第二个参数)。
图7-5 调用longjmp后的栈帧(f1代表do_line、f2代表cmd_add)
1、自动、寄存器和易失变量
调用longjmp时,大多数实现并不回滚自动变量和寄存器变量的值,而所有标准则说它们的值是不确定的。如果你有一个自动变量,而又不想使其值回滚,则可定义其为具有volatile的属性。声明为全局或静态变量的值在执行longjmp时保持不变。
程序清单7-6 longjmp对各类变量的影响
[root@localhost apue]# cat prog7-6.c #include "apue.h" #include <setjmp.h> static void f1(int, int, int, int); static void f2(void); static jmp_buf jmpbuffer; static int globval; int main(void) { int autoval; register int regival; volatile int volaval; static int statval; globval = 1; autoval = 2; regival = 3; volaval = 4; statval = 5; if(setjmp(jmpbuffer) != 0) { printf("after longjmp:\n"); printf("globval = %d, autoval = %d, regival = %d," "volaval = %d, statval = %d\n", /* ISO C的字符串连接功能 */ globval, autoval, regival, volaval, statval); exit(0); } /* * Change variables after setjmp, but before longjmp. */ globval = 95; autoval = 96; regival = 97; volaval = 98; statval = 99; f1(autoval, regival, volaval, statval); /* never returns */ exit(0); } static void f1(int i, int j, int k, int l) { printf("in f1():\n"); printf("globval = %d, autoval = %d, regival = %d," "volaval = %d, statval = %d\n", globval, i, j, k, l); f2(); } static void f2(void) { longjmp(jmpbuffer, 1); }
如果以不带优化和带优化选项对此程序分别进行编译,然后运行它们,则得到的结果是不同的:
[root@localhost apue]# cc -o prog7-6 prog7-6.c 不进行任何优化的编译 [root@localhost apue]# ./prog7-6 in f1(): globval = 95, autoval = 96, regival = 97,volaval = 98, statval = 99 after longjmp: globval = 95, autoval = 96, regival = 97,volaval = 98, statval = 99 [root@localhost apue]# cc -o prog7-6 -O prog7-6.c 进行全部优化的编译 [root@localhost apue]# ./prog7-6 in f1(): globval = 95, autoval = 96, regival = 97,volaval = 98, statval = 99 after longjmp: globval = 95, autoval = 2, regival = 3,volaval = 98, statval = 99
注意,全局、静态和易失变量不受优化的影响,在调用longjmp后,它们的值是最近所呈现的值。
某个系统的setjmp(3)手册页上说明,存放在存储器中的变量将具有longjmp时的值,而在CPU和浮点寄存器中的变量则恢复为调用setjmp时的值。
不进行优化时,所有这5个变量都存放在存储器中(亦即忽略了对regival变量的register存储类说明)。而进行了优化后,autoval和regival都存放在寄存器中(即使autoval并未声明为register),volatile变量则仍存放在存储器中。
通过这一实例我们可以理解到,如果要编写一个使用非局部跳转的可移植程序,则必须使用volatile属性。
2、自动变量的潜在问题
基本规则是声明自动变量的函数已经返回后,不能再引用这些自动变量。
本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/。