在C中,goto语句是不能跨越函数的,而执行这类跳转功能的是函数setjmp和longjmp。这两个函数对于处理发生在深层嵌套函数调用中的出错情况是非常有用的。
setjmp和longjmp可在栈上跳过若干个调用帧,返回到当前函数调用路径上的某一个函数中。
#include <setjmp.h> int setjmp(jmp_buf env); // 返回值:若直接调用则返回0,若从longjmp调用返回则返回非0值 void longjmp(jmp_buf env, int val);
在 希望返回到的位置调用setjmp。因为我们直接调用该函数,所以其返回值为0。setjmp参数env的类型是一个特殊类型jmp_buf。这一数据类 型是某种形式的数组,其中存放在调用longjmp时能用来恢复栈状态的所有信息。因为需要在另一个函数中引用env变量,所以规范的处理方式是将env 变量定义为全局变量。
当检查到一个错误时,则以两个参数调用longjmp函数。第一个就是在调用setjmp时所用的env;第二个参数是具有非0值的val,它将成为从setjmp处返回的值。使用第二个参数的原因是对于一个setjmp可以有多个longjmp。
《UNIX环境高级编程》P161:程序清单7-5 setjmp和longjmp实例(有改动)
#include <stdlib.h> #include <setjmp.h> #define TOK_ADD 5 #define MAXLINE 1024 void do_line(char *ptr); void cmd_add(void); int get_token(void); jmp_buf jmpbuffer; int main() { char line[MAXLINE]; if (setjmp(jmpbuffer) != 0) // 将所需的信息记入变量jmpbuffer中并返回0 printf("error"); while (fgets(line, MAXLINE, stdin) != NULL) do_line(line); exit(0); } char *tok_ptr; void do_line(char *ptr) { int cmd; tok_ptr = ptr; while ((cmd = get_token()) > 0) { switch(cmd) { case TOK_ADD: cmd_add(); break; } } } void cmd_add(void) { int token; token = get_token(); if (token < 0) longjmp(jmpbuffer, 1); // 调用longjmp造成main中setjmp返回,此时setjmp返回值为1 } int get_token(void) { int n = (*tok_ptr) - '0'; tok_ptr++; return n; }
编译并执行程序:
$ gcc 05.c -o 05 -Wall ./05 5/ error
调用cmd_add后的各个栈帧如下所示:
调用longjmp使栈反绕到执行main的情况,也就是抛弃cmd_add和do_line的栈帧。
调用longjmp时后,在main函数中,自动变量和寄存器变量能否恢复到以前调用setjmp时的值?大多数实现并不回滚这些自动变量和寄存器变量的值,而所有标准则说它们的值是不确定的。如果你有一个自动变量,而又不像使其回滚,则可以定义其为具有volatile属性。声明为全局变量或静态变量的值在执行longjmp时保持不变。
下面的程序清单说明在调用longjmp后,自动变量、全局变量、寄存器变量、静态变量和易失变量的不同情况。
《UNIX环境高级编程》P163:程序清单7-6 longjmp对各类变量的影响(有改动)
#include <stdio.h> #include <stdlib.h> #include <setjmp.h> static void f1(int i, int j, int k, int l); 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", globval, autoval, regival, volaval, statval); exit(0); } // 改变各个变量中的值 globval = 95; autoval = 96; regival = 97; volaval = 98; statval = 99; f1(autoval, regival, volaval, statval); 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(); // 调用longjmp使栈反绕到执行main函数的情况 } static void f2(void) { longjmp(jmpbuffer, 1); }
不带优化和带优化选项对此程序分别进行编译,然后运行它们,则得到的结果是不同的:
$ gcc 06.c 不进行任何优化的编译 $ ./a.out in f1().: globval = 95, autoval = 96, regival = 97, volaval = 98, statval = 99 after longjmp: globval = 95, autoval = 96, regival = 97, volaval = 98, statval = 99 $ gcc -O 06.c 进行全部优化的编译 $ ./a.out 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都存放在寄存器中,volatile变量则仍存放在存储器中。如果要编写一个使用非局部跳转的可移植程序,则必须使用volatile属性。但是从一个系统移植到另一个系统,任何事情都可能改变。
声明自动变量的函数已经返回后,不能再引用这些自动变量。
《UNIX环境高级编程》P164:程序清单7-7 自动变量的不正确使用
#include <stdio.h> #define DATAFILE "datafile" FILE *open_data(void) { FILE *fp; char databuf[BUFSIZ]; if ((fp = fopen(DATAFILE, "r")) == NULL) return(NULL); if (setvbuf(fp, databuf, _IOLBF, BUFSIZ) != 0) return(NULL); return(fp); }
问题是:当open_data返回时,它在栈上所使用的空间将由下一个被调用函数的栈帧使用。这就产生了冲突和混乱。为了校正这一问题,应在全局存储空间静态地(如static或extern)或者动态地(使用一种alloc函数)为数组databuf分配空间。