2012軟考軟件設(shè)計師輔導(dǎo):C++編程易范錯誤集合

字號:

C/C++語言中有許多對初學(xué)者(甚至是有經(jīng)驗的軟件設(shè)計師)來說很輕易范的錯誤。通曉這樣的錯誤可使你免于陷入其中。
    忘記初始化指針
    這種錯誤只是一般“忘記初始化變量”錯誤的一個非凡形式(C/C++中變量不會自動初始化,而Basic可以)。使這種錯誤更糟糕的原因是它的后果往往更加糟糕:
    void SomeFunction()
    {
    int *pnVar
    int nVal;
    nVal = *pnVar; // Bad enough.
    *pnVar = nVal; // MUCh worse.
    }
    在這個例子中,指針變量pnVar從未被賦值。因此你必須假設(shè)它含有的是雜亂的數(shù)據(jù),從一個混亂信息指針中讀數(shù)糟糕的很,因為結(jié)果肯定是雜亂數(shù)據(jù),向一個混亂信息指針寫數(shù)據(jù)更糟,因為它將導(dǎo)致一些不知道什么地方的數(shù)據(jù)被重寫。
    假如被重寫的區(qū)域無用,這到?jīng)]什么危害。假如被重寫的區(qū)域有用,數(shù)據(jù)就會丟失。這種類型的錯誤那么難找,是因為直到程序企圖使用已丟失的數(shù)據(jù)時問題才會呈現(xiàn)出來。這種問題可能是在數(shù)據(jù)丟失后好久才發(fā)生的。
    由于這一問題手工判定很困難,Visual C++編譯器就通過一些努力來避免它的發(fā)生。例如,當(dāng)你編譯上述函數(shù)時就會產(chǎn)生一個警告。在這種情況下,編譯器會告訴你變量在使用前未被賦值。在很多情況下,它不可能告訴你。
    Windows 95操作系統(tǒng)試圖用保護(hù)存儲器在一定程度上幫助解決難題:假如應(yīng)用程序企圖從不屬于它的存儲器讀或?qū)?,Windows通常能截獲該請求,并立即終止該程序??上В琖indows 95不能截獲對應(yīng)用程序擁有的存儲器的無效訪問,它也不能截獲所有非法訪問,因為必須保留某些缺口,以與Windows 3.1的兼容性名義開放。
    忘記釋放堆內(nèi)存
    請記住從堆獲得分配的任何內(nèi)存都必須要釋放。假如你用完內(nèi)存以后,忘記釋放它,系統(tǒng)內(nèi)存就會變得愈來愈小,直到最后你的程序不能運行而崩潰。
    這個問題會出現(xiàn)在諸如下列的一些情況中:
    Car* GetAnewCar(int nOccupants)
    {
    Car* pCar;
    if(nOccupants < 4)
    {
    pCar = new Car(2); // get a two-door.
    }
    else
    {
    pCar = new Car(4); // otherwise, a four-door.
    }
    return pCar;
    }
    void GoToTheStore(int nOccupants)
    {
    // get a car。
    Car* pCar = GetAnewCar(nOccupants);
    // Now drive to the store。
    if(pCar)
    {
    pCar->Drive(Store);
    }
    }
    在此例中,函數(shù)GoToTheStore()首先分配一輛新車來開--這有點浪費,但你肯定會同意這種算法可以正常工作。只要分配了新車,它就會開到有調(diào)用pCar->Drive(Store)所指向的商店。
    問題是在它安全到達(dá)目的地之后,函數(shù)不破壞Car對象。它只是簡單地退出,從而使內(nèi)存丟失。
    通常,當(dāng)對象pCar出了程序中的作用域時,程序員應(yīng)該依靠析構(gòu)函數(shù)~Car釋放內(nèi)存。但這里辦不到,因為pCar的類型不是Car而是Car*,當(dāng)pCar出了作用域時不會調(diào)用析構(gòu)函數(shù)。
    修正的函數(shù)如下:
    void GoToTheStore(int nOccupants)
    {
    // get a car。
    Car* pCar = GetAnewCar(nOccupants);
    // Now drive to the store。
    if(pCar)
    {
    pCar->Drive(Store);
    }
    // Now delete the object,returning the memory.
    delete pCar;
    }
    使用new操作符構(gòu)造的對象都應(yīng)該用delete運算符刪除,這一點必須牢記。
    返回對局部內(nèi)存的引用
    另一個常見的與內(nèi)存有關(guān)的問題是從函數(shù)返回局部內(nèi)存對象的地址。當(dāng)函數(shù)返回時,對象不再有效。下一次調(diào)用某函數(shù)時,這個內(nèi)存地址可能會被這個新函數(shù)使用。繼續(xù)使用這個內(nèi)存指針就有可能會寫入新函數(shù)的局部內(nèi)存。
    這個常見問題以這種方式出現(xiàn):
    Car* GetAnewCar(int nOccupants)
    {
    Car* pCar;
    if(nOccupants < 4)
    {
    pCar = &Car(2); // get a two-door.
    }
    else
    {
    pCar = &Car(4); // otherwise, a four-door.
    }
    return pCar;
    }
    請注重指針pCar怎樣被賦予由構(gòu)造函數(shù)Car()建立的未命名對象的局部地址的。到目前為止,沒有問題。然而一旦函數(shù)返回這個地址,問題就產(chǎn)生了,因為在封閉的大括號處臨時對象會被析構(gòu)。
    使運算符混亂
    C++從它的前輩C那里繼續(xù)了一套含義相當(dāng)混亂模糊的運算符。再加上語法規(guī)則的靈活性,就使它很輕易對程序員造成混亂,使程序員去使用錯誤的運算符。
    這個情況的最出名的例子如下:
    if(nVal = 0)
    {
    // do something if nVal is nonzero.
    }
    程序員顯然想要寫if(nVal == 0)。不幸的是,上述語句是完全合法的,雖然沒有什么意義,C++語句將nVal賦值為0,然后檢查結(jié)果看看是否為非零(這是不可能發(fā)生的)。結(jié)果是大括號內(nèi)的代碼永遠(yuǎn)不會被執(zhí)行。
    其它幾對輕易弄錯的運算符是&和&&,以及/和//。
    0的四種面孔
    根據(jù)使用它的方式,常數(shù)0有四種可能的含義:
    ☆ 整數(shù)0
    ☆ 不能是對象地址的地址
    ☆ 邏輯FALSE
    ☆ 字符串的終結(jié)符
    我可以向你證實這些含義的差別是很實際的。例如,下列賦值是合法的:
    int *pInt;
    pInt = 0;// this is leagal.
    而下列賦值是不合法的:
    int *pInt;
    pInt = 1;// this is not.
    第一個賦值是合法的,因為表中的第二定義:常數(shù)0可以是地址,然而常數(shù)1則不行。
    這個含義的多重性能導(dǎo)致一些難以發(fā)現(xiàn)的錯誤:
    // copy a string from pSource to pTarget -- incorrect version.
    while(pSource)
    {
    *pTarget++ = *pSource++;
    }
    此例中的while循環(huán)試圖把由pSource指向的源字符串復(fù)制到由pTarget指向的內(nèi)存塊。但不幸的是,條件寫錯了,它應(yīng)這樣寫出:
    // copy a string from pSource to pTarget -- incorrect version.
    while(*pSource)
    {
    *pTarget++ = *pSource++;
    }
    你可以看到,當(dāng)由pSource指向的字符為NULL時,終止條件出現(xiàn)。這是0的第四定義。然而,這里寫出的代碼卻是去查看地址pSource是否為零,這是第二定義。
    最終結(jié)果是while()循環(huán)繼續(xù)寫入內(nèi)存直到程序崩潰。
    0的其他定義之間也可能產(chǎn)生混亂。的解決辦法就是當(dāng)你使用常數(shù)0的時候小心一點。
    聲明的混亂處
    復(fù)合聲明是非常混亂的,但C++--以它的熱忱保持了與C的反向兼容性--但也產(chǎn)生了一些聲明間的矛盾,你必須避免這種矛盾。
    class Myclass
    {
    public:
    Myclass(int nArg1 = 0,int nArg2 = 0);
    };
    Myclass mcA(1,2);
    Myclass mcB(1);
    Myclass mcC();
    mcA是參數(shù)1和2構(gòu)成的對象,而mcB是參數(shù)1和0構(gòu)成的對象。因此你可能認(rèn)為mcC是參數(shù)0和0構(gòu)成的對象,然而情況不是這樣。而mcC()是一個不帶參數(shù)的函數(shù),它用數(shù)值返回類Myclass的對象。
    另一個混亂產(chǎn)生于初始化運算符=的使用:
    Myclass mcB = nA; // same as Myclass mcB(nA)
    為了增強(qiáng)與C的兼容性,答應(yīng)這樣使用=;然而你應(yīng)該避免這種結(jié)構(gòu),因為它不是一貫適用的。例如下列程序就不會有預(yù)期的效果:
    Myclass mcA = nA,nB;
    這說明一個對象mcA(nA),它后面有一個獨立的使用缺省構(gòu)造符的對象nB,而不是說明一個對象mcA(nA,nB)。
    堅持使用C++格式--這是最安全的。
    計算順序混亂
    C和C++運算符的先后順序,使你能夠知道怎樣計算諸如下列表達(dá)式:
    a = b * c + d;
    然而先后次序不會影響子表達(dá)式的計算順序。讓我們以看上去不重要的方式改變示例的表達(dá)式:
    a = b() * c() + d();
    現(xiàn)在的問題是,在這個表達(dá)式中以什么樣的順序調(diào)用函數(shù)b(),c()和d()?答案是,順序是完全不確定的。更糟的是,順序不能借助圓括號的使用而確定。所以下列表達(dá)式?jīng)]有作用:
    a = (b() * c()) + d();
    函數(shù)計算順序通常不值得去關(guān)心。然而,假如這些函數(shù)有副作用,以某種方式彼此影響(稱為相互副作用),那么順序就是重要的了。例如,假如這些函數(shù)改變相同的全局變量,則結(jié)果就是不同的,這取決于其中函數(shù)被調(diào)用的順序。
    甚至當(dāng)不涉及函數(shù)調(diào)用時,相互副作用也會產(chǎn)生影響:
    int nI = 0;
    cout《“nA[0]=”<這個表達(dá)式的問題是單個表達(dá)式包含有相互副作用的兩個子表達(dá)式--變量nI是增量。哪個nA[nI++]首先被執(zhí)行,左邊的nA[nI++]還是右邊的nA[nI++]?沒法說,上述代碼可能會以預(yù)期的方式工作,但也可能不會。
    說明虛擬成員函數(shù)
    為了在子類中重載虛擬成員函數(shù),必須用和基本類中函數(shù)一樣的形式說明子類中函數(shù)的參數(shù)和返回類型。這并不總是清楚的。例如,下列代碼似乎講得通:
    class Base
    {
    public:
    virtual void AFunc(Base *pB);
    };
    class Subclass:public Base
    {
    public:
    virtual void AFunc(Subclass *pS);
    };
    這個代碼會編譯通過,但不會有遲后聯(lián)編。函數(shù)Base::AFunc()的參數(shù)是Base*類型的,而函數(shù)Subclass::AFunc()的參數(shù)是Subclass*,它們是不同的。
    這個規(guī)則的例外是下面的例子,它符合ANSI C++標(biāo)準(zhǔn):
    class Base
    {
    public:
    virtual void Base* AFunc();
    };
    class Subclass:public Base
    {
    public:
    virtual void Subclass* AFunc();
    };
    在此例中,每個函數(shù)返回其固有類型對象的地址。這種技術(shù)很通用,所以標(biāo)準(zhǔn)委員會決定承認(rèn)它。