在 C 語言中,我們不能使用 goto 語句來跳轉到另一個函數中的某個 label 處;但提供了兩個函數——setjmp 和 longjmp來完成這種類型的分支跳轉。后面我們會看到這兩個函數在處理異常上面的非常有用。
setjmp 和 longjmp 使用方法
我們都知道要想在一個函數內進行跳轉,可以使用 goto 語句(不知怎么該語句在中國學生眼中就是臭名昭著,幾乎所有國內教材都一刀切地教大家盡量不要使用它,但在我看來,這根本不是語言的問題,而是使用該語言的人,看看 Linux 內核中遍地是 goto 語句的應用吧!),但如果從一個函數內跳轉到另一個函數的某處,goto 是不能完成的,那該如何實現呢?
函數間跳轉原理
我們要實現的一個 GOTO 語句(我自己定義的),能實現在函數間進行任意跳轉,如下例,在函數 g() 中有條語句GOTO Label;
可以跳轉到 f() 函數的 Label:
標簽所指向的位置,那么我們該如何實現呢?
void f()
{
//...
Label:
//...
}
void g()
{
//...
GOTO Label;
//...
}
首先我們要知道,實現這種類型的跳轉,和操作系統中任務切換的上下文切換有點類似,我們只需要恢復 Label 標簽處函數上下文即可。函數的上下文包括以下內容:
- 函數棧幀,主要是棧幀指針BP和棧頂指針SP
- 程序指針PC,此處為指向 Label 語句的地址
- 其它寄存器,這是和體系相關的,在 x86 體系下需要保存有的 AX/BX/CX 等等 callee-regs。
這樣,在執行 GOTO Label;
這條語句,我們恢復 Label 處的上下文,即完成跳轉到 Label 處的功能。
如果你讀過 Linux 操作系統進程切換的源碼,你會很明白 Linux 會把進程的上下文保存在 task_struct 結構體中,切換時直接恢復。這里我們也可以這樣做,將 Label 處的函數上下文保存在某個結構體中,但執行到 GOTO Label 語句時,我們從該結構體中恢復函數的上下文。
這就是函數間進行跳轉的基本原理,而 C 語言中 setjmp 和 longjmp 就為我們完成了這樣的保存上下文和切換上下文的工作。
函數原型
#include <setjmp.h>
int setjmp(jmp_buf env);
setjmp 函數的功能是將函數在此處的上下文保存在 jmp_buf 結構體中,以供 longjmp 從此結構體中恢復。
- 參數 env 即為保存上下文的 jmp_buf 結構體變量;
- 如果直接調用該函數,返回值為 0; 若該函數從 longjmp 調用返回,返回值為非零,由 longjmp 函數提供。根據函數的返回值,我們就可以知道 setjmp 函數調用是第一次直接調用,還是由其它地方跳轉過來的。
void longjmp(jmp_buf env, int val);
longjmp 函數的功能是從 jmp_buf 結構體中恢復由 setjmp 函數保存的上下文,該函數不返回,而是從 setjmp 函數中返回。
- 參數 env 是由 setjmp 函數保存過的上下文。
- 參數 val 表示從 longjmp 函數傳遞給 setjmp 函數的返回值,如果 val 值為0, setjmp 將會返回1,否則返回 val。
- longjmp 不直接返回,而是從 setjmp 函數中返回,longjmp 執行完之后,程序就像剛從 setjmp 函數返回一樣。
簡單實例
下面是個簡單的例子,雖然還只是函數內跳轉,但足以說明這兩個函數的功能了。
運行該程序得到的結果為:
i = 0
i = 2
C 語言異常處理
Java、C# 等面向對象語言中都有異常處理的機制,如下就是典型的 Java 中異常處理的代碼,兩個數相除,如果被除數為0拋出異常,在函數 f() 中可以獲取該異常並進行處理:
double divide(double to, double by) throws Bad {
if(by == 0)
throw new Bad ("Cannot / 0");
return to / by;
}
void f() {
try {
divide(2, 0);
//...
} catch (Bad e) {
print(e.getMessage());
}
print("done");
}
在 C 語言中雖然沒有類似的異常處理機制,但是我們可以使用 setjmp 和 longjmp 來模擬實現該功能,這也是這兩個函數的一個重要的應用:
static jmp_buf env;
double divide(double to, double by)
{
if(by == 0)
longjmp(env, 1);
return to / by;
}
void f()
{
if (setjmp(env) == 0)
divide(2, 0);
else
printf("Cannot / 0");
printf("done");
}
如果復雜一點,可以根據 longjmp 傳遞的返回值來判斷各種不同的異常,來進行區別的處理,代碼結構如下:
switch(setjmp(env)):
case 0: //default
//...
case 1: //exception 1
//...
case 2: //exception 2
//...
//...
關於使用 C 語言來處理異常,可以參見這篇文章,介紹了更多復雜的結構,但無外乎就是 setjmp 和 longjmp 的應用。