setjmp & longjmp 深度详解 + 代码示例

setjmp & longjmp 深度详解 + 代码示例

setjmplongjmp 是 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

  • 返回规则(核心)

    1. 第一次直接调用 :正常保存上下文,返回 0
    2. longjmp 跳转回来 :恢复上下文,返回 longjmp 传入的第二个参数(非 0 值)。
(2)longjmp
arduino 复制代码
void longjmp(jmp_buf env, int val);
  • 功能 :根据 env 中保存的上下文,跳转到对应 setjmp 位置继续执行。

  • 规则

    1. 跳转后不会回到 longjmp 所在函数,程序流直接切走;
    2. val = 0,标准会强制转为 1 (避免和 setjmp 原生返回 0 冲突);
    3. 必须保证 env 对应的 setjmp 所在栈帧未被销毁,否则行为未定义。

3. 核心原理

程序每次调用函数都会在进程栈上创建独立栈帧(局部变量、返回地址、寄存器)。

  1. setjmp:把当前栈帧、CPU 所有寄存器、指令地址全部存入 jmp_buf
  2. longjmp:反向恢复寄存器、栈指针、指令指针,程序直接跳回 setjmp 执行点,实现跨栈帧跳转

4. 和 goto 的区别

特性 goto setjmp/longjmp
跳转范围 仅限同一函数内部 跨函数、跨多层栈帧
实现方式 编译期静态跳转 运行时上下文恢复
适用场景 跳出单层循环 异常、多层函数退出

5. 典型使用场景

  1. C 语言模拟 try-catch 异常处理;
  2. 深层函数 / 递归快速退出,避免层层返回错误码;
  3. 信号处理函数中跳转回主逻辑;
  4. 嵌入式 / 底层库简易上下文切换(早期协程雏形)。

二、高频踩坑点(重中之重)

使用这组函数极易出 Bug,务必遵守以下规则:

1. jmp_buf 生命周期必须有效

setjmp 所在的函数不能提前返回 。一旦函数返回,其栈帧被系统销毁,再调用 longjmp 会触发未定义行为(程序崩溃、数据乱码)。

2. 局部变量必须加 volatile

编译器会优化局部变量(把变量放到 CPU 寄存器而非内存)。

setjmp 只会保存内存栈、寄存器上下文 ,跳转后未加 volatile 的局部变量值会错乱

凡是在 setjmplongjmp 之间被修改的自动局部变量 ,必须用 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
流程解析
  1. setjmp 首次调用 → 返回 0,调用 test()
  2. test() 中执行 longjmp(env, 100) → 恢复上下文,跳回 setjmp 处;
  3. 此时 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 ===
程序继续运行...
流程解析
  1. 正常执行 setjmp → 返回 0,执行业务调用链 layer1 -> layer2 -> layer3
  2. layer3 发现参数错误,调用 longjmp 直接跳回 mainsetjmp
  3. 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. 现代开发建议

  1. 普通业务代码 :优先使用错误码、返回值,可读性远高于跳转;
  2. C++ 项目 :完全弃用,使用标准 try/catch 异常;
  3. 底层 / 嵌入式 / 信号处理:可按需使用(Linux 内核、嵌入式驱动仍在大量使用);
  4. 协程 / 上下文切换 :现代系统推荐 ucontext、libco、操作系统原生协程,不再裸用 setjmp

setjmp/longjmp 是 C 语言的「底层利器」,理解它也能加深对进程栈、函数调用、寄存器上下文的理解。

相关推荐
To_OC1 小时前
我一直以为 Ajax 是个黑盒,直到我写了这 50 行代码
前端·后端·全栈
她的男孩1 小时前
AI 自动化编写 SQL 脚本,更要守住 Flyway 版本管理的防线
人工智能·后端
卷无止境1 小时前
Python的ABC库探索:能不能在系统设计之初就定义好所有抽象类?
后端
卷无止境1 小时前
Python collections 库深度解析:那些被低估的数据结构利器
后端
XovH1 小时前
Redis 从入门到精通:分布式锁 —— 从 SETNX 到 Redlock
后端
用户329901675051 小时前
用 Web Speech API 给 AI 回答加"朗读"功能,边读边高亮 🔊
后端
ALianBlank1 小时前
一个 Unity 框架能做多少事?86 个模块 + 21 个小游戏平台
前端·后端·游戏开发
m0_547722921 小时前
从零搭建乒乓球比赛管理系统——Spring Boot + 原生 HTML 实战
spring boot·后端·html
用户637328456111 小时前
MyBatis与MyBatis-Plus区别
后端