天天看點

編寫優質無錯C程式秘訣!《經驗談》

編寫優質無錯C程式秘訣!《經驗談》

這裡我将陸續給大家載出我在以前學習和編寫c代碼時遇到的問題和解決方法、學習的心得,有些是經過查詢一些曾經參加微軟microsoft的開發小組的老程式員的書籍和資料後提供給大家!

首先,當發現錯誤時,要不斷就以下兩個問題追問自己的結果:

1、怎樣才能自動地查出這個錯誤?

2、怎樣才能避免這個錯誤?

關于錯誤:

錯誤可以分為兩類:

1、開發某一功能時産生的錯誤。

2、程式員認為該功能已經開發完成之後仍然遺留在代碼中的錯誤。

第一種錯誤好解決,可以把編譯器可以設定的警告等級開關打開,以及文法檢查來排除;邏輯錯誤也可以使用跟蹤手段來排除。跟蹤邏輯錯誤就相對麻煩一些,要消除這些麻煩就要養成一個好的程式設計習慣和方法。

第二種錯誤時非常隐蔽的,需要長期的實踐和經驗在其中,還要對c語言具有深刻的了解才能夠提高上來,這裡就是要告訴大家一些這樣的事情,通過代碼解說來闡明具體事實。

以下的文章裡,實際上有許多是微軟 microsoft 的老程式員開發 word 和 excel 的經驗之談,這也是我當初學習他們的經驗時的體會和材料的總結和整理。

總之,這些對于在c道路上前進的人們是非常重要的,不敢獨占,先拿出來以供大家享受

(第一個問題)

考慮自己所用的語言和程式設計環境?使空語句明顯化!

充分利用語言的特性和程式設計環境,把所有環境下的調試報錯等級開關都打開,注意使用語言的保留字,例如下面的兩段程式對比:

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

{

    byte *pbto = (byte *)pvto;

    byte *pbfrom = (byte *)pvfrom;

    while(size-- > 0);

        *pbto++ = *pbfrom++;

    return (pvto);

}

從以上縮進格式可以看出,while後的分号肯定是一個錯誤。但編譯器認為這是一個合法的語句,允許循環體為空語句。報警開關都打開時,大多編譯器都能夠報出這一錯誤。但需要用空語句時,最好實用null(大寫)明确出來:

char *strcpy(char *pchto, char *pchfrom)

{

    char *pchstart = pchto;

    while(*pchto++ = *pchfrom++)

        null;

    return (pchstart);

}

這樣,編譯器編譯程式接受顯式的null語句,把隐式空語句自動地當做錯誤标出。

(第二個問題)

無意的指派。

例如:

if(ch = '/t')

    expandtab();

有些編譯器允許在程式&&和||表達式以及if、for和while中直接使用指派的地方禁止簡單指派,如果以上五種情況将==偶然地鍵入為=号,就會報錯。

while(*pchto++ = *pchfrom++)

    null;

編譯程式就會産生警告資訊,為了防止這種情況出現,可以這樣做:

while((*pchto++ = *pchfrom++) != '/0')

    null;

這樣做的結果由兩個好處:

1、現在的編譯器不會為這種備援的比較産生額外的代碼和開銷,可以将其優化掉。

2、可以少冒風險,盡管以上兩種都合法,但這是更安全的用法。

(第三個問題)

參數錯誤:

例如:

fprintf(stderr, "unable to open file %s./n",filename);

......

fputc(stderr,'/n');

這個程式看上去好像沒有問題,實際上fputc的參數順序錯了。幸好ansi c提供了函數原型,在編譯時自動查出這些錯誤。

ansi c标準要求每個庫函數都必須有原型,stdio.h中可以查到:

int fputc(int c, file *stream);

如果在程式檔案頭裡給出了原型,這類錯誤就可以檢查出。

ansi c雖然要求标準庫函數必須有原型,但并不要求使用者編寫的函數也必須有原型。可以有,也可以沒有。有些程式員經常抱怨對函數的原型進行維護,如果沒有原型,就不得不依靠傳統的測試方法來查出程式中的調用錯誤,大家可以扪心自問:究竟哪個更重要?

利用原型可以生成品質更好的代碼。ansi c标準使得編譯程式可以根據原型資訊進行相應的優化。

有這樣的名言:

投資者與賭徒之間的差別在于投資者利用每一次機會,無論它是多麼小,去争取利益;而賭徒則隻靠運氣。我們應該将這一概念同樣應用于程式設計活動。

把所有的警告開關都打開,除非有極好的理由才不這樣做!

(原則一)

如果有單元測試,就進行單元測試。

你認識那個程式員甯願花費時間去跟蹤排錯,而不是編寫新的代碼?肯定有這樣的程式員,但我至今還沒有見到一個。

當你寫程式時,要在心中時刻牢記着假想編譯程式這一概念,這樣就可以毫不費力或者直費很少力氣利用每個機會抓住錯誤。

如果想要快速容易地發現錯誤,就要利用工具的相應特性對錯誤進行定位。錯誤定位的越早,就能夠越早地投身于更有趣的工作。

努力減少程式員查錯的技巧。可以選擇編譯程式的環境來實作。進階的編碼方法雖然可以查出或減少錯誤,但它們也要求程式要有較多的技巧,因為程式員必須學習這些進階的編碼方法。

(原則二)

自己設計并使用斷言。

    利用編譯器自動查錯固然好,但實際上隻是很少一部分。如果排除掉了程式中的所有錯誤,大部分時間程式會正确工作。

看一下下列代碼:

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

    該語句在多數情況下會工作的很好,除非malloc的調用産生失敗。一旦産生,就會給memcpy傳回一個null指針,而memcpy處理不了null指針,這樣的錯誤産生,如果在傳遞使用者之前将導緻程式的癱瘓。但如果傳遞了使用者,那使用者就一定“走運”了。

解決方法:

對null指針進行檢查,如果為null,就給出一條錯誤資訊,并終止memcpy執行。ee

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

{

    void *pbto = (byte *)pvto;

    void *pbfrom = (byte *)pvfrom;

    if(pvto == null || pvfrom == null)

    {

        fprintf(stderr, "bad args in memcpy!/n");

        abort();

    } 

    while(size-- > 0)

        *pbto++ = *pbfrom++;

    return(pvto);

}

    隻要調用時錯用了null指針,這個函數就會查出來。但測試的代碼增加了一倍,降低了執行速度,這樣“越治病越糟”,還有沒有更好的方法?

    有,利用c的預處理程式!

    這樣就會儲存兩個版本。一個整潔快速,用于傳遞使用者;另一個臃腫緩慢(包含了額外的檢查),用于調試。這樣就要同時維護同一個程式的兩個版本,利用c的預處理程式有條件地包含相應的部分。

例如:隻有定義了debug時,才對應null指針測試。

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

{

    void *pbto = (byte *)pvto;

    void *pbfrom = (byte *)pvfrom;

    #ifdef debug

        if(pvto == null || pvfrom == null)

        {

            fprintf(stderr, "bad args in memcpy!/n");

            abort();

        } 

    #endif

    while(size-- > 0)

        *pbto++ = *pbfrom++;

    return(pvto);

}

這樣,調試編譯時開放debug,進行測試程式和找錯;傳遞使用者時,關閉debug後進行編譯,封裝之後交給經銷商。

    這種方法的關鍵是保證調試代碼不在最終産品中出現。

那麼還有沒有比以上兩種更好的方法,有!下次再講。

(準則二續)

利用斷言進行補救。

    實際上,memcpy中的調試代碼編的非常蹩腳,喧賓奪主。他能産生好的效果,這無疑,但許多程式員不會讓他這樣存在的,聰明的程式員會讓調試代碼隐藏在斷言assert中。

    assert是個宏,定義在頭檔案assert.h中,每個編譯器都自帶。前面的程式完全可以使用assert來處理,看一下下面代碼,把7行減為了1行代碼。

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

{

    void *pbto = (byte *)pvto;

    void *pbfrom = (byte *)pvfrom;

    assert(pvto != null  &&  pvfrom != null);

    while(size-- > 0)

        *pbto++ = *pbfrom++;

    return(pvto);

}

這裡要強調的是:assert是個隻有定義了debug才起作用的宏,如果其參數的計算結果為假,就中止調用程式的執行。

    當然程式編制也可以編制自己的斷言宏,但要注意不要和assert沖突,因為assert是全局的。舉個例子:

先定義宏assert:

#ifdef debug

    void _assert(char *,  unsigned);  

    #define assert(f)

    if(f)

        null;

    esle

        _assert(_file_ ,  _line_ );

#else

    #define  assert(f)    null

#endif

    從上述我們可以看到,如果定義了debug,assert将擴充為一個if語句。

    當assert失敗時,他就是用預處理程式根據 _file_ 和 _line_ 所提供的檔案名和行号參數調用 _assert。 _assert在标準錯誤輸出裝置stderr上列印一條錯誤資訊,然後中止:

void  _assert(char *strfile,  unsigned uline)

{

    fflush(stdout);

    fprintf(stderr, "/nassertion failed: %s, line %u/n", strfile, uline);

    fflush(stderr);

    abort();

}

程式中的相關函數,大家可以查閱頭檔案幫助來了解,這裡就不在詳述了。

下一講:使用斷言對函數參數确認。

(準則二  續二)

使用斷言對函數參數确認。

掌握原則為:“無定義”就意味着“要避開”。

    讀一下ansi c的memcpy函數的定義,最後一行這樣說:“如果在存儲空間互相重疊的對象之間進行了拷貝,其結果無意義。”那麼當使用互相重疊的記憶體塊調用該函數時,實際上在做一個編譯程式(包括同一編譯程式的不同版本),結果可能也不同的荒唐的假定。

    對于程式員來說,無定義的特性就相當于非法的特性,是以要利用斷言對其進行檢查。

通過增加一個驗證兩個記憶體塊決不重疊的斷言,可以把memcpy加強:

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

{

    void *pbto = (byte *)pvto;

    void *pbfrom = (byte *)pvfrom;

    assert(pvto != null  &&  pvfrom != null);

    assert(pbto >= pbfrom+size  ||  pbfrom >= pbto+size);

    while(size-- > 0)

        *pbto++ = *pbfrom++;

    return(pvto);

}

    從今以後,在程式設計時,要經常停下來看看程式中有沒有使用了無定義的特性。如果使用了,就要把它從相應的設計中去掉,或者在程式中包含相應的斷言,以便在使用了無定義的特性時,能夠向程式員發出通報。

(須注意問題一)

    前面所述的做法,為其他的程式員提供代碼庫(或作業系統——例如各個廠家的編譯器)時顯得特别重要。如果為他人提供過類似的庫(或者自己使用自己以前編過的庫時,或者使用了不同廠家提供的庫時),就應該知道當程式員試圖得到所需要的結果時,就會利用各種各樣的無定義特性。更大的挑戰在于改進後新庫的發行,因為盡管新庫與老庫完全相容,但總有半數的應用程式在試圖使用新庫時會産生癱瘓現象。問題在于新庫在其“無定義的特性”方面,與老庫并不100%相容。

    明白了這些,在程式設計時,就要考慮程式的移植性、相容性、容錯性、安全性、可發行性、商品性等等方面。而不是說在一個程式設計環境下能夠實作功能就萬事大吉了。程式員的道路不知是停留在程式設計的文法學習、技巧、實作功能上。要全面、全方位考慮所編制的程式的有可能造成的後果。

    各個廠家的編譯器都有所不同,一個廠家的編譯器版本不同時,特性也不同。要想很好的程式設計,這些都是需要了解的,去盡量了解這些特性,才能真正學到程式設計。才能提高程式設計效率。有時會出現這樣的情況。看人家的代碼,感覺到非常傻,自以為很聰明,實際上是自己錯誤,因為人家考慮的更加廣泛,考慮的更多,實際上那樣的代碼特别具有可移植性和容錯性。隻是自己的思想受到了局限,隻從一個角度來看問題造成的。勸告大家:千萬不要夜郎自大!

大呼“危險”的代碼:

    我們再談談memcpy中的重疊檢查斷言。對于上面的重疊檢查斷言:

    assert(pbto >= pbfrom + size || pbfrom >= pbto + size);

    假如在調用memcpy時這個斷言測試的條件為真,那麼在發現這個斷言失敗了之後,如果你以前從來沒有見過重疊檢查,不知道它是怎麼回事,你能想到發生的是什麼差錯嗎?但這并不是說上面的斷言技巧性太強、清晰度不夠,因為不管從哪個角度看這個斷言都很直覺。然而,隻管并不等于明顯。

    很少比跟蹤到了一個程式中用到的斷言,但卻不知道該斷言的作用這件事更令人沮喪的了。你浪費了大量的時間,不是為了排除錯誤,而隻是為了弄清楚這個錯誤到底是什麼。這還不是事情的全部,更有甚者,有的程式員偶爾還會設計出有錯的斷言。是以如果搞不清楚相應的斷言檢查的是什麼,就很難知道錯誤是出現在程式中,還是出現在斷言中。解決這個問題的辦法,是給不夠清晰的斷言加上注解即可。這是顯而易見的事情,但令人驚奇的是很少有程式員這樣做。為了使使用者避免錯誤的危險,程式員經曆了各種磨難,但卻沒有說明危險到底誰什麼。程式員不了解的斷言也會被忽視。在這種情況下,程式員會認為相應的斷 言是錯誤的,并把它們從程式中去掉。是以,為了使程式員能夠了解斷言的意圖,要給不夠清楚的斷言加上注解。

    如果在斷言中的注解中還注明了相應錯誤的其他可能解法,效果更好。例如在程式員使用互相重疊的記憶體塊調用memcpy時,就是這樣做的一個好機會。程式員可以利用注解指出此時應該使用memmove,它不但能夠正好完成你想做的事情,而且沒有不能重疊的限制:

    assert(pbto >= pbfrom + size || pbfrom >= pbto + size);

    在寫斷言注解時,不要長篇大論。一般的方法是使用經過認真考慮過的簡短問句,它可以比用一整段的文字系統地解釋出每個細節的指導性更強。但要注意,不要在注解中建議解決問題的辦法,除非你能夠确信它對其他程式員确有幫助。做注解的人當然不想讓注解把别人引入歧途。

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

《斷言不是用來檢查錯誤的》

當程式員使用斷言時,有時會錯誤地利用斷言去檢查真正的錯誤,而不去檢查非法的情況。看一下下面函數strdup中的兩個斷言:

char *strdup( char *str)

{

    char *strnew;

    assert( str != null );

    strnew = ( char *)malloc( strlen(str) + 1 );

    assert( strnew != null);

    strcpy( strnew,  str);

    return(strnew);

}

第一個斷言的用法是正确的,它被用來檢查該程式正常工作時,絕對不應該發生的非法情況.

第二個斷言的用法相當不同,它測試的是錯誤情況,是在其最終産品中肯定會出現,并且必須對其進行處理的錯誤情況.

也就是說:斷言是用來檢查非法情況的,而不是測試和處理錯誤的。

《你又做假定了嗎?》

    有時在程式設計式時,有必要對程式的運作環境做出某些假定。但這并不是說在程式設計式時,總要對運作環境作出假定。例如:下面的函數memset就沒有對其運作環境做出任何假定。是以它雖然未必效率很高,但卻能夠運作在任何的ansi c 編譯程式之下。也就是說,編譯程式時要考慮移植,有許多是和編譯器有關的——是獨立于ansi c 之外的特定編譯器才能夠運作正确的程式,這就是自己所做得假定,這種假定程式設計者心裡一定要清楚才行:

void  *memset(  void  *pv, byte b, size_t  size)

{

    byte  *pb = (byte *)pv;

    while(size-- > 0)

        *pb++ = b;

    return( pv );

}

    但在許多計算機上,先通過将要填充到記憶體塊中的小值拼成較大的資料類型,然後用較大的大值填充記憶體,由于實際填充的次數減少了,可使編出的memset函數速度更快,在68000上,下面的memset函數的填充速度比上面的要快四倍。

long *longfill(long *pl, long l, size_t size);  

void *memset(void *pv, byte b, size_t size)

{

    byte  *pb = (byte *)pv;

    if(size >= sizethreshold)

    {

       unsigned long l;

       l = ( b<< 8) | b;

       l = ( l << 16 ) | l;

       pb = (byte *)longfill( (long *)pb, l, size/4 );

       size=size%4;

   }

   while(size-- > 0)

        *pb++ = b;

    return( pv );

}

    在上面的程式中,可能除了對sizethreshold所進行的測試之外,其他的内容都很直覺。如果還不大明白為什麼要進行這一測試,那麼可以想一想無論是将4個位元組拼成一個long,還是調用函數都化一定的時間。對sizethreshold進行測試是為了使memset隻有在用long進行填充,使速度更快時才進行相應的填充。否則,就仍使用byte進行填充。

    這個memeset新版本的唯一問題是他對編譯程式和作業系統都作了一些假定。這段代碼很明顯的假定long占用4個記憶體位元組,該位元組的寬度是8位。這些假定對許多計算機都正确。不過這并不意味着是以就應該對這一問題置之不理,因為現在正确并不等于今後也正确。

    有的程式員“改進”這一程式的方法,是把它寫成如下可移植性更好的形式:

void *memset(void *pv, byte b, size_t size)

{

    byte  *pb = (byte *)pv;

    if(size >= sizethreshold)

    {

       unsigned long l;

       size_t sizesize;

       l = 0;

       for( sizesize = sizeof(long); sizesize-- > 0; null)

           l = (l << char_bit) | b;

       pb = (byte *)longfill( (long *)pb, l, size/sizeof(long) );

       size=size%sizeof(long);

   } 

   while(size-- > 0)

        *pb++ = b;

    return( pv );

}

    由于在程式中大量的使用了運算符sizeof,這個程式看起來移植性更好,但“看起來”不等于“就是”。如果要把它移植到新的環境,還是要對其進行考察才行。如果在macintosh plus或者其他基于68000的計算機上運作這個程式,假如pv開始指向的是奇數位址,該程式就會癱瘓。這是因為在68000上,byte * 和 long * 是不可以互相轉換的類型,是以如果在奇數位址上存儲long 就會引起硬體錯誤。

    那麼到底應該怎麼做呢?

    其實在這種情況下,就不應該企圖将memset寫成一個可移植的函數。要接受不可移植這一事實,不要對其改動。對于68000,要避免上述的奇數位址問題,可以先用byte進行填充,填到偶數位址之後,再換用long繼續填充。雖然将long對齊在偶數上已經可以工作了,但在各種基于68020、68030、68040等新型的machintosh上,如果使其對齊在4位元組的邊界上,性能會更好。至于對程式中所作的其他假定,可以利用斷言和條件編譯進行相應的驗證:

void *memset(void *pv, byte b, size_t size)

{

    byte  *pb = (byte *)pv;

    #ifdef mc680x0

    if(size >= sizethreshold)

    {

       unsigned long l;

       assert( sizeof(long) == 4 && char_bit == 8);

       assert( sizethreshold >= 3);

       while((( unsigned long) pb & 3) != 0)

       {

           *pb++ = b;

           size--;

       }

       l = ( b<< 8) | b;

       l = ( l << 16 ) | l;

       pb = (byte *)longfill( (long *)pb, l, size/sizeof(long) );

       size=size%sizeof(long);

   } 

   #endif  

   while(size-- > 0)

        *pb++ = b;

    return( pv );

}

    正如所見,程式中與具體及其相關的部分已經被mc680x0預處理程式定義設施括起。這樣不僅可以避免這部分不可移植的代碼被不小心地用到其他不同的機器上,而且通過在程式中搜尋m680x0這個字元串,可以找出所有與目标機器有關的代碼。

    為了驗證long占用個記憶體位元組、byte的寬度是8,還在程式中加了一個相當直覺的斷言。雖然暫時不太可能發生改變,但誰知道以後會不會發生改變呢?

    為了在調用longfill之前使pv指向4位元組的邊界上,程式中使用了一個循環。由于不管size的值如何,這個循環最終都會執行到size等于3的倍數,是以在循環之前還加了一個檢查sizethreshold是否至少是3的斷言(sizethreshold應該取較大的值。但他至少應該是3,否則程式就不會工作)。

    經過這些改動,很明顯這個程式已不再可移植,原先所作的假定或者已經被消除,或者通過斷言進行了驗證。這些措施使得程式極少可能被不正确地使用。

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

《光承認編譯程式還不夠》

    在微軟的曆史上,曾經有過這麼一件事情。他們的一些小組漸漸發現他們不得不對其代碼進行重新的考察和整理,因為相當多的代碼充滿了“+2”,而不是“+sizeof(int)”、與上了0xffff,而不是unit_max進行無符号數的比較、在資料結構中使用的是int,而不是真正想用的16位資料類型這一類問題。

    也許有人認為這是因為這些程式員太懶惰,但他們不會同意這一看法。事實上,他們認為有很好的理由說明他們可以安全地使用“+2”這種形式,及相應的c編譯程式是由microsoft自己編寫的。這一點給程式員造成了安全的家假象,正如幾年前一位程式員所說:“編譯程式組從來沒有做使我們所有程式垮掉的改變”。

    但這位程式員錯了。

    為了在intel 80386和更新的處理器上生成更快更小的程式,編譯程式組改變了int的大小(以及其他一些方面)。雖然編譯程式組并不想使公司内部的代碼垮掉,但是保持在市場上的競争地位顯然更重要。畢竟,這是那些自己做了錯誤假定的microsoft程式員的過錯。

    是以,大家應該考慮,自己的程式是否可以在以後發展了的64位機器上運作呢?是否可以在目前的其他系統上運作呢?是否具有移植性呢?是否具有代碼可重用性呢?如果沒有,那麼你現在所編的程式的價值就非常的小,隻是在學習而已。

《不可能的事情也能發生?》

    函數的形參并不一定總是給出所有輸入資料,有時它給出的隻是一個指向函數輸入資料的指針。例如:下面這個簡單的壓縮還原程式:

byte *pbexpand(byte *pbfrom, byte *pbto, size_t sizefrom)

{

    byte b, *pbend;

    size_t size;

    pbend = pbfrom + sizefrom;  

    while( pbfrom < pbend)

    {

        b = *pbfrom++;

        if( b == brepeatcode)

        {

            b = *pbfrom++;

            size = (size_t) *pbfrom++;

            while( size-- > 0)

                *pbto++ = b;

        }

        else

             *pcto++ = b;

    }

    return(pbto);

}

    本程式将一個資料緩沖區中的内容拷貝到另一個資料緩沖區中。但在拷貝過程中,它要找出所有的壓縮字元序列。如果在輸入資料中找到了特殊的位元組brepeatcode,它就認為其後的下兩個位元組分别是要重複的還原字元以及字元的重複次數。盡管這一過程顯得有點過于簡單,但我們還是可以把它們用在某些類似于程式編輯的場合下。那裡,正文中常常包括有許多表示縮進的連續水準制表符和空格符。

    為了使pbexpand更健壯,可以在該程式的入口點加上一個斷言,來對pbfrom、sizefrom和pbto的有效性進行檢查。實際上,還有許多其他可以做的事情。例如:可以對緩沖區中的資料進行确認。

    由于進行一次譯碼總需要三個位元組,是以相應的壓縮程式從不對兩個連續的字元進行壓縮。另外,雖然也可以對三個連續的字元進行壓縮,但這樣做并不能得到什麼便宜。是以,壓縮程式隻對三個以上的連續字元進行壓縮。

    存在一個例外的情況。如果原始資料中有brepeatcode,就必須對其進行特殊的處理。否則當使用pbexpand時,就會把它誤認為是一個壓縮字元序列的開始。當壓縮程式在原始資料中發現了brepeatcode時,就把它再重複一次,以便和真正的壓縮字元序列差別。

    總之,對于每個字元壓縮序列,其重複次數至少是4,或者是1。在後一種情況下,相應的重複字元一定是brepeatcode本身。我們可以使用斷言對這一點進行驗證:

byte *pbexpand(byte *pbfrom, byte *pbto, size_t sizefrom)

{

    byte b, *pbend;

    size_t size;

    assert(pbfrom != null  &&  pbto != null  &&  sizefrom != 0);

    pbend = pbfrom + sizefrom;  

    while( pbfrom < pbend)

    {

        b = *pbfrom++;

        if( b == brepeatcode)

        {

            b = *pbfrom++;

            size = (size_t) *pbfrom++;

            assert(size >= 4  ||  (size == 1  &&  b == brepeatcode));

            while( size-- > 0)

                *pbto++ = b;

        }

        else

             *pcto++ = b;

    }

    return(pbto);

}

    如果這一斷言失敗,說明pbfrom指向的内容不對或者字元壓縮程式中有錯誤。無論那哪種情況都是錯誤,而且是不用斷言就很難發現的錯誤

《利用斷言來檢查不可能發生的情況》

《安靜的處理》

      程式員,尤其是有經驗的程式員編的程式通常都是這樣:當某些意料不到的事情發生時,程式隻進行無聲無息的安靜處理,甚至有些程式員會有意識的使程式這樣做。也許你自己用的是另一種方法。

      當然,我們現在談的是所謂的防錯性程式設計。

      前面,我們介紹了pbexpand程式。該函數使用的就是防錯程式設計。但從其循環條件可以看出,下面的修改版本并沒有使用防錯性程式設計。

byte *pbexpand(byte *pbfrom, byte *pbto, size_t sizefrom)

{

    byte b, *pbend;

    size_t size;

    pbend = pbfrom + sizefrom;

    while(pbfrom != pbend)

    {

        b = *pbfrom++;

        if(b == brepeatcode)

        {

            b = *pbfrom++;

            size = (size_t) *pbfrom++;

            do

                *pbto ++= b;

            while(size -- != 0)

        }

        elae

            *pbto ++ = b;

    }

    return(pbto);

}

      雖然這一程式更精确地反應了相應的算法,但有經驗的程式員很少會這樣編碼。否則好機會就來了,我們可以把他們塞進一輛既沒有安全帶又沒有車門的雙人cessna車種。上面的程式使人感到太危險了。

      有經驗的程式員會這樣想:“我知道在外循環中pbfrom絕不應該大于pbend,但如果确實出現了這種情況怎樣辦呢?還是在這種不可能的情況出現時,讓外循環退出為好。”

同樣對于内循環,即使size總應該大于或等于1,但使用while循環代替do循環,可以保證進入内循環時一旦size為0,不至于使整個程式癱瘓。

      使自己免受這些“不可能”的打擾似乎很合理,甚至很聰明。但如果出于某種原因pbfrom被加過了pbend,那麼會發生什麼事情呢?在上面這個充滿危險的版本或者前面看到的防錯性版本中,找出這一錯誤的可能性又有多大呢?當發生這一錯誤時,上面的危險版本也許會引起整個系統的癱瘓,因為pbexpand會企圖對記憶體中的所有内容進行壓縮還原。在這種情況下,使用者肯定會發現這一錯誤。相反,對于前面的防錯性版本來說,由于在pbexpand還沒有來得及造成過多的損害(如果有的話)之前,它就會退出。是以雖然使用者仍然可能發現這一錯誤,但這種可能性不大。

      實際的情況就是這樣,防錯性程式設計雖然常常被譽為有較好的編碼風格,但它卻隐瞞了錯誤。要記住,我們正在談論的錯誤決不應該再發生,而對這些錯誤所進行的安全處理又使編寫無錯代碼變得更加困難。當程式中有了一個類似于pbfrom這樣的跳躍性指針,并且其值在每次循環都增加不同的量時,編寫無錯誤代碼尤其困難。

       這是否意味着我們應該放棄防錯性程式設計呢?

       答案是否定的。盡管防錯性程式設計會隐瞞錯誤,但它确實有價值。一個程式所能導緻的最壞結果是執行癱瘓,并使使用者可能花幾個小時建立的資料全部丢掉。在非理想的世界中,程式确實會癱瘓,是以為了防止使用者資料丢失而采取的任何措施都是值得的。防錯性程式設計要實作的就是這個目标。如果沒有它,程式就會如同一個用紙牌搭起的房子,哪怕硬體和作業系統中發生了最輕微的變化,都會塌落。同時,我們還希望在進行防錯性程式設計時,錯誤不要被隐瞞。

       假定某個函數以無效的參數調用了pbexpand,比如sizefrom 比較小并且資料緩沖區最後一個位元組的内容碰巧是brepeatcode。由于這種情況類似于一個壓縮字元序列,是以pbexpand将從資料緩沖區外多讀2個位元組,進而使pbfrom超過pbend。結果呢?pbexpand的危險版本可能會癱瘓,但其防錯性版本或許可以避免使用者資料的丢失,盡管它也可能沖掉255個位元組的未知資料。既然兩者都想得到,既需要調試版本對錯誤進行報警,又需要傳遞版本對錯誤進行安全的恢複,那麼可以一方面一如既往地利用防錯性程式設計編碼,另一方面在事情變糟的情況下利用斷言進行報警。

byte *pbexpand(byte *pbfrom, byte *pbto, size_t sizefrom)

{

    byte b, *pbend;

    size_t size;

    pbend = pbfrom + sizefrom;

    while(pbfrom < pbend)

    {

        b = *pbfrom++;

        ……

    }

    assert(pbfrom == pbend);

    return(pbto);

}

      上面的斷言隻是用來驗證該函數的正常終止。在該函數的傳遞版本中,相應的防錯措施可以保證當出了毛病時,使用者可以不受損失;而在該函數的調試版本中,錯誤仍然可以被報告出來。

      但是在實際的程式設計中,也不必過分拘泥于此。例如,如果每次循環pbfrom的内容總是增1,那麼要使pbfrom超過pbend進而引起問題,恐怕需要一束宇宙射線的偶然轟擊才行。在這種情況下,相應的斷言沒有什麼用處,是以可以從程式中删除。在程式中究竟是否需要使用斷言,要根據常識視具體情況而定。最後應該說明的是,循環隻是程式員通常用來進行防錯性程式設計的一個方面。實際上,無論把這種程式設計風格用在哪裡,在編碼之前都要問自己:“在進行防錯性程式設計時,程式中隐瞞錯誤了嗎?”如果答案是肯定的,就要在程式中加上相應的斷言,以對這些錯誤進行報警

待續。。。