天天看點

程式設計精粹--編寫高品質的C語言代碼(2):自己設計并使用斷言(一)

上一篇文章中講述了如何利用編譯程式的所有警告設施以及lint程式等來更加容易地自動發現程式中的錯誤。但是即使使用編譯程式提供的所有警告設施,編譯程式所發現的錯誤,也隻是程式錯誤中的一小部分。例如以下一行代碼:

當malloc 調用失敗時,傳回一個空指針,而memcpy如果沒有處理空指針,程式就會出現錯誤。編譯程式是無法查出這種或其他類似的錯誤。同樣編譯程式也無法查出算法的錯誤,無法驗證程式員的假設。編譯程式也查不出傳遞的參數是否有效。那如何自動尋找出這些錯誤呢?接着上面那個例子,最初的解決辦法就是在memcpy對NULL指針進行檢查。

當空指針調用memcpy時,函數就會檢查出這個錯誤,并列印出錯誤資訊。可是這樣編寫代碼帶來兩個問題:1,測試空指針的代碼增加了函數的代碼量;2,降低了函數的執行速度。

我們應當知道,當程式正常運作時,是不應該出現把空指針傳給memcpy函數的。但是當程式還處于調試過程中,這種情況是會出現的。是以應該儲存兩個版本,一個整潔快速用于程式的傳遞,另一個臃腫緩慢(因外需要包過額外的檢查),用于調試。是以應當維護程式的兩個版本,并利用C語言的條件編譯,有條件的包含或不包含相應的檢查部分。

這樣就同時維護了兩個版本,在調試時,編譯其調試版本,這樣就會包含空指針的測試代碼,便于自動差錯,而在程式編寫完成後,編譯其傳遞版本,保證最終産品中不會包含調試代碼,保證了程式的整潔快速。

         既要維護程式的傳遞版本,又要維護程式的調試版本。

上面的memcpy的調試代碼略顯複雜,聰明的程式員會把所有的調試代碼隐藏在斷言assert中。assert是個宏,定義在assert.h中。雖然assert 隻不過是#ifdef 部分代碼的替換,但是使用assert宏使代碼更簡潔。

assert是個隻有定義了DEBUG才會起作用的宏,是以是用于程式的調試,而程式的最終産品是不會包含assert的代碼。正是因為assert隻會在程式的調試版本中起作用,是以為了避免程式的傳遞版本和調試版本之間引起重要的差别,需要對assert宏進行仔細定義。assert宏不應該弄亂記憶體,不應該對未初始化的資料進行初始化。也就是說assert宏不應該有其它副作用。如果assert的參數計算結果為假,就會終止程式的執行。而且還要意識到,一旦程式員學會了斷言,就常常會對宏assert進行重定義。這樣就可以自定義assert宏的行為。

不管斷言宏最終以什麼樣的方法定義,都要使用它來對傳遞的函數參數進行确認。斷言宏的最好作用是使使用者在發生錯誤時,就可以自動地把它們檢查出來。

         要使用斷言對函數參數進行确認

在ANSI C 中,如果是對兩個存儲空間互相重疊的對象間進行拷貝,memcpy的結果是未定義的。結果未定義就意味着不同的編譯程式,其結果也可能不同。是以對于無定義的特性,我們可以通過斷言來對其進行确認。

代碼中隻利用了ASSERT(pbTo>=pvFrom+size||pbFrom>=pbTo+size),就可以完成兩個記憶體塊是否重疊。是以如果程式中使用了無定義的特性就要把它從相應的設計裡去掉,或者在程式中包含相應的斷言,以便在使用了無定義的特性,能夠向程式員通報。

          要從程式中删去無定義的特性或者在程式中使用斷言來檢查無定義特性的非法使用。

還有一點需要注意,當我們千辛萬苦地跟蹤到一個斷言時,卻有可能不知道該斷言的作用。是以為了使程式員了解斷言的意圖,要給不夠清楚的斷言加上注解。

          不要浪費别人的時間—詳細說明不清楚的斷言。

當程式員剛開始使用斷言時,有時會錯誤地利用斷言去檢查真正地錯誤,而不是去檢查非法的狀況。

第一個斷言是正确的,因為它用來檢查程式正常運作時絕對不應該發生的非法狀況,而第二個斷言的用法是不當的,它所測試的是錯誤狀況,程式運作時的确可能會出現記憶體配置設定失敗,我們應該在最終産品中對這種錯誤進行處理而不是使用斷言。

對于程式中使用的假定,要使用斷言和條件編譯進行相應的驗證,例如:有時候我們會不自覺的認為一個位元組占8位,或者說一個long型占據4個位元組,這些都是對編譯程式或作業系統做的一些假定。這使得我們需要在程式中使用斷言例如ASSERT(sizeof(long)==4&&CHAR_BIT==8)。

          消除所做的隐式假定,或者利用斷言檢查其正确性。

總結:

1,同時維護程式的調試版本和傳遞版本,封裝傳遞版本,應盡可能地利用調試版本自動查錯。

2,斷言是進行調試檢查的簡單方法。要使用斷言來檢查絕對不應該發生的非法情況,不要混淆非法情況和錯誤情況,錯誤情況是需要在最終産品中處理的。

3,利用斷言對函數的參數進行确認,并且當程式員使用了無定義特性時向程式員報警。

4,當編寫函數時,應反複問自己做了哪些假定,一旦确定了相應的假定,就要使用斷言對所做的假定進行檢驗,或者重新編寫代碼去除假定。

最後用作者在這章中的一句話結束這篇文章:

          如果我告訴大家出現了斷言失敗是件好事,也許這個程式員就不會這麼驚慌了。