#include <setjmp.h> int setjmp(jmp_buf env); /* 返回值:若直接调用,返回 0;若从 longjmp 返回,则为非 0 */ void longjmp(jmp_buf env, int val);
setjmp 的参数 env 是一个特殊类型 jmp_buf,这种类型是某种形式的数组,其中存放了在调用 longjmp 时能用来恢复栈状态的所有信息。因为需要在另一函数中引用 env 变量,所以通常将 env 定义为全局变量。
longjmp 的第一个参数就是在调用 setjmp 时所用的 env,第二个 val 应是一个非 0 值,它将成为从 setjmp 处返回的值。使用第二个参数是因为对于一个 setjmp 可以有多个 longjmp,因此通过测试返回值就可判断造成返回的 longjmp 是位于哪个函数。
想象一下有这样的几个函数调用:main 函数调用 do_line 函数,do_line 函数又调用一个 cmd_add 函数,下图显示了调用 cmd_add 之后栈通常的大致使用情况(虽然栈并不一定要像低地址方向扩充,例如在某些没有对栈提供特殊硬件支持的系统上,栈帧可能是用链表实现的,但这是一种典型的栈安排)。
如果某个时候在 cmd_add 函数中遇到了一个非致命性的错误,那可能不得不以检查返回值的方法逐层返回到 main,这可能会变得很麻烦。而如果利用 setjmp 和 longjmp 函数,就可在栈上跳过若干调用帧,直接返回到当前函数调用路径上的某一个函数中。所以如果在 main 中预先使用 setjmp 设置了跳转标记,那么当在 cmd_add 中遇到非致命性的错误时,就可调用 longjmp 使栈反绕到执行 main 函数时的情况,也就是抛弃了 cmd_add 和 do_line 的栈帧,如同下图所示。
但接下来的问题是:当 longjmp 返回到 main 函数时,其中的自动变量和寄存器变量等的值是否能恢复到以前调用 setjmp 时的值?遗憾的是,对此问题的回答是“看情况”。大多数实现并不回滚这些自动变量和寄存器变量的值,而所有标准则称它们的值是不确定的。如果你有一个自动变量,而又不想使其值回滚,则可定义其为具有 volatile 属性。声明为全局变量和静态变量的值在调用 longjmp 时保持不变。
下面这个程序说明了在调用 longjmp 后各种类型的变量的变化情况。
#include <stdio.h> #include <stdlib.h> #include <setjmp.h> static void f1(int, int, int, int); static void f2(void); static jmp_buf jmpbuffer; int exteval; static int globval; int main(void){ int autoval = 2; register int regival = 3; volatile int volaval = 4; static int statval = 5; exteval = 0; globval = 1; if(setjmp(jmpbuffer) != 0){ printf("after longjmp:\n"); printf("exteval=%d, global=%d, autoval=%d, regival=%d, " "volaval=%d, statval=%d\n", exteval, globval, autoval, regival, volaval, statval); exit(0); } // Change variables after setjmp, buf before longjmp. exteval = 94; 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("exteval=%d, global=%d, autoval=%d, regival=%d, " "volaval=%d, statval=%d\n", exteval, globval, i, j, k, l); f2(); } static void f2(void){ longjmp(jmpbuffer, 1); }
如果以带优化和不带优化选项编译后运行本程序,得到的结果是不一样的。
$ gcc longjmpDemo.c -o longjmpDemo.out # 不进行任何优化的编译 $ ./longjmpDemo.out in f1(): exteval=94, global=95, autoval=96, regival=97, volaval=98, statval=99 after longjmp: exteval=94, global=95, autoval=96, regival=97, volaval=98, statval=99 $ $ gcc -O longjmpDemo.c -o longjmpDemo2.out # 进行全部优化的编译 $ ./longjmpDemo2.out in f1(): exteval=94, global=95, autoval=96, regival=97, volaval=98, statval=99 after longjmp: exteval=94, global=95, autoval=2, regival=3, volaval=98, statval=99 $
由此可见,全局变量、静态变量和易失变量不受优化的影响,在 longjmp 之后,它们的值是最近所呈现的值。setjmp 的手册页上说明,存放在存储器中的变量将具有 longjmp 时的值,而在 CPU 和浮点寄存器中的变量则恢复为调用 setjmp 时的值。由于当不进行优化时,这几个变量都存放在存储器中(即忽略了 register 存储类说明),而进行了优化后,autoval 和 regival 都存放在寄存器中(即使 autoval 没有用 register 说明),volatile 变量则仍存放在存储器中,所以才能看到上面的输出情况。因此若要编写一个使用非局部跳转的可移植程序,应该使用 volatile 属性。但是从一个系统移植到另一个系统,其他任何事情都可能改变。
而关于自动变量,还有一种潜在的出错情况。基本规则是声明自动变量的函数返回后,不能再引用这些自动变量。下面这个函数就说明了自动变量的不正确使用的情况:它为它所打开的一个标准 I/O 流设置缓冲。
#include <stdio.h> FILE *open_data(void){ FILE *fp; char databuf[BUFSIZE]; // setvbuf makes this the stdio buffer. if((fp=fopen("datafile", "r")) == NULL) return NULL; if(setvbuf(fp, databuf, _IOLBF, BUFSIZE) != 0) return NULL; return fp; // error }
这里存在的问题是,当 open_data 返回时,它在栈上所使用的空间将由下一个被调用函数的栈帧使用。但是,标准 I/O 库函数仍将使用这部分存储空间作为该流的缓冲区,这就产生了冲突和混乱。所以正确的做法应是在全局存储空间静态地(如 static 或 extern)或者动态地为数组 databuf 分配空间。