C 語言缺乏內置異常處理,但通過 setjmp/longjmp 可模擬非局部跳轉實現類 try-catch 語義。本文從棧幀(Active Record)佈局原理出發,推導長跳轉機制,展示異常處理模擬代碼,並深度剖析在 C++ 中使用長跳轉的致命缺陷(析構函數不調用)。
每次函數調用,編譯器在棧上分配一個活動記錄(AR),包含:
高地址 +------------------+ | 調用者局部變量 | +------------------+ | 參數 (右→左) | +------------------+ | 返回地址 | ← 調用指令壓入 +------------------+ | 舊 EBP | ← 被調用者建立幀 +------------------+ | 被調用者局部變量 | +------------------+ 低地址
#includeint setjmp(jmp_buf env); // 保存當前棧幀環境到 env,首次返回 0 void longjmp(jmp_buf env, int val); // 恢復 env 環境,跳轉到 setjmp 後,setjmp 返回 val (≠0)
jmp_buf 通常包含:EBP/RBP、ESP/RSP、EIP/RIP(返回地址)、被調用者保存寄存器(RBX, RSI, RDI, R12-R15 等)。longjmp 相當於「偷偷修改 setjmp 返回值並跳回其下一條指令」。longjmp 直接切換 ESP/RSP 到目標幀,中間幀不執行任何清理代碼。#include#include #include jmp_buf g_env; void f1() { printf("enter f1\n"); if (0 /* error */) longjmp(g_env, 1); printf("exit f1\n"); } void f2() { printf("enter f2\n"); if (1 /* error */) longjmp(g_env, 2); printf("exit f2\n"); } int main() { int r = setjmp(g_env); if (r == 0) { f1(); f2(); // 正常流程 } else if (r == 1) { printf("catch error 1\n"); exit(1); } else if (r == 2) { printf("catch error 2\n"); exit(2); } return 0; }
enter f1 exit f1 enter f2 catch error 2
f2 中 longjmp 直接跳回 main 的 setjmp 後,f2 後續代碼與 f1 的 exit f1 均不執行——棧幀直接展開。
#include#include #include class MyClass { public: MyClass() { printf("MyClass::MyClass()\n"); } ~MyClass() { printf("MyClass::~MyClass()\n"); } }; jmp_buf env; void f1() { MyClass obj; // 局部對象 printf("enter f1\n"); longjmp(env, 1); // 直接跳轉 printf("exit f1\n"); } int main() { if (setjmp(env) == 0) f1(); else printf("catch error\n"); return 0; }
| 編譯器 | 輸出 | 析構調用 |
|---|---|---|
| GCC (g++) | MyClass::MyClass() enter f1 catch error | 否 (未調用 ~MyClass) |
| MSVC (cl.exe) | MyClass::MyClass() enter f1 MyClass::~MyClass() catch error | 是 (Microsoft 扩展行為) |