1.1 基本概念 斷言是一種讓錯誤在運行時候自我暴露的簡單有效實用的技術。它們協助你較早較輕易地發現錯誤,使得整個調試過程效率更高。 斷言是布爾調試語句,用來檢測在程式正常啟動並執行時候某一個條件的值是否總為真,它能讓錯誤在運行時刻暴露在程式員面前。使用斷言的最大好處在於,能在更解決錯誤的發源地的地方發現錯誤。斷言具有以下特徵:n 斷言是用來發現運行時刻錯誤的,發現的錯誤是關於程式實現方面的。n 斷言中的布林運算式顯示的是某個對象或者狀態的有效性而不是正確性。n 斷言在條件編譯後只存在於調試版本中,而不是發布版本裡。n 斷言不能包含程式碼。n 斷言是為了給程式員而不是使用者提供資訊。 使用斷言最根本的好處是自動探索許多運行時產生的錯誤,但斷言不能發現所有錯誤。斷言檢查的是程式的有效性而不是正確性,可通過斷言把錯誤限制在一個有限的範圍內。當斷言為假,啟用調試器顯示出錯代碼時,通常能檢查出導致宣告失敗的原因。_ASSERTE宏(屬於C已耗用時間庫)還能在宣告失敗時顯示出失效斷言。
1.2 C中的斷言(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 __cplusplusextern "C"{#endif _CRTIMP void __cdecl _assert(void *, void *, unsigned);#ifdef __cplusplus}#endif#define assert(exp) (void)( (exp) || (_assert(#exp, __FILE__, __LINE__), 0) )#endif /* NDEBUG */ 如果程式不在debug模式下,assert宏實際上什麼都不做;而在debug模式下,實際上是對_assert()函數的調用,此函數將輸出發生錯誤的檔案名稱、程式碼、條件運算式。一定要記住的是assert本質上是一個宏,而不是一個函數,因而不能把帶有副作用的運算式放入assert的"參數"中。
1.3 C中的異常終止(exit) 標準C庫提供了abort()和exit()兩個函數,它們可以強行終止程式的運行,其聲明處於<stdlib.h>標頭檔中。這兩個函數本身不能檢測異常,但在C程式發生異常後經常使用這兩個函數進行程式終止。 對於exit函數,可以利用atexit函數為exit事件"掛接"另外的函數,這種"掛接"有點類似Windows編程中的"鉤子"(Hook)。程式輸出"atexit掛接的函數"後即終止,即便是我們不調用exit函數,當程式本身退出時,atexit掛接的函數仍然會被執行。 atexit可以被多次執行,並掛接多個函數,這些函數的執行順序為後掛接的先執行。 在Visual C++中,如果以abort函數(此函數不帶參數,原型為void abort(void))終止程式,則會在debug模式運行時彈出錯誤提示的對話方塊。
1.4 MFC中的斷言
1.4.1 ASSERT(布林運算式)在VC中檢查變數合法性一般利用ASSERT(x)宏,ASSERT的作用在於檢查運算式是否為假或為NULL,如果為假則會引發異常。在MFC中ASSERT宏被大量使用,它的優點是即使出現了WM_QUIT訊息也能顯示斷言失效訊息框。當ASSERT失敗並引發異常時會有對話方塊彈出並報告發生該ASSERT失敗位置。並允許選擇繼續運行(Ignore)或是終止(Abort)程式。(當然選擇繼續運行是很危險的)選擇Retry將會啟動調試軟體對程式進行調試。 此外還有一點,ASSERT宏只在調試版本中才會有作用,在調試版本中ASSERT(f)宏被展開為 do{ if (!(f) && AfxAssertFailedLine(THIS_FILE, __LINE__)) AfxDebugBreak();} while (0)而在發行版本中會被展開為:((void)0)所以對程式內部狀態改變的代碼不能夠放置在ASSERT宏中否則在發行版中會出現不正常的現象。
1.4.2 VERIFY(布林運算式)如果希望合法檢查在發行版本中同樣起作用則可以利用VERIFY宏,VERIFY宏簡化了對函數傳回值的檢查,一般用來檢查Windows API的傳回值。VERIFY宏與ASSERT宏的不同在與VERIFY在發行版本中同樣會起作用,但是使用VERIFY會導致非常不友好的使用者介面,因此最好盡量不要使用這個宏以實現程式碼和調試代碼的完全分離。
1.4.3 ASSERT_VALID(指向CObject衍生類別對象的指標)對象的合法性檢查需要根據對象自身的狀態和一些對象自己的邏輯來作出判斷,因此在對象外部就無法正確判斷,一個省時有效辦法是在對象內部進行檢查,由對象自己負責合法性檢查,MFC利用成員函數 void CObject::AssertValid() const來實現對象的合法性檢查,所以新的類必須是CObject的衍生類別,(在MFC中幾乎所有的類都由CObject派生)由於C++的多態性衍生類別的AssertValid函數會被正確的調用。函數定義中的const表示該函數體中不能改變成員變數的值。 我們所需要做的就是重載AssertValid,並實現對象狀態合法性的檢查。在AssertValid我們不但可以檢查資料的正確性,也可以對資料的邏輯性進行檢查。此外MFC中定義了ASSERT_VALID宏來執行安全的對象檢查,ASSERT_VALID宏會展開AfxAssertValidObject,並先檢查指標的合法性。這樣避免了下面的錯誤: CView *pV=NULL;pV->AssertValid();//安全的方法是利用ASSERT_VALID(pView);ASSERT_VALID宏通過調用重載的AssertValid函數來確定指向CObject衍生類別對象的指標是否有效。無論你什麼時候從CObject衍生類別中得到一個對象,在對這個對象做任何操作之前都應該調用ASSERT_VALID宏。與ASSERT宏一樣,ASSERT_VALID宏只在調試版本中起作用。
1.4.4 其它ASSERT_KINDOF(類名, 指向CObject衍生類別對象的指標):這個宏用來驗證指向CObject衍生類別對象的指標是否從某個特殊類中派生,在調用它之前先調用ASSERT_VALID宏。只有在很特殊的場合下才用得到,如檢測編譯器可能錯過的物件類型問題。MFC還有兩個沒有正式檔案的ASSERT宏的變種:ASSERT_POINTER(指標,指標類型),ASSERT_NULL_OR_POINTER(指標,指標類型)。
1.5 什麼時候使用斷言 把斷言看作一種簡單的製造柵欄的方法,這種柵欄能使錯誤在穿過自己時暴露。u 檢查函數的輸入u 檢查函數的輸出u 檢查對象的目前狀態u 堅持邏輯變數的合理性和一致性u 檢查類中的不變數 公有成員函數比私人和保護的成員函數需要更全面的斷言。 不正確地使用斷言會導致錯誤。斷言應該檢測那些在程式正常啟動並執行時候永遠都不可能出現的狀態。斷言是用來揭示錯誤的,而不是用來糾正運行時刻錯誤的。
1.6 斷言與防禦性編程(Defensive Programming)斷言在調試的時候向程式員揭示運行時刻錯誤(調試版本裡),而防禦性編程使使用者在運行程式(發布版本裡)時,當出現意外情況時程式仍能繼續工作。實際上,防禦性的編程要求程式在檢測到意外時返回一個“安全”的值(比如布爾函數返回false,指標和控制代碼返回空值),一個錯誤碼或者拋出一個異常來解決問題。特定的防禦性編程技術包括:處理無效函數參數和資料、出現問題的時候程式失敗、檢查臨界函數返回的錯誤碼以及處理異常。需要防禦性編程的標準問題包括:錯誤的輸入資料、記憶體或者硬碟空間不夠、不能開啟一個檔案、外部裝置不能訪問、網路連接不上或者甚至在程式中還有錯誤,目的是保持程式的運行狀態。如果你的程式是防禦性的,別忘了使用斷言。如果你使用斷言,也別忘了防禦性編程。這兩種技術最好結合在一起使用。