這裡介紹 C 語言的 setjmp
與 longjmp
函數的用法,還有典型的使用範例。
在 C 語言中的 goto
只能跳到函數內部的 label 位置,若要跳到其他函數中則必須使用 setjmp
與 longjmp
,這兩個函數在深層的程式錯誤處理上非常好用。
以下是一段簡單的 C 語言程式碼:
#include <stdio.h> int fun_a(int v) { int r = v * 2 - 1; return r; } int fun_b(int v) { int r = fun_a(v) + 6; return r; } int fun_c(int v) { int r = fun_b(v) * 5 - 21; return r; } int main() { int x = 5; int result = fun_c(x); printf("Result = %dn", result); return 0; }
這個程式在執行時首先會呼叫 fun_c
函數,而在 fun_c
函數中又呼叫了 fun_b
函數,以此類推最後呼叫到 fun_a
函數,形成了一個比較深層的函數呼叫堆疊(call stack),一般來說,函數呼叫堆疊結構通常是這樣:
每個函數的區域變數存在於自己的 stack frame 當中,在這樣的程式呼叫結構中,如果在比較深層的函數裡需要處理錯誤的狀況時,就會比較麻煩,例如在 fun_a
函數中若出現錯誤,我們可能會需要中止之前一連串的呼叫動作,回到 main
中繼續其他動作,如果按照一般的作法,我們會需要定義每個函數的錯誤傳回值,逐層檢查與處理,動作相當繁雜。
setjmp
與 longjmp
函數可以讓程式往回跳到函數呼叫堆疊中的某個函數中,就像是一種跨函數的 goto
。
首先使用 setjmp
在程式中標示一個目標位置(跳躍的目的地),然後在程式要進行跳躍的地方呼叫 longjmp
即可。
#include <stdio.h> #include <setjmp.h> // 儲存程式跳躍時所需之資訊 jmp_buf jmpbuffer; int fun_a(int v) { int r = v * 2 - 1; if (r < 0) { // 跳躍至 main 函數 longjmp(jmpbuffer, 1); } return r; } int fun_b(int v) { int r = fun_a(v) + 6; if (r > 10) { // 跳躍至 main 函數 longjmp(jmpbuffer, 2); } return r; } int fun_c(int v) { int r = fun_b(v) * 5 - 21; return r; } int main() { // 設定跳躍目標位置 int jmpVal = setjmp(jmpbuffer); if ( jmpVal == 1 ) { printf("fun_a errorn"); } else if ( jmpVal == 2 ) { printf("fun_b errorn"); } else { // jmpVal == 0 int x = -5; int result = fun_c(x); printf("Result = %dn", result); } return 0; }
在 setjmp
設定跳躍目標位置時,會需要指定一個特殊的 jmp_buf
變數,用來儲存程式跳躍時所需之資訊,而在直接呼叫 setjmp
函數時其傳回值為 0
,若是透過 longjmp
跳回這裡時,其傳回值就會是呼叫 longjmp
時所指定的值,程式設計者可以靠著這個傳回值來判斷並處理不同的錯誤情形。
用 setjmp
設定好目標位置之後,接著在深層的函數呼叫中出現錯誤並呼叫 longjmp
時,傳入同一個 jmp_buf
變數,並加上一個整數傳回值,這樣程式就會跳到當初設定的目標位置繼續執行,而 setjmp
的傳回值就會是 longjmp
所指定的整數值。
當程式透過 longjmp
返回 main
函數之後,main
之後的函數呼叫堆疊會自動被清空,並從 setjmp
的位置開始執行,就跟程式一開始的狀態類似,只不過 setjmp
的傳回值不同。
程式跳躍後的變數
當呼叫 longjmp
之後,程式返回至 main
函數中,而函數呼叫堆疊也跟著回復至原來的樣子,不過接下來的問題就是 main
函數中的變數值會不會也跟著回復原來的值?
以下是一段測試各種不同變數類型的程式碼:
#include <stdio.h> #include <setjmp.h> jmp_buf jmpbuffer; static int g; // global void f1() { longjmp(jmpbuffer, 1); } void f2(int l, int g, int r, int v, int s) { printf("local = %d, global = %d, register = %d, " "volatile = %d, static = %dn", l, g, r, v, s); f1(); } int main() { int l; // local register int r; // register volatile int v; // volatile static int s; // static l = 1, g = 2, r = 3, v = 4, s = 5; if ( setjmp(jmpbuffer) != 0 ) { printf("local = %d, global = %d, register = %d, " "volatile = %d, static = %dn", l, g, r, v, s); return 0; } l = 101, g = 102, r = 103, v = 104, s = 105; f2(l, g, r, v, s); return 0; }
同樣的程式在編譯時使用不同的參數會造成不同的結果:
gcc source.c ./a.out
local = 101, global = 102, register = 103, volatile = 104, static = 105 local = 101, global = 102, register = 3, volatile = 104, static = 105
gcc -O source.c
./a.out
local = 101, global = 102, register = 103, volatile = 104, static = 105 local = 1, global = 102, register = 3, volatile = 104, static = 105
加入 -O
讓編譯器進行基本的最佳化之後,global
、volatile
與 static
三種變數還是維持其最後所指定的值,而 register
變數則始終都會自動回復原來的值,但 local
變數的值卻改變了。
使用 setjmp
與 longjmp
轉跳時,位於記憶體中的變數會維持其最後的值,而位於 CPU 或浮點數 registers 中的變數則會回復至原來的值,在上面的例子中,local
變數原本位於記憶體中,經過最佳化之後,被放置在 register 中,所以產生了不同的結果。
根據 gcc 的說明文件,在呼叫 longjmp
進轉跳之後,除了 volatile
變數之外,其他的變數都有可能產生不可預期的結果,所以若需要撰寫一個可移植(portable)的程式,請將變數宣告為 volatile
。
JackKuo
請問 `jongjmp` 是否為筆誤?
G. T. Wang
是的,已修正,謝謝您。