新聞中心
C++是一種復(fù)雜的編程語言,其中充滿了各種微妙的陷阱。在C++中幾乎有數(shù)不清的方式能把事情搞砸。幸運(yùn)的是,如今的編譯器已經(jīng)足夠智能化了,能夠檢測(cè)出相當(dāng)多的這類編程陷阱并通過編譯錯(cuò)誤或編譯警告來通知程序員。最終,如果處理得當(dāng)?shù)脑?,任何編譯器能檢查到的錯(cuò)誤都不會(huì)是什么大問題,因?yàn)樗鼈冊(cè)诰幾g時(shí)會(huì)被捕捉到,并在程序真正運(yùn)行前得到解決。最壞的情況下,一個(gè)編譯器能夠捕獲到的錯(cuò)誤只會(huì)造成程序員一些時(shí)間上的損失,因?yàn)樗麄儠?huì)尋找解決編譯錯(cuò)誤的方法并修正。

創(chuàng)新互聯(lián)公司專注于任丘網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠(chéng)為您提供任丘營(yíng)銷型網(wǎng)站建設(shè),任丘網(wǎng)站制作、任丘網(wǎng)頁(yè)設(shè)計(jì)、任丘網(wǎng)站官網(wǎng)定制、微信平臺(tái)小程序開發(fā)服務(wù),打造任丘網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供任丘網(wǎng)站排名全網(wǎng)營(yíng)銷落地服務(wù)。
那些編譯器無法捕獲到的錯(cuò)誤才是最危險(xiǎn)的。這類錯(cuò)誤不太容易察覺到,但可能會(huì)導(dǎo)致嚴(yán)重的后果,比如不正確的輸出、數(shù)據(jù)被破壞以及程序崩潰。隨著項(xiàng)目的膨脹,代碼邏輯的復(fù)雜度以及眾多的執(zhí)行路徑會(huì)掩蓋住這些bug,導(dǎo)致這些bug只是間歇性的出現(xiàn),因此使得這類bug難以跟蹤和調(diào)試。盡管本文的這份列表對(duì)于有經(jīng)驗(yàn)的程序員來說大部分都只是回顧,但這類bug產(chǎn)生的后果往往根據(jù)項(xiàng)目的規(guī)模和商業(yè)性質(zhì)有不同程度的增強(qiáng)效果。
這些示例全部都在Visual Studio 2005 Express上測(cè)試過,使用的是默認(rèn)告警級(jí)別。根據(jù)你選擇的編譯器,你得到的結(jié)果可能會(huì)有所不同。我強(qiáng)烈建議所有的程序員朋友都采用***等級(jí)的告警級(jí)別!有一些編譯提示在默認(rèn)告警級(jí)別下可能不會(huì)被標(biāo)注為一個(gè)潛在的問題,而在***等級(jí)的告警級(jí)別下就會(huì)被捕捉到?。ㄗⅲ罕疚氖沁@個(gè)系列文章的第1部分)
1)變量未初始化
變量未初始化是C++編程中最為常見和易犯的錯(cuò)誤之一。在C++中,為變量所分配的內(nèi)存空間并不是完全“干凈的”,也不會(huì)在分配空間時(shí)自動(dòng)做清零處理。其結(jié)果就是,一個(gè)未初始化的變量將包含某個(gè)值,但沒辦法準(zhǔn)確地知道這個(gè)值是多少。此外,每次執(zhí)行這個(gè)程序的時(shí)候,該變量的值可能都會(huì)發(fā)生改變。這就有可能產(chǎn)生間歇性發(fā)作的問題,是特別難以追蹤的??纯慈缦碌拇a片段:
- if (bValue)
- // do A
- else
- // do B
如果bValue是未經(jīng)初始化的變量,那么if語句的判斷結(jié)果就無法確定,兩個(gè)分支都可能會(huì)執(zhí)行。在一般情況下,編譯器會(huì)對(duì)未初始化的變量給予提示。下面的代碼片段在大多數(shù)編譯器上都會(huì)引發(fā)一個(gè)警告信息。
- int foo()
- {
- int nX;
- return nX;
- }
但是,還有一些簡(jiǎn)單的例子則不會(huì)產(chǎn)生警告:
- void increment(int &nValue)
- {
- ++nValue;
- }
- int foo()
- {
- int nX;
- increment(nX);
- return nX;
- }
以上的代碼片段可能不會(huì)產(chǎn)生一個(gè)警告,因?yàn)榫幾g器一般不會(huì)去跟蹤查看函數(shù)increment()到底有沒有對(duì)nValue賦值。
未初始化變量更常出現(xiàn)于類中,成員的初始化一般是通過構(gòu)造函數(shù)的實(shí)現(xiàn)來完成的。
- class Foo
- {
- private:
- int m_nValue;
- public:
- Foo();
- int GetValue() { return m_bValue; }
- };
- Foo::Foo()
- {
- // Oops, 我們忘記初始化m_nValue了
- }
- int main()
- {
- Foo cFoo;
- if (cFoo.GetValue() > 0)
- // do something
- else
- // do something else
- }
注意,m_nValue從未初始化過。結(jié)果就是,GetValue()返回的是一個(gè)垃圾值,if語句的兩個(gè)分支都有可能會(huì)執(zhí)行。
新手程序員通常在定義多個(gè)變量時(shí)會(huì)犯下面這種錯(cuò)誤:
- int nValue1, nValue2 = 5;
這里的本意是nValue1和nValue2都被初始化為5,但實(shí)際上只有nValue2被初始化了,nValue1從未被初始化過。
由于未初始化的變量可能是任何值,因此會(huì)導(dǎo)致程序每次執(zhí)行時(shí)呈現(xiàn)出不同的行為,由未初始化變量而引發(fā)的問題是很難找到問題根源的。某次執(zhí)行時(shí),程序可能工作正常,下一次再執(zhí)行時(shí),它可能會(huì)崩潰,而再下一次則可能產(chǎn)生錯(cuò)誤的輸出。當(dāng)你在調(diào)試器下運(yùn)行程序時(shí),定義的變量通常都被清零處理過了。這意味著你的程序在調(diào)試器下可能每次都是工作正常的,但在發(fā)布版中可能會(huì)間歇性的崩掉!如果你碰上了這種怪事,罪魁禍?zhǔn)壮36际俏闯跏蓟淖兞俊?/p>
2)整數(shù)除法
C++中的大多數(shù)二元操作都要求兩個(gè)操作數(shù)是同一類型。如果操作數(shù)的不同類型,其中一個(gè)操作數(shù)會(huì)提升到和另一個(gè)操作數(shù)相匹配的類型。在C++中,除法操作符可以被看做是2個(gè)不同的操作:其中一個(gè)操作于整數(shù)之上,另一個(gè)是操作于浮點(diǎn)數(shù)之上。如果操作數(shù)是浮點(diǎn)數(shù)類型,除法操作將返回一個(gè)浮點(diǎn)數(shù)的值:
- float fX = 7;
- float fY = 2;
- float fValue = fX / fY; // fValue = 3.5
如果操作數(shù)是整數(shù)類型,除法操作將丟棄任何小數(shù)部分,并只返回整數(shù)部分。
- int nX = 7;
- int nY = 2;
- int nValue = nX / nY; // nValue = 3
如果一個(gè)操作數(shù)是整型,另一個(gè)操作數(shù)是浮點(diǎn)型,則整型會(huì)提升為浮點(diǎn)型:
- float fX = 7.0;
- int nY = 2;
- float fValue = fX / nY;
- // nY 提升為浮點(diǎn)型,除法操作將返回浮點(diǎn)型值
- // fValue = 3.5
有很多新手程序員會(huì)嘗試寫下如下的代碼:
- int nX = 7;
- int nY = 2;
- float fValue = nX / nY; // fValue = 3(不是3.5哦!)
這里的本意是nX/nY將產(chǎn)生一個(gè)浮點(diǎn)型的除法操作,因?yàn)榻Y(jié)果是賦給一個(gè)浮點(diǎn)型變量的。但實(shí)際上并非如此。nX/nY首先被計(jì)算,結(jié)果是一個(gè)整型值,然后才會(huì)提升為浮點(diǎn)型并賦值給fValue。但在賦值之前,小數(shù)部分就已經(jīng)丟棄了。
要強(qiáng)制兩個(gè)整數(shù)采用浮點(diǎn)型除法,其中一個(gè)操作數(shù)需要類型轉(zhuǎn)換為浮點(diǎn)數(shù):
- int nX = 7;
- int nY = 2;
- float fValue = static_cast
(nX) / nY; // fValue = 3.5
因?yàn)閚X顯式的轉(zhuǎn)換為float型,nY將隱式地提升為float型,因此除法操作符將執(zhí)行浮點(diǎn)型除法,得到的結(jié)果就是3.5。
通常一眼看去很難說一個(gè)除法操作符究竟是執(zhí)行整數(shù)除法還是浮點(diǎn)型除法:
- z = x / y; // 這是整數(shù)除法還是浮點(diǎn)型除法?
但采用匈牙利命名法可以幫助我們消除這種疑惑,并阻止錯(cuò)誤的發(fā)生:
- int nZ = nX / nY; // 整數(shù)除法
- double dZ = dX / dY; // 浮點(diǎn)型除法
有關(guān)整數(shù)除法的另一個(gè)有趣的事情是,當(dāng)一個(gè)操作數(shù)是負(fù)數(shù)時(shí)C++標(biāo)準(zhǔn)并未規(guī)定如何截?cái)嘟Y(jié)果。造成的結(jié)果就是,編譯器可以自由地選擇向上截?cái)嗷蛘呦蛳陆財(cái)?!比如?5/2可以既可以計(jì)算為-3也可以計(jì)算為-2,這和編譯器是向下取整還是向0取整有關(guān)。大多數(shù)現(xiàn)代的編譯器是向0取整的。
3)= vs ==
這是個(gè)老問題,但很有價(jià)值。許多C++新手會(huì)弄混賦值操作符(=)和相等操作符(==)的意義。但即使是知道這兩種操作符差別的程序員也會(huì)犯下鍵盤敲擊錯(cuò)誤,這可能會(huì)導(dǎo)致結(jié)果是非預(yù)期的。
- // 如果nValue是0,返回1,否則返回nValue
- int foo(int nValue)
- {
- if (nValue = 0) // 這是個(gè)鍵盤敲擊錯(cuò)誤 !
- return 1;
- else
- return nValue;
- }
- int main()
- {
- std::cout << foo(0) << std::endl;
- std::cout << foo(1) << std::endl;
- std::cout << foo(2) << std::endl;
- return 0;
- }
函數(shù)foo()的本意是如果nValue是0,就返回1,否則就返回nValue的值。但由于無意中使用賦值操作符代替了相等操作符,程序?qū)a(chǎn)生非預(yù)期性的結(jié)果:
- 0
- 0
- 0
當(dāng)foo()中的if語句執(zhí)行時(shí),nValue被賦值為0。if (nValue = 0)實(shí)際上就成了if (nValue)。結(jié)果就是if條件為假,導(dǎo)致執(zhí)行else下的代碼,返回nValue的值,而這個(gè)值剛好就是賦值給nValue的0!因此這個(gè)函數(shù)將永遠(yuǎn)返回0。
在編譯器中將告警級(jí)別設(shè)置為***,當(dāng)發(fā)現(xiàn)條件語句中使用了賦值操作符時(shí)會(huì)給出一個(gè)警告信息,或者在條件判斷之外,應(yīng)該使用賦值操作符的地方誤用成了相等性測(cè)試,此時(shí)會(huì)提示該語句沒有做任何事情。只要你使用了較高的告警級(jí)別,這個(gè)問題本質(zhì)上都是可修復(fù)的。也有一些程序員喜歡采用一種技巧來避免=和==的混淆。即,在條件判斷中將常量寫在左邊,此時(shí)如果誤把==寫成=的話,將引發(fā)一個(gè)編譯錯(cuò)誤,因?yàn)槌A坎荒鼙毁x值。
4)混用有符號(hào)和無符號(hào)數(shù)
如同我們?cè)谡麛?shù)除法那一節(jié)中提到的,C++中大多數(shù)的二元操作符需要兩端的操作數(shù)是同一種類型。如果操作數(shù)是不同的類型,其中一個(gè)操作數(shù)將提升自己的類型以匹配另一個(gè)操作數(shù)。當(dāng)混用有符號(hào)和無符號(hào)數(shù)時(shí)這會(huì)導(dǎo)致出現(xiàn)一些非預(yù)期性的結(jié)果!考慮如下的例子:
- cout << 10 – 15u; // 15u是無符號(hào)整數(shù)
有人會(huì)說結(jié)果是-5。由于10是一個(gè)有符號(hào)整數(shù),而15是無符號(hào)整數(shù),類型提升規(guī)則在這里就需要起作用了。C++中的類型提升層次結(jié)構(gòu)看起來是這樣的:
- long double (***)
- double
- float
- unsigned long int
- long int
- unsigned int
- int (***)
因?yàn)閕nt類型比unsigned int要低,因此int要提升為unsigned int。幸運(yùn)的是,10已經(jīng)是個(gè)正整數(shù)了,因此類型提升并沒有使解釋這個(gè)值的方式發(fā)生改變。因此,上面的代碼相當(dāng)于:
- cout << 10u – 15u;
好,現(xiàn)在是該看看這個(gè)小把戲的時(shí)候了。因?yàn)槎际菬o符號(hào)整型,因此操作的結(jié)果也應(yīng)該是一個(gè)無符號(hào)整型的變量!10u-15u = -5u。但是無符號(hào)變量不包括負(fù)數(shù),因此-5這里將被解釋為4,294,967,291(假設(shè)是32位整數(shù))。因此,上面的代碼將打印出4,294,967,291而不是-5。
這種情況可以有更令人迷惑的形式:
- int nX;
- unsigned int nY;
- if (nX – nY < 0)
- // do something
由于類型轉(zhuǎn)換,這個(gè)if語句將永遠(yuǎn)判斷為假,這顯然不是程序員的原始意圖!
5) delete vs delete []
許多C++程序員忘記了關(guān)于new和delete操作符實(shí)際上有兩種形式:針對(duì)單個(gè)對(duì)象的版本,以及針對(duì)對(duì)象數(shù)組的版本。new操作符用來在堆上分配單個(gè)對(duì)象的內(nèi)存空間。如果對(duì)象是某個(gè)類類型,該對(duì)象的構(gòu)造函數(shù)將被調(diào)用。
- Foo *pScalar = new Foo;
delete操作符用來回收由new操作符分配的內(nèi)存空間。如果被銷毀的對(duì)象是類類型,則該對(duì)象的析構(gòu)函數(shù)將被調(diào)用。
- delete pScalar;
現(xiàn)在考慮如下的代碼片段:
- Foo *pArray = new Foo[10];
這行代碼為10個(gè)Foo對(duì)象的數(shù)組分配了內(nèi)存空間,因?yàn)橄聵?biāo)[10]放在了類型名之后,許多C++程序員沒有意識(shí)到實(shí)際上是操作符new[]被調(diào)用來完成分配空間的任務(wù)而不是new。new[]操作符確保每一個(gè)創(chuàng)建的對(duì)象都會(huì)調(diào)用該類的構(gòu)造函數(shù)一次。相反的,要?jiǎng)h除一個(gè)數(shù)組,需要使用delete[]操作符:
- delete[] pArray;
這將確保數(shù)組中的每個(gè)對(duì)象都會(huì)調(diào)用該類的析構(gòu)函數(shù)。如果delete操作符作用于一個(gè)數(shù)組會(huì)發(fā)生什么?數(shù)組中僅僅只有***個(gè)對(duì)象會(huì)被析構(gòu),因此會(huì)導(dǎo)致堆空間被破壞!
6) 復(fù)合表達(dá)式或函數(shù)調(diào)用的副作用
副作用是指一個(gè)操作符、表達(dá)式、語句或函數(shù)在該操作符、表達(dá)式、語句或函數(shù)完成規(guī)定的操作后仍然繼續(xù)做了某些事情。副作用有時(shí)候是有用的:
- x = 5;
賦值操作符的副作用是可以***地改變x的值。其他有副作用的C++操作符包括*=、/=、%=、+=、-=、<<=、>>=、&=、|=、^=以及聲名狼藉的++和—操作符。但是,在C++中有好幾個(gè)地方操作的順序是未定義的,那么這就會(huì)造成不一致的行為。比如:
- void multiply(int x, int y)
- {
- using namespace std;
- cout << x * y << endl;
- }
- int main()
- {
- int x = 5;
- std::cout << multiply(x, ++x);
- }
因?yàn)閷?duì)于函數(shù)multiply()的參數(shù)的計(jì)算順序是未定義的,因此上面的程序可能打印出30或36,這完全取決于x和++x誰先計(jì)算,誰后計(jì)算。
另一個(gè)稍顯奇怪的有關(guān)操作符的例子:
- int foo(int x)
- {
- return x;
- }
- int main()
- {
- int x = 5;
- std::cout << foo(x) * foo(++x);
- }
因?yàn)镃++的操作符中,其操作數(shù)的計(jì)算順序是未定義的(對(duì)于大多數(shù)操作符來說是這樣的,當(dāng)然有一些例外),上面的例子也可能會(huì)打印出30或36,這取決于究竟是左操作數(shù)先計(jì)算還是右操作數(shù)先計(jì)算。
另外,考慮如下的復(fù)合表達(dá)式:
- if (x == 1 && ++y == 2)
- // do something
程序員的本意可能是說:“如果x是1,且y的前自增值是2的話,完成某些處理”。但是,如果x不等于1,C++將采取短路求值法則,這意味著++y將永遠(yuǎn)不會(huì)計(jì)算!因此,只有當(dāng)x等于1時(shí),y才會(huì)自增。這很可能不是程序員的本意!一個(gè)好的經(jīng)驗(yàn)法則是把任何可能造成副作用的操作符都放到它們自己獨(dú)立的語句中去。
7)不帶break的switch語句
另一個(gè)新手程序員常犯的經(jīng)典錯(cuò)誤是忘記在switch語句塊中加上break:
- switch (nValue)
- {
- case 1: eColor = Color::BLUE;
- case 2: eColor = Color::PURPLE;
- case 3: eColor = Color::GREEN;
- default: eColor = Color::RED;
- }
當(dāng)switch表達(dá)式計(jì)算出的結(jié)果同case的標(biāo)簽值相同時(shí),執(zhí)行序列將從滿足的***個(gè)case語句處執(zhí)行。執(zhí)行序列將繼續(xù)下去,直到要么到達(dá)switch語句塊的末尾,或者遇到return、goto或break語句。其他的標(biāo)簽都將忽略掉!
考慮下如上的代碼,如果nValue為1時(shí)會(huì)發(fā)生什么。case 1滿足,所以eColor被設(shè)為Color::BLUE。繼續(xù)處理下一個(gè)語句,這又將eColor設(shè)為Color::PURPLE。下一個(gè)語句又將它設(shè)為了Color::GREEN。最終,在default中將其設(shè)為了Color::RED。實(shí)際上,不管nValue的值是多少,上述代碼片段都將把eColor設(shè)為Color::RED!
正確的方法是按照如下方式書寫:
- switch (nValue)
- {
- case 1: eColor = Color::BLUE; break;
- case 2: eColor = Color::PURPLE; break;
- case 3: eColor = Color::GREEN; break;
- default: eColor = Color::RED; break;
- }
break語句終止了case語句的執(zhí)行,因此eColor的值將保持為程序員所期望的那樣。盡管這是非?;A(chǔ)的switch/case邏輯,但很容易因?yàn)槁┑粢粋€(gè)break語句而造成不可避免的“瀑布式”執(zhí)行流。
8)在構(gòu)造函數(shù)中調(diào)用虛函數(shù)
考慮如下的程序:
- class Base
- {
- private:
- int m_nID;
- public:
- Base()
- {
- m_nID = ClassID();
- }
- // ClassID 返回一個(gè)class相關(guān)的ID號(hào)
- virtual int ClassID() { return 1;}
- int GetID() { return m_nID; }
- };
- class Derived: public Base
- {
- public:
- Derived()
- {
- }
- virtual int ClassID() { return 2;}
- };
- int main()
- {
- Derived cDerived;
- cout << cDerived.GetID(); // 打印出1,不是2!
- return 0;
- }
在這個(gè)程序中,程序員在基類的構(gòu)造函數(shù)中調(diào)用了虛函數(shù),期望它能被決議為派生類的Derived::ClassID()。但實(shí)際上不會(huì)這樣——程序的結(jié)果是打印出1而不是2。當(dāng)從基類繼承的派生類被實(shí)例化時(shí),基類對(duì)象先于派生類對(duì)象被構(gòu)造出來。這么做是因?yàn)榕缮惖某蓡T可能會(huì)對(duì)已經(jīng)初始化過的基類成員有依賴關(guān)系。結(jié)果就是當(dāng)基類的構(gòu)造函數(shù)被執(zhí)行時(shí),此時(shí)派生類對(duì)象根本就還沒有構(gòu)造出來!所以,此時(shí)任何對(duì)虛函數(shù)的調(diào)用都只會(huì)決議為基類的成員函數(shù),而不是派生類。
根據(jù)這個(gè)例子,當(dāng)cDerived的基類部分被構(gòu)造時(shí),其派生類的那一部分還不存在。因此,對(duì)函數(shù)ClassID的調(diào)用將決議為Base::ClassID()(不是Derived::ClassID()),這個(gè)函數(shù)將m_nID設(shè)為1。一旦cDerived的派生類部分也構(gòu)造好時(shí),在cDerived這個(gè)對(duì)象上,任何對(duì)ClassID()的調(diào)用都將如預(yù)期的那樣決議為Derived::ClassID()。
注意到其他的編程語言如C#和Java會(huì)將虛函數(shù)調(diào)用決議為繼承層次最深的那個(gè)class上,就算派生類還沒有被初始化也是這樣!C++的做法與這不同,這是為了程序員的安全而考慮的。這并不是說一種方式就一定好過另一種,這里僅僅是為了表示不同的編程語言在同一問題上可能有不同的表現(xiàn)行為。
結(jié) 論
因?yàn)檫@只是這個(gè)系列文章的***篇,我認(rèn)為以新手程序員可能遇到的基礎(chǔ)問題入手會(huì)比較合適。今后這個(gè)系列的文章將致力于解決更加復(fù)雜的編程錯(cuò)誤。無論一個(gè)程序員的經(jīng)驗(yàn)水平如何,錯(cuò)誤都是不可避免的,不管是因?yàn)橹R(shí)上的匱乏、輸入錯(cuò)誤或者只是一般的粗心大意。意識(shí)到其中最有可能造成麻煩的問題,這可以幫助減少它們出來?yè)v亂的可能性。雖然對(duì)于經(jīng)驗(yàn)和知識(shí)并沒有什么替代品,良好的單元測(cè)試可以幫我們?cè)趯⑦@些bug深埋于我們的代碼中之前將它們捕獲。
英文原文:ALEX
文章題目:C++編譯器無法捕捉到的8種錯(cuò)誤
本文來源:http://www.dlmjj.cn/article/cdhdcdg.html


咨詢
建站咨詢
