歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> C 語言中 setjmp 和 longjmp

C 語言中 setjmp 和 longjmp

日期:2017/3/1 9:36:33   编辑:Linux編程

在 C 語言中,我們不能使用 goto 語句來跳轉到另一個函數中的某個 label 處;但提供了兩個函數——setjmplongjmp來完成這種類型的分支跳轉。後面我們會看到這兩個函數在處理異常上面的非常有用。

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 的應用。

參考資料

  • http://en.wikipedia.org/wiki/Longjmp
  • www.cs.purdue.edu/homes/cs240/lectures/Lecture-19.pdf

C++ 設計新思維》 下載見 http://www.linuxidc.com/Linux/2014-07/104850.htm

C++ Primer Plus 第6版 中文版 清晰有書簽PDF+源代碼 http://www.linuxidc.com/Linux/2014-05/101227.htm

讀C++ Primer 之構造函數陷阱 http://www.linuxidc.com/Linux/2011-08/40176.htm

讀C++ Primer 之智能指針 http://www.linuxidc.com/Linux/2011-08/40177.htm

讀C++ Primer 之句柄類 http://www.linuxidc.com/Linux/2011-08/40175.htm

將C語言梳理一下,分布在以下10個章節中:

  1. Linux-C成長之路(一):Linux下C編程概要 http://www.linuxidc.com/Linux/2014-05/101242.htm
  2. Linux-C成長之路(二):基本數據類型 http://www.linuxidc.com/Linux/2014-05/101242p2.htm
  3. Linux-C成長之路(三):基本IO函數操作 http://www.linuxidc.com/Linux/2014-05/101242p3.htm
  4. Linux-C成長之路(四):運算符 http://www.linuxidc.com/Linux/2014-05/101242p4.htm
  5. Linux-C成長之路(五):控制流 http://www.linuxidc.com/Linux/2014-05/101242p5.htm
  6. Linux-C成長之路(六):函數要義 http://www.linuxidc.com/Linux/2014-05/101242p6.htm
  7. Linux-C成長之路(七):數組與指針 http://www.linuxidc.com/Linux/2014-05/101242p7.htm
  8. Linux-C成長之路(八):存儲類,動態內存 http://www.linuxidc.com/Linux/2014-05/101242p8.htm
  9. Linux-C成長之路(九):復合數據類型 http://www.linuxidc.com/Linux/2014-05/101242p9.htm
  10. Linux-C成長之路(十):其他高級議題

Copyright © Linux教程網 All Rights Reserved