C++編譯器沒法捕獲到的8種毛病實例剖析。本站提示廣大學習愛好者:(C++編譯器沒法捕獲到的8種毛病實例剖析)文章只能為提供參考,不一定能成為您想要的結果。以下是C++編譯器沒法捕獲到的8種毛病實例剖析正文
本文實例剖析了C++編譯器沒法捕獲到的8種毛病,分享給年夜家供年夜家參考之用。有助於深刻懂得C++運轉道理,詳細剖析以下:
盡人皆知,C++是一種龐雜的編程說話,個中充斥了各類奧妙的圈套。在C++中簡直稀有不清的方法能把工作弄砸。榮幸的是,現在的編譯器曾經足夠智能化了,可以或許檢測出相當多的這類編程圈套並經由過程編譯毛病或編譯正告來告訴法式員。終究,假如處置適合的話,任何編譯器能檢討到的毛病都不會是甚麼年夜成績,由於它們在編譯時會被捕獲到,並在法式真正運轉前獲得處理。最壞的情形下,一個編譯器可以或許捕捉到的毛病只會形成法式員一些時光上的喪失,由於他們會尋覓處理編譯毛病的辦法並修改。
那些編譯器沒法捕捉到的毛病才是最風險的。這類毛病不太輕易發覺到,但能夠會招致嚴重的效果,好比不准確的輸入、數據被損壞和法式瓦解。跟著項目標收縮,代碼邏輯的龐雜度和浩瀚的履行途徑會掩飾住這些bug,招致這些bug只是間歇性的湧現,是以使得這類bug難以跟蹤和調試。雖然本文的這份列表關於有經歷的法式員來講年夜部門都只是回想,但這類bug發生的效果常常依據項目標范圍和貿易性質有分歧水平的加強後果。
這些示例全體都在Visual Studio 2005 Express上測試過,應用的是默許告警級別。依據你選擇的編譯器,你獲得的成果能夠會有所分歧。我激烈建議一切的法式員同伙都采取最高級級的告警級別!有一些編譯提醒在默許告警級別下能夠不會被標注為一個潛伏的成績,而在最高級級的告警級別下就會被捕獲到!
1)變量未初始化
變量未初始化是C++編程中最為罕見和易犯的毛病之一。在C++中,為變量所分派的內存空間其實不是完整“清潔的”,也不會在分派空間時主動做清零處置。其成果就是,一個未初始化的變量將包括某個值,但沒方法精確地曉得這個值是若干。另外,每次履行這個法式的時刻,該變量的值能夠都邑產生轉變。這就有能夠發生間歇性發生發火的成績,是特殊難以追蹤的。看看以下的代碼片斷:
if (bValue) // do A else // do B
假如bValue是未經初始化的變量,那末if語句的斷定成果就沒法肯定,兩個分支都能夠會履行。在普通情形下,編譯器會對未初始化的變量賜與提醒。上面的代碼片斷在年夜多半編譯器上都邑激發一個正告信息。
int foo() { int nX; return nX; }
然則,還有一些簡略的例子則不會發生正告:
void increment(int &nValue) { ++nValue; } int foo() { int nX; increment(nX); return nX; }
以上的代碼片斷能夠不會發生一個正告,由於編譯器普通不會去跟蹤檢查函數increment()究竟有無對nValue賦值。
未初始化變量更常湧現於類中,成員的初始化普通是經由過程結構函數的完成來完成的。
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從未初始化過。成果就是,GetValue()前往的是一個渣滓值,if語句的兩個分支都有能夠會履行。
老手法式員平日在界說多個變量時會犯上面這類毛病:
int nValue1, nValue2 = 5;
這裡的本意是nValue1和nValue2都被初始化為5,但現實上只要nValue2被初始化了,nValue1從未被初始化過。
因為未初始化的變量能夠是任何值,是以會招致法式每次履行時出現出分歧的行動,由未初始化變量而激發的成績是很難找到成績本源的。某次履行時,法式能夠任務正常,下一次再履行時,它能夠會瓦解,而再下一次則能夠發生毛病的輸入。當你在調試器下運轉法式時,界說的變量平日都被清零處置過了。這意味著你的法式在調試器下能夠每次都是任務正常的,但在宣布版中能夠會間歇性的崩失落!假如你碰上了這類怪事,禍首罪魁經常都是未初始化的變量。
2)整數除法
C++中的年夜多半二元操作都請求兩個操作數是統一類型。假如操作數的分歧類型,個中一個操作數會晉升到和另外一個操作數相婚配的類型。在C++中,除法操作符可以被看作是2個分歧的操作:個中一個操作於整數之上,另外一個是操作於浮點數之上。假如操作數是浮點數類型,除法操作將前往一個浮點數的值:
float fX = 7; float fY = 2; float fValue = fX / fY; // fValue = 3.5
假如操作數是整數類型,除法操作將拋棄任何小數部門,並只前往整數部門。
int nX = 7; int nY = 2; int nValue = nX / nY; // nValue = 3
假如一個操作數是整型,另外一個操作數是浮點型,則整型會晉升為浮點型:
float fX = 7.0; int nY = 2; float fValue = fX / nY; // nY 晉升為浮點型,除法操作將前往浮點型值 // fValue = 3.5
有許多老手法式員會測驗考試寫下以下的代碼:
int nX = 7; int nY = 2; float fValue = nX / nY; // fValue = 3(不是3.5哦!)
這裡的本意是nX/nY將發生一個浮點型的除法操作,由於成果是賦給一個浮點型變量的。但現實上並不是如斯。nX/nY起首被盤算,成果是一個整型值,然後才會晉升為浮點型並賦值給fValue。但在賦值之前,小數部門就曾經拋棄了。
要強迫兩個整數采取浮點型除法,個中一個操作數須要類型轉換為浮點數:
int nX = 7; int nY = 2; float fValue = static_cast<float>(nX) / nY; // fValue = 3.5
由於nX顯式的轉換為float型,nY將隱式地晉升為float型,是以除法操作符將履行浮點型除法,獲得的成果就是3.5。
平日一眼看去很難說一個除法操作符畢竟是履行整數除法照樣浮點型除法:
z = x / y; // 這是整數除法照樣浮點型除法?
但采取匈牙利定名法可以贊助我們清除這類困惑,並阻攔毛病的產生:
int nZ = nX / nY; // 整數除法 double dZ = dX / dY; // 浮點型除法
有關整數除法的另外一個風趣的工作是,當一個操作數是正數時C++尺度並未劃定若何截斷成果。形成的成果就是,編譯器可以自在地選擇向上截斷或許向下截斷!好比,-5/2可以既可以盤算為-3也能夠盤算為-2,這和編譯器是向下取整照樣向0取整有關。年夜多半古代的編譯器是向0取整的。
3)= vs ==
這是個老成績,但很有價值。很多C++老手會弄混賦值操作符(=)和相等操作符(==)的意義。但即便是曉得這兩種操作符差異的法式員也會犯下鍵盤敲擊毛病,這能夠會招致成果長短預期的。
// 假如nValue是0,前往1,不然前往nValue int foo(int nValue) { if (nValue = 0) // 這是個鍵盤敲擊毛病 ! 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; }
函數foo()的本意是假如nValue是0,就前往1,不然就前往nValue的值。但因為有意中應用賦值操作符取代了相等操作符,法式將發生非預期性的成果:
0 0 0
當foo()中的if語句履行時,nValue被賦值為0。if (nValue = 0)現實上就成了if (nValue)。成果就是if前提為假,招致履行else下的代碼,前往nValue的值,而這個值恰好就是賦值給nValue的0!是以這個函數將永久前往0。
在編譯器中將告警級別設置為最高,當發明前提語句中應用了賦值操作符時會給出一個正告信息,或許在前提斷定以外,應當應用賦值操作符的處所誤用成了相等性測試,此時會提醒該語句沒有做任何工作。只需你應用了較高的告警級別,這個成績實質上都是可修復的。也有一些法式員愛好采取一種技能來防止=和==的混雜。即,在前提斷定中將常量寫在右邊,此時假如誤把==寫成=的話,將激發一個編譯毛病,由於常量不克不及被賦值。
4)混用有符號和無符號數
好像我們在整數除法那一節中提到的,C++中年夜多半的二元操作符須要兩頭的操作數是統一品種型。假如操作數是分歧的類型,個中一個操作數將晉升本身的類型以婚配另外一個操作數。當混用有符號和無符號數時這會招致湧現一些非預期性的成果!斟酌以下的例子:
cout << 10 – 15u; // 15u是無符號整數
有人會說成果是-5。因為10是一個有符號整數,而15是無符號整數,類型晉升規矩在這裡就須要起感化了。C++中的類型晉升條理構造看起來是如許的:
long double (最高)
double
float
unsigned long int
long int
unsigned int
int (最低)
由於int類型比unsigned int要低,是以int要晉升為unsigned int。榮幸的是,10曾經是個正整數了,是以類型晉升並沒有使說明這個值的方法產生轉變。是以,下面的代碼相當於:
cout << 10u – 15u;
好,如今是該看看這個小花招的時刻了。由於都是無符號整型,是以操作的成果也應當是一個無符號整型的變量!10u-15u = -5u。然則無符號變量不包含正數,是以-5這裡將被說明為4,294,967,291(假定是32位整數)。是以,下面的代碼將打印出4,294,967,291而不是-5。
這類情形可以有更使人困惑的情勢:
int nX; unsigned int nY; if (nX – nY < 0) // do something
因為類型轉換,這個if語句將永久斷定為假,這明顯不是法式員的原始意圖!
5) delete vs delete []
很多C++法式員忘卻了關於new和delete操作符現實上有兩種情勢:針對單個對象的版本,和針對對象數組的版本。new操作符用來在堆上分派單個對象的內存空間。假如對象是某個類類型,該對象的結構函數將被挪用。
Foo *pScalar = new Foo;
delete操作符用往返收由new操作符分派的內存空間。假如被燒毀的對象是類類型,則該對象的析構函數將被挪用。
delete pScalar;
如今斟酌以下的代碼片斷:
Foo *pArray = new Foo[10];
這行代碼為10個Foo對象的數組分派了內存空間,由於下標[10]放在了類型名以後,很多C++法式員沒無意識到現實上是操作符new[]被挪用來完成份配空間的義務而不是new。new[]操作符確保每個創立的對象都邑挪用該類的結構函數一次。相反的,要刪除一個數組,須要應用delete[]操作符:
delete[] pArray;
這將確保數組中的每一個對象都邑挪用該類的析構函數。假如delete操作符感化於一個數組會產生甚麼?數組中僅僅只要第一個對象會被析構,是以會招致堆空間被損壞!
6) 復合表達式或函數挪用的反作用
反作用是指一個操作符、表達式、語句或函數在該操作符、表達式、語句或函數完陳規定的操作後依然持續做了某些工作。反作用有時刻是有效的:
x = 5;
賦值操作符的反作用是可以永遠地轉變x的值。其他有反作用的C++操作符包含*=、/=、%=、+=、-=、<<=、>>=、&=、|=、^=和申明狼籍的++和—操作符。然則,在C++中有好幾個處所操作的次序是不決義的,那末這就會形成紛歧致的行動。好比:
void multiply(int x, int y) { using namespace std; cout << x * y << endl; } int main() { int x = 5; std::cout << multiply(x, ++x); }
由於關於函數multiply()的參數的盤算次序是不決義的,是以下面的法式能夠打印出30或36,這完整取決於x和++x誰先盤算,誰後盤算。
另外一個稍顯奇異的有關操作符的例子:
int foo(int x) { return x; } int main() { int x = 5; std::cout << foo(x) * foo(++x); }
由於C++的操作符中,其操作數的盤算次序是不決義的(關於年夜多半操作符來講是如許的,固然有一些破例),下面的例子也能夠會打印出30或36,這取決於畢竟是左操作數先盤算照樣右操作數先盤算。
別的,斟酌以下的復合表達式:
if (x == 1 && ++y == 2) // do something
法式員的本意能夠是說:“假如x是1,且y的前自增值是2的話,完成某些處置”。然則,假如x不等於1,C++將采用短路求值軌則,這意味著++y將永久不管帳算!是以,只要當x等於1時,y才會自增。這極可能不是法式員的本意!一個好的經歷軌則是把任何能夠形成反作用的操作符都放到它們本身自力的語句中去。
7)不帶break的switch語句
另外一個老手法式員常犯的經典毛病是忘卻在switch語句塊中加上break:
switch (nValue) { case 1: eColor = Color::BLUE; case 2: eColor = Color::PURPLE; case 3: eColor = Color::GREEN; default: eColor = Color::RED; }
當switch表達式盤算出的成果同case的標簽值雷同時,履行序列將從知足的第一個case語句處履行。履行序列將持續下去,直到要末達到switch語句塊的末尾,或許碰到return、goto或break語句。其他的標簽都將疏忽失落!
斟酌下如上的代碼,假如nValue為1時會產生甚麼。case 1知足,所以eColor被設為Color::BLUE。持續處置下一個語句,這又將eColor設為Color::PURPLE。下一個語句又將它設為了Color::GREEN。終究,在default中將其設為了Color::RED。現實上,不論nValue的值是若干,上述代碼片斷都將把eColor設為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語句的履行,是以eColor的值將堅持為法式員所希冀的那樣。雖然這長短常基本的switch/case邏輯,但很輕易由於漏失落一個break語句而形成弗成防止的“瀑布式”履行流。
8)在結構函數中挪用虛函數
斟酌以下的法式:
class Base { private: int m_nID; public: Base() { m_nID = ClassID(); } // ClassID 前往一個class相干的ID號 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; }
在這個法式中,法式員在基類的結構函數中挪用了虛函數,希冀它能被決定為派生類的Derived::ClassID()。但現實上不會如許——法式的成果是打印出1而不是2。當從基類繼續的派生類被實例化時,基類對象先於派生類對象被結構出來。這麼做是由於派生類的成員能夠會對曾經初始化過的基類成員有依附關系。成果就是當基類的結構函數被履行時,此時派生類對象基本就還沒有結構出來!所以,此時任何對虛函數的挪用都只會決定為基類的成員函數,而不是派生類。
依據這個例子,當cDerived的基類部門被結構時,其派生類的那一部門還不存在。是以,對函數ClassID的挪用將決定為Base::ClassID()(不是Derived::ClassID()),這個函數將m_nID設為1。一旦cDerived的派生類部門也結構好時,在cDerived這個對象上,任何對ClassID()的挪用都將如預期的那樣決定為Derived::ClassID()。
留意到其他的編程說話如C#和Java會將虛函數挪用決定為繼續條理最深的誰人class上,就算派生類還沒有被初始化也是如許!C++的做法與這分歧,這是為了法式員的平安而斟酌的。這其實不是說一種方法就必定好過另外一種,這裡僅僅是為了表現分歧的編程說話在統一成績上能夠有分歧的表示行動。
結論:
小我以為以老手法式員能夠碰到的基本成績動手會比擬適合。不管一個法式員的經歷程度若何,毛病都是弗成防止的,不論是由於常識上的匮乏、輸出毛病或許只是普通的粗枝大葉。認識到個中最有能夠形成費事的成績,這可以贊助削減它們出來擾亂的能夠性。固然關於經歷和常識並沒有甚麼替換品,優越的單位測試可以幫我們在將這些bug深埋於我們的代碼中之前將它們捕捉。
信任本文所述對年夜家的C++法式設計有必定的進修自創價值。