最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++編譯器無法捕捉到的8種錯誤實例分析

 更新時間:2014年09月04日 15:02:22   投稿:shichen2014  
這篇文章主要介紹了C++編譯器無法捕捉到的8種錯誤,是深入學習C++所必須加以掌握的排錯技能,需要的朋友可以參考下

本文實例分析了C++編譯器無法捕捉到的8種錯誤,分享給大家供大家參考之用。有助于深入理解C++運行原理,具體分析如下:

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

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

這些示例全部都在Visual Studio 2005 Express上測試過,使用的是默認告警級別。根據你選擇的編譯器,你得到的結果可能會有所不同。我強烈建議所有的程序員朋友都采用最高等級的告警級別!有一些編譯提示在默認告警級別下可能不會被標注為一個潛在的問題,而在最高等級的告警級別下就會被捕捉到!

1)變量未初始化

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

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

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

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語句的兩個分支都有可能會執(zhí)行。

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

int nValue1, nValue2 = 5;

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

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

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型,因此除法操作符將執(zhí)行浮點型除法,得到的結果就是3.5。

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

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

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

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

有關整數除法的另一個有趣的事情是,當一個操作數是負數時C++標準并未規(guī)定如何截斷結果。造成的結果就是,編譯器可以自由地選擇向上截斷或者向下截斷!比如,-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語句執(zhí)行時,nValue被賦值為0。if (nValue = 0)實際上就成了if (nValue)。結果就是if條件為假,導致執(zhí)行else下的代碼,返回nValue的值,而這個值剛好就是賦值給nValue的0!因此這個函數將永遠返回0。

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

4)混用有符號和無符號數

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

cout << 10 – 15u; // 15u是無符號整數

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

long double (最高)
double
float
unsigned long int
long int
unsigned int
int               (最低)

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

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[]操作符確保每一個創(chuàng)建的對象都會調用該類的構造函數一次。相反的,要刪除一個數組,需要使用delete[]操作符:

delete[] pArray;

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

6)  復合表達式或函數調用的副作用

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

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的標簽值相同時,執(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)在構造函數中調用虛函數

考慮如下的程序:

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。當從基類繼承的派生類被實例化時,基類對象先于派生類對象被構造出來。這么做是因為派生類的成員可能會對已經初始化過的基類成員有依賴關系。結果就是當基類的構造函數被執(zhí)行時,此時派生類對象根本就還沒有構造出來!所以,此時任何對虛函數的調用都只會決議為基類的成員函數,而不是派生類。

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

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

結論:

個人認為以新手程序員可能遇到的基礎問題入手會比較合適。無論一個程序員的經驗水平如何,錯誤都是不可避免的,不管是因為知識上的匱乏、輸入錯誤或者只是一般的粗心大意。意識到其中最有可能造成麻煩的問題,這可以幫助減少它們出來搗亂的可能性。雖然對于經驗和知識并沒有什么替代品,良好的單元測試可以幫我們在將這些bug深埋于我們的代碼中之前將它們捕獲。

相信本文所述對大家的C++程序設計有一定的學習借鑒價值。

相關文章

  • C語言實現小貓釣魚算法

    C語言實現小貓釣魚算法

    這篇文章主要為大家詳細介紹了C語言實現小貓釣魚算法,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-01-01
  • C++詳細講解print緩沖區(qū)的刷新

    C++詳細講解print緩沖區(qū)的刷新

    這篇文章主要介紹了print緩沖區(qū)刷新問題,實現代碼簡單易懂,具有很好的參考價值,希望對大家有所幫助,需要的朋友可以參考下
    2022-05-05
  • Qt實現簡易秒表設計

    Qt實現簡易秒表設計

    這篇文章主要為大家詳細介紹了Qt實現簡易秒表設計,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-08-08
  • c++從函數返回數組的方法代碼

    c++從函數返回數組的方法代碼

    在本篇文章里小編給大家整理的是關于c++從函數返回數組的方法代碼,有興趣的朋友們可以學習參考下。
    2020-02-02
  • C語言實現flappy bird游戲

    C語言實現flappy bird游戲

    這篇文章主要為大家詳細介紹了C語言實現flappy bird小游戲,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-12-12
  • 基于Qt OpenCV實現圖像數據采集軟件

    基于Qt OpenCV實現圖像數據采集軟件

    這篇文章主要為大家詳細介紹了如何利用Qt+OpenCV實現圖像數據采集軟件,文中的示例代碼講解詳細,對我學習或工作有一定參考價值,感興趣的可以了解一下
    2022-07-07
  • C++編程中逗號運算符和條件運算符的使用方法講解

    C++編程中逗號運算符和條件運算符的使用方法講解

    這篇文章主要介紹了C++編程中逗號運算符和條件運算符的使用方法講解,需要的朋友可以參考下
    2016-01-01
  • C語言編寫學生成績管理系統(tǒng)

    C語言編寫學生成績管理系統(tǒng)

    這篇文章主要為大家詳細介紹了C語言編寫學生成績管理系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-01-01
  • C++中int類型按字節(jié)打印輸出的方法

    C++中int類型按字節(jié)打印輸出的方法

    這篇文章主要給大家介紹了關于C++中int類型按字節(jié)打印輸出的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用C++具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧
    2019-05-05
  • Vscode中接入Deepseek的實現

    Vscode中接入Deepseek的實現

    本文主要介紹了Vscode中接入Deepseek的實現,包括登錄Deepseek官網、申請APIKEY、安裝和配置VSCode插件,具有一定的參考價值,感興趣的可以了解一下
    2025-02-02

最新評論

五大连池市| 新兴县| 大化| 呼和浩特市| 余江县| 文成县| 原平市| 隆昌县| 垦利县| 万全县| 师宗县| 安溪县| 玉林市| 班玛县| 张家口市| 定西市| 嘉鱼县| 屏边| 汤阴县| 绵阳市| 鄂温| 清流县| 富源县| 新和县| 长白| 太原市| 如皋市| 沭阳县| 北安市| 寻乌县| 于都县| 来安县| 武汉市| 原阳县| 贵溪市| 二连浩特市| 治县。| 紫金县| 武穴市| 黑龙江省| 榆树市|