第71章 setjmp 与 longjmp:非局部跳转的实现
说实话,C语言里有个特性,很多工程师写了十年代码都没碰过——setjmp 和 longjmp。我第一次接触它是在一个嵌入式实时系统里,当时要处理一个深层嵌套的错误恢复场景,用 goto 根本跳不出去,用返回值一层层传又太啰嗦。嗯,就是那次,我认识了这对「非局部跳转」的兄弟。
什么是非局部跳转?
普通的 goto 只能在同一个函数内部跳转。但 setjmp 和 longjmp 可以让你从一个函数直接跳回到另一个函数的某个位置——说白了,就是跨函数跳转。你想想看,这在处理深层错误时有多方便。
核心概念: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 特别合适:
- 深层错误恢复:比如一个协议栈解析,嵌套了五六层函数调用,中间某层出错时,直接跳回顶层主循环,比一层层返回错误码干净得多。
- 信号处理:在信号处理函数中,可以用
longjmp跳回主程序的安全位置,避免信号处理函数里做复杂操作。 - 协程模拟:虽然不推荐,但早期有些嵌入式系统用
setjmp/longjmp来模拟简单的协程切换。
避坑指南
我曾经犯过一个严重的错误:在 setjmp 和 longjmp 之间,修改了局部变量的值,然后假设跳回去后这些值还是原来的。结果呢?行为是未定义的!
具体来说,有这么几个坑你必须知道:
- 局部变量可能被恢复,也可能不会——这取决于编译器优化。如果变量被优化到寄存器里,
longjmp恢复栈时可能不会恢复寄存器中的值。我建议把这类变量声明为volatile。 - 不要跳过资源释放:
longjmp跳走时,中间函数里 malloc 的内存、打开的文件都不会自动释放。我曾经见过一个同事用longjmp做错误处理,结果内存泄漏得一塌糊涂。 - 不要跨线程使用:
jmp_buf是线程不安全的,别想着在线程 A 里 setjmp,在线程 B 里 longjmp——这会导致栈混乱,程序直接崩溃。
与 goto 的对比
| 特性 | goto | setjmp/longjmp |
|---|---|---|
| 跳转范围 | 同一函数内 | 跨函数 |
| 栈状态 | 不改变 | 恢复到 setjmp 时的栈 |
| 资源管理 | 容易控制 | 容易泄漏 |
| 可读性 | 一般 | 较差,需谨慎使用 |
知识结构图
下面这张图帮你理清 setjmp/longjmp 的核心逻辑:
使用建议
说实话,setjmp/longjmp 是个双刃剑。用好了能简化错误处理,用不好就是灾难。我个人建议:
- 只在嵌入式底层代码中使用,应用层尽量别碰
- 配合 volatile 使用,避免优化导致的问题
- 做好资源清理,跳转前确保所有资源都已释放
- 代码注释要详细,不然三个月后你自己都看不懂跳转逻辑
一个小技巧:如果你一定要用 longjmp,可以在 setjmp 之后立即保存当前栈指针和帧指针,这样调试时能快速定位问题。我习惯在关键位置加个日志打印,记录跳转前后的状态。
好了,关于 setjmp 和 longjmp 就讲这么多。记住一句话:非局部跳转是 C 语言里最危险也最强大的特性之一,用之前先问问自己——真的没有更好的办法了吗?