setjmp、longjmp 实现长跳转我们已经见过 goto,它在函数作用域内跳转。但 longjmp() 允许你跳回到执行过程中的更早位置,跳回到调用了当前函数的那个函数。
它有很多限制和注意事项,但要从调用栈深处跳出去、回到较早的状态时,它可能很有用。
以我的经验,这是一个极少被用到的功能。
setjmp 与 longjmp
我们这里要跳的这套舞,基本上就是用 setjmp() 在执行过程中放一个书签。稍后我们会调用 longjmp(),它就会跳回到此前用 setjmp() 设置书签的那个执行位置。
即使你已经调用了几层子函数,它也能做到这一点。
下面是一个快速演示:我们调用几层深的函数,然后从中跳出来。
我们用一个文件作用域变量 env 来保存调用 setjmp() 时的状态,以便稍后调用 longjmp() 时恢复它们。我们就是靠这个变量记住自己“在哪儿”的。
变量 env 的类型是 jmp_buf,这是在 <setjmp.h> 中声明的一种不透明类型。
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;
void depth2(void)
{
printf("Entering depth 2\n");
longjmp(env, 3490); // 跳出
printf("Leaving depth 2\n"); // 这不会发生
}
void depth1(void)
{
printf("Entering depth 1\n");
depth2();
printf("Leaving depth 1\n"); // 这不会发生
}
int main(void)
{
switch (setjmp(env)) {
case 0:
printf("Calling into functions, setjmp() returned 0\n");
depth1();
printf("Returned from functions\n"); // 这不会发生
break;
case 3490:
printf("Bailed back to main, setjmp() returned 3490\n");
break;
}
}运行时输出如下:
Calling into functions, setjmp() returned 0
Entering depth 1
Entering depth 2
Bailed back to main, setjmp() returned 3490如果你把那段输出和代码对照起来看,就会明显发现里面有一些相当古怪的事在发生。
最引人注目的现象之一是 setjmp() 会返回两次。这到底是什么鬼?这是什么巫术?!
事情是这样的:如果 setjmp() 返回 0,就说明你已经成功在那个位置设好了“书签”。
如果它返回非零值,就说明你刚刚回到了先前设好的“书签”处。(而返回的值就是你传给 longjmp() 的那个值。)
这样你就能区分“设置书签”和“稍后回到书签”这两种情况。
所以上面那段代码第一次调用 setjmp() 时,setjmp() 会把状态存储到 env 变量里,并返回 0。之后我们用同一个 env 调用 longjmp(),它会恢复状态,而 setjmp() 则返回传给 longjmp() 的那个值。
在底层,这件事其实相当直接。通常由栈指针记录局部变量在内存中的存储位置,由程序计数器记录当前正在执行的指令的地址180。
所以如果我们想跳回到更早的某个函数,基本上只需要把栈指针和程序计数器恢复成 jmp_buf 变量里保存的值,并确保返回值设置正确。然后执行就会从那里继续。
但各种因素会让这件事变得复杂,从而制造出相当多的未定义行为陷阱。
如果你希望自动变量(即非 static、非 extern 的局部变量)的值在 longjmp() 发生之后,仍能在调用 setjmp() 的那个函数里保持有效,就必须把这些变量声明为 volatile。
严格来说,只有当它们在调用 setjmp() 之后、调用 longjmp() 之前发生了改变时,才必须声明为 volatile181。
例如,如果我们运行下面这段代码:
int x = 20;
if (setjmp(env) == 0) {
x = 30;
}然后稍后再用 longjmp() 跳回来,x 的值就会是不确定的。
如果想修正这个问题,x 必须声明为 volatile:
volatile int x = 20;
if (setjmp(env) == 0) {
x = 30;
}现在,当 longjmp() 把我们带回这个位置时,值就会是正确的 30。
当你调用 longjmp() 时,执行会从对应的 setjmp() 那一点继续。仅此而已。
规范指出,这就好像你跳回到函数中的那一点,而局部变量被设为调用 longjmp() 时它们各自的值。
套用规范的说法,不会被恢复的东西包括:
setjmp不能有任何名为 setjmp 的 extern 标识符。或者,如果 setjmp 是宏,你不能取消定义它。
这两种情况都是未定义行为。
setjmp()也就是说,你不能做下面这样的事:
if (x == 12 && setjmp(env) == 0) { ... }规范不允许这么复杂,因为在展开栈之类的过程中必须做许多精密的操作。我们不能用 longjmp() 跳回某个只执行了一部分的复杂表达式中间。
所以那个表达式的复杂度是有限制的。
它可以是条件语句的整个控制表达式。
if (setjmp(env)) {...}switch (setjmp(env)) {...}它可以是关系表达式或相等表达式的一部分,只要另一个操作数是整型常量。而整个表达式就是条件语句的控制表达式。
if (setjmp(env) == 0) {...}作为逻辑非(!)运算的操作数,而这个操作数就是整个控制表达式。
if (!setjmp(env)) {...}作为独立表达式,也可以强制转换为 void。
setjmp(env);(void)setjmp(env);longjmp()?以下情况属于未定义行为:
setjmp()setjmp()setjmp(),而在调用 longjmp() 之前,执行已经离开了该 VLA 的作用域。setjmp() 的函数在调用 longjmp() 之前就已经退出。关于最后一条,“退出”既包括从该函数正常返回,也包括另一次 longjmp() 跳回到调用栈中比该函数更“早”的位置这种情况。
longjmp() 传 0如果你试图把值 0 传给 longjmp(),它会悄悄把这个值改成 1。
由于 setjmp() 最终会返回这个值,而 setjmp() 返回 0 又有特殊含义,所以禁止返回 0。
longjmp() 与变长数组如果你处在某个 VLA 的作用域内,却用 longjmp() 跳了出去,那么分配给该 VLA 的内存就可能泄漏182。
如果你用 longjmp() 越过某些更早的函数,而那些函数里还有作用域内的 VLA,也会发生同样的事。
这就是 VLA 让我非常不爽的一点——你完全可以写出完全合法的 C 代码,却浪费了内存。不过,嘿——规范又不是我说了算。