C++編程易範的錯誤

來源:互聯網
上載者:User

C/C++語言中有許多對初學者(甚至是有經驗的編程人員)來說很容易範的錯誤。通曉這樣的錯誤可使你免於陷入其中。
忘記初始化指標
這種錯誤只是一般"忘記初始設定變數"錯誤的一個特殊形式(C/C++中變數不會自動初始化,而Basic可以)。使這種錯誤更糟糕的原因是它的後果往往更加糟糕:
void SomeFunction()
{
int *pnVar
int nVal;
nVal = *pnVar; // Bad enough.
*pnVar = nVal; // Much worse.
}
在這個例子中,指標變數pnVar從未被賦值。因此你必須假設它含有的是雜亂的資料,從一個混亂資訊指標中讀數糟糕的很,因為結果肯定是雜亂資料,向一個混亂資訊指標寫資料更糟,因為它將導致一些不知道什麼地方的資料被重寫。
如果被重寫的地區無用,這到沒什麼危害。如果被重寫的地區有用,資料就會丟失。這種類型的錯誤那麼難找,是因為直到程式企圖使用已丟失的資料時問題才會呈現出來。這種問題可能是在資料丟失後好久才發生的。
由於這一問題手工判斷很困難,Visual C++編譯器就通過一些努力來避免它的發生。例如,當你編譯上述函數時就會產生一個警告。在這種情況下,編譯器會告訴你變數在使用前未被賦值。在很多情況下,它不可能告訴你。
Windows 95作業系統試圖用保護儲存空間在一定程度上協助解決難題:如果應用程式企圖從不屬於它的儲存空間讀或寫,Windows通常能截獲該請求,並立即終止該程式。可惜,Windows 95不能截獲對應用程式擁有的儲存空間的無效訪問,它也不能截獲所有非法訪問,因為必須保留某些缺口,以與Windows 3.1的相容性名義開放。
忘記釋放堆記憶體
請記住從堆獲得分配的任何記憶體都必須要釋放。如果你用完記憶體以後,忘記釋放它,系統記憶體就會變得愈來愈小,直到最後你的程式不能運行而崩潰。
這個問題會出現在諸如下列的一些情況中:
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);
}
}
在此例中,函數GoToTheStore()首先分配一輛新車來開——這有點浪費,但你肯定會同意這種演算法可以正常工作。只要分配了新車,它就會開到有調用pCar->Drive(Store)所指向的商店。
問題是在它安全到達目的地之後,函數不破壞Car對象。它只是簡單地退出,從而使記憶體丟失。
通常,當對象pCar出了程式中的範圍時,程式員應該依靠解構函式~Car釋放記憶體。但這裡辦不到,因為pCar的類型不是Car而是Car*,當pCar出了範圍時不會調用解構函式。
修正的函數如下:
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操作符構造的對象都應該用delete運算子刪除,這一點必須牢記。
返回對局部記憶體的引用
另一個常見的與記憶體有關的問題是從函數返回局部記憶體對象的地址。當函數返回時,對象不再有效。下一次調用某函數時,這個記憶體位址可能會被這個新函數使用。繼續使用這個記憶體指標就有可能會寫入新函數的局部記憶體。
這個常見問題以這種方式出現:
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;
}
請注意指標pCar怎樣被賦予由建構函式Car()建立的未命名物件的局部地址的。到目前為止,沒有問題。然而一旦函數返回這個地址,問題就產生了,因為在封閉的大括弧處臨時對象會被析構。
使運算子混亂
C++從它的前輩C那裡繼承了一套含義相當混亂模糊的運算子。再加上文法規則的靈活性,就使它很容易對程式員造成混亂,使程式員去使用錯誤的運算子。
這個情況的最出名的例子如下:
if(nVal = 0)
{
// do something if nVal is nonzero.
}
程式員顯然想要寫if(nVal == 0)。不幸的是,上述語句是完全合法的,雖然沒有什麼意義,C++語句將nVal賦值為0,然後檢查結果看看是否為非零(這是不可能發生的)。結果是大括弧內的代碼永遠不會被執行。
其它幾對容易弄錯的運算子是&和&&,以及/和//。
0的四種面孔
根據使用它的方式,常數0有四種可能的含義:
☆ 整數0
☆ 不能是對象地址的地址
☆ 邏輯FALSE
☆ 字串的終結符
我可以向你證明這些含義的差別是很實際的。例如,下列賦值是合法的:
int *pInt;
pInt = 0;// this is leagal.
而下列賦值是不合法的:
int *pInt;
pInt = 1;// this is not.
第一個賦值是合法的,因為表中的第二定義:常數0可以是地址,然而常數1則不行。
這個含義的多重效能導致一些難以發現的錯誤:
// copy a string from pSource to pTarget -- incorrect version.
while(pSource)
{
*pTarget++ = *pSource++;
}
此例中的while迴圈試圖把由pSource指向的源字串複製到由pTarget指向的記憶體塊。但不幸的是,條件寫錯了,它應這樣寫出:
// copy a string from pSource to pTarget -- incorrect version.
while(*pSource)
{
*pTarget++ = *pSource++;
}
你可以看到,當由pSource指向的字元為NULL時,終止條件出現。這是0的第四定義。然而,這裡寫出的代碼卻是去查看地址pSource是否為零,這是第二定義。
最終結果是while()迴圈繼續寫入記憶體直到程式崩潰。
0的其他定義之間也可能產生混亂。唯一的解決辦法就是當你使用常數0的時候小心一點。
聲明的混亂處
複合聲明是非常混亂的,但C++——以它的熱忱保持了與C的反向相容性——但也產生了一些聲明間的矛盾,你必須避免這種矛盾。
class Myclass
{
public:
Myclass(int nArg1 = 0,int nArg2 = 0);
};
Myclass mcA(1,2);
Myclass mcB(1);
Myclass mcC();
mcA是參數1和2構成的對象,而mcB是參數1和0構成的對象。因此你可能認為mcC是參數0和0構成的對象,然而情況不是這樣。而mcC()是一個不帶參數的函數,它用數值返回類Myclass的對象。
另一個混亂產生於初始化運算子=的使用:
Myclass mcB = nA; // same as Myclass mcB(nA)
為了增強與C的相容性,允許這樣使用=;然而你應該避免這種結構,因為它不是一貫適用的。例如下列程式就不會有預期的效果:
Myclass mcA = nA,nB;
這說明一個對象mcA(nA),它後面有一個獨立的使用預設構造符的對象nB,而不是說明一個對象mcA(nA,nB)。
堅持使用C++格式——這是最安全的。
計算順序混亂
C和C++運算子的先後順序,使你能夠知道怎樣計算諸如下列運算式:
a = b * c + d;
然而先後次序不會影響子運算式的計算順序。讓我們以看上去不重要的方式改變樣本的運算式:
a = b() * c() + d();
現在的問題是,在這個運算式中以什麼樣的順序調用函數b(),c()和d()?答案是,順序是完全不確定的。更糟的是,順序不能藉助圓括弧的使用而確定。所以下列運算式沒有作用:
a = (b() * c()) + d();
Function Compute順序通常不值得去關心。然而,如果這些函數有副作用,以某種方式彼此影響(稱為相互副作用),那麼順序就是重要的了。例如,如果這些函數改變相同的全域變數,則結果就是不同的,這取決於其中函數被調用的順序。
甚至當不涉及函數調用時,相互副作用也會產生影響:
int nI = 0;
cout<<"nA[0]="<這個運算式的問題是單個運算式包含有相互副作用的兩個子運算式——變數nI是增量。哪個nA[nI++]首先被執行,左邊的nA[nI++]還是右邊的nA[nI++]?沒法說,上述代碼可能會以預期的方式工作,但也可能不會。
說明虛擬成員函數
為了在子類中重載虛擬成員函數,必須用和基本類中函數一樣的形式說明子類中函數的參數和傳回型別。這並不總是清楚的。例如,下列代碼似乎講得通:
class Base
{
public:
virtual void AFunc(Base *pB);
};
class Subclass:public Base
{
public:
virtual void AFunc(Subclass *pS);
};
這個代碼會編譯通過,但不會有遲後聯編。函數Base::AFunc()的參數是Base*類型的,而函數Subclass::AFunc()的參數是Subclass*,它們是不同的。
這個規則的唯一例外是下面的例子,它符合ANSI C++標準:
class Base
{
public:
virtual void Base* AFunc();
};
class Subclass:public Base
{
public:
virtual void Subclass* AFunc();
};
在此例中,每個函數返回其固有類型對象的地址。這種技術很通用,所以標準委員會決定承認它。
從建構函式內調用虛擬成員函數
從構造符內調用虛擬函數是前期聯編的,這樣,它就短路掉了那些原本可能的簡潔的能力:
class Base
{
public:
Base();
virtual void BuildSection();
};
class Subclass:public Base
{
public:
Subclass();
virtual void BuildSection();
};
Base::Base()
{
BuildSection();
};
在此例中,程式員希望建構函式能夠多態地調用BuildSection(),當正在構造的對象是Base對象時調用Base::BuildSection(),當對象是類Subclass對象時調用Subclass::BuildSection()。
由於下列簡單的原因這個例子不起作用:當調用BuildSection()完成時,正在構造的對象僅僅是一個Base對象。即使對象最終成為Subclass對象,也要等到Subclass的建構函式把它過一遍以後。在這些情況下調用Subclass::BuildSection()可能是致命的。即使對象將最終成為Subclass對象,但在調用BuildSection()的時候,對象只不過是Base對象,而且,這個調用必須要前期聯編到函數Base::BuildSection()。
指標對準
當你在80x86處理器(例如,你的PC機的晶片)上執行你的程式時,這個問題不是致命的,但對其他的絕大多數晶片來說,這就是致命的了。它還會對你的應用程式移植到某個其他環境的能力產生影響。此外,甚至對於Intel 處理器來說,這個問題也將導致低於標準的效能。
當你的指標從一種類型轉換到另一種類型的時候,就有可能產生一個非對準指標(misaligned pointer)。處理器一般要求記憶體塊的地址要與一個和這個記憶體塊的尺寸匹配的邊界對齊。例如,字只能在字邊界上被訪問(地址是二的倍數),雙字只能在雙字邊界上被訪問(地址是四的倍數),依次類推。
編譯器通常確保監視這個規則。但是當你的指標類型從一種類型轉換成較大類型時,你就可以很容易地違反這個規則:
char cA;
char* pC = &cA;
int* pI;
pI = (int*)pC;
*pI = 0; // this may be fatal.
因為字元僅僅是一個位元組長,所以地址&cA可能有任意值,包括奇數值。可是,pI應只包含四的倍數的地址。通過轉換,允許把pC賦給pI,但是如果地址不是四的倍數,則接著發生的賦值可能使程式崩潰。
對於Intel處理器來說,甚至當pC值為奇數時,該賦值也不是致命的;雖然佔用的時間要長得多,但是賦值還是能夠正常執行。請你謹防非對準指標。
這種情況只在你正在把你的指標從指向一種類型轉換成指向較大類型時才會出現

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.