程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C語言中利用setjmp和longjmp做異常處理

C語言中利用setjmp和longjmp做異常處理

編輯:關於C語言

C語言中利用setjmp和longjmp做異常處理


錯誤處理是任何語言都需要解決的問題,只有不能保證100%的正確運行,就需要有處理錯誤的機制。異常處理就是其中的一種錯誤處理方式。

1 過程活動記錄(Active Record)

C語言中每當有一個函數調用時,就會在堆棧(Stack)上准備一個被稱為AR的結構,拋開具體編譯器實現細節的不同,這個AR基本結構如下所示。
這裡寫圖片描述

每當遇到一次函數調用的語句,C編譯器都會產生出匯編代碼來在堆棧上分配這個AR。例如下面的C代碼:

void a(int i)
{
    if(i==0){
        i = 1;
    }
    else
    {
        printf("i = %d \n", i);
    }
}

int main(int argc, char** argv)
{
    a(1);
}

當程序運行後執行到printf()語句時,堆棧上的AR布局如下:
這裡寫圖片描述

2 通過setjmp和longjmp操縱AR,完成任意跳轉

那麼如何來操縱AR呢,一個可能的方法是,根據局部變量的地址進行推算,例如對於上面的a函數,執行a函數時的當前AR地址就是參數i的地址偏移8個字節,也就是 ((char*)&i) - 8。然而,不同的C編譯器,以及不同的硬件平台都會產生不同的AR結構布局,甚至在一些平台上,AR根本不會存放到Stack中。所以這種方式操縱AR是不通用的。

為此,C語言通過庫函數的方式提供了操縱AR的統一方法,那就是setjmp和longjmp函數。

int setjmp(jmp_buf jb);
void longjmp(jmp_buf jb, int r);

setjmp用於保存當前AR到jb變量中;
而longjmp用於設置當前AR為jb,並跳轉到調用setjmp();之後的第一個語句處。其結果就相當於回到了setjmp()剛執行完畢,只是偷偷的修改了setjmp的返回值。

setjmp()第一次調用時總是返回0,而通過longjmp(jb,r)跳轉後其返回值總是被修改為r,並且r不能為0。這樣程序中就很容易根據setjmp()的返回值來判斷是否是longjmp()導致了跳轉才執行到此。

setjmp/longjmp主要從嵌套的函數調用中跳出來。

#include 
#include 

jmp_buf jb;
void a();
void b();
void c();

int main()
{
    if(setjmp(jb)==0){
        a();
    }
    printf("after a(); \n");
    return 0;
}
void a()
{
    b();
    printf("a() is called\n");
}
void b()
{
    c();
    printf("b() is called\n");
}
void c()
{
    printf("c() is called\n");
    longjmp(jb, 1);
}

在c()中可以直接跳轉到main()中,實際上longjmp不限制跳轉的目的地,可以跳轉到任意位置並恢復當時的堆棧環境(堆棧平衡)。

3 C語言中實現異常處理

異常處理是錯誤處理的一種方式,C語言中更常用的錯誤處理方式是檢測函數返回值。

#include 

int f1()
{
    if(1/*正確執行*/) { return 1; }
    else { return -1; }
}
int f2()
{
    if(0/*正確執行*/) { return 1; }
    else { return -1; }
}

int main()
{
    if(f1()<0){
        printf("錯誤處理1\n");
        exit(1);
    }

    if(f2()<0){
        printf("錯誤處理2\n");
        exit(2);
    }
    return 0;
}

上面代碼顯示了常見的C語言錯誤處理方式。嚴謹的軟件開發中,必須檢測每一次函數調用可能出現的錯誤,並做相應的處理。造成的後果就是冗長繁瑣的代碼。為了統一處理錯誤,C++,C#,Java等現代語言引入了異常處理機制。同樣功能的C++代碼大概如下:

#include 

class Ex1{
};
class Ex2{
};
void f1()
{
    printf("進入f1()\n");
    if(0/*正確執行*/){ }
    else {
        throw Ex1();
    }
    printf("退出f1()\n");
}
void f2()
{
    printf("進入f2()\n");
    if(1/*正確執行*/) {  }
    else {
        throw Ex2();
    }
    printf("退出f2()\n");
}

int main()
{
    try{
        f1();
        f2();
    }catch(Ex1 &ex){
        printf("處理錯誤1\n");
        exit(1);
    }
    catch(Ex2 &ex){
        printf("處理錯誤2\n");
        exit(2);
    }
    return 0;
}

程序輸出:

進入f1()
處理錯誤1

可見,異常處理讓代碼看起來更加整潔,邏輯代碼在一起,錯誤處理代碼在一起。throw後面的語句不再執行,執行流直接跳轉到最近的try對應的catch塊。

可以推測,

throw要負責兩件事情:(1)完成跳轉;(2)恢復堆棧AR; try則負責保存當前AR

可見這與setjmp/longjmp基本相當。於是可以在C中近似寫成。

#include 
#include 
#include 

jmp_buf jb;

void f1()
{
    printf("進入f1()\n");
    if(0/*正確執行*/){ }
    else {
        longjmp(jb,1);
    }
    printf("退出f1()\n");
}
void f2()
{
    printf("進入f2()\n");
    if(1/*正確執行*/) {  }
    else {
        longjmp(jb, 2);
    }
    printf("退出f2()\n");
}

int main()
{
    int r = setjmp(jb);
    if(r==0){
        f1();
        f2();
    }else if(r==1){
        printf("處理錯誤1\n");
        exit(1);
    }else if(r==2){
        printf("處理錯誤2\n");
        exit(2);
    }
    return 0;
}

當然完整的異常處理遠比這裡的代碼要復雜,需要考慮異常的嵌套等,這裡僅僅給出最簡單的思路。

4 不要在C++中使用setjmp和longjmp

C++為異常處理提供了直接支持。除非極特殊需要,不要再重新實現自己的異常機制,尤其需要說明的是,簡單的調用setjmp/longjmp有可能帶來問題。如

#include 
#include 
#include 

class MyClass
{
public:
    MyClass(){ printf("MyClass::MyClass()\n");}
    ~MyClass(){ printf("MyClass::~MyClass()\n");}
};
jmp_buf jb;

void f1()
{
    MyClass obj;
    printf("進入f1()\n");
    if(0/*正確執行*/){ }
    else {
        longjmp(jb,1);
    }
    printf("退出f1()\n");
}
void f2()
{
    printf("進入f2()\n");
    if(1/*正確執行*/) {  }
    else {
        longjmp(jb, 2);
    }
    printf("退出f2()\n");
}

int main()
{
    int r = setjmp(jb);
    if(r==0){
        f1();
        f2();
    }else if(r==1){
        printf("處理錯誤1\n");
        exit(1);
    }else if(r==2){
        printf("處理錯誤2\n");
        exit(2);
    }
    return 0;
}

g++編譯,程序輸出:

MyClass::MyClass()
進入f1()
處理錯誤1

vc++編譯,程序輸出:

MyClass::MyClass()
進入f1()
MyClass::~MyClass()
處理錯誤1

longjmp()跳轉前局部對象可能並不會析構(g++),也可能析構(VC++),C++標准對此並無明確要求。這種依賴於具體編譯器版本的代碼是應該避免的。

而C++本身的throw關鍵字,卻能嚴格保證局部對象構造和析構的成對調用。

5 辯證看待異常處理

為實現異常處理,C++編譯器為此必須做更多的工作,也必然導致在AR中直接或間接地存放更多的信息,並產生操作這些信息的匯編代碼,最終必然導致運行效率的降低。

另一方面,已經存在大量沒有嚴格使用異常處理C++函數庫和類庫,兼容的C庫更是沒有異常的概念,歷史的包袱讓C++很難完全采用異常處理。在這個方面,Java和C#從頭開始,重要的庫都實現了標准的異常處理規范,完全采用異常機制切實可行。

有趣的是C++11在標准中刪除了異常規范,而且添加了 noexcept關鍵字來聲明一個函數不會拋出異常,可見異常並不是那麼受歡迎。

C++編譯器也會提供一個禁用異常的選項,下面是VC++中禁用異常的方法。
這裡寫圖片描述

然而,C++的STL廣泛使用異常,所以實際上使用了STL的C++程序是不可能禁用異常的,要是沒有了STL,C++又有什麼優勢了呢?C++在不斷的矛盾沖突中向前發展者。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved