Visual C++中的異常處理淺析
在進入正式的講解之前,先說幾句廢話。許多的編程新手對異常處理視而不見,程序里很少考慮異常情況。一部分人甚至根本就不考慮,以為程序總是能以正確的途徑運行。譬如我們有的程序設計者調用fopen打開一個文件后,立馬就開始進行讀寫操作,根本就不考慮文件
在進入正式的講解之前,先說幾句廢話。許多的
編程新手對異常處理視而不見,程序里很少考慮異常情況。一部分人甚至根本就不考慮,以為程序總是能以正確的途徑運行。譬如我們有的程序設計者調用fopen打開一個文件后,立馬就開始進行讀寫操作,根本就不考慮文件是否正常打開了。這種習慣一定要改掉,縱使你再不愿意!這是軟件健壯性的需要!異常處理不是浪費時間!
1.C語言異常處理 1.1 異常終止
標準C庫提供了abort()和exit()兩個函數,它們可以強行終止程序的運行,其聲明處于<s
tdlib.h>頭文件中。這兩個函數本身不能檢測異常,但在C程序發生異常后經常使用這兩個函數進行程序終止。下面的這個例子描述了exit()的行為:
clearcase/" target="_blank" >cccccc width="90%" align=center bgColor=#e3e3e3 border=1>
#include <stdio.h> #include <stdlib.h> int main(void) { exit(EXIT_SUCCESS); printf("程序不會執行到這里\n"); return 0; } |
在這個例子中,main函數一開始就執行了exit函數(此函數原型為void exit(int)),因此,程序不會輸出"程序不會執行到這里"。程序中的exit(EXIT_SUCCESS)表示程序正常結束,與之對應的exit(EXIT_FAILURE)表示程序執行錯誤,只能強行終止。EXIT_SUCCESS、EXIT_FAILURE分別定義為0和1。
對于exit函數,我們可以利用atexit函數為exit事件"掛接"另外的函數,這種"掛接"有點類似
Windows編程中的"鉤子"(Hook)。譬如:
#include <stdio.h> #include <stdlib.h> static void atExitFunc(void) { printf("atexit掛接的函數\n"); } int main(void) { atexit(atExitFunc); exit(EXIT_SUCCESS); printf("程序不會執行到這里\n"); return 0; } |
程序輸出"atexit掛接的函數"后即終止。來看下面的程序,我們不調用exit函數,看看atexit掛接的函數會否執行:
#include <stdio.h> #include <stdlib.h> static void atExitFunc(void) { printf("atexit掛接的函數\n"); } int main(void) { atexit(atExitFunc); //exit(EXIT_SUCCESS); printf("不調用exit函數\n"); return 0; } |
程序輸出:
不調用exit函數
atexit掛接的函數
這說明,即便是我們不調用exit函數,當程序本身退出時,atexit掛接的函數仍然會被執行。
atexit可以被多次執行,并掛接多個函數,這些函數的執行順序為后掛接的先執行,例如:
#include <stdio.h> #include <stdlib.h>
static void atExitFunc1(void) { printf("atexit掛接的函數1\n"); }
static void atExitFunc2(void) { printf("atexit掛接的函數2\n"); }
static void atExitFunc3(void) { printf("atexit掛接的函數3\n"); }
int main(void) { atexit(atExitFunc1); atexit(atExitFunc2); atexit(atExitFunc3); return 0; } |
輸出的結果是:
atexit掛接的函數3
atexit掛接的函數2
atexit掛接的函數1
在Visual
C++中,如果以abort函數(此函數不帶參數,原型為void abort(void))終止程序,則會在de
bug模式運行時彈出如圖1所示的對話框。
 圖1 以abort函數終止程序 |
1.2 斷言(assert)
assert宏在C語言程序的調試中發揮著重要的作用,它用于檢測不會發生的情況,表明一旦發生了這樣的情況,程序就實際上執行錯誤了,例如strcpy函數:
char *strcpy(char *strDest, const char *strSrc) { char *address = strDest; assert((strDest != NULL) && (strSrc != NULL)); while ((*strDest++ = *strSrc++) != ’\0’) ; return address; } |
其中包含斷言assert( (strDest != NULL) && (strSrc != NULL) ),它的意思是源和目的字符串的地址都不能為空,一旦為空,程序實際上就執行錯誤了,會引發一個abort。
assert宏的定義為:
#ifdef NDEBUG #define assert(exp) ((void)0) #else #ifdef __cplusplus extern "C" { #endif
_CRTIMP void __cdecl _assert(void *, void *, unsigned); #ifdef __cplusplus } #endif #define assert(exp) (void)( (exp) || (_assert(#exp, __FILE__, __LINE__), 0) ) #endif /* NDEBUG */ |
如果程序不在de
bug模式下,assert宏實際上什么都不做;而在de
bug模式下,實際上是對_assert()函數的調用,此函數將輸出發生錯誤的文件名、代碼行、條件表達式。例如下列程序:
#include <stdio.h> #include <stdlib.h> #include <assert.h> char * myStrcpy( char *strDest, const char *strSrc ) { char *address = strDest; assert( (strDest != NULL) && (strSrc != NULL) ); while( (*strDest++ = *strSrc++) != ’\0’ ); return address; } int main(void) { myStrcpy(NULL,NULL); return 0; } |
在此程序中,為了避免我們的strcpy與C庫中的strcpy重名,將其改為myStrcpy。程序的輸出如圖2:
 圖2 assert的輸出 |
失敗的斷言也會彈出如圖1所示的對話框,這是因為_assert()函數中也調用了abort()函數。
一定要記住的是assert本質上是一個宏,而不是一個函數,因而不能把帶有副作用的表達式放入assert的"參數"中。
1.3 errno
errno在C程序中是一個全局變量,這個變量由C運行時庫函數設置,用戶程序需要在程序發生異常時檢測之。C運行庫中主要在math.h和stdio.h頭文件聲明的函數中使用了errno,前者用于檢測數學運算的合法性,后者用于檢測I/O操作中(主要是文件)的錯誤,例如:
#include <errno.h> #include <math.h> #include <stdio.h> int main(void) { errno = 0; if (NULL == fopen("d:\\1.txt", "rb")) { printf("%d", errno); } else { printf("%d", errno); } return 0; } |
在此程序中,如果文件打開失?。╢open返回NULL),證明發生了異常。我們讀取error可以獲知錯誤的原因,如果D盤根目錄下不存在"1.txt"文件,將輸出2,表示文件不存在;在文件存在并正確打開的情況下,將執行到else語句,輸出0,證明errno沒有被設置。
Visual C++提供了兩種版本的C運行時庫。-個版本供單線程應用程序調用,另一個版本供多線程應用程序調用。多線程運行時庫與單線程運行時庫的一個重大差別就是對于類似errno的全局變量,每個線程單獨設置了一個。因此,對于多線程的程序,我們應該使用多線程C運行時庫,才能獲得正確的error值。
另外,在使用errno之前,我們最好將其設置為0,即執行errno = 0的賦值語句。
1.4 其它
除了上述異常處理方式外,在C語言中還支持非局部跳轉(使用setjmp和longjmp)、信號(使用signal、raise)、返回錯誤值或回傳錯誤值給參數等方式進行一定能力的異常處理,但是其使用不如1.1~1.3節所介紹方式常用,我們不必過細研究。
從以上分析可知,C語言的異常處理是簡單而不全面的。與C++的異常處理比起來,C語言異常處理相形見絀,它就像娘胎里的雛嬰。