在C里,我们不能用goto进入另一个函数的标签。相反,我们必须使用setjmp和longjmp函数来执行这种类型的跳转。正如我们将看到的那样,这两个函数对于处理发生在嵌套地很深的函数调用里的错误情况很有用。
考虑下面代码的seketon。它由一个从标准输入读取行的主循环和处理各行的函数do_line调用组成。这个函数随后调用get_token来从输入
行里获取下一个语素。一行的第一个语素被视为某种格式的一个命令,而switch语句选择每个命令。对显示的单个命令,cmd_add函数被调用。
- #include <stdio.h>
- #define TOK_ADD 5
- #define MAXLINE 4096
- void do_line(char *);
- void cmd_add(void);
- int get_token(void);
- int
- main(void)
- {
- char line[MAXLINE];
- while (fgets(line, MAXLINE, stdin) != NULL)
- do_line(line);
- exit(0);
- }
- char *tok_ptr; /* global pointer for get_token() */
- void
- do_line(char *ptr) /* process one line of input */
- {
- int cmd;
- tok_ptr = ptr;
- while ((cmd = get_token()) > 0) {
- switch (cmd) { /* one case for each command */
- case TOK_ADD:
- cmd_add();
- break;
- }
- }
- }
- void
- cmd_add(void)
- {
- int token;
- token = get_token();
- /* rest of processing for this command */
- }
- int
- get_token(void)
- {
- /* fetch next token from line pointed to by tok_ptr */
- }
上面的代码是典型的程序:读取命令,决定命令类型,然后调用函数来处理每个命令。栈的底部往上分别是main、do_line和cmd_add的栈框架。
自动变量存储在每个函数的栈框架里。数据line在main的栈框架里,整型cmd在do_line的栈框架里,而整型token在cmd_add的栈框
架里。
正如我们已经说过的,这种栈的排列类型是典型的,但不是必需的。栈不必向更低内存地址增长。在没有支持栈的内置硬件的系统上,C实现可能用一个链表来作为它的栈框架。
就像上述代码一样的程序进程碰到的编码问题是如何处理非致命错误。例如,如果cmd_add函数碰到一个错误--比如一个无效数字--它可以想打印一个错
误,忽略输入行的剩余部分,并返回到main函数来读取下个输入行。但是当我们从main函数很深地嵌套了很多层时,在C里面很难做到。(在这个例子里,
在cmd_add函数,我们只从main里往下两层,但从我们从想要回到的地方到当前位置有五层或更多并不是不普遍的事。)如果我们必须在每个函数里返回
一个特殊的值来返回一层会变得很凌乱。
这个问题的解决方案是使用一个非本地的的goto:setjmp和longjmp函数。形容词“非本地的”是因为我们不能在一个函数里用普通的C goto语句,相反,我们要通过调用框架来跳转到一个当前函数的调用路径里的一个函数。
- #include <setjmp.h>
- int setjmp(jmp_buf env);
- 如果直接调用返回0,如果从longjmp调用返回则返回非0。
- void longjmp(jmp_buf env, int val);
我们从我们想回到的地点里调用setjmp,在这个例子里是main函数。这种情况下,
setjmp返回0因为我们直接调用它。在这个setjmp的调用里,env参数是一个特殊的类型jmp_buf。这个数据类型是某种格式的数组,能够存
储所有所需的信息,当我们调用longjmp时用来恢复栈的状态。通常,env变量是一个全局变量,因为我们将需要从另一个函数里引用它。
当我们碰到一个错误--比如在cmd_add函数里--我们用两个参数调用longjmp。第一个和我们在setjmp调用里一样的env,而第二个,是
个作为setjmp的返回值的一个非0值。例如,我们可以从cmd_add用一个值为1的val调用longjmp,也可以从get_token以值为2
的val调用longjmp。在main函数里,从setjmp返回的值是1或2,而如果我们愿意的话,可以测试这个值,来决定longjmp是从
cmd_add还是get_token出来的。
让我们回到这个例子。下面的代码展示了main和cmd_add函数。(另两个函数,do_line和get_token,没有改变。)
- #include <setjmp.h>
- jmp_buf jmpbuffer;
- int
- main(void)
- {
- char line[MAXLINE];
- if (setjmp(jmpbuffer) != 0)
- printf("error");
- while (fgets(line, MAXLINE, stdin) != NULL)
- do_line(line);
- exit(0);
- }
- void
- cmd_add(void)
- {
- int token;
- token = get_token();
- if (token < 0)
- longjmp(jmpbuffer, 1);
- /* rest of processing for this command */
- }
当main被执行时,我们调用setjmp,它在变量jmpbuffer里记录任何它需要信息并返回0。我们然后调用do_line,它会用
cmd_add,并假定察觉到某种形式的一个错误。在cmd_add里调用longjmp之前,栈里有main、do_line和cmd_add函数的框
架。但是longjmp导致栈直接回到main函数,把cmd_add和do_line的栈框架给丢弃了。调用longjmp导致main里的
setjmp返回,但这次它返回的值为1(longjmp的第二个参数。)
自动、寄存器、和易变变量(Automatic, Register, and Volatile Variables)
我们已经看到调用longjmp之后的栈是怎么样的。下一个问题是:“在main函数里的自动变量和寄存器变量是什么状态?”当通过longjmp回到
main,这些变量是当setjmp上次调用时对应的值(也就是说,它们的值被回滚),还是没有被干涉,以致它们的值是当do_line被调用时的任何一
个值(do_line调用了cmd_add,而cmd_add调用了longjmp)?不幸的是,答案是“看情况”。多数实现都不尝试回滚这些自动变量和
寄存器变量,但是标准只说它们的值是不确定的。如果你有一个不想回滚的自动变量,把它定义成易变变量。被声明为全局或静态的变量当longjmp被执行时
不会被干涉。
看下面的例子:
- #include <stdio.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",
- 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);
- }
不使用编译优化的结果:
$ cc longjmp_on_variables.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
使用编译优化的结果:
$ cc longjmp_on_variables.c -O
$ ./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手册页表明存储在内存里的变
量将会有longjmp调用时的值,而CPU和浮点寄存器里的变量在setjmp调用时会回复它们的值。这些确实是我们上面代码所看到的。在没有优化的情
况下,这五个变量都存储在内存里(register提示被忽略)。当我们启用优化时,autoval和regival都放入寄存器里,即使前者没有显式声
明为register,而volatile变量保留在内存里。这个例子里要知道的事是你如果在写一个使用非本地跳转的可移植的程序时,你必须使用
volatile属性。其它任何东西在各个系统上都会改变。
在上面代码里的一些printf格式字符串太长,不能在程序文本里很好地显示。我们依赖ISO C的字符串连接特性,而不是调用多次printf。代码"string1" "string2"等价于"string1string2"。
我们将在第10章讨论信号处理和它们的信号版本:sigsetjmp和siglongjmp时再回到这两个函数:setjmp和longjmp。
自动变量的潜在问题
看过栈框架通常被处理的方式,我们应该看下处理自动变量的潜在问题。基本原则是一个自动变量绝不能在声明它的函数返回后被引用。贯穿整个UNIX系统手册,有许多关于它的警告。
下面的代码展示了一个名为open_data的函数,它打开一个标准I/O流,并为这个流设置缓冲:
- #include <stdio.h>
- #define DATAFILE "datafile"
- FILE *
- open_data(void)
- {
- FILE *fp;
- char databuf[BUFSIZ]; /* setvbuf makes this the stdio buffer */
- if ((fp = fopen(DATAFILE, "r")) == NULL)
- return(NULL);
- return(fp); /* error */
- }
问题是当open_data返回时,它在栈上使用的空间将会被下一个被调用的函数使用。然而标准I/O仍在使用那块作为它的流缓冲的内存部分。这肯定会引
起混乱。为了更正这个问题,数组databuf应该从全局内存里分配,或者静态的(static或extern)或者动态的(某个alloc函数)。