這裡介紹 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 ;
}
這個程式在執行時首先會呼叫 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 < ) {
// 跳躍至 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 ==
int x = -5;
int result = fun_c(x);
printf("Result = %dn", result);
}
return ;
}
在 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) != ) {
printf("local = %d, global = %d, register = %d, "
"volatile = %d, static = %dn",
l, g, r, v, s);
return ;
}
l = 101, g = 102, r = 103, v = 104, s = 105;
f2(l, g, r, v, s);
return ;
}
同樣的程式在編譯時使用不同的參數會造成不同的結果:
# 編譯
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。
