C 語言本身沒有原生的異常處理機制(如 C++ 的 try/catch),但可以通過 setjmp()(標記 “異常捕獲點”)和 longjmp()(拋出 “異常”)模擬異常處理流程。核心思路是:

  • 用 setjmp() 標記 try 塊的起始位置;
  • 用 longjmp() 模擬 throw 拋出異常;
  • 通過 setjmp() 的返回值區分 “正常執行” 和 “不同異常類型”。

一、核心設計思路

  1. 異常上下文管理:用 jmp_buf 存儲異常捕獲點的上下文,作為 “異常棧” 的基礎;
  2. 異常類型定義:通過枚舉 / 宏定義異常類型(如錯誤碼),區分不同異常;
  3. 封裝接口:模擬 try/catch/throw 語義,簡化使用(如封裝為宏);
  4. 資源清理:跳轉前手動清理資源(C 無析構函數,需顯式處理)。

二、完整實現代碼

c

運行

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

// ===================== 異常處理核心定義 =====================
// 定義異常類型(枚舉)
typedef enum {
    EXCEPTION_NONE,       // 無異常
    EXCEPTION_DIV_ZERO,   // 除零異常
    EXCEPTION_NULL_PTR,   // 空指針異常
    EXCEPTION_OUT_OF_RANGE// 越界異常
} ExceptionType;

// 全局異常上下文(也可封裝為結構體,支持嵌套異常)
static jmp_buf exception_env;
// 存儲當前異常類型
static ExceptionType current_exception = EXCEPTION_NONE;

// 模擬 try 塊:標記異常捕獲點,初始化異常類型
#define try \
    current_exception = EXCEPTION_NONE; \
    if (setjmp(exception_env) == 0)

// 模擬 catch 塊:匹配異常類型
#define catch(except_type) \
    else if (current_exception == except_type)

// 模擬 throw 語句:拋出指定類型的異常
#define throw(except_type) \
    do { \
        current_exception = except_type; \
        longjmp(exception_env, 1); \
    } while (0)

// 模擬 finally 塊(無論是否異常都執行)
#define finally \
    if (1)

// ===================== 測試用函數 =====================
// 除法函數:觸發除零異常
int divide(int a, int b) {
    if (b == 0) {
        throw(EXCEPTION_DIV_ZERO); // 拋出除零異常
    }
    return a / b;
}

// 數組訪問函數:觸發空指針/越界異常
int get_array_val(int *arr, int size, int index) {
    if (arr == NULL) {
        throw(EXCEPTION_NULL_PTR); // 拋出空指針異常
    }
    if (index < 0 || index >= size) {
        throw(EXCEPTION_OUT_OF_RANGE); // 拋出越界異常
    }
    return arr[index];
}

// ===================== 主函數測試 =====================
int main() {
    // 測試1:除零異常
    printf("=== Test 1: Division by zero ===\n");
    try {
        int res = divide(10, 0);
        printf("Division result: %d\n", res); // 不會執行
    } catch (EXCEPTION_DIV_ZERO) {
        printf("Caught exception: Division by zero\n");
    } finally {
        printf("Finally block executed (Test 1)\n");
    }
    printf("\n");

    // 測試2:空指針異常
    printf("=== Test 2: Null pointer ===\n");
    int *null_arr = NULL;
    try {
        int val = get_array_val(null_arr, 5, 2);
        printf("Array value: %d\n", val); // 不會執行
    } catch (EXCEPTION_NULL_PTR) {
        printf("Caught exception: Null pointer\n");
    } finally {
        printf("Finally block executed (Test 2)\n");
    }
    printf("\n");

    // 測試3:數組越界異常
    printf("=== Test 3: Out of range ===\n");
    int arr[] = {1, 2, 3, 4, 5};
    try {
        int val = get_array_val(arr, 5, 10); // 索引10越界
        printf("Array value: %d\n", val); // 不會執行
    } catch (EXCEPTION_OUT_OF_RANGE) {
        printf("Caught exception: Out of range\n");
    } finally {
        printf("Finally block executed (Test 3)\n");
    }
    printf("\n");

    // 測試4:無異常場景
    printf("=== Test 4: No exception ===\n");
    try {
        int res = divide(20, 4);
        printf("Division result: %d\n", res); // 正常執行
        int val = get_array_val(arr, 5, 2);
        printf("Array value: %d\n", val); // 正常執行
    } catch (EXCEPTION_DIV_ZERO) {
        printf("Caught exception: Division by zero\n");
    } catch (EXCEPTION_NULL_PTR) {
        printf("Caught exception: Null pointer\n");
    } catch (EXCEPTION_OUT_OF_RANGE) {
        printf("Caught exception: Out of range\n");
    } finally {
        printf("Finally block executed (Test 4)\n");
    }

    return 0;
}

三、輸出結果

plaintext

=== Test 1: Division by zero ===
Caught exception: Division by zero
Finally block executed (Test 1)

=== Test 2: Null pointer ===
Caught exception: Null pointer
Finally block executed (Test 2)

=== Test 3: Out of range ===
Caught exception: Out of range
Finally block executed (Test 3)

=== Test 4: No exception ===
Division result: 5
Array value: 3
Finally block executed (Test 4)

四、關鍵細節解析

1. 宏封裝的語義對應

自定義宏

對應 C++ 語義

作用

try

try

標記異常捕獲點,setjmp() 返回 0 時執行塊內邏輯

catch(類型)

catch(類型)

匹配 longjmp() 拋出的異常類型

throw(類型)

throw

設置異常類型,跳回 setjmp() 標記點

finally

finally

無論是否異常,必執行的代碼塊

2. 異常上下文的存儲
  • 示例中用全局變量 exception_envjmp_buf 類型)存儲跳轉上下文,current_exception 存儲當前異常類型;
  • 若需支持嵌套異常(如 try 塊內嵌套 try),需將上下文封裝為棧結構(如 jmp_buf 數組 + 棧指針),避免全局變量覆蓋。
3. 資源清理(關鍵!)

C 語言沒有自動析構,longjmp() 跳轉時會跳過局部變量的清理邏輯,需手動在 finally 塊或 throw 前清理資源(如釋放內存、關閉文件)。示例如下:

c

運行

// 帶資源清理的示例
void test_resource() {
    FILE *fp = NULL;
    try {
        fp = fopen("test.txt", "r");
        if (fp == NULL) {
            throw(EXCEPTION_NULL_PTR);
        }
        // 模擬文件操作異常
        throw(EXCEPTION_OUT_OF_RANGE);
    } catch (EXCEPTION_NULL_PTR) {
        printf("Caught: File open failed\n");
    } catch (EXCEPTION_OUT_OF_RANGE) {
        printf("Caught: File operation error\n");
    } finally {
        // 無論是否異常,關閉文件
        if (fp != NULL) {
            fclose(fp);
            printf("Resource cleaned: File closed\n");
        }
    }
}

五、擴展:支持嵌套異常

全局上下文無法支持嵌套 try/catch,需實現 “異常棧” 管理。簡化示例如下:

c

運行

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

#define MAX_EXCEPTION_DEPTH 10 // 最大嵌套深度
jmp_buf exception_stack[MAX_EXCEPTION_DEPTH];
int exception_stack_ptr = -1; // 棧指針
ExceptionType current_exception;

// 重定義宏,支持嵌套
#define try \
    do { \
        if (exception_stack_ptr + 1 >= MAX_EXCEPTION_DEPTH) { \
            fprintf(stderr, "Exception stack overflow\n"); \
            exit(1); \
        } \
        exception_stack_ptr++; \
        current_exception = EXCEPTION_NONE; \
        if (setjmp(exception_stack[exception_stack_ptr]) == 0) {

#define catch(except_type) \
        } else if (current_exception == except_type) {

#define throw(except_type) \
        current_exception = except_type; \
        longjmp(exception_stack[exception_stack_ptr], 1); \
    } while (0)

#define finally \
        } else {

#define end_try \
        exception_stack_ptr--; \
    } while (0)

// 嵌套測試
void nested_try() {
    try {
        try {
            throw(EXCEPTION_DIV_ZERO);
        } catch (EXCEPTION_DIV_ZERO) {
            printf("Inner catch: Division by zero\n");
            throw(EXCEPTION_NULL_PTR); // 向上拋出新異常
        } end_try;
    } catch (EXCEPTION_NULL_PTR) {
        printf("Outer catch: Null pointer\n");
    } end_try;
}

六、侷限性與注意事項

  1. 不支持異常類型自動匹配:需手動寫 catch 分支匹配異常類型,無 “父異常” 繼承機制;
  2. 資源泄漏風險longjmp() 不會調用局部變量的析構(如 C++ 的 ~Class()),必須在 finally 或 throw 前手動清理資源;
  3. 嵌套限制:需手動管理異常棧,否則全局上下文會被覆蓋;
  4. 調試難度高:跳轉打破了正常的函數調用棧,調試時難以追蹤執行流程;
  5. 標準兼容性setjmp()/longjmp() 的行為在某些場景下未定義(如跳轉出信號處理函數),需嚴格遵守標準。

七、適用場景

  • 嵌入式系統 / 內核開發(無 C++ 異常機制,需輕量級錯誤處理);
  • 遺留 C 項目的錯誤處理重構(避免逐層返回錯誤碼);
  • 簡單程序的異常模擬(無需引入 C++ 編譯環境)。

八、與 C++ 異常的對比

特性

setjmp/longjmp 模擬

C++ 原生異常

資源自動清理

不支持(需手動)

支持(析構函數)

異常類型繼承

不支持

支持(多態捕獲)

嵌套異常

需手動實現棧

原生支持

調試友好性

差(棧斷裂)

好(保留調用棧)

性能

略快(無額外開銷)

略慢(棧展開開銷)

綜上,setjmp()/longjmp() 是 C 語言模擬異常處理的 “權宜之計”,雖能實現核心語義,但需手動處理資源清理和嵌套邏輯,實際開發中需謹慎使用。