setjmp & longjmp 深度详解 + 代码示例
setjmp 和 longjmp 是 C 标准库提供的非局部跳转(Non-local Jump) 接口,头文件为 <setjmp.h>。
C 语言没有 try/catch 异常机制,这组函数常用来模拟异常、跨多层函数快速退出、上下文跳转 ,和只能函数内跳转的 goto 有本质区别。
一、基础概念
1. 核心数据类型:jmp_buf
arduino
typedef ... jmp_buf;
jmp_buf 是不透明类型 (内部由平台实现,通常是结构体 / 数组),作用是存储程序当前执行上下文:
- 栈指针 SP、指令指针 IP(下一条执行地址)
- CPU 通用寄存器、状态寄存器
- 栈帧信息
简单理解:它就是一张「快照」,setjmp 拍照,longjmp 恢复快照并跳转。
2. 函数原型
(1)setjmp
arduino
int setjmp(jmp_buf env);
-
功能 :捕获当前执行上下文,存入
env。 -
返回规则(核心) :
- 第一次直接调用 :正常保存上下文,返回 0;
- 被
longjmp跳转回来 :恢复上下文,返回longjmp传入的第二个参数(非 0 值)。
(2)longjmp
arduino
void longjmp(jmp_buf env, int val);
-
功能 :根据
env中保存的上下文,跳转到对应setjmp位置继续执行。 -
规则:
- 跳转后不会回到
longjmp所在函数,程序流直接切走; - 若
val = 0,标准会强制转为 1 (避免和setjmp原生返回 0 冲突); - 必须保证
env对应的setjmp所在栈帧未被销毁,否则行为未定义。
- 跳转后不会回到
3. 核心原理
程序每次调用函数都会在进程栈上创建独立栈帧(局部变量、返回地址、寄存器)。
setjmp:把当前栈帧、CPU 所有寄存器、指令地址全部存入jmp_buf;longjmp:反向恢复寄存器、栈指针、指令指针,程序直接跳回setjmp执行点,实现跨栈帧跳转。
4. 和 goto 的区别
| 特性 | goto | setjmp/longjmp |
|---|---|---|
| 跳转范围 | 仅限同一函数内部 | 跨函数、跨多层栈帧 |
| 实现方式 | 编译期静态跳转 | 运行时上下文恢复 |
| 适用场景 | 跳出单层循环 | 异常、多层函数退出 |
5. 典型使用场景
- C 语言模拟
try-catch异常处理; - 深层函数 / 递归快速退出,避免层层返回错误码;
- 信号处理函数中跳转回主逻辑;
- 嵌入式 / 底层库简易上下文切换(早期协程雏形)。
二、高频踩坑点(重中之重)
使用这组函数极易出 Bug,务必遵守以下规则:
1. jmp_buf 生命周期必须有效
setjmp 所在的函数不能提前返回 。一旦函数返回,其栈帧被系统销毁,再调用 longjmp 会触发未定义行为(程序崩溃、数据乱码)。
2. 局部变量必须加 volatile
编译器会优化局部变量(把变量放到 CPU 寄存器而非内存)。
setjmp 只会保存内存栈、寄存器上下文 ,跳转后未加 volatile 的局部变量值会错乱。
凡是在
setjmp和longjmp之间被修改的自动局部变量 ,必须用volatile修饰。
3. longjmp 第二个参数不要传 0
传 0 会被强制转为 1,失去区分「首次执行 / 跳转返回」的意义。
4. 跳转不会自动清理资源
跳转不会执行:文件关闭、堆内存释放、锁释放、C++ 对象析构。
跳转前必须手动清理资源,否则会造成内存 / 句柄泄漏。
C++ 严禁滥用:不会调用栈对象析构函数,优先使用 C++ 标准异常。
5. 禁止跨线程 / 跨进程跳转
jmp_buf 绑定当前线程栈,跨线程 / 进程调用 longjmp 属于未定义行为。
三、代码示例(由浅入深,可直接编译运行)
编译命令(GCC/Clang):
bash
gcc demo.c -o demo && ./demo
示例 1:最简入门(基础跳转逻辑)
演示最基本的调用流程、返回值区别。
arduino
#include <stdio.h>
#include <setjmp.h>
// 定义上下文快照
jmp_buf env;
void test(void)
{
printf("进入 test 函数,准备执行 longjmp\n");
// 跳转到 setjmp 位置,让 setjmp 返回 100
longjmp(env, 100);
// 下面代码永远不会执行
printf("这句永远打印不出来\n");
}
int main(void)
{
int ret = setjmp(env);
if (ret == 0)
{
// 第一次执行 setjmp,返回 0,走这里
printf("首次调用 setjmp,返回值:%d\n", ret);
test();
}
else
{
// 被 longjmp 跳转回来,走这里
printf("被 longjmp 跳转回来,返回值:%d\n", ret);
}
return 0;
}
运行结果
bash
首次调用 setjmp,返回值:0
进入 test 函数,准备执行 longjmp
被 longjmp 跳转回来,返回值:100
流程解析
setjmp首次调用 → 返回 0,调用test();test()中执行longjmp(env, 100)→ 恢复上下文,跳回setjmp处;- 此时
setjmp返回 100,进入else分支。
示例 2:volatile 关键字的必要性(经典坑)
对比 不加 volatile 和 加 volatile 的差异,理解编译器优化问题。
(1)错误写法:无 volatile,变量值错乱
arduino
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;
int flag = 0;
void func(void)
{
flag = 1; // 修改变量
longjmp(env, 1);
}
int main(void)
{
// 局部变量,未加 volatile,会被编译器优化到寄存器
int val = 0;
if (setjmp(env) == 0)
{
val = 10;
func();
}
// 跳转后 val 值可能不是 10,取决于编译器优化
printf("val = %d\n", val);
return 0;
}
现象 :高优化级别(-O2)下,val 大概率输出 0,而非预期的 10。
(2)正确写法:添加 volatile
arduino
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;
void func(void)
{
longjmp(env, 1);
}
int main(void)
{
// volatile 强制变量常驻内存,禁止寄存器优化
volatile int val = 0;
if (setjmp(env) == 0)
{
val = 10;
func();
}
printf("val = %d\n", val); // 固定输出 10
return 0;
}
运行结果
ini
val = 10
原理说明
- 不加
volatile:编译器把val放入 CPU 寄存器,setjmp不会保存所有通用寄存器,跳转后寄存器值被篡改; - 加
volatile:强制变量读写走内存,跳转后值保持正确。
示例 3:危险用法:栈帧销毁后调用 longjmp(未定义行为)
演示最常见的致命错误:setjmp 所在函数已返回,再执行 longjmp。
arduino
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;
// setjmp 定义在这个函数内
void set_env(void)
{
setjmp(env);
printf("setjmp 执行完毕\n");
}
void bad_jump(void)
{
// set_env 已经返回,栈帧销毁!
longjmp(env, 1);
}
int main(void)
{
set_env(); // 函数返回,栈帧被回收
bad_jump(); // 触发未定义行为:崩溃/乱码/死循环
return 0;
}
结论 :绝对不要让 setjmp 所在函数提前返回。
示例 4:实战场景 ------ C 语言模拟 try-catch 异常
C 没有原生异常,这是 setjmp/longjmp 最经典的工业用法:
多层函数调用,底层出错直接跳转到顶层统一处理错误,无需层层传递错误码。
arduino
#include <stdio.h>
#include <setjmp.h>
jmp_buf exception_env;
// 底层业务函数:模拟出错
void layer3(int num)
{
if (num < 0)
{
printf("layer3: 参数非法,抛出异常\n");
// 异常跳转:返回错误码 2
longjmp(exception_env, 2);
}
printf("layer3: 正常执行,num = %d\n", num);
}
void layer2(int num)
{
printf("进入 layer2\n");
layer3(num);
}
void layer1(int num)
{
printf("进入 layer1\n");
layer2(num);
}
int main(void)
{
int ret = setjmp(exception_env);
if (ret == 0)
{
// try 代码块:正常业务逻辑
printf("=== 开始执行业务 ===\n");
layer1(-10); // 传入非法参数,触发异常
printf("=== 业务执行完成 ===\n");
}
else
{
// catch 代码块:统一异常处理
printf("=== 捕获异常,错误码:%d ===\n", ret);
}
printf("程序继续运行...\n");
return 0;
}
运行结果
diff
=== 开始执行业务 ===
进入 layer1
进入 layer2
layer3: 参数非法,抛出异常
=== 捕获异常,错误码:2 ===
程序继续运行...
流程解析
- 正常执行
setjmp→ 返回 0,执行业务调用链layer1 -> layer2 -> layer3; layer3发现参数错误,调用longjmp直接跳回main的setjmp;setjmp返回 2,进入异常处理分支,一次性跳出三层函数。
示例 5:多层嵌套快速退出(替代多层 return)
模拟递归 / 多层循环场景,一键退出:
arduino
#include <stdio.h>
#include <setjmp.h>
jmp_buf buf;
void func3(void)
{
printf("func3: 发现需要退出所有层级\n");
longjmp(buf, 1);
}
void func2(void)
{
printf("进入 func2\n");
func3();
printf("func2 后续代码(不会执行)\n");
}
void func1(void)
{
printf("进入 func1\n");
func2();
printf("func1 后续代码(不会执行)\n");
}
int main(void)
{
if (setjmp(buf) == 0)
{
printf("正常流程开始\n");
func1();
}
else
{
printf("已从多层函数中统一退出\n");
}
return 0;
}
运行结果
makefile
正常流程开始
进入 func1
进入 func2
func3: 发现需要退出所有层级
已从多层函数中统一退出
四、总结 & 现代使用建议
1. 核心回顾
setjmp:拍照(保存上下文) ,首次返回 0,跳转后返回指定值;longjmp:还原快照 + 跳转,跨函数运行时跳转;- 两大硬性要求:
jmp_buf栈帧有效 + 被修改局部变量加volatile。
2. 现代开发建议
- 普通业务代码 :优先使用错误码、返回值,可读性远高于跳转;
- C++ 项目 :完全弃用,使用标准
try/catch异常; - 底层 / 嵌入式 / 信号处理:可按需使用(Linux 内核、嵌入式驱动仍在大量使用);
- 协程 / 上下文切换 :现代系统推荐
ucontext、libco、操作系统原生协程,不再裸用setjmp。
setjmp/longjmp 是 C 语言的「底层利器」,理解它也能加深对进程栈、函数调用、寄存器上下文的理解。