荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: jek (Super Like Cat), 信区: Program
标  题: 完美程式設計指南--附錄C 解答
发信站: 荔园晨风BBS站 (Thu Apr  4 07:05:01 2002), 转信


本附錄中包含所有本書中所有"該想一想的事情"小節中的問題的解答,不過那些開
放思考性的"學習計劃"並不包含在這附錄中。


------------------------------------------------------------------------
--------
第一章
1. 由於編譯器把那個運算順序的錯誤解釋成底下這樣,而能找出問題來:

while (ch  =  (getchar() != EOF))
換句話說,編譯器發現一個運算式的結果被指派給ch時,認為你把一個==打成了=,
 就發出一個可能指派錯誤的警告。

2a.一個簡單找出不小心打出來的"八進位數字"錯誤的辦法是打開讓編譯器在碰到
八進位數字常數十發出錯誤的編譯器選項。變通繞過這錯誤的辦法是使用十進位或
十六進位的數字來替換八進位數字的值。

2b.要找出程式員把&&打成&(或是把||打成|)的錯誤,編譯器只要打開找出==錯
打成=時用的那個編譯器選項就好了。編譯器會在發現你在任何if敘述中或是任何
複合條件式中使用&(或|)而沒將結果與0明確進行比較時發出錯誤訊息,所以底
下這樣應該會產生錯誤訊息:

if (u & 1)            /* u是奇數? */
而這樣子不會產生錯誤訊息:

if ((u & 1) != 0)     /* u是奇數? */
2c.一個簡單找出不必要的注釋的方法,就是讓編譯器在注釋的第一個字元是個字
母或是括號時發出警告訊息。這樣的測試會逮到底下兩種可疑的情形:

quot=numer/*pdenom;

quot=numer/*(pointer expression);
要避開這種警告,你可以用空格或括號隔開/跟*:

quot = numer / *pdenom;

quot=numer/(*pdenom);

/*But note:這個注釋會產生警告訊息。 */
/* 這樣子不會發出警告訊息,因為開頭隔了一個空格。 */
/*----------這樣子也不會產生警告訊息。---------*/
2d.你可以讓編譯器找出沒括號起來的運算式中可能出問題的運算子對來找出運算
順序可能出錯的地方。例如,程式員們有時會在混用<<跟+運算子時出現運算順序
錯誤的問題,而編譯器就會對如下這般的程式碼發出警告:

word = bHigh << 8 + bLow;
編譯器不會對底下的敘述發出警告,因為它們用了括號:

word = (bHigh << 8) + bLow;

word = bHigh << (8 + bLow);
一種降低混淆的做法是採用如"如果兩個運算子有不同的運算順序而它們沒被括號
圍起來,就發出警告"的測試法則。這樣子判斷其實過於簡化,不過還是能有效的
讓你得到警示。開發一種好的測試法則需要在編譯器中執行大量程式來檢查法則中
的各項條件,直到編譯器產生出有用的結果。你當然不想在下面這樣常見的運算式
中碰到編譯器發出的警告訊息:

word = bHigha256 + bLow;

if (ch == ' '  ??  ch == '\t'  ??  ch == '\n')
3.編譯器會在碰到兩個連續的if敘述接著一個else時發出可能出現孤懸else敘述的
警告訊息:

if (expression1)             / if (expression1)
    if (expression2)               if (expression2)
        .                              .
        .                              .
        .                              .

else                               else
    .                                  .
    .                                  .
    .                                  .
要避開這種警告,你可以在裡頭那個if敘述周圍加上中括號,讓else的歸屬問題明
朗化:

if (expression1)              if (expression1)
{                             {
    if (expression2)              if (expression2)
        .                             .
        .                             .
        .                             .
}                                 else
else                                 .
  .
  .
  .                           }
4. 將常數跟運算式放在比較式左邊有幫助,是因為它可以提供另一種自動偵測錯
誤的方法,不過這方法不幸的只有在有運算元是常數或運算式時才有用-如果兩邊
的運算元都是變數時,這方法就毫無用處了。這方法的另一個問題是程式員們必須
記得用這方法來寫程式。

如果,另一方面,你打開編譯器選項,編譯器就會對每個可能有指派問題的地方發
出警告。更棒的,這個選項對那些只上過程式設計課程而不知道要用調換比較式中
運算元位置的方法的程式員們一樣管用。

如果你有這種編譯器選項,就把它打開來;如果你沒有,在你獲得一個更有幫助的
編譯器之前,就把比較式中的常數跟運算式放到式子的左邊去吧。

5. 要避免未定義的前置處理器巨集產生意料之外的結果,編譯器(其實真正作這
件事的是前置處理器)應該有個讓程式員把未定義巨集的使用當成錯誤狀態的選項
。現在的ANSI C編譯器都支援原來的#ifdef前置處理指示跟在#if運算式中使用新
的defined運算子的做法,就不太有必要再將未定義的巨集當成0處理了。替代底下
這種在#if運算式中使用未定義巨集的做法,

/* 判斷目的平台條件 */

#if    INTEL8080
    .
    .
    .
#elif  INTEL80x86
    .
    .
    .
#elif  MC680x0
    .
    .
    .
#endif
如果你打開禁止使用未定義巨集的編譯器選項,上頭的寫法就會出現錯誤,你應該
改用defined運算子來寫:

/* 判斷目的平台條件 */

#if    defined(INTEL8080)
    .
    .
    .
#elif  defined(INTEL80x86)
    .
    .
    .
#elif  defined(MC680x0)
    .
    .
    .
#endif
這編譯器選項在碰到在#ifdef敘述中使用未定義巨集時,不應該發出警告訊息,因
為這種巨集用法應該是故意寫出來的。


------------------------------------------------------------------------
--------

------------------------------------------------------------------------
--------
第二章
一個ASSERTMSG巨集的可能寫法可以讓這巨集同時核對運算式跟顯示檢查錯誤訊息
的字串。例如要印出memcpy訊息時,你可以這樣子呼叫ASSERTMSG:

ASSERTMSG(pbTo >= pbFrom+size  ||  pbFrom >=
          pbTo+size,"memcpy: the blocks overlap");
在後頭列出來的ASSERTMSG巨集的實作中,你應該把巨集寫在一個表頭檔中,而把
_AsserMsg常式寫在另一個方便取用的原始碼檔案中。



#ifdef DEBUG

    void _AssertMsg(char *strMessage); /* 函式雛形宣告
                                         */
        #define ASSERTMSG(f,str)      \
            if (f)                \
                {}                \
            else                  \
                _AssertMsg(str)

#else

    #define ASSERTMSG(f,str)

#endif
底下是另一個檔案中的常式實作:



#ifdef DEBUG

    void _AssertMsg(char *strMessage)
    {
        fflush(NULL);
        fprintf(stderr, "\n\nAssertion failure in
                %s\n",strMessage);
        fflush(stderr);
        abort();
    }

#endif
簡單的做法是-如果編譯器支援的話-把編譯器選項打開,讓編譯器將所有相同的
字串都存放在相同位置。如此一來,即使除錯檢查宣告了73份相同檔名的字串,編
譯器也只會配置一個字串的空間出來。這樣子作的缺點是,它會將原始碼檔案中所
有相同的字串疊放在一起,不光是除錯檢查使用的那些字串會被存放在一塊而已,
你可能不想碰到這種情形。
一個變通的辦法是改變ASSERT巨集的寫法,讓它故意在檔案中參考相同的檔名字串
。唯一的困難在於建立檔名字串,不過那並不是一件很難的事情-你可以將細節放
在每個原始碼檔案開頭都使用一遍的新ASSERTFILE巨集中:



#include <stdio.h>
.
.
.
#include <debug.h>

ASSERTFILE(__FILE__)
.
.
.

void *memcpy(void *pvTo, void *pvFrom, size_t size)
{
    byte *pbTo   = (byte *)pvTo;
    byte *pbFrom = (byte *)pvFrom;

    ASSERT(pvTo != NULL  &&  pvFrom != NULL);
    .
    .
    .
看得出來ASSERT的用法還是沒變吧?底下是實作ASSERTFILE巨集跟ASSERT巨集新版
本的寫法:



#ifdef DEBUG

    #define ASSERTFILE(str)       \
             static char strAssertFile[] = str;
    #define ASSERT(f)             \
            if (f)                \
                {}                \
            else                  \
                _Assert(strAssertFile, __LINE__)
#else

    #define ASSERTFILE(str)
    #define ASSERT(f)


#endif
使用這個版本的ASSERT,你可以回收大量記憶體.例如在我在書中用來測試這巨集
的小程式中,新的實作只用掉3K的資料空間。

使用這種除錯檢查的問題是測試中包含了應該放在函式的非除錯版中的程式碼,而
且這個非除錯版的程式碼的do loop無窮迴圈只有在碰到ch等於換行字元時才會結
束。這函式應該寫成這樣子:

void getline(char *pch)
{
    int ch;       /* ch必須是個整數。 */

    do
    {
        ch = getchar();
        ASSERT(ch != EOF);
    }
    while ((*pch++ = ch) != '\n');
}
一個簡單找出switch敘述中沒有更新過的錯誤的辦法,是把除錯檢查放在default
狀態中,對非預期的狀態發出警告。有時default狀態應該永遠不會發生,因為所
有可能狀態都被明確處理掉了。當所有狀態都明確處理完成後,加上這樣子的檢查


.
         .
         .
default:
    ASSERT(FALSE);     /* 我們應該永遠碰不到這個檢查。*/
    break;
}
設計上,表格中的每個項目的位元樣式都應該是個對應遮罩的子集合。例如說,在
遮罩為0xFF00時,位元樣式就不能有任何設立位元出現在低位元組中;不然就不可
能有任何指令在遮罩後吻合那樣的位元樣式了。CheckIdInst副程式可以透過核對
位元樣式是不是遮罩的子集合的方式來加強:

void CheckIdInst(void)
{
    identity *pid, *pidEarlier;
    instruction inst;

    for (pid = &idInst[0]; pid->mask != 0; pid++)
    {
        /* 確定位元樣式是遮罩的子集合。 */
        ASSERT((pid->pat & pid->mask) == pid->pat);
        .
        .
        .
用除錯檢查來核對inst內部不存在有問題的設定:

instruction *pcDecodeEOR(instruction inst,
        instruction *pc,opcode *popc)
{
    /* 我們有將CMPM或CMPA.L指令弄錯嗎? */
    ASSERT(eamode(inst) != 1  &&  mode(inst) != 3);

    /* 在非暫存器狀態,只允許絕對字組跟長字組模式。 */
    ASSERT(eamode(inst) != 7  ||
           (eareg(inst) == 0  ||  eareg(inst) == 1));
    .
    .
    .
選擇備用演算法的重點在於跟本來的演算法的不同性。要核對qsort是否正確運作
,你可以檢查排序後的資料順序是否正確。檢查順序並不會更動資料的順序,所以
夠格當成不同的演算法來處理。要檢查二元搜尋是否正確,可以用線性搜尋法來檢
查兩種搜尋方式是否得到同樣的結果。最後,要檢查itoa函式,可以將它傳回的字
串結果再轉換成整數,跟原來傳給itoa的整數值比較;這兩個整數值應該是相同的

當然,你大概不會在每一份你寫的程式中使用備用的演算法-除非你在替太空梭、
核子發電廠或任何出錯就會造成生命威脅的機械裝置寫程式。不過你或許應該在程
式所有重要的部分使用備用演算法來檢查錯誤。


------------------------------------------------------------------------
--------

------------------------------------------------------------------------
--------
第三章
你可以使用不同的除錯值來清理未初始化跟已釋放記憶體的內容,讓未初始化記憶
體與已釋放記憶體的使用更容易現形。例如fNewMemory會使用bNewGarbage清除新
配置的未初始化記憶體內容,而FreeMemory會在釋放記憶體時使用bFreeGarbage毀
掉本來的記憶體內容:

#define bNewGarbage    0xA3
#define bFreeGarbage   0xA5
fResizeMemory會產生這兩種類型的垃圾值-你可以用上述兩個值,也可以自己再
設定兩個不同值。

要抓出"超出邊界寫入"的錯誤的一個辦法是,定期檢查每個已配置記憶體塊後頭的
位元組有沒被更動過。不過這種測試看來雖然簡單,卻需要紀錄每一塊記憶體後頭
的位元組內容,而且帶來讀取不屬於已配置記憶體空間內容的問題。幸運的,要寫
出這樣的測試方式有個簡單的辦法,只要你在配置記憶體時多配置一位元組的空間
就好了。
舉例來說,當你以size=36的參數呼叫fNewMemory時,你可以一次配置37位元組的
記憶體,並將一個已知的"除錯位元組"放到多出來的那一個位元組內。相似的,你
也可以在fResizeMemory呼叫realloc時多配置並設定一個位元組。要找出"超出邊
界寫入"的問題,你可以把除錯檢查放到sizeofBlock, fValidPointer,
FreeBlockInfo, NoteMemoryRef跟CheckMemoryRefs中,來檢查除錯位元組有沒有
被動過。

下面是一種實作這個測試程式的一種方式。首先,你應該定義bDebugByte跟
sizeofDebugByte:



/*  bDebugByte是個存放在除錯版本程式配置的每一塊記憶體末端
 *  的神奇數字。sizeofDebugByte式傳給malloc跟realloc
 *  時真正增加的配置記憶體數量。
 */

#define bDebugByte 0xE1

#ifdef DEBUG
    #define sizeofDebugByte 1
#else
    #define sizeofDebugByte 0
#endif
接下來,你可以用sizeofDebugByte來調整fNewMemory跟fResizeMemory中對
malloc跟realloc的呼叫參數,並用bDebugByte在記憶體配置成功時填入多餘的位
元組:



flag fNewMemory(void **ppv, size_t size)
{
    byte **ppb = (byte **)ppv;

    ASSERT(ppv != NULL  &&  size != 0);

    *ppb = (byte *)malloc(size + sizeofDebugByte);

    #ifdef DEBUG
    {
        if (*ppb != NULL)
        {
            *(*ppb + size) = bDebugByte;

            memset(*ppb, bGarbage, size);
            .
            .
            .

flag fResizeMemory(void **ppv, size_t sizeNew)
{
    byte **ppb = (byte **)ppv;
    byte **bNew;
    .
    .
    .

    pbNew = (byte *)realloc(*ppb, sizeNew +
    sizeofDebugByte);if (pbNew != NULL)
    {
        #ifdef DEBUG
        {
            *(pbNew + sizeNew) = bDebugByte;

            UpdateBlockInfo(*ppb, pbNew, sizeNew);
            .
            .
            .
最後,在sizeofBlock, fValidPointer, FreeblockInfo, NoteMemoryRef跟
CheckMemoryRefs這些附錄B中的副程式中加上:



/* 核對記憶體塊末端的內容有無被更動過。 */
ASSERT(a(pbi->pb + pbi->size) == bDebugByte);
修改了這些部分後,記憶體子系統就能夠在程式寫入超出已配置記憶體塊末端時找
出問題來了。

你有許多種方法可以找出不完全孤懸指標的問題來。一種做法是將除錯版的
FreeMemory改成不會在收到指標時立刻釋放記憶體塊,而是將記憶體塊的指標放到
一個自由記憶體串列中。(串列中都是向系統配置好了,對程式卻算是已釋放了的
記憶體塊。)這樣子修改FreeMemory可以避免一個"已釋放"記憶體塊在記憶體子系
統能夠經由CheckMemoryRefs核對前被重新配置出去。CheckMemoryRefs會檢查記憶
體系統,並將FreeMemory留下的自由記憶體串列中的記憶體塊真正釋放掉。
現在,雖然這種做法可以找到不完全孤懸指標的問題,你大概不會在程式碰到這樣
的問題之前使用這個方法。理由:這做法違反了除錯碼應該是多餘-而非不同-的
程式碼的原則。

要核對指標參考到的記憶體塊大小,你必須考慮兩種情形:指向整塊記憶體的指標
跟指向記憶體塊內部配置區域的指標。對於前者,你所能採用的最嚴格檢查是核對
指標是否指向記憶體塊開頭,而記憶體塊大小是否吻合sizeofBlock函式所傳回的
結果。對於後者,則只能採取較鬆散的檢查:指標必須指向一塊記憶體,而且大小
必須小於記憶體塊的長度。
所以我們不使用現成的NoteMemoryRef常式來標示兩種指標指向的記憶體,而用兩
個函式來標示兩種類型的記憶體塊。對於指向記憶體塊內部配置區域的指標,你可
以將現成的NoteMemoryRef函式加上一個size參數,在標示指向整塊記憶體的指標
時,你可以建立一個新的NoteMemoryBlock函式。



/*  NoteMemoryRef(pv, size)
 *
 *  NoteMemoryRef將pv指向的記憶體塊標示成已參考狀態。注
 *  意:pv不用指向一塊記憶體的開頭;它只要指向一塊已配置記憶
 *  體內的位址就可以了,不過那塊記憶體在被指到的位址之後至少
 *  要有size個位元組的空間才行。注意:用NoteMemoryBlock
 *  來檢查指向記憶體塊開頭的指標-那個函式可以提供更嚴格的檢
 *  查。
 */
void NoteMemoryRef(void *pv, size_t size);

/*  NoteMemoryBlock(pv, size)
 *
 *  NoteMemoryBlock將pv指向的記憶體塊標示成已參考狀態。注
 *  注意:pv必須指向一塊剛好有size個位元組長度的記憶體塊開
 *  頭。
 */
void NoteMemoryBlock(void *pv, size_t size);
這兩個函式可以讓你找出這個問題中提出的錯誤情形。

要改善附錄B中常式的完整度檢查,你可以先把blockinfo結構中的參考旗標改成參
考次數,再修改ClearMemoryrefs跟NoteMemoryRef來處理這個參考次數。那部分簡
單,問題在於你該如何修改CheckMemoryRefs, 好讓它能夠對某些被多重參考的記
憶體塊發出錯誤訊息而讓其他也被多重參考了的記憶體塊過關?
這問題的一個做法是增強NoteMemoryRef函式,讓它在指向記憶體塊的指標之外再
接受一個記憶體塊ID的標籤輸入。NoteMemoryRef可以將這標籤存放在blockinfo結
構中,而CheckMemoryRefs可以在之後用這標籤來核對參考次數。底下就是這個修
改版的寫法。表頭注釋可以參考附錄B中本來的函式。



/*  blocktag是個程式中所有類型已配置記憶體塊的串列。
 *  ClearmemoryRefs將所有記憶體塊設定成tagNone.
 *  NoteMemoryRef將記憶體塊的標籤設定成某個指定類型。
 */

typedef enum
{
    tagNone,
    tagSymName,
    tagSymStruct,
    tagListNode,        /* 串列的節點必須被參考兩次。*/
    .
    .
    .
} blocktag;

void ClearMemoryRefs(void)
{
    blockinfo *pbi;

    for (pbi = pbiHead; pbi != NULL; pbi = pbi-
        >pbiNext)
    {
        pbi->nReferenced = 0;
        pbi->tag = tagNone;
    }
}

void NoteMemoryRef(void *pv, blocktag tag)
{
        blockinfo *pbi;

        pbi = pbiGetBlockInfo((byte a)pv);

        pbi->nReferenced++;

        ASSERT(pbi->tag == tagNone  ||  pbi->tag ==
        tag);pbi->tag = tag;
}


void CheckMemoryRefs(void)
{
    blockinfo *pbi;

    for (pbi = pbiHead; pbi != NULL; pbi = pbi-
        >pbiNext)
    {
        /*  一個記憶體塊完整度的簡單檢查。如果除錯檢查失敗
         *  了,就表示管理blockinfo的除錯碼出問題了,或者
         *  可能是有錯誤的記憶體寫入毀了記憶體配置紀錄的資
         *  料結構。無論何者,都是錯誤的情形。
         */
        ASSERT(pbi->pb != NULL  &&  pbi->size != 0);

        /*  對遺漏的記憶體塊進行檢查。如果除錯檢查失敗了,
         *  就表示程式遺失了一塊記憶體的配置紀錄,或是有整
         *  體記憶體塊的指標沒被NoteMemoryRef納入管理。
         *  有些型態的記憶體塊可能被參考多次。
         */
        switch (pbi->tag)
        {
        default:
            /* 大部分記憶體塊都只會被參考一次而已。 */
            ASSERT(pbi->nReferenced == 1);
            break;

        case tagListNode:
            ASSERT(pbi->nReferenced == 2);
            break;
        .
        .
        .
        }
    }
}
MS-DOS, Windows跟麥金塔程式開發人員一般會使用一種會快速吞噬記憶體空間的
工具讓程式的記憶體配置失敗,以測試程式如何處理記憶體耗盡的情形。這做法可
行,可是不精確-它沒辦法準確控制程式中記憶體配置失敗處的位置。如果你要測
試個別功能,這做法就不怎麼管用了。一個更好的辦法是直接在記憶體管理程式中
實作一個記憶體耗盡狀態的模擬程式。
不過要注意,記憶體配置失敗祇是資源配置失敗的一種而已-你可能會碰到磁碟空
間耗盡,列表紙張用完,電話忙線中等等各種錯誤。你真正需要的,是一種能夠偽
造各種失誤狀態的泛用工具。

一種可行做法是建立一個failureinfo結構,其中包含告訴失誤處理機制該如何處
理各種狀態的資訊。這構想是讓程式員跟測試圓能夠從外部測試中考驗特定功能的
失誤處理能力,微軟的應用程式常有除錯專用的對話視窗,在Excel這樣有巨集語
言的程式中也有除錯專用的巨集,讓測試人員能用這些除錯工具來自動測試各種失
誤情形。

要在記憶體管理程式中宣告一個failureinfo結構,你可以加上



failureinfo fiMemory;
然後在fNewMemory跟fResizeMemory中模擬記憶體用完的失誤狀態,你可以在兩個
函式中加上一小段除錯碼:



flag fNewMemory(void **pv, size_t size)
{
    byte **ppb = (byte **)ppv;

    #ifdef DEBUG
        if (fFakeFailure(&fiMemory))
        {
            *ppb = NULL;
            return (FALSE);
        }
    #endif
    .
    .
    .

flag fResizeMemory(void **ppv, size_t sizeNew)
{
    byte **ppb = (byte **)ppv;
    byte *pbNew;

    #ifdef DEBUG
        if (fFakeFailure(&fiMemory))
            return (FALSE);
    #endif
     .
     .
     .
修改了這些東西之後,新的失誤處理機制就等著上路了。要讓它開始動作,你可以
呼叫SetFailures函式來初始化failureinfo結構:



SetFailures(&fiMemory, 5, 7);
以5跟7的參數呼叫SetFailures是在讓失誤處理系統知道你在引起七次連續的錯誤
之前會呼叫這個系統五次。SetFailures的兩種常見呼叫方式是



/* 不偽造任何失誤狀態。*/
SetFailures(&fiMemory, UINT_MAX, 0);

/* 永遠偽造失誤狀態。 */
SetFailures(&fiMemory, 0, UINT_MAX);
使用SetFailures, 你可以設計出重複呼叫相同程式碼而每次呼叫時都設定不同
SetFailures參數來模擬所有可能錯誤型態的單元測試。一種常見的測試方式是將
第二個"失誤"參數值設定在UINT_MAX, 而讓第一個"成功"次數逐一從0遞增-"一定
失誤"-到某個夠大到測試系統中所有對這系統的成功呼叫的次數。

最後,你有時會想呼叫記憶體系統,磁碟系統跟其他提供資源的子系統而不想碰到
任何偽造出來失誤狀態;這通常是當你從其他除錯程式碼中配置資源時。後面兩個
可巢狀使用的函式讓你能夠暫時關閉這個失誤處理機制。



DisableFailures(&fiMemory);
        配置一些記憶體資源
EnableFailures(&fiMemory);
底下的程式碼實作了構成失誤處理機制的四個函式:



typedef struct
{
    unsigned nSucceed;   /* 偽裝失誤之前的呼叫次數 */
    unsigned nFail;      /* 偽裝失誤的次數 */
    unsigned nTries;     /* 已經呼叫過的次數 */
    int      lock; /* 如果大於零,就會關閉失誤偽裝機制。*/
} failureinfo;



void SetFailures(failureinfo *pfi, unsigned
                 nSucceed,unsigned nFail)
{
    /* 如果nFail為零,nSucceed就必須為UINT_MAX。 */
    ASSERT(nFail != 0  ||  nSucceed == UINT_MAX);

    pfi->nSucceed = nSucceed;
    pfi->nFail    = nFail;
    pfi->nTries   = 0;
    pfi->lock     = 0;
}



void EnableFailures(failureinfo *pfi)
{
    ASSERT(pfi->lock > 0);
    pfi->lock-;
}



void DisableFailures(failureinfo *pfi)
{
    ASSERT(pfi->lock >= 0  &&  pfi->lock < INT_MAX);
    pfi->lock++;
}


flag fFakeFailure(failureinfo *pfi)
{
    ASSERT(pfi != NULL);

    if (pfi->lock > 0)
        return (FALSE);

    /* nTries最大不超過UINT_MAX。 */
    if (pfi->nTries != UINT_MAX)
        pfi->nTries++;

    if (pfi->nTries <= pfi->nSucceed)
        return (FALSE);

    if (pfi->nTries - pfi->nSucceed <= pfi->nFail)
        return (TRUE);

    return (FALSE);
}

------------------------------------------------------------------------
--------

------------------------------------------------------------------------
--------
第四章
第四章中沒有任何問題,雖然給了幾個學習計劃。


------------------------------------------------------------------------
--------

------------------------------------------------------------------------
--------
第五章
strdup的介面設計有危險性,因為它發生錯誤時會傳回NULL指標,就像malloc的情
形,這種錯誤可能會被忽略掉。一個降低錯誤性的介面會將錯誤狀況與指標輸出分
開,讓錯誤狀態明顯化。一個這樣的介面可以寫成底下那樣

char *strDup;           /* 指向複製好的字串 */

if (fStrDup(&strDup, strToCopy))
    成功 – strDup指向新的字串
else
    不成功 – strDup為NULL
getchar一種比fGetChar更好的介面設計方式會傳回一個錯誤代碼,而不光只是傳
回一個TRUE或FALSE的"成功"與否的值-例如

/* These are the errors that errGetChar may return. */

typedef enum
{
    errNone = 0,
    errEOF,
    errBadRead,
    .
    .
    .
} error;

void ReadSomeStuff(void)
{
    char    ch;
    error   err;

    if ((err = errGetChar(&ch)) == errNone)
         成功 – ch含有下一個字元的內容
    else
         失敗 – err有錯誤原因的類型
    .
    .
    .
這個介面比fGetChar好的原因在於它讓errGetChar能夠傳回多種錯誤狀態(也因此
能傳回多種成功狀態)。如果你不在乎傳回來的特定錯誤類型,你可以將區域變數
err拿掉,而回頭用fGetChar的介面:



if ((errGetChar(&ch)) == errNone)
     成功 – ch含有下一個字元的內容
else
     失敗 – err有錯誤原因的類型
strncpy的麻煩之處在於它的行為不一致:有時會將目的字串以nul字元結尾,有時
則不。strncpy與其他通用字串處理函式擺在一起,讓程式員們會誤以為strncpy本
身也是個通用函式,事實不然。有那種不尋常行為的strncpy應該不在ANSI標準中
,它被放進了標準中只是因為它被廣泛用在ANSI標準制定之前的C語言程式中。
C++的inline函式限定字的用處在於它讓你可以把函式定義得根巨集一般有效率-
如果你的編譯器夠好-而不會有巨集"函式"在評估參數值時的麻煩副作用。
C++新的&參考參數的嚴重問題在於隱藏了你實際上傳遞的是變數參考位址,而不是
變數值,以致產生混淆。舉例來說,假設你將fResizeMemory函式重新定義來使用
參考參數,程式員們會寫成

if (fResizeMemory(pb, sizeNew))
    記憶體塊大小改變成功
不過,要注意,不熟悉這函式的程式員們沒理由相信pb在呼叫中會被改變。你覺得
這對程式的維護會有什麼影響?

一個相關的問題是,使用C語言的程式員們在知道傳入的參數是變數值而不是變數
參考時,常會改變形式參數的內容。可是如果維護程式員在修理不是他自己寫的函
式的錯誤,他沒注意到函式宣告中的&, 他可能會在函式裡頭改變參數的內容,而
不知道這樣的改變對影響到函式之外的情形。&參考參數的危險性在於會隱藏這樣
重要的實作細節。

strcmp介面的問題在於傳回值會讓呼叫處產生不良程式碼。要改善strcmp,你會將
介面設計成簡單易懂的樣子,即使對不了解這函式的人來說也是如此。
一種可行的介面設計方式是現有strcmp介面的一種輕微變形。對不相等的字串,不
再傳回任意的正負值-那會讓程式員們必須把所有結果都對0比較過-你可以把
strcmp改成會傳回三個定義完善的有名稱常數:



if (strcmp(strLeft, strRight) == STR_LESS)

if (strcmp(strLeft, strRight) == STR_GREATER)

if (strcmp(strLeft, strRight) == STR_EQUAL)
另一種可行的介面設計方式是使用不同的函式來處理不同類型的比較:



if (fStrLess(strLeft, strRight))

if (fStrGreater(strLeft, strRight))

if (fStrEqual(strLeft, strRight))
第二種介面設計方式的優點在於讓你能夠使用巨集透過現成的strcmp函式來實作這
些函式:



#define fStrLess(strLeft, strRight)     \
        (strcmp(strLeft, strRight) < 0)

#define fStrGreater(strLeft, strRight)  \
        (strcmp(strLeft, strRight) > 0)

#define fStrEqual(strLeft, strRight)    \
        (strcmp(strLeft, strRight) == 0)
你可以再更進一步定義出使用<=跟>=進行比較的巨集來增加可讀性。這樣子可以增
進可讀性,而不會造成程式大小或執行速度上的損失。


------------------------------------------------------------------------
--------

------------------------------------------------------------------------
--------
第六章
"普通"單位元欄位的可攜性值域只有0,所以沒什麼用處。它還有個非零狀態-可
是你不知道那是-1還是1,這完全看你的編譯器內定位元欄位為有號數還是無號數
而定。如果你將所有比較都限定成對0的比較,你就可以安全的使用單位元欄位的
兩種狀態。舉例來說,如果你假設psw.carry是個"普通"的單位元欄位,你就可以
安全的使用底下四種測試寫法:

if (psw.carry == 0)            if (!psw.carry)

if (psw.carry != 0)            if (psw.carry)
不過底下的測試方式則因為它們依賴了你用的編譯器設定而有危險性:



if (psw.carry == 1)            if (psw.carry == -1)

if (psw.carry != 1)            if (psw.carry != -1)
傳回布林(Boolean)值的函式如同單位元欄位般,你不能安全預測"true"傳回來
的是什麼值。你可以信賴FALSE會為零的假設,可是程式員們經常會傳回任何方便
的非零值來表示"true",使傳回來的結果不等於常數TRUE。如果你假設
fNewMemory會傳回一個布林值,你可以安全的使用底下的比較式

if (fNewMemory(...) == FALSE)

if (fNewMemory(...) != FALSE)
或更好些,



if (!fNewMemory(...))

if (fNewMemory(...))
不過底下的寫法則有危險,因為它假設fNewMemory永遠不會傳回一個TRUE以外的非
零值:



if (fNewMemory(...) == TRUE)      /* 危險! */
有條好規則要記得,永遠不要將布林值跟TRUE進行比較。

如果你把wndDisplay宣告成一個window型態的整體資料結構,你就給了它一個其他
視窗結構沒有的性質:它是個整體資料結構。這看來只是微不足道的小地方,卻可
能帶來不可預期的問題。例如當你要寫一個視窗中所有子視窗跟它本身通通釋放掉
的函式時,你把函式寫成這樣:

void FreeWindowTree(window *pwndRoot)
{
    if (pwndRoot != NULL)
    {
        window *pwnd, *pwndNext;

        ASSERT(fValidWindow(pwndRoot));

        for (pwnd = pwndRoot->pwndChild; pwnd !=
                NULL;pwnd = pwndNext)
        {
            /* 在釋放記憶體之前先取得"Next"指標的值。 */
            pwndNext = pwnd->pwndSibling;
            FreeWindowTree(pwnd);
        }

        if (pwndRoot->strWndTitle != NULL)
            FreeMemory(pwndRoot->strWndTitle);
        FreeMemory(pwndRoot);
    }
}
現在注意到,因為一般的視窗的指標pwndDisplay都指向一塊已配置好的記憶體,
如果你要釋放每一個視窗,你可以安全把pwndDisplay傳進去這個函式中。可是你
不能把&wndDisplay傳進去,因為這程式會試著釋放wndDisplay,可是那是個整體
資料結構而不可能被釋放掉。要讓這個程式碼能夠處理&wndDisplay, 你得加上



if (pwndRoot != &wndDisplay)
到FreeMemory(pwndRoot)的呼叫之前。如此一來,你就讓程式與一個整體資料結構
牽扯在一起了,有夠討厭。

要避免錯誤的一個最好辦法就是避免寫出隨便設計出來的東西。

第二個版本有好幾個理由比第一個版本要危險。因為A, D跟運算是在第一個版本中
是常見的程式碼,它們會被執行到-也因此會被測試到-不管f的值是什麼。而在第
二個版本中,A跟D的程式碼會被分開測試,除非它們相同,不然你就冒著遺漏其中
一種情形中的錯誤的風險。(A跟D的程式碼在它們為B或C特別特別最佳化後不會相
同的。)
在第二個版本中,你也會碰到讓A跟D在程式員們修正跟增強程式時的同步問題,尤
其當A跟D寫得不一樣時。所以,除非計算f的代價高到會讓使用者注意到效率上的
差異,不然就乖乖用第一個版本吧。記住這邊的一條好規則:增加常見程式碼的量
來降低程式碼的差異。

使用相似的名稱如s1跟s2是危險的,因為你容易在要打s2時誤打成s1. 更糟糕的,
即使你打錯字了,程式編譯時也完全不會碰到問題。使用相似的名稱也讓你在用錯
變數名稱時更難找出錯誤來:

int strcmp(const char *s1, const char *s2)
{
    for ( ; *s1 == *s2; s1++, s2++)
    {
        if (*s1 == '\0')   /* 到了字串尾端了嗎? */
            return (0);
    }

    return ((a(unsigned char a)s2 < a(unsigned char
            a)s1) ?-1 : 1);
}
上頭的程式錯再返回敘述中的測試反了,可是因為變數命名得沒什麼意義,你很難
看出裡頭有什麼錯誤。如果你使用具描述性而且不同的名稱,像是sLeft跟sRight
,打錯字或是用錯變數的錯誤機會就大大消失了,而程式也可以更具可讀性。

ANSI標準保證你可以定址一個宣告好了的陣列後頭一個位元組,可是它不保證你可
以參考這樣的陣列之前一個位元組。標準中也不保證你可以參考用malloc配置得來
的記憶體塊之前一個位元組。
舉例來說,某些80x86記憶體模式中的指標是採用基底:位移的方式存放的,而只有
位移值會被處理到。如果pchStart是這樣子的指標,而且指向一塊已配置記憶體的
開頭,它的位移量就會是0。如果你假設pch的值為pchStart+size, pch就永遠不可
以小於pchStart, 因為它的位移值永遠不可小於pchStart的0位移-位移量再降下
去也只會回捲成0xFFFF。

a. 使用printf(str); 而不用printf("%s", str); 會在str包含任何%字元時發生
錯誤;printf會誤將這些%字元當成格式限定字元使用。使用printf("%s", str);
 的麻煩則是它可能被明顯最佳化成printf(str); 粗新的程式員們有時會這樣子更
動程式而造成錯誤。

b. 用f=1-f; 代替f=!f; 的危險在於它假設f為0或1, 而!f清楚說明你只是在翻轉
一個旗標狀態,不管f的值為何都適用。使用1-f的唯一理由是這樣子可以比!f產生
出稍微有效率些的程式碼,可是記住一點,區域效率上的改善很少對程式的執行效
率產生什麼整體影響。使用1-f的寫法只會增加多出一個錯誤的風險。

c. 在一個敘述中使用多個指派運算的危險在於它會帶來預料之外的資料型態轉換
。以這裡的例子來說,程式員們小心地把ch宣告成int, 讓它能夠正確處理
getchar會傳回的EOF值,可是由於getchar的值先存放在一個字串中,以致於這個
結果先被轉換成了字元型態,然後這個字元值-而不是本來傳回的整數值-才指定
給ch。這個非預期的型態轉換重新帶來了第五章中提到的getchar問題,儘管ch被
小心的定義成了一個整數變數。

一般說來,查表法可以讓程式變得又小又快,也增進了程式的正確率。不過你可以
從另一個角度來看待這個問題,當程式變小時,存放在表格中的資料也要佔用記憶
體,所以整體上,查表法可能會比非查表法的實作方式用掉更多記憶體。查表法的
另一個問題是危險-你必須確定表格中的資料是正確的。有時檢查表格資料的正確
性很簡單,像tolower跟uCyclecheckBox中的表格那樣,可是在一個像第二章的反
組譯器中那樣的大表格裡頭,就很容易潛藏問題。你要遵守的規則是,除非你能核
對表格中的資料,不然不要用查表法。
就算你的編譯器不提供將乘法跟除法(在適當時)轉換成位元位移的最佳化,你也
不用擔心產生出來的程式碼執行效率不好;即使你把一個除法改用位元位移來處理
,程式效率也不會改善多少。不要為了一丁點編譯器沒最佳化好的效率問題,就去
那樣子改寫程式。你該做的是把程式寫好,換個好的編譯器。
要確保使用者的資料永遠可以存檔,只要在使用者更動檔案之前先配置好緩衝區就
好了。如果每個檔案都要有一個緩衝區,就將檔案開啟成唯讀狀態,或是根本就不
要開啟檔案。不果如果你處理所有開著的檔案就只需要一個緩衝區,你可以在程式
初始化時就將這緩衝區配置好。不要擔心一直閒置這緩衝區會造成記憶體"浪費"的
問題,至少這樣的浪費保證你的使用者資料可以存檔,而不會讓他或她工作了五個
小時,卻因為你的程式沒辦法配置緩衝區而存不了檔。

------------------------------------------------------------------------
--------

------------------------------------------------------------------------
--------
第七章
底下的程式碼改變了這函式的兩個輸入參數,pchTo跟pchFrom:

char *strcpy(char *pchTo, char *pchFrom)
{
        char *pchStart = pchTo;

        while (*pchTo++ = *pchFrom++)
                {}

        return (pchStart);
}
改變pchTo跟pchFrom不違反這些參數的寫入權限,因為它們是傳值參數,strcpy收
到的是輸入值的內容複製品,所以strcpy可以改變這些參數內容。不過並不是所有
的電腦程式語言-FORTRAN就是個例子-都將參數以傳值的方式傳遞。這做法在C語
言中相當安全,不過在其他語言中可能是危險的。

strDigits的麻煩在於它被宣告成一個靜態指標,而不是靜態緩衝區,這樣潛藏的
差異可能在你開啟允許編譯器將所有字串內容當成常數看待的選項時造成問題。有
些編譯器支援"常數字串內容"的選項,將所有字串與其他常數存放在一起,由於常
數不會被改變,編譯器會檢查所有常數字串,將相同的疊放在一起。換句話說,如
果strFromUns跟strFromInt都宣告了指向如"?????"這樣的字串的靜態指標,編譯
器只會配置一份-而不是兩份-那個字串。有些編譯器甚至會更徹底的將吻合其他
字串尾端內容的字串合併,像是"her"吻合"mother"末尾的三個字母。改變這種字
串的一個,就會動到另一個的內容。
將所有字串內容視為常數並限制程式讀取是個安全多了的做法。如果你要改變一個
字串,宣告一個字元緩衝區,而非一個字串指標:



char *strFromUns(unsigned u)
{
    static char strDigits[] = "?????";
不過即使這樣的程式都還有危險存在,因為它依賴程式員輸入正確的問號數,而且
假設末尾的nul字元一定不會被毀掉,使用問號字元來保留空間也不是個好點子。
字串內容是問號還是別的東西會有影響嗎?如果你不確定,你就知道為何你應該用
不同的字元來保留空間了。

一個較安全的寫法是宣告緩衝區的大小,然後把除錯檢查換成一個寫入確認:



char *strFromUns(unsigned u)
{
        static char strDigits[6];   /* 5位數跟一個
         .                             '\0' */
         .
         .
        pch = &strDigits[5];
        *pch = '\0';            /* 取代ASSERT。 */
使用memset來處理化相鄰的區域變數除了特別危險,還比直接初始化變數內容要沒
效率:

i = 0;               /* 將i, j跟k設定為0。 */
j = 0;
k = 0;
或是這樣子更簡單



i = j = k = 0;          /* 將i, j跟k設定為0。 */
這些程式碼既有效率又具可攜性,而且顯然不需要註解,使用memset的版本就相反
了。

我不確定本來的程式員試圖用memset來改善什麼,不過我確定他或她一定得不到好
結果。對初學者來說,如果不是使用最好的編譯器,呼叫memset的負擔也比明確清
除i, j跟k要重多了。不過即使程式員用的編譯器聰明到將已知填入值跟長度的小
型填入記憶體動作在編譯時期變成內含指令處理,狀況也不會改善多少:程式還是
假設編譯器會將i, j跟k在堆疊上相鄰的配置在一起,k在記憶體的最低位址上。程
式碼也假設i, j跟k彼此相鄰而沒有任何"填充"位元組將這些變數有效率的對齊特
定記憶體位址。

不過誰說這些變數在堆疊框內有地方擺?好的編譯器會常常作變數生命週期的分析
,把區域變數在生命週期中用這些資訊存放在暫存器內。i, j跟k這些區域變數在
生命週期中可能都放在暫存器內,i跟j可能就一生都在暫存器中,永遠不會存放在
堆疊框內。變數k另一方面必須在堆疊框內存放,因為它的位址傳給了memset-你
沒辦法取得暫存器的位址。如此一來,i跟j依然是未初始化狀態,而接在k後頭的
2*sizeof(int)個位元組會被錯設成0。

當你呼叫或跳到機器的ROM中一個特定位址時,你面對兩種風險。首先,雖然你機
器中的ROM永遠不變,可是在你硬體的未來機種上,ROM幾乎是一定會被改過的。不
過就算ROM中的長是永遠不變,硬體廠商有時還是會用RAM中的常駐程式來彌補系統
介面上的錯誤。如果你略過這些介面,你就連帶的略過了這些修補錯誤的程式。
把val當成不需要的參數而不傳入的問題在於,呼叫者對DoOperation的內部運作方
式作了跟FILL對CMOVE所作的一樣的假設。假使程式員增強了DoOperation,並在過
程中改寫過程式,使val永遠會被參考到:

void DoOperation(operation op, int val)
{
    if (op < opPrimaryOps)
        DoPrimaryOps(op, val);
    else if (op < opFloatOps)
        DoFloatOps(op, val);
    else
        .
        .
        .
那當DoOperation參考到不存在的val時,會發生什麼事?視你的作業系統而定,不
過大概會因為val處於堆疊框的唯讀區域內而使程式終止執行。

你可以強迫程式員們傳入東西給沒用到的變數,讓他們難以在你的函式呼叫上省下
這個動作。在文件中,你可以說,"當你以opNegAcc的參數呼叫DoOperation時,
val參數就該為0。"一個放得好的除錯檢查可以讓程式員忠實作到這點:



case opNegAcc:
    ASSERT(val == 0);              /* 傳0給val。 */
    accumulator = -accumulator;
    break;
那個除錯檢查核對f是不是TRUE或FALSE。這除錯檢查不光是不清楚,更重要的,除
錯碼沒必要這麼挑剔;畢竟,這些程式碼在發行版本的程式中都會被去掉。那個除
錯檢查最好改寫成

ASSERT(f == TRUE  ??  f == FALSE);
不要把所有工作都擠在一行內完成。宣告個函式指標,把事情分成兩行去作:

void *memmove(void *pvTo, void *pvFrom, size_t size)
{
    void (*pfnMove)(byte *, byte *, size_t);
    byte *pbTo   = (byte *)pvTo;
    byte *pbFrom = (byte *)pvFrom;

    pfnMove = (pbTo > pbFrom) ? tailmove : headmove;
    (*pfnMove)(pbTo, pbFrom, size);

    return (pvTo);
}
簡單說,這程式碼依賴了Print程式碼的內部運作方式來呼叫Print。如果一名程式
員改了Print的程式碼而不知道有其他程式碼以跳到Print進入點後四個位元組的方
式呼叫它,這名程式員就可能把程式改得讓那樣子呼叫Print的程式動彈不得。如
果你發現你必須把程式寫成那樣子跳進一個常式中間去,那就讓維護程式員們可以
一眼看出這些進入點必須滿足的條件:

move  r0,#PRINTER
              call  PrintDevice
               .
               .
               .
PrintDisplay:  move  r0,#DISPLAY
PrintDevice:                        ; r0 == 裝置代號
                   .
               .
               .
在微電腦的記憶體容量還很小,每個位元組都還很珍貴的時候,跳進一個指令中間
去執行是種常見的最佳化方式,使用這種技巧通常會省下一兩個位元組。不過這樣
子的做法不管是當時還是現在,都是不好的。如果你團隊中有人還這樣子寫程式,
禮貌的要求他們改變做法,不然就請他們離開你的小組。你不需要那樣令人頭痛的
程式。

------------------------------------------------------------------------
--------

------------------------------------------------------------------------
--------
第八章
第八章中沒有任何問題,只給了幾個學習計劃。



--
 === I love Puss forever ===

※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.1.241]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店