溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

C++編譯器無法捕捉到的錯誤有哪些

發(fā)布時間:2021-11-30 15:59:40 來源:億速云 閱讀:133 作者:iii 欄目:編程語言

這篇文章主要講解了“C++編譯器無法捕捉到的錯誤有哪些”,文中的講解內(nèi)容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“C++編譯器無法捕捉到的錯誤有哪些”吧!

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

那些編譯器無法捕獲到的錯誤才是最危險的。這類錯誤不太容易察覺到,但可能會導致嚴重的后果,比如不正確的輸出、數(shù)據(jù)被破壞以及程序崩潰。隨著項目的膨脹,代碼邏輯的復雜度以及眾多的執(zhí)行路徑會掩蓋住這些bug,導致這些bug只是間歇性的出現(xiàn),因此使得這類bug難以跟蹤和調(diào)試。盡管本文的這份列表對于有經(jīng)驗的程序員來說大部分都只是回顧,但這類bug產(chǎn)生的后果往往根據(jù)項目的規(guī)模和商業(yè)性質(zhì)有不同程度的增強效果。

1)變量未初始化

變量未初始化是C++編程中最為常見和易犯的錯誤之一。在C++中,為變量所分配的內(nèi)存空間并不是完全“干凈的”,也不會在分配空間時自動做清零處理。其結(jié)果就是,一個未初始化的變量將包含某個值,但沒辦法準確地知道這個值是多少。此外,每次執(zhí)行這個程序的時候,該變量的值可能都會發(fā)生改變。這就有可能產(chǎn)生間歇性發(fā)作的問題,是特別難以追蹤的??纯慈缦碌拇a片段:

if (bValue)       // do A  else      // do B

如果bValue是未經(jīng)初始化的變量,那么if語句的判斷結(jié)果就無法確定,兩個分支都可能會執(zhí)行。在一般情況下,編譯器會對未初始化的變量給予提示。下面的代碼片段在大多數(shù)編譯器上都會引發(fā)一個警告信息。

int foo()  {      int nX;      return nX;  }

但是,還有一些簡單的例子則不會產(chǎn)生警告:

void increment(int &nValue)  {      ++nValue;  }  int foo()  {      int nX;      increment(nX);      return nX;  }

以上的代碼片段可能不會產(chǎn)生一個警告,因為編譯器一般不會去跟蹤查看函數(shù)increment()到底有沒有對nValue賦值。

未初始化變量更常出現(xiàn)于類中,成員的初始化一般是通過構(gòu)造函數(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()返回的是一個垃圾值,if語句的兩個分支都有可能會執(zhí)行。

新手程序員通常在定義多個變量時會犯下面這種錯誤:

int nValue1, nValue2 = 5;

這里的本意是nValue1和nValue2都被初始化為5,但實際上只有nValue2被初始化了,nValue1從未被初始化過。

由于未初始化的變量可能是任何值,因此會導致程序每次執(zhí)行時呈現(xiàn)出不同的行為,由未初始化變量而引發(fā)的問題是很難找到問題根源的。某次執(zhí)行時,程序可能工作正常,下一次再執(zhí)行時,它可能會崩潰,而再下一次則可能產(chǎn)生錯誤的輸出。當你在調(diào)試器下運行程序時,定義的變量通常都被清零處理過了。這意味著你的程序在調(diào)試器下可能每次都是工作正常的,但在發(fā)布版中可能會間歇性的崩掉!如果你碰上了這種怪事,罪魁禍首常常都是未初始化的變量。

2)整數(shù)除法

C++中的大多數(shù)二元操作都要求兩個操作數(shù)是同一類型。如果操作數(shù)的不同類型,其中一個操作數(shù)會提升到和另一個操作數(shù)相匹配的類型。在C++中,除法操作符可以被看做是2個不同的操作:其中一個操作于整數(shù)之上,另一個是操作于浮點數(shù)之上。如果操作數(shù)是浮點數(shù)類型,除法操作將返回一個浮點數(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

如果一個操作數(shù)是整型,另一個操作數(shù)是浮點型,則整型會提升為浮點型:

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將產(chǎn)生一個浮點型的除法操作,因為結(jié)果是賦給一個浮點型變量的。但實際上并非如此。nX/nY首先被計算,結(jié)果是一個整型值,然后才會提升為浮點型并賦值給fValue。但在賦值之前,小數(shù)部分就已經(jīng)丟棄了。

要強制兩個整數(shù)采用浮點型除法,其中一個操作數(shù)需要類型轉(zhuǎn)換為浮點數(shù):

int nX = 7;  int nY = 2;  float fValue = static_cast<float>(nX) / nY;  // fValue = 3.5

因為nX顯式的轉(zhuǎn)換為float型,nY將隱式地提升為float型,因此除法操作符將執(zhí)行浮點型除法,得到的結(jié)果就是3.5。

通常一眼看去很難說一個除法操作符究竟是執(zhí)行整數(shù)除法還是浮點型除法:

z = x / y; // 這是整數(shù)除法還是浮點型除法?

但采用匈牙利命名法可以幫助我們消除這種疑惑,并阻止錯誤的發(fā)生:

int nZ = nX / nY;     // 整數(shù)除法  double dZ = dX / dY; // 浮點型除法

有關整數(shù)除法的另一個有趣的事情是,當一個操作數(shù)是負數(shù)時C++標準并未規(guī)定如何截斷結(jié)果。造成的結(jié)果就是,編譯器可以自由地選擇向上截斷或者向下截斷!比如,-5/2可以既可以計算為-3也可以計算為-2,這和編譯器是向下取整還是向0取整有關。大多數(shù)現(xiàn)代的編譯器是向0取整的。

3)= vs ==

這是個老問題,但很有價值。許多C++新手會弄混賦值操作符(=)和相等操作符(==)的意義。但即使是知道這兩種操作符差別的程序員也會犯下鍵盤敲擊錯誤,這可能會導致結(jié)果是非預期的。

// 如果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;  }

函數(shù)foo()的本意是如果nValue是0,就返回1,否則就返回nValue的值。但由于無意中使用賦值操作符代替了相等操作符,程序?qū)a(chǎn)生非預期性的結(jié)果:

0  0  0

當foo()中的if語句執(zhí)行時,nValue被賦值為0。if (nValue = 0)實際上就成了if (nValue)。結(jié)果就是if條件為假,導致執(zhí)行else下的代碼,返回nValue的值,而這個值剛好就是賦值給nValue的0!因此這個函數(shù)將永遠返回0。

在編譯器中將告警級別設置為***,當發(fā)現(xiàn)條件語句中使用了賦值操作符時會給出一個警告信息,或者在條件判斷之外,應該使用賦值操作符的地方誤用成了相等性測試,此時會提示該語句沒有做任何事情。只要你使用了較高的告警級別,這個問題本質(zhì)上都是可修復的。也有一些程序員喜歡采用一種技巧來避免=和==的混淆。即,在條件判斷中將常量寫在左邊,此時如果誤把==寫成=的話,將引發(fā)一個編譯錯誤,因為常量不能被賦值。

4)混用有符號和無符號數(shù)

如同我們在整數(shù)除法那一節(jié)中提到的,C++中大多數(shù)的二元操作符需要兩端的操作數(shù)是同一種類型。如果操作數(shù)是不同的類型,其中一個操作數(shù)將提升自己的類型以匹配另一個操作數(shù)。當混用有符號和無符號數(shù)時這會導致出現(xiàn)一些非預期性的結(jié)果!考慮如下的例子:

cout << 10 &ndash; 15u; // 15u是無符號整數(shù)

有人會說結(jié)果是-5。由于10是一個有符號整數(shù),而15是無符號整數(shù),類型提升規(guī)則在這里就需要起作用了。C++中的類型提升層次結(jié)構(gòu)看起來是這樣的:

long double (***)  double float unsigned long int long int unsigned int int               (***)

因為int類型比unsigned int要低,因此int要提升為unsigned int。幸運的是,10已經(jīng)是個正整數(shù)了,因此類型提升并沒有使解釋這個值的方式發(fā)生改變。因此,上面的代碼相當于:

cout << 10u &ndash; 15u;

好,現(xiàn)在是該看看這個小把戲的時候了。因為都是無符號整型,因此操作的結(jié)果也應該是一個無符號整型的變量!10u-15u = -5u。但是無符號變量不包括負數(shù),因此-5這里將被解釋為4,294,967,291(假設是32位整數(shù))。因此,上面的代碼將打印出4,294,967,291而不是-5。

這種情況可以有更令人迷惑的形式:

int nX;  unsigned int nY;  if (nX &ndash; nY < 0)      // do something

由于類型轉(zhuǎn)換,這個if語句將永遠判斷為假,這顯然不是程序員的原始意圖!

5) delete vs delete []

許多C++程序員忘記了關于new和delete操作符實際上有兩種形式:針對單個對象的版本,以及針對對象數(shù)組的版本。new操作符用來在堆上分配單個對象的內(nèi)存空間。如果對象是某個類類型,該對象的構(gòu)造函數(shù)將被調(diào)用。

Foo *pScalar = new Foo;

delete操作符用來回收由new操作符分配的內(nèi)存空間。如果被銷毀的對象是類類型,則該對象的析構(gòu)函數(shù)將被調(diào)用。

delete pScalar;

現(xiàn)在考慮如下的代碼片段:

Foo *pArray = new Foo[10];

這行代碼為10個Foo對象的數(shù)組分配了內(nèi)存空間,因為下標[10]放在了類型名之后,許多C++程序員沒有意識到實際上是操作符new[]被調(diào)用來完成分配空間的任務而不是new。new[]操作符確保每一個創(chuàng)建的對象都會調(diào)用該類的構(gòu)造函數(shù)一次。相反的,要刪除一個數(shù)組,需要使用delete[]操作符:

delete[] pArray;

這將確保數(shù)組中的每個對象都會調(diào)用該類的析構(gòu)函數(shù)。如果delete操作符作用于一個數(shù)組會發(fā)生什么?數(shù)組中僅僅只有***個對象會被析構(gòu),因此會導致堆空間被破壞!

6) 復合表達式或函數(shù)調(diào)用的副作用

副作用是指一個操作符、表達式、語句或函數(shù)在該操作符、表達式、語句或函數(shù)完成規(guī)定的操作后仍然繼續(xù)做了某些事情。副作用有時候是有用的:

x = 5;

賦值操作符的副作用是可以***地改變x的值。其他有副作用的C++操作符包括*=、/=、%=、+=、-=、<<=、>>=、&=、|=、^=以及聲名狼藉的++和&mdash;操作符。但是,在C++中有好幾個地方操作的順序是未定義的,那么這就會造成不一致的行為。比如:

void multiply(int x, int y)  {      using namespace std;      cout << x * y << endl;  }     int main()  {      int x = 5;      std::cout << multiply(x, ++x);  }

因為對于函數(shù)multiply()的參數(shù)的計算順序是未定義的,因此上面的程序可能打印出30或36,這完全取決于x和++x誰先計算,誰后計算。

另一個稍顯奇怪的有關操作符的例子:

int foo(int x)  {      return x;  }     int main()  {      int x = 5;      std::cout << foo(x) * foo(++x);  }

因為C++的操作符中,其操作數(shù)的計算順序是未定義的(對于大多數(shù)操作符來說是這樣的,當然有一些例外),上面的例子也可能會打印出30或36,這取決于究竟是左操作數(shù)先計算還是右操作數(shù)先計算。

另外,考慮如下的復合表達式:

if (x == 1 && ++y == 2)      // do something

程序員的本意可能是說:“如果x是1,且y的前自增值是2的話,完成某些處理”。但是,如果x不等于1,C++將采取短路求值法則,這意味著++y將永遠不會計算!因此,只有當x等于1時,y才會自增。這很可能不是程序員的本意!一個好的經(jīng)驗法則是把任何可能造成副作用的操作符都放到它們自己獨立的語句中去。

7)不帶break的switch語句

另一個新手程序員常犯的經(jīng)典錯誤是忘記在switch語句塊中加上break:

switch (nValue)  {      case 1: eColor = Color::BLUE;      case 2: eColor = Color::PURPLE;      case 3: eColor = Color::GREEN;      default: eColor = Color::RED;  }

當switch表達式計算出的結(jié)果同case的標簽值相同時,執(zhí)行序列將從滿足的***個case語句處執(zhí)行。執(zhí)行序列將繼續(xù)下去,直到要么到達switch語句塊的末尾,或者遇到return、goto或break語句。其他的標簽都將忽略掉!

考慮下如上的代碼,如果nValue為1時會發(fā)生什么。case 1滿足,所以eColor被設為Color::BLUE。繼續(xù)處理下一個語句,這又將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語句的執(zhí)行,因此eColor的值將保持為程序員所期望的那樣。盡管這是非?;A的switch/case邏輯,但很容易因為漏掉一個break語句而造成不可避免的“瀑布式”執(zhí)行流。

8)在構(gòu)造函數(shù)中調(diào)用虛函數(shù)

考慮如下的程序:

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;  }

在這個程序中,程序員在基類的構(gòu)造函數(shù)中調(diào)用了虛函數(shù),期望它能被決議為派生類的Derived::ClassID()。但實際上不會這樣&mdash;&mdash;程序的結(jié)果是打印出1而不是2。當從基類繼承的派生類被實例化時,基類對象先于派生類對象被構(gòu)造出來。這么做是因為派生類的成員可能會對已經(jīng)初始化過的基類成員有依賴關系。結(jié)果就是當基類的構(gòu)造函數(shù)被執(zhí)行時,此時派生類對象根本就還沒有構(gòu)造出來!所以,此時任何對虛函數(shù)的調(diào)用都只會決議為基類的成員函數(shù),而不是派生類。

根據(jù)這個例子,當cDerived的基類部分被構(gòu)造時,其派生類的那一部分還不存在。因此,對函數(shù)ClassID的調(diào)用將決議為Base::ClassID()(不是Derived::ClassID()),這個函數(shù)將m_nID設為1。一旦cDerived的派生類部分也構(gòu)造好時,在cDerived這個對象上,任何對ClassID()的調(diào)用都將如預期的那樣決議為Derived::ClassID()。

注意到其他的編程語言如C#和Java會將虛函數(shù)調(diào)用決議為繼承層次最深的那個class上,就算派生類還沒有被初始化也是這樣!C++的做法與這不同,這是為了程序員的安全而考慮的。這并不是說一種方式就一定好過另一種,這里僅僅是為了表示不同的編程語言在同一問題上可能有不同的表現(xiàn)行為。

感謝各位的閱讀,以上就是“C++編譯器無法捕捉到的錯誤有哪些”的內(nèi)容了,經(jīng)過本文的學習后,相信大家對C++編譯器無法捕捉到的錯誤有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

c++
AI