優質C程式秘訣第2章自己設計並使用斷言

2021-08-02 13:28:26 字數 4026 閱讀 5707

第2章自己設計並使用斷言

利用編譯程式自動查錯固然好,但我敢說只要你觀察一下專案中那些比較明顯的錯誤,就會發現編譯程式所查出的只是其中的一小部分。我還敢說,如果排除掉了程式中的所有錯誤那麼在大部分時間內程式都會正確工作。

還記得第1章中的下面**嗎?

strcopy = memcpy(malloc(length), str, length);

該語句在多數情況下都會工作得很好,除非malloc的呼叫產生失敗。當malloc失敗時,就會給memcpy返回乙個null指標。由於memcpy處理不了null指標,所以出現了錯誤。

如果你很走運,在交付之前這個錯誤導致程式的癱瘓,從而暴露出來。但是如果你不走運,沒有及時地發現這個錯誤,那某位顧客就一定會「走運」了。

編譯程式查不出這種或其他類似的錯誤。同樣,編譯程式也查不出演算法的錯誤,無法驗證程式設計師所作的假定。或者更一般地,編譯程式也查不出所傳遞的引數是否有效。

尋找這種錯誤非常艱苦,只有技術非常高的程式設計師或者測試者才能將它們**並且不會引起其他的問題。

然而假如你知道應該怎樣去做的話,自動尋找這種錯誤就變得很容易了。

兩個版本的故事

讓我們直接進入memcpy,看看怎樣才能查出上面的錯誤。最初的解決辦法是使memcpy對null指標進行檢查,如果指標為null,就給出一條錯誤資訊,並中止memcpy的執行。下面是這種解法對應的程式。

/* memcpy ─── 拷貝不重疊的記憶體塊 */

void memcpy(void* pvto, void* pvfrom, size_t size)

while(size-->0)

*pbto++ == *pbfrom++;

return(pvto);

}只要呼叫時錯用了null指標,這個函式就會查出來。所存在的唯一問題是其中的測試**使整個函式的大小增加了一倍,並且降低了該函式的執行速度。如果說這是「越治病越糟」,確實有理,因為它一點不實用。

要解決這個問題需要利用c的預處理程式。

如果儲存兩個版本怎麼樣?乙個整潔快速用於程式的交付;另乙個臃腫緩慢件(因為包括了額外的檢查),用於除錯。這樣就得同時維護同一程式的兩個版本,並利用c的預處理程式有條件地包含或不包含相應的檢查部分。

void memcpy(void* pvto, void* pvfrom, size_t size)

#endif

while(size-->0)

*pbto++ == *pbfrom++;

return(pvto);

}這種想法是同時維護除錯和非除錯(即交付)兩個版本。在程式的編寫過程中,編譯其除錯版本,利用它提供的測試部分在增加程式功能時自動地查錯。在程式編完之後,編譯其交付版本,封裝之後交給經銷商。

當然,你不會傻到直到交付的最後一刻才想到要執行打算交付的程式,但在整個的開發工程中,都應該使用程式的除錯版本。正如在這一章和下一章所建,這樣要求的主要原因是它可以顯著地減少程式的開發時間。讀者可以設想一下:

如果程式中的每個函式都進行一些最低限度的錯誤檢查,並對一些絕不應該出現的條件進行測試的活,相應的應用程式會有多麼健壯。

這種方法的關鍵是要保證除錯**不在最終產品中出現。

利用斷言進行補救

說老實話memcpy中的除錯碼編得非常蹩腳,且頗有點喧賓奪主的意味。因此儘管它能產生很好的結果,多數程式設計師也不會容忍它的存在,這就是聰明的程式設計師決定將所有的除錯**隱藏在斷言assert中的緣故。assert是個巨集,它定義在標頭檔案assert.

h中。assert雖然不過是對前面所見#ifdef部分**的替換,但利用這個巨集,原來的**從7行變成了1行。

void memcpy(void* pvto, void* pvfrom, size_t size)

aasert是個只有定義了debug才起作用的巨集,如果其引數的計算結果為假,就中止呼叫程式的執行。因此在上面的程式中任何乙個指標為null都會引發assert。

assert並不是乙個倉促拼湊起來的巨集,為了不在程式的交付版本和除錯版本之間引起重要的差別,需要對其進行仔細的定義。巨集assert不應該弄亂記憶體,不應該對未初始化的資料進行初始化,即它不應該產主其他的***。正是因為要求程式的除錯版本和交付版本行為完全相同,所以才不把assert作為函式,而把它作為巨集。

如果把assert作為函式的話,其呼叫就會引起不期望的記憶體或**的兌換。要記住,使用assert的程式設計師是把它看成乙個在任何系統狀態下都可以安全使用的無害檢測手段。

讀者還要意識到,一旦程式設計師學會了使用斷言,就常常會對巨集assert進行重定義。例如,程式設計師可以把assert定義成當發生錯誤時不是中止呼叫程式的執行,而是在發生錯誤的位置轉入除錯程式。assert的某些版本甚至還可以允許使用者選擇讓程式繼續執行,就彷彿從來沒有發生過錯誤一樣。

如果使用者要定義自己的斷言巨集,為不影響標準assert的使用,最好使用其它的名字。本書將使用乙個與標準不同的斷言巨集,因為它是非標準的,所以我給它起名叫做assert,以使它在程式中顯得比較突出。巨集assert和assert之間的主要區別是assert是個在程式中可以隨便使用的表示式,而assert則是乙個比較受限制的語句。

例如使用assert,你可以寫成:

if(assert(p != null), p->foo!=bar)

……但如果用assert試試就會產生語法錯誤。這種區別是作者有意造成的。除非打算在表示式環境中使用斷言,否則就應該將assert定義為語句。

只有這樣,編譯程式才能夠在它被錯誤地用到表示式時產生語法錯誤。記住,在同錯誤進行鬥爭時每一點幫助都有助於錯誤的發現。我們為什麼要那些自己從來用不著的靈活性呢?

下面是一種使用者自己定義巨集assert的方法:

#ifdef debug

void _assert(char* , unsigned原型 */

#define assert(f

if(f

null

else

_assert(__file__ , __line__)

#else

#define assert(fnull

#endif

從中我們可以看到,如果定義了debug,assert將被擴充套件為乙個if語句。if語句中的null語句讓人感到很奇怪,這是因為要避免if不配對,所以它必須要有else語句。也許讀者認為在_assert呼叫的閉括號之後需要乙個分號,但並不需要。

因為使用者在使用assert時,已經給出了乙個分號.

當assert失敗時,它就使用預處理程式根據巨集__file__和__line__所提供的檔名和行號引數呼叫_assert。_assert在標準錯誤輸出裝置stderr上列印一條錯誤訊息,然後中止:

void _assert(char* strfile, unsigned uline)

在執行abort之前,需要呼叫fflush將所有的緩衝輸出寫到標準輸出裝置stdout上。同樣,如果stdout和stderr都指向同乙個裝置,fflush stdout仍然要放在fflush stderr之前,以確保只有在所有的輸出都送到stdout之後,fprintf才顯示相應的錯誤資訊。

現在如果用null指標呼叫memcpy,assert就會抓住這個錯誤,並顯示出如下的錯誤資訊:

assertion failed: string.c , line 153

這給出了assert與assert之間的另一點不同。標準巨集assert除了給出以上資訊之外,還顯示出已經失敗了的測試條件。例如對這個問題,我通常所用編譯程式的assert會顯示出如下資訊:

assertion failed: pvto != null && pbfrom != null

file string.c , line 153

在錯誤訊息中包括測試表示式的唯一麻煩是每當使用assert時,它都必須為_assert產生一條與該條件對應的正文形式列印訊息。但問題是,編譯程式要在哪兒儲存這個字串呢?macintosh、dos和windows上的編譯程式通常在全域性資料區儲存字串,但在macintosh上,通常把最大的全域性資料區限制為32k,在dos和windows上限制為64k。

因此對於象microsoft word和excel這樣的大程式,斷言字串立刻會佔掉這塊記憶體。

關於這個問題存在一些解決的辦法,但最容易的辦法是在錯誤資訊中省去測試表示式字串。畢竟只要檢視了string.c的第153行,就會知道出了什麼問題以及相應的測試條件是什麼。

《C語言程式設計》第2章作業

布置日期 2012 2 17截止日期 2012 2 23 一 單選題 每小題5分,共100分 1 1.關於c程式的構成描述是不正確的。a 乙個源程式至少且僅包含乙個main函式,也可包含乙個main函式和若干個其他函式。b 函式由函式首部和函式體兩部分組成,二者缺一不可。c 函式首部通常是函式的第1...

第2章程序設計基礎

考點精講 考點 1 程式設計的方法與風格 養成良好的程式設計風格,主要考慮下述因素 1 源程式文件化 1 符號名的命名 符號名的命名應具有一定的實際含義,以便於對程式功能的理解。2 程式注釋 在源程式中新增正確的注釋可幫助人們理解程式。程式注釋可分為序言 性注釋和功能性注釋。語句結構清晰第 一 效率...

c 譚浩強教學第2章修訂 周四學時

第二章類和物件 2.1物件導向程式設計方法概述 2 1 1 物件導向程式設計的基本概念 一 物件 二 類三 訊息 四 方法 2 1 2 物件導向程式設計的基本特徵 一 抽象 二 封裝 三 繼承 四 多型 2 1 3 物件導向的程式設計的特點 p40 2 1 4 物件導向的軟體開發過程 p42 43 ...