C/C++語言中有許多對初學者(甚至是有經驗的編程人員)來說很輕易范的錯誤。通曉這樣的錯誤可使你免於陷入其中。
忘記初始化指針
<!-- frame contents -->
<!-- /frame contents -->
這種錯誤只是一般"忘記初始化變量"錯誤的一個非凡形式(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);
}
}
更多內容請看C/C++技術專題 Java編程開發手冊專題,或
在此例中,函數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.
}
更多內容請看C/C++技術專題 Java編程開發手冊專題,或 程序員顯然想要寫if(nVal == 0)。不幸的是,上述語句是完全合法的,雖然沒有什麼意義,C++語句將nVal賦值為0,然後檢查結果看看是否為非零(這是不可能發生的)。結果是大括號內的代碼永遠不會被執行。
其它幾對輕易弄錯的運算符是&和&&,以及/和//。
<!-- frame contents -->
<!-- /frame contents -->
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++技術專題 Java編程開發手冊專題,或 計算順序混亂
C和C++運算符的先後順序,使你能夠知道怎樣計算諸如下列表達式:
a = b * c + d;
然而先後次序不會影響子表達式的計算順序。讓我們以看上去不重要的方式改變示例的表達式:
a = b() * c() + d();
<!-- frame contents -->
<!-- /frame contents -->
現在的問題是,在這個表達式中以什麼樣的順序調用函數b(),c()和d()?答案是,順序是完全不確定的。更糟的是,順序不能借助圓括號的使用而確定。所以下列表達式沒有作用:
a = (b() * c()) + d();
函數計算順序通常不值得去關心。然而,假如這些函數有副作用,以某種方式彼此影響(稱為相互副作用),那麼順序就是重要的了。例如,假如這些函數改變相同的全局變量,則結果就是不同的,這取決於其中函數被調用的順序。
甚至當不涉及函數調用時,相互副作用也會產生影響:
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();
}; 更多內容請看C/C++技術專題 Java編程開發手冊專題,或 在此例中,程序員希望構造函數能夠多態地調用BuildSection(),當正在構造的對象是Base對象時調用Base::BuildSection(),當對象是類Subclass對象時調用Subclass::BuildSection()。
<!-- frame contents -->
<!-- /frame contents -->
由於下列簡單的原因這個例子不起作用:當調用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值為奇數時,該賦值也不是致命的;雖然占用的時間要長得多,但是賦值還是能夠正常執行。請你謹防非對准指針。
這種情況只在你正在把你的指針從指向一種類型轉換成指向較大類型時才會出現。[完] 更多內容請看C/C++技術專題 Java編程開發手冊專題,或