第71章 setjmp 与 longjmp:非局部跳转的实现

说实话,C语言里有个特性,很多工程师写了十年代码都没碰过——setjmplongjmp。我第一次接触它是在一个嵌入式实时系统里,当时要处理一个深层嵌套的错误恢复场景,用 goto 根本跳不出去,用返回值一层层传又太啰嗦。嗯,就是那次,我认识了这对「非局部跳转」的兄弟。

什么是非局部跳转?

普通的 goto 只能在同一个函数内部跳转。但 setjmplongjmp 可以让你从一个函数直接跳回到另一个函数的某个位置——说白了,就是跨函数跳转。你想想看,这在处理深层错误时有多方便。

核心概念setjmp 设置一个「跳转点」,longjmp 负责「跳回去」。跳回去时,栈状态会恢复到 setjmp 调用时的样子。

基本用法

先看一个最简单的例子:

#include <stdio.h>
#include <setjmp.h>

jmp_buf buf;

void func() {
    printf("进入 func\n");
    longjmp(buf, 42);  // 跳回 setjmp 处,返回值为 42
    printf("这行不会执行\n");
}

int main() {
    int ret = setjmp(buf);
    if (ret == 0) {
        printf("第一次调用 setjmp,ret = %d\n", ret);
        func();
    } else {
        printf("从 longjmp 跳回,ret = %d\n", ret);
    }
    return 0;
}

输出结果:

第一次调用 setjmp,ret = 0
进入 func
从 longjmp 跳回,ret = 42

看到了吗?func 里的 longjmp 直接跳回了 main 中的 setjmp 位置,而且第二次进入时 ret 变成了 42。这就是非局部跳转的威力。

setjmp 的返回值机制

这里有个细节我当年踩过坑:setjmp 有两种返回值。

  • 直接调用时:返回 0
  • 被 longjmp 跳回时:返回 longjmp 的第二个参数

如果 longjmp 的第二个参数传了 0,实际会变成 1。为什么?因为 0 已经被「首次调用」占用了,得区分开。我曾经在代码里传了 0 进去,结果死活判断不出是哪种情况,查了半天才发现这个设计。

个人习惯:我一般用大于 0 的整数作为 longjmp 的返回值,比如用枚举定义不同的错误码,这样可读性更好。

典型应用场景

我在项目中遇到过这么几个场景,用 setjmp/longjmp 特别合适:

  1. 深层错误恢复:比如一个协议栈解析,嵌套了五六层函数调用,中间某层出错时,直接跳回顶层主循环,比一层层返回错误码干净得多。
  2. 信号处理:在信号处理函数中,可以用 longjmp 跳回主程序的安全位置,避免信号处理函数里做复杂操作。
  3. 协程模拟:虽然不推荐,但早期有些嵌入式系统用 setjmp/longjmp 来模拟简单的协程切换。

避坑指南

我曾经犯过一个严重的错误:在 setjmplongjmp 之间,修改了局部变量的值,然后假设跳回去后这些值还是原来的。结果呢?行为是未定义的!

具体来说,有这么几个坑你必须知道:

  • 局部变量可能被恢复,也可能不会——这取决于编译器优化。如果变量被优化到寄存器里,longjmp 恢复栈时可能不会恢复寄存器中的值。我建议把这类变量声明为 volatile
  • 不要跳过资源释放longjmp 跳走时,中间函数里 malloc 的内存、打开的文件都不会自动释放。我曾经见过一个同事用 longjmp 做错误处理,结果内存泄漏得一塌糊涂。
  • 不要跨线程使用jmp_buf 是线程不安全的,别想着在线程 A 里 setjmp,在线程 B 里 longjmp——这会导致栈混乱,程序直接崩溃。

与 goto 的对比

特性 goto setjmp/longjmp
跳转范围 同一函数内 跨函数
栈状态 不改变 恢复到 setjmp 时的栈
资源管理 容易控制 容易泄漏
可读性 一般 较差,需谨慎使用

知识结构图

下面这张图帮你理清 setjmp/longjmp 的核心逻辑:

setjmp / longjmp 执行流程 main() 函数 setjmp(buf) → ret = 0 ret == 0 ? (首次调用) 调用 func() func() 函数 longjmp(buf, 42) 跳回 setjmp(buf) → ret = 42 进入 func 虚线箭头表示 longjmp 跳回路径,实线箭头表示正常函数调用路径

使用建议

说实话,setjmp/longjmp 是个双刃剑。用好了能简化错误处理,用不好就是灾难。我个人建议:

  • 只在嵌入式底层代码中使用,应用层尽量别碰
  • 配合 volatile 使用,避免优化导致的问题
  • 做好资源清理,跳转前确保所有资源都已释放
  • 代码注释要详细,不然三个月后你自己都看不懂跳转逻辑

一个小技巧:如果你一定要用 longjmp,可以在 setjmp 之后立即保存当前栈指针和帧指针,这样调试时能快速定位问题。我习惯在关键位置加个日志打印,记录跳转前后的状态。

好了,关于 setjmplongjmp 就讲这么多。记住一句话:非局部跳转是 C 语言里最危险也最强大的特性之一,用之前先问问自己——真的没有更好的办法了吗?


公众号:蓝海资料掘金营,微信deep3321