讓我們直切正題:在程序進(jìn)行構(gòu)造或析構(gòu)期間,你絕不能調(diào)用虛函數(shù),這是因?yàn)檫@樣的調(diào)用并不會(huì)按你所期望的執(zhí)行,即使能夠順利執(zhí)行,你也不會(huì)覺(jué)得十分舒服。如果你曾經(jīng)是一個(gè)Java或C#的程序員,并且在最近期望返回C++的懷抱,那么請(qǐng)你格外留意本條目,因?yàn)樵谶@一問(wèn)題上,C++與其他語(yǔ)言走的是完全不同的兩條路線。
假設(shè)有一個(gè)股票交易模擬系統(tǒng),你為它編寫了一個(gè)類的層次化結(jié)構(gòu),其中包括實(shí)現(xiàn)購(gòu)買、拋售等功能的類。這類交易應(yīng)該是可以審計(jì)的,這一點(diǎn)很重要,所以說(shuō)每創(chuàng)建一次交易時(shí),都應(yīng)該在日志中創(chuàng)建一條審計(jì)相關(guān)內(nèi)容的記錄。下面是一個(gè)看似合理的解決方案:
class Transaction { // 所有交易的基類
public:
Transaction();
virtual void logTransaction() const = 0; // 作類型相關(guān)的記錄
...
};
Transaction::Transaction() // 基類構(gòu)造函數(shù)的實(shí)現(xiàn)
{
...
logTransaction(); // 最后,記錄這次交易
}
class BuyTransaction: public Transaction { // 派生類
public:
virtual void logTransaction() const; // 當(dāng)前類型交易是如何記錄的
...
};
class SellTransaction: public Transaction { // 派生類
public:
virtual void logTransaction() const; // 當(dāng)前類型交易是如何記錄的
...
};
請(qǐng)考慮一下在下邊的代碼運(yùn)行時(shí)會(huì)發(fā)生什么:
很明顯的是此時(shí)BuyTransaction的構(gòu)造函數(shù)將被調(diào)用,但是,首先必須調(diào)用Transaction的構(gòu)造函數(shù)。對(duì)于一個(gè)派生的對(duì)象,其基類那一部分會(huì)首先得到構(gòu)造,然后才是派生類的部分。Transaction的構(gòu)造函數(shù)中最后一行調(diào)用了虛函數(shù)logTransaction,意外的事情就從這里發(fā)生了:此處調(diào)用的是Translation版本的logTransaction函數(shù),而不是BuyTransaction版本的——即使此處創(chuàng)建的對(duì)象是BuyTransaction類型的。在基類部分的構(gòu)造過(guò)程中,虛函數(shù)永遠(yuǎn)也不會(huì)嘗試去匹配派生類部分。取而代之的是,對(duì)象仍然保持基類的行為。更隨意一點(diǎn)的說(shuō)法是,在基類部分構(gòu)造的過(guò)程中,虛函數(shù)并不會(huì)被構(gòu)造。
這一行為看上去匪夷所思,但是這里有很充足的理由來(lái)解釋它。由于基類的構(gòu)造函數(shù)先于派生類運(yùn)行,在基類構(gòu)造函數(shù)運(yùn)行的時(shí)候,派生類的數(shù)據(jù)成員還沒(méi)有被初始化。如果在基類構(gòu)造函數(shù)向下匹配派生類時(shí)調(diào)用了虛函數(shù),那么基類的函數(shù)幾乎一定會(huì)調(diào)用局部數(shù)據(jù)成員,但此時(shí)這些數(shù)據(jù)成員此時(shí)尚未得到初始化。你的程序?qū)?huì)出現(xiàn)無(wú)盡的未定義行為,你也會(huì)在整夜受到瑣碎的調(diào)試工作的折磨。當(dāng)一個(gè)對(duì)象中某些部分尚未初始化的時(shí)候,此時(shí)對(duì)其進(jìn)行調(diào)用會(huì)存在內(nèi)在的危險(xiǎn),所以C++不允許你這樣做。
實(shí)際情況比上文介紹的更為基礎(chǔ)。對(duì)于一個(gè)派生類的對(duì)象來(lái)說(shuō),在其進(jìn)行基類部分構(gòu)造工作的時(shí)候,這一對(duì)象的類型就是基類的。不僅僅虛函數(shù)會(huì)解析為基類的,而且C++中“使用運(yùn)行時(shí)類型信息”的部分(比如dynamic_cast(參見(jiàn)條目27)和typeid)也會(huì)將其看作基類類型的對(duì)象。在我們的示例中,當(dāng)調(diào)用Transaction的構(gòu)造函數(shù)以初始化一個(gè)BuyTransaction對(duì)象的基類部分時(shí),這一對(duì)象是Transaction類型的。C++的任何一部分都會(huì)這樣處理,這種處理方式是有意義的:由于這個(gè)對(duì)象的BuyTransaction部分尚未得到初始化,所以假定它們不存在才是最安全的處理方法。對(duì)于一個(gè)派生類對(duì)象來(lái)說(shuō),只有派生類的構(gòu)造函數(shù)開(kāi)始執(zhí)行,這個(gè)對(duì)象才會(huì)變成該派生類的對(duì)象。
對(duì)于析構(gòu)過(guò)程可以應(yīng)用同樣的推理方式。一旦派生類的析構(gòu)函數(shù)運(yùn)行完畢,對(duì)象中派生類的那一部分?jǐn)?shù)據(jù)成員將取得未定義的值,所以C++會(huì)認(rèn)為它們不再存在。在進(jìn)入基類的析構(gòu)函數(shù)時(shí),這個(gè)對(duì)象將成為一個(gè)基類對(duì)象,C++的所有部分——包括虛函數(shù)、dynamic_cast等等——都會(huì)這樣對(duì)待該對(duì)象。
在上文的示例代碼中,Transaction的構(gòu)造函數(shù)對(duì)一個(gè)虛函數(shù)進(jìn)行了一次直接調(diào)用,很顯然這樣做是違背本條中的指導(dǎo)方針的。這樣的違規(guī)實(shí)在太容易發(fā)現(xiàn)了,一些編譯器都會(huì)對(duì)其做出警告。(其他一些則不會(huì)。參見(jiàn)條目53對(duì)編譯器警告信息的討論)即使沒(méi)有警告,問(wèn)題也一定會(huì)在運(yùn)行之前變得很明顯,這是因?yàn)?span style="font-family:"Courier New";">Transaction中的logTransaction函數(shù)是純虛函數(shù),除非它得到了定義(不像是真的,但存在這種可能,參見(jiàn)條目34),程序才有可能會(huì)得到連接,其他情況都會(huì)報(bào)錯(cuò):連接器無(wú)法找到必要的Transaction::logTransaction的具體實(shí)現(xiàn)。
查找構(gòu)造或析構(gòu)過(guò)程中對(duì)虛函數(shù)的調(diào)用并不總是一帆風(fēng)順的。如果Transaction擁有多個(gè)構(gòu)造函數(shù),它們所進(jìn)行的工作中有一部分是相同的,那么可以將這些公共的初始化代碼(包括對(duì)logTransaction的調(diào)用)放入一個(gè)私有的非虛擬的初始化函數(shù)中,這樣做可以避免代碼重復(fù),從軟件工程角度來(lái)講這似乎是一個(gè)很好的做法,我們將這一函數(shù)命名為init:
class Transaction {
public:
Transaction()
{ init(); } // 調(diào)用非虛函數(shù)...
virtual void logTransaction() const = 0;
...
private:
void init()
{
...
logTransaction(); // ...而它卻調(diào)用一個(gè)虛函數(shù)!
}
};
這樣的代碼與前文中的版本使用的是同一理念,但是這樣做所帶來(lái)的危害更為隱蔽和嚴(yán)重,這是因?yàn)檫@樣的代碼會(huì)得到正常的編譯和連接而不會(huì)報(bào)錯(cuò)。這種情況下,由于logTransaction是Transaction中的一個(gè)純虛函數(shù),大多數(shù)運(yùn)行時(shí)系統(tǒng)將會(huì)在調(diào)用這個(gè)純虛函數(shù)時(shí)中止程序(通常情況下會(huì)針對(duì)這一結(jié)果顯示出一個(gè)消息)。然而如果logTransaction是一個(gè)“正常的”虛函數(shù)(也就是說(shuō),不是純虛的),并且在Transaction中給出了一些實(shí)現(xiàn),那么此時(shí)將調(diào)用這一版本的logTranscation,程序?qū)?huì)“愉快地一路小跑”下去,至于為什么在創(chuàng)建派生類對(duì)象時(shí)會(huì)調(diào)用錯(cuò)誤的logTransaction版本,程序可就不管這一套了。避免這類問(wèn)題的唯一途徑就是:在正在創(chuàng)建或銷毀的對(duì)象的構(gòu)造函數(shù)和析構(gòu)函數(shù)中,確保永遠(yuǎn)不要調(diào)用虛函數(shù),對(duì)于構(gòu)造函數(shù)和析構(gòu)函數(shù)所調(diào)用的所有函數(shù)都應(yīng)遵守這一約定。
那么,每當(dāng)創(chuàng)建一個(gè)Transaction層次結(jié)構(gòu)中的對(duì)象時(shí),如何確保去調(diào)用正確的logTransaction版本呢?顯然地,在Transaction的構(gòu)造函數(shù)中調(diào)用一個(gè)虛函數(shù)是一個(gè)錯(cuò)誤的做法。
為解決這一問(wèn)題我們可以另辟蹊徑。方案之一就是:將Transaction中的logTransaction變?yōu)橐粋€(gè)非虛函數(shù),然后要求派生類的構(gòu)造函數(shù)把必要的日志記錄傳遞給Transaction的構(gòu)造函數(shù)。這個(gè)構(gòu)造函數(shù)對(duì)于非虛logTransaction的調(diào)用就是安全的。就像這樣:
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const;
// 現(xiàn)在logTransaction是非虛函數(shù)
...
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo); // 現(xiàn)在調(diào)用的是一個(gè)非虛函數(shù)
}
class BuyTransaction: public Transaction {
public:
BuyTransaction( parameters )
: Transaction(createLogString( parameters ))
{ ... } // 將記錄傳遞給基類構(gòu)造函數(shù)
...
private:
static std::string createLogString( parameters );
};
換句話說(shuō),你不能使用虛函數(shù)在基類構(gòu)造過(guò)程中向下調(diào)用派生類的部分,作為一種補(bǔ)償,你可以讓派生類將一些必要的構(gòu)造信息向上傳遞給基類的構(gòu)造函數(shù)。
請(qǐng)注意上述示例里BuyTransaction類中(私有的)靜態(tài)函數(shù)createLogString的使用。這里使用了一個(gè)輔助函數(shù)創(chuàng)建一個(gè)值來(lái)傳遞給基類的構(gòu)造函數(shù),通常情況下這樣做更為方便(而且更具備可讀性),這樣做使為基類提供所需信息的成員初始化表變得更加直觀。這是因?yàn)檫@樣做解決了“為基類提供所需信息的成員初始化表”不直觀的問(wèn)題。意外調(diào)用初生的BuyTransaction對(duì)象中那些尚未初始化的數(shù)據(jù)成員是十分危險(xiǎn)的,由于createLogString是靜態(tài)的,此處便不存在這一危險(xiǎn)。這一點(diǎn)很重要,因?yàn)檫@些數(shù)據(jù)成員正處于未定義的狀態(tài),這一事實(shí)便解釋了為什么“在基類部分構(gòu)造或析構(gòu)期間調(diào)用虛函數(shù),不會(huì)在第一時(shí)間向下匹配派生類”。
時(shí)刻牢記
l 不要在構(gòu)造和析構(gòu)的過(guò)程中調(diào)用虛函數(shù),因?yàn)檫@樣的調(diào)用永遠(yuǎn)不會(huì)轉(zhuǎn)向當(dāng)前執(zhí)行的析構(gòu)函數(shù)或構(gòu)造函數(shù)更深層的派生類中執(zhí)行。