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

字號(hào):

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