| 目录 |

34 用 setjmp、longjmp 实现长跳转

我们已经见过 goto,它在函数作用域内跳转。但 longjmp() 允许你跳回到执行过程中的更早位置,跳回到调用了当前函数的那个函数。

它有很多限制和注意事项,但要从调用栈深处跳出去、回到较早的状态时,它可能很有用。

以我的经验,这是一个极少被用到的功能。

34.1 使用 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() 的那个值。

34.2 陷阱

在底层,这件事其实相当直接。通常由栈指针记录局部变量在内存中的存储位置,由程序计数器记录当前正在执行的指令的地址180。

所以如果我们想跳回到更早的某个函数,基本上只需要把栈指针和程序计数器恢复成 jmp_buf 变量里保存的值,并确保返回值设置正确。然后执行就会从那里继续。

但各种因素会让这件事变得复杂,从而制造出相当多的未定义行为陷阱。

34.2.1 局部变量的值

如果你希望自动变量(即非 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。

34.2.2 保存了多少状态?

当你调用 longjmp() 时,执行会从对应的 setjmp() 那一点继续。仅此而已。

规范指出,这就好像你跳回到函数中的那一点,而局部变量被设为调用 longjmp() 时它们各自的值。

套用规范的说法,不会被恢复的东西包括:

34.2.3 不能把任何东西命名为 setjmp

不能有任何名为 setjmp 的 extern 标识符。或者,如果 setjmp 是宏,你不能取消定义它。

这两种情况都是未定义行为。

34.2.4 不能在更大的表达式里调用 setjmp()

也就是说,你不能做下面这样的事:

if (x == 12 && setjmp(env) == 0) { ... }

规范不允许这么复杂,因为在展开栈之类的过程中必须做许多精密的操作。我们不能用 longjmp() 跳回某个只执行了一部分的复杂表达式中间。

所以那个表达式的复杂度是有限制的。

34.2.5 什么时候不能 longjmp()?

以下情况属于未定义行为:

关于最后一条,“退出”既包括从该函数正常返回,也包括另一次 longjmp() 跳回到调用栈中比该函数更“早”的位置这种情况。

34.2.6 不能给 longjmp() 传 0

如果你试图把值 0 传给 longjmp(),它会悄悄把这个值改成 1。

由于 setjmp() 最终会返回这个值,而 setjmp() 返回 0 又有特殊含义,所以禁止返回 0。

34.2.7 longjmp() 与变长数组

如果你处在某个 VLA 的作用域内,却用 longjmp() 跳了出去,那么分配给该 VLA 的内存就可能泄漏182。

如果你用 longjmp() 越过某些更早的函数,而那些函数里还有作用域内的 VLA,也会发生同样的事。

这就是 VLA 让我非常不爽的一点——你完全可以写出完全合法的 C 代码,却浪费了内存。不过,嘿——规范又不是我说了算。


| 目录 |