深入淺出話異常-(1) (轉)
深入淺出話異常-(1)
Robert Schmidt
May 10, 1999
本期討論要點: 標準C的異常處理機制。
前言
標準C提供了幾種異常管理機制,這些機制在標準C++裡也可用,但是相關的頭名稱作了改變:舊的標準C標頭檔案名會從
雖然在C++的向後相容裡保留了C的標頭檔案,但我勸告你在任何可能的地方使用新的標頭檔案。對於許多實際使用中,最大的改變是在新的標頭檔案與namespace std內進行宣告。請看以下三種不同型別的示例:
//舊的使用使用方法,在標準C++裡被替換成#include
#include
FILE *f = fopen("blarney.txt", "r");
//現在的用法,與舊方法很相似
std::FILE *f = std::fopen("blarney.txt", "r");
//混合使用,Visual C++支援這種用法?????
#include
using namespace std;
FILE *f = fopen("blarney.txt", "r");
不幸的是,'s Visual C++不能在新的標頭檔案與namespace std同時具備的條件下進行宣告,即使這種行為是標準C必需的。除非等到Visual C++支援這種行為,我將在本行使用舊的C風格名字。
(對於像Microt這樣的庫供應商來說,實現這些C庫標頭檔案的正確性需要維護與測試兩套不同的程式碼,這是一項艱鉅的任務,且不帶來任何商業價值)
一、絕對終止:
這是一種徹底忽略異常的方法,大概這種簡單的響應是一種的退出方法。在一些情形裡,這是最正確的方法。
C庫標頭檔案
雖然兩者在概念上是相同的,但使用它們的結果是不同的:
- abort: 粗魯地結束程式。這是預設的,在執行時診斷裡呼叫abort來安全結束程式。這種結束方式可能會或可能不會重新整理與關閉開啟的檔案或刪除臨時檔案。
- exit:文明地結束程式。它附加了關閉開啟的檔案與返回狀態碼給環境,exit還呼叫你用atexit註冊的回撥函式。
你通常是在發生嚴重異常的情況下呼叫abort,由於abort預設行為是立即結束程式,你需要在呼叫abort之前儲存你的資料。(在討論
對於兩者的差異,exit執行客戶用atexit註冊的清除程式碼,它們的呼叫順序是按它們被註冊的相反順序來的。示例:
#include (注意:如果你的程式在main函式結束時沒有顯式呼叫exit,那麼你用atexit註冊的處理函式也會被呼叫)。 abort與exit無條件終止你的程式。你也可以有條件地結束你的程式,這種機制是每一個程式設計師喜受的診斷工具:assert宏定義在 #if defined NDE #define assert(condition) ((void) 0) #else #define assert(condition) _assert((condition), #condition, __FILE__, __LINE__) #endif //譯註:各家產品提供的assert的實現並不一樣,比如: Visual C++ 6.0的實現是:#define assert(exp) (void)((exp)||(_assert(#exp, __FILE__, __LINE__), 0)); Borland C++ 5.5的實現是:#define assert(exp) ((exp) ? (void)0 : _assert(#exp, __FILE__, __LINE__)) 至於函式_assert(在gcc的庫中_assert是一個宏)是各家的內部實現,不一定得非要_assert這個名字,其內容一般是利用printf函式(在WIN平臺上往往是呼叫MessageBox)輸出出錯資訊(檔名及行號)並呼叫abort終止程式。//end 譯註 在這個定義裡,當定義了預處理符號NDEBUG的時候,斷言是無效的,這意味著assert斷言宏只在你的Debug版本中有效。在Release版本里,assert斷言宏不進行任何計算。由於這個而會引起一些側面效應,比如: /* 版本 */ #undef NDEBUG #include 那麼現在改變程式碼版本到release版本,定義NDEBUG: /* release版本*/ #defing NDEBUG #include 所以在assert中只能是比較而不能有實質性的動作,否則除錯和釋出版的結果可能會大相徑庭。 因此,為了避免這種差異,確保在assert不能包含有側面影響的程式碼。 只在Debug版本里,assert會呼叫_assert函式。以下是相似程式碼: void _assert(int test, char const *test_image, char const *file, int line) { if (!test) { printf("Assertion failed: %s, file %s, line %dn", test_image, file, line); abort(); } } 在斷言失敗將產生出詳細的診斷資訊,包含源程式檔名與行號,之後呼叫abort,我給這種機制的示例是相當的粗糙;你的庫實現者可能更復雜。 assert典型是用在除錯邏輯錯誤,它永遠不會存在於release程式裡。 static void f(int *p) { assert(p != NULL);//這兒! /* ... */ } 在使用assert中要注意邏輯錯誤與執行時錯誤的區別: /* ...讓輸入檔名... */ FILE *file = fopen(name, mode); assert(file != NULL); /* 相當可疑的用法??? */ 這種錯誤出現在assert表示式裡,但它不是BUG,它是執行時異常,assert可能會不正確地響應,你應該使用其它機制,我在下面介紹。 對比於abort與exit,goto 讓你有更多地管理異常的方法,不幸的是gotos是區域性的,goto只能在它們函式的內部跳轉,因此不能在程式的任意地方控制它。 為了克服這種限制,標準C提供了setjmp與longjmp函式,它可以goto到任何地方。標頭檔案 這裡有兩種型別的返回值,setjmp讓你來如何使用它。當設定j的時候,setjmp工作在正常預期的行為,但當目標是long jump, setjmp "wakes up" from outs its normal context. 如果使用longjmp來引發終止異常,setjmp可以標記相應的異常處理過程。 #include 注意:用jmp_buf來恢復其它上下文是無效的,請看以下示例: jmp_buf j; void f(void) { setjmp(j); } int main(void) { f(); longjmp(j, 1); /* 邏輯錯誤 */ return 0; } 你必須在當前呼叫上下文中只認為setjmp是非區域性goto。 標準C也標準化事件(event)管理包(雖然較原始)。這個管理包定義了設定事件與訊號,連同標準的引發與處理方法。那些訊號可在異常表示式或不同的擴充套件事件裡引發它們。這也是要討論的目的。我只集中在異常訊號. 對於使用這些管理包,應該包含標準標頭檔案 當你註冊訊號處理過程的時候,一般你要提供處理函式地址。每一個函式必需接受int值,且返回void。在這種方法,訊號處理方法象setjmp;只有異常上下文能接收單個整數: void handler(int signal_value); void f(void) { signal(SIGFPE, handler); /* 註冊處理過程*/ /* ... */ raise(SIGFPE); /* 透過 'SIGFPE'來呼叫處理過程 */ } 有兩種安裝指定處理方法可供選擇:
在所有情形裡,訊號返回指向先前的處理過程的指標或SIG_ERR(意味著註冊失敗) 當處理方法被呼叫的時候,這意味訊號開始進行異常處理。你可以在處理方法裡自由呼叫abort,exit或longjmp來效地結束異常。一些有趣的地方:實際上,abort自已在內部也呼叫raise(SIGABRT),預設的SIGABRT異常處理方法顯示診斷資訊與結束程式。但你可以安裝你自已的SIGABRT異常處理方法來改變這種行為: 但你不能改變abort的終止程式的行為,以下是abort的相似程式碼: void abort(void) { raise(SIGABRT); exit(EXIT_FAILURE); } 這兒,如果你SIGABRT異常處理方法返回後,abort也結束程式。 在標準C庫裡,在訊號異常處理方法行為也是有限制的。請看標準7.7.1.1的細節。 (譯者注:以下是標準C的草案檔案:裡的n843.pdf) errno,包含設定與獲取:當程式碼產生異常物件(單個整數)時,複製異常物件的值給予errno,然後在使用者中檢測異常。 主要使用errno的庫函式集中在 #include 說明:errno不需要引用到物件: int *_errno_function() { static int real_errno = 0; return &real_errno;//不需要這樣做 } #define errno (*_errno_function()) int main(void) { errno = 0; /* ... */ if (errno == EDOM) /* ... */ } errno-像異常物件但沒有限制:
總結:每一個物件都是脆弱的:你太容易濫用它們,在你的編譯器沒有警告資訊裡,你的程式可能出現不可預測的行為。 去掉這些缺陷,你需要的物件應該是:
函式的返回值應該符合這些標準,因為它們是呼叫函式里建立未命名的臨時物件,且只能被呼叫者理解。當一個呼叫完成,呼叫者可能檢測或複製返回物件的值;之後,返回的原始物件消失了,因此不能在使用這個物件了。由於物件是未命名的物件,它是不能被隱藏的。 (在C++裡,我假定在函式呼叫表示式只返回左值,意味著呼叫者不能返回引用,我的這種限制只在我討論的C相容技術這部分裡,而且C沒有引用(C標準-C98也加入支援引用),所以我的這個假設是合理的) int f() { int error; /* ... */ if (error) /* 存在錯誤 */ return -1; /* 產生異常物件 */ /* ... */ } int main(void) { if (f() != 0) /* 檢測異常 */ { /* 處理異常 */ } /* 再次執行 */ } 返回值是標準C庫用來傳播異常的較好的方法,請思考以下示例: if ((p = malloc(n)) == NULL) /* ... */ if ((c = getchar()) == EOF) /* ... */ if ((ticks = clock()) < 0) /* ... */ 說明:這種在一個語句裡進行捕捉返回值與測試異常的方法是較典型的慣用法。它有兩個不同的含義:合法的資料值與異常值。程式碼必須解釋這兩種計算路徑在哪兒知道它是正確的。 函式返回值的方法被運用於許多公共語言,Microsoft運用在它的COM模型。COM方法透過返回HRESULT來通報異常物件,Microsoft對這個值使用32位無符號整數。不像當才的例子只是討論。COM的返回值只返回狀態與異常資訊,其它資訊透過指標指向引數。 外部指標與C++引用引數是變種的函式返回值,但它們有以下幾點不同:
本期圍繞著介紹標準C支援的一般異常的處理方法。在第二期,我將介紹Microsoft擴充套件了這些標準C的方法:專用的異常處理宏與結構化異常處理(SEH)。 (譯註:說來慚愧,本人對C是一知半解,對於在C++裡,這些方法都不被推薦,以至於沒有深入過,這篇文章我翻譯得挺吃力的,如果有錯誤,請大家指點。)二、條件結束:
三、非區域性goto:
四、訊號(Signals):
五、公共變數:
六、返回值與引數:
結尾
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-993608/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- RxRetrofit - 終極封裝 - 深入淺出 & 異常封裝
- 淺談java異常[Exception] (轉)JavaException
- 深入淺出Netty(1)Netty
- RxJava+Retrofit+OkHttp深入淺出-終極封裝七(異常處理)RxJavaHTTP封裝
- 深入淺出OOD(一) (轉)
- ActiveX深入淺出(一) (轉)
- ActiveX深入淺出(二) (轉)
- 深入淺出HOOKS(之伍) (轉)Hook
- 深入淺出HOOKS(之陸) (轉)Hook
- 深入淺出談防火牆(轉)防火牆
- 深入淺出RxJava(1):基礎篇RxJava
- 深入淺出FE(十四)深入淺出websocketWeb
- 深入淺出 Java 併發程式設計 (1)Java程式設計
- 深入理解Java異常Java
- 深入淺出——MVCMVC
- 深入淺出mongooseGo
- HTTP深入淺出HTTP
- 深入淺出IO
- 深入淺出 RabbitMQMQ
- 深入淺出PromisePromise
- ArrayList 深入淺出
- mysqldump 深入淺出MySql
- 深入淺出decorator
- 深入淺出 ZooKeeper
- 機器學習深入淺出機器學習
- 深入淺出HTTPHTTP
- http 深入淺出HTTP
- 深入淺出 ARCore
- 深入淺出 synchronizedsynchronized
- 深入淺出WebpackWeb
- 深入淺出 blockBloC
- block深入淺出BloC
- <Win32_1>深入淺出windows訊息機制[轉自crocodile_]Win32Windows
- 深入淺出說強制型別轉換型別
- 淺讀-《深入淺出Nodejs》NodeJS
- 《深入淺出webpack》有感Web
- 深入淺出 Laravel MacroableLaravelMac
- 反射的深入淺出反射