1. 程式人生 > >C語言中的異常處理機制

C語言中的異常處理機制

軟件測試 如何實現 char* oar 朋友 核心 初始化 flag out

#define try if(!setjmp(Jump_Buffer)) 返回try現場後重新執行判斷,所以有兩次執行。

http://blog.csdn.net/tian_dao_chou_qin/article/details/6386621

1.概述

什麽是異常?異常一般指的是程序運行期(Run-Time)發生的非正常情況。異常一般是不可預測的,如:內存不足、打開文件失敗、範圍溢出等。UNIX 使用信號給出異常,並當發生異常時轉跳到信號處理過程進行異常處理。DOS下的信號對比UNIX系統而言相對較少。

我們知道,不管是在c++還是在Java中,異常都被認為是一種很優雅的處理錯誤的機制。而如果想在C語言中使用異常就比較麻煩,但是我們仍然可以使用c語言中強大的setjmp和longjmp函數實現類似於c++的異常處理機制。

異常處理的核心思想是,把功能模塊代碼與系統中可能出現錯誤的處理代碼分離開來,以此來達到使我們的代碼組織起來更美觀、邏輯上更清晰,並且同時從根本上來提高我們軟件系統長時間穩定運行的可靠性。那麽,現在回過頭來看,實際上在計算機系統的硬件設計中,操作系統的總體設計中,早期的許多面向結構化程序設計語言中(例如C語言),都有異常處理的機制和方法的廣泛運用。

2.基於goto語句的異常處理

goto語句,程序員朋友們對它太熟悉了,它是C語言中使用最為靈活的一條語句,由它也充分體現出了C語言的許多特點或者說是優點。它雖然是一條高級語言中提供的語句,但是它一般卻直接對應一條“無條件直接跳轉的機器指令”,所以說它非常地特別,它引起過許多爭議,但是這條語句仍然一直被保留了下來,即便是今天的C++語言中,也有對它的支持(雖然不建議使用它)。

goto語句有非常多的用途或優點,例如,它特別適合於在編寫系統程序中被使用,它能使編寫出來的代碼非常簡練。另外,goto語句另外一個最重要的作用就是,它實際上是一種對異常處理編程,最初也最原始的支持手段或方法。它能把錯誤處理模塊的代碼有效與其它代碼分離開來。例程如下

[cpp] view plain copy print?
  1. void main(int argc, char* argv[])
  2. {
  3.   if (Call_Func1(in, param out) {
  4.     // 函數調用成功,我們正常的處理
  5.     if (Call_Func2(in, param out) {
  6.     // 函數調用成功,我們正常的處理
  7.       while(condition) {
  8.         //do other job
  9.         // 如果錯誤直接跳轉
  10.         if (has error) goto Error;
  11.       //do other job
  12.       }
  13.     }
  14.     // 如果錯誤直接跳轉
  15.     else goto Error;
  16.   }
  17.   // 如果錯誤直接跳轉
  18.   else goto Error;
  19.   // 錯誤處理模塊
  20. Error:
  21.   process_error();
  22.   exit();
  23. }

雖然goto 語句能有效地支持異常處理編程的實現。但是沒有人卻建議使用它,即便是在C語言中。因為:

  (1) goto語句能破壞程序的結構化設計,使代碼難於測試,且包含大量goto的代碼模塊不易理解和閱讀。它一直遭結構化程序設計思想所拋棄,強烈建議程序員不易使用它;

  (2) 與C++語言中提供的異常處理編程模型相比,它的確是太弱了一些。例如,它一般只能是在某個函數的局部作用域內跳轉,也即它不能有效和方便地實現程序控制流的跨函數遠程的跳轉。

  (3) 如果在C++語言中,用goto語句來實現異常處理,那麽它將給面向對象構成極大破壞,並影響到效率。這一點,以後會繼續深入闡述。

雖然goto語句缺點多多,但不管如何,goto語句的確為程序員朋友們,在C語言中,有效運用異常處理思想來進行編程處理,提供了一種途徑或簡易的手段。當然,運用goto語句來進行異常處理編程已經成為歷史。因為,在C語言中,早就已經提供了一種更加優雅的異常處理機制。

3.更優雅的異常處理機制:setjmp()函數與longjmp()函數

C標準庫提供兩個特殊的函數:setjmp() 及 longjmp(),這兩個函數是結構化異常的基礎,正是利用這兩個函數的特性來實現異常。
所以,異常的處理過程可以描述為這樣:
·首先設置一個跳轉點(setjmp() 函數可以實現這一功能),然後在其後的代碼中任意地方調用 longjmp() 跳轉回這個跳轉點上,以此來實現當發生異常時,轉到處理異常的程序上,在其後的介紹中將介紹如何實現。
·setjmp() 為跳轉返回保存現場並為異常提供處理程序,longjmp() 則進行跳轉(拋出異常),setjmp() 與 longjmp() 可以在函數間進行跳轉,這就像一個全局的 goto 語句,可以跨函數跳轉。
舉個例子,程序在 main() 函數內使用 setjmp() 設置跳轉,並調用另一函數A,函數A內調用B,B拋出異常(調用longjmp() 函數),則程序直接跳回到 main() 函數內使用 setjmp() 的地方返回,並且返回一個值。

jmp_buf 異常結構

使用 setjmp() 及 longjmp() 函數前,需要先認識一下 jmp_buf 異常結構。jmp_buf 將使用在 setjmp() 函數中,用於保存當前程序現場(保存當前需要用到的寄存器的值),jmp_buf 結構在 setjmp.h 文件內聲明:

typedef struct

{

unsigned j_sp; // 堆棧指針寄存器

unsigned j_ss; // 堆棧段

unsigned j_flag; // 標誌寄存器

unsigned j_cs; // 代碼段

unsigned j_ip; // 指令指針寄存器

unsigned j_bp; // 基址指針

unsigned j_di; // 目的指針

unsigned j_es; // 附加段

unsigned j_si; // 源變址

unsigned j_ds; // 數據段

} jmp_buf;

jmp_buf 結構存放了程序當前寄存器的值,以確保使用 longjmp() 後可以跳回到該執行點上繼續執行。

setjmp() 與 longjmp() 函數詳細說明

setjmp() 與 longjmp() 函數原型如下:

void _Cdecl longjmp(jmp_buf jmpb, int retval);

int _Cdecl setjmp(jmp_buf jmpb);

_Cdecl 聲明函數的參數使用標準C的進棧方式(由右向左)壓棧,_Cdecl 是C語言的一種調用約定,除此以外,PASCAL 也是調用約定之一。C標準調用約定(_Cdecl)所聲明的函數不自動清除堆棧,這一事務由調用者自行負責——這也是C可以支持不固定個數的參數的原因。此外,這一調用約定將在函數名前添加一個下劃線字符,如某一函數聲明為:

int cdecl DoSomething(void);

編譯時將自動為 DoSomething 加上下劃線前綴,即函數名變為: _DoSomething。

setjmp() 與 longjmp() 函數都使用了 jmp_buf 結構作為形參,它們的調用關系是這樣的:

首先調用 setjmp() 函數來初始化 jmp_buf 結構變量 jmpb,將當前CPU中的大部分影響到程序執行的寄存器的值存入 jmpb,為 longjmp() 函數提供跳轉,setjmp() 函數是一個有趣的函數,它能返回兩次,它應該是所有庫函數中唯一一個能返回兩次的函數,第一次是初始化時,返回零,第二次遇到 longjmp() 函數調用後,longjmp() 函數使 setjmp() 函數發生第二次返回,返回值由 longjmp() 的第二個參數給出(整型,這時不應該再返回零)。

在使用 setjmp() 初始化 jmpb 後,可以其後的程序中任意地方使用 longjmp() 函數跳轉會 setjmp() 函數的位置,longjmp() 的第一個參數便是 setjmp() 初始化的 jmpb,若想跳轉回剛才設置的 setjmp() 處,則 longjmp() 函數的第一個參數是 setjmp() 所初始化的 jmpb 這個異常,這也說明一件事,即 jmpb 這個異常,一般需要定義為全局變量,否則,若是局部變量,當跨函數調用時就幾乎無法使用(除非每次遇到函數調用都將 jmpb 以參數傳遞,然而明顯地,是不值得這樣做的);longjmp() 函數的第二個參數是傳給 setjmp() 的第二次返回值,這在介紹 setjmp() 函數時已經介紹過。

異常處理過程

先來對比(參考)一下 C++ 的異常處理,C++ 在語言層上便添加了異常處理機制,使用 try 塊來包含那些可能出現錯誤的代碼,你可以在 try 塊代碼中拋出異常,C++ 使用 throw 來拋出異常。拋出異常後,將轉到異常處理程序中執行,C++ 使用 catch 塊來包含那些處理異常的代碼,catch 塊可以接收不同類型的異常。需要說明的是,throw 一般不在 try 塊內的代碼中拋出異常,try 塊內的代碼調用了別的函數,如函數A,函數A 又調用了函數 B,throw 可以在函數B中拋出異常,或者更深的函數調用層,無論如何,只要有異常拋出,程序將轉到 catch 處執行。

C中如何實現,或者明確地說是模擬這一功能?

下面介紹的是一些簡單的方法。

現在假設 longjmp() 第二個值為1,即 setjmp() 第二次將返回1。我們使用一組簡單的宏來替代 setjmp() 和 longjmp() 以便使用:

首先定義一個全局的異常:

jmp_buf Jump_Buffer;

因為 setjmp() 第一次調用初始化後返回0,第二次返回非0,可以這樣定義一個宏使得它功能接近於 C++ 的 try。

#define try if(!setjmp(Jump_Buffer))

當 setjmp() 函數第一次0 時,取非為真,則執行 try 塊內的代碼,如:

try {

Test();

}

當因為調用 longjmp() 拋出異常而導致 setjmp() 第二次返回時(程序將會轉到 setjmp() 函數處返回,這時,這時應該執行的是異常處理代碼。longjmp() 使 setjmp() 函數返回非0,if(!setjmp(JumpBuffer)) 中將值取非則為假,是以,異常處理放在其後應該使用一個 else:

#define catch else

如此看起來便跟 C++ 相似了,setjmp() 函數的第二次返回導致 if() 中表達式值為假,剛好使 catch 塊得以執行,如:

try {

Test();

} catch {

puts("Error");

}

實現如 C++ 的 throw 語句,事實上以宏替換 longjmp(jmp_buf, int) 的調用:

#define throw longjmp(Jump_Buffer, 1)

下面的例程解釋如何使用這些宏:

[cpp] view plain copy print?
  1. #include"stdio.h"
  2. #include"conio.h"
  3. #include"setjmp.h"
  4. jmp_buf Jump_Buffer;
  5. #define try if(!setjmp(Jump_Buffer))
  6. #define catch else
  7. #define throw longjmp(Jump_Buffer,1)
  8. int Test(int T)
  9. {
  10. if(T>100)
  11. throw;
  12. else
  13. puts("OK.");
  14. return 0;
  15. }
  16. int Test_T(int T)
  17. {
  18. Test(T);
  19. return 0;
  20. }
  21. int main()
  22. {
  23. int T;
  24. try{
  25. puts("Input a value:");
  26. scanf("%d",&T);
  27. T++;
  28. Test_T(T);
  29. } catch{
  30. puts("Input Error!");
  31. }
  32. getch();
  33. return 0;
  34. }

C語言中的異常處理機制