首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C 標準庫參考指南:setjmp.h 非局部跳轉機制詳解 (setjmp、longjmp、jmp_buf)

     編輯:關於C語言

非局部跳轉核心概念

<setjmp.h> 提供跨函數棧幀跳轉能力:setjmp 保存當前執行環境,longjmp 恢復環境並跳轉回 setjmp 處。類似異常處理的底層機制,但不調用析構函數/清理代碼,屬「非受控跳轉」。

核心類型與函數

jmp_buf - 環境緩衝區

  • 數組類型,存儲棧指針、指令指針、寄存器等上下文
  • 大小架構相關,通常 16-200 位元組
  • 僅作 setjmp/longjmp 之間傳遞

int setjmp(jmp_buf env)

  • 首次調用:保存環境到 env,返回 0
  • longjmp 跳回:返回 longjmp 的 val 參數 (非 0)
  • 不可在嵌套表達式中調用 (如 if (setjmp(env)) 以外)

void longjmp(jmp_buf env, int val)

  • 恢復 env 保存的環境,跳轉到對應 setjmp
  • val 成為 setjmp 的返回值
  • val == 0 會被強制轉為 1
  • 若 setjmp 所在函數已返回,行為未定義 (棧已銷毀)

基礎示例:模擬異常跳出深層調用


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

jmp_buf env_buf;

void deep_function(int level) {
    printf("深度 %d: 進入\n", level);
    if (level == 3) {
        printf("深度 3: 觸發錯誤,長跳轉!\n");
        longjmp(env_buf, 42);  // 跳回 main 的 setjmp
    }
    deep_function(level + 1);
    printf("深度 %d: 返回 (不會執行)\n", level);
}

int main() {
    int ret = setjmp(env_buf);
    if (ret == 0) {
        printf("main: 設置跳轉點,調用深層函數\n");
        deep_function(1);
    } else {
        printf("main: 從 longjmp 返回, 值=%d\n", ret);
    }
    return 0;
}

輸出

main: 設置跳轉點,調用深層函數
深度 1: 進入
深度 2: 進入
深度 3: 進入
深度 3: 觸發錯誤,長跳轉!
main: 從 longjmp 返回, 值=42

注意:deep_function 的後續 printf 和 return 完全被跳過,棧幀直接展開。

關鍵限制與未定義行為

場景後果標準分類
longjmp 目標函數已返回棧損壞、崩潰、靜默數據錯誤未定義行為
跳過變長數組 (VLA) 作用域棧指針錯誤未定義行為 (C99)
跳過 C++ 對象構造/析構資源洩漏、雙重釋放、邏輯錯誤C++ 禁用,用異常
信號處理函數中 longjmp僅 async-signal-safe 函數安全POSIX 限制
多線程共享 jmp_buf競態條件、棧混亂每線程獨立 env

實際應用場景

1. 深層錯誤快速返回 (C 語言無異常時)


jmp_buf error_env;

int parse_config(const char *file) {
    if (setjmp(error_env) != 0) return -1;  // 錯誤返回點
    
    FILE *fp = fopen(file, "r");
    if (!fp) longjmp(error_env, 1);
    
    char line[256];
    while (fgets(line, sizeof(line), fp)) {
        if (parse_line(line) != 0) longjmp(error_env, 2);
    }
    fclose(fp);
    return 0;
}

// 深層調用
int parse_line(char *line) {
    if (strncmp(line, "ERROR", 5) == 0) longjmp(error_env, 3);
    return 0;
}

2. 協程/用戶級線程上下文切換核心


typedef struct {
    jmp_buf ctx;
    char stack[8192];
    void (*entry)(void);
} coroutine_t;

void co_yield(coroutine_t *from, coroutine_t *to) {
    if (setjmp(from->ctx) == 0) longjmp(to->ctx, 1);
}

void co_entry(void *arg) {
    coroutine_t *co = arg;
    co->entry();
    longjmp(co->ctx, 0);  // 結束返回調度器
}

這是 ucontext/libco/protothreads 等協程庫的核心原理。

3. 測試框架斷言失敗跳出


jmp_buf test_env;

#define ASSERT(cond) do { if(!(cond)) longjmp(test_env, __LINE__); } while(0)

void run_test(void (*test_fn)(void)) {
    if (setjmp(test_env) == 0) {
        test_fn();
        printf("PASS\n");
    } else {
        printf("FAIL at line %d\n", (int)setjmp(test_env));  // 實際需保存值
    }
}

與信號處理結合 (POSIX)


#include <signal.h>
#include <setjmp.h>

jmp_buf sig_env;
volatile sig_atomic_t sig_caught = 0;

void sig_handler(int sig) {
    sig_caught = sig;
    longjmp(sig_env, 1);  // 從阻塞系統調用跳出
}

int main() {
    signal(SIGALRM, sig_handler);
    alarm(5);  // 5 秒超時
    
    if (setjmp(sig_env) == 0) {
        // 執行可能阻塞的操作
        blocking_operation();
    } else {
        printf("操作超時或收到信號 %d\n", sig_caught);
    }
    return 0;
}

現代替代方案

  • C++ 異常:類型安全、自動棧展開、RAII 兼容
  • 錯誤碼返回:Result<T, E> 模式、errno、std::expected (C++23)
  • 協程 (C++20):co_await、co_yield、編譯器支持棧幀管理
  • longjmp 安全替代:libunwind、setjmp/longjmp 封裝庫 (如 ckjmp)

最佳實踐總結

  1. 僅在 C 代碼中使用,C++ 嚴禁
  2. jmp_buf 必須全局/靜態/主函數局部,確保生命周期覆蓋跳轉點
  3. 跳轉目標函數不能返回,保持在調用棧上
  4. 跳過的資源必須手動清理 (文件、鎖、內存),或設計為無資源持有
  5. volatile 修飾共享變量,防編譯器優化導致值不一致
  6. 文檔明確標註非局部跳轉,維護者易忽略控制流
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved