現(xiàn)在考慮一個(gè)計(jì)時(shí)器的問題,我們首先創(chuàng)建一個(gè)名為TimeKeeper的基類,然后在它的基礎(chǔ)上創(chuàng)建各種派生類,從而用不同手段來(lái)計(jì)時(shí)。由于計(jì)時(shí)有很多方式,所以這樣做是值得的:
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper { ... }; // 原子鐘
class WaterClock: public TimeKeeper { ... }; // 水鐘
class WristWatch: public TimeKeeper { ... }; // 腕表
許多客戶希望在訪問時(shí)間時(shí)不用關(guān)心計(jì)算的細(xì)節(jié),所以在此可以使用一個(gè)工廠函數(shù)來(lái)返回一個(gè)指向計(jì)時(shí)器對(duì)象的指針,工廠函數(shù)會(huì)返回一個(gè)基類指針,這個(gè)指針將指向一個(gè)新創(chuàng)建的派生類對(duì)象:
TimeKeeper* getTimeKeeper(); // 返回一個(gè)繼承自TimeKeeper的動(dòng)態(tài)分配的對(duì)象
為了不破壞工廠函數(shù)的慣例,getTimeKeeper返回的對(duì)象將被放置在堆上,所以必須要在適當(dāng)?shù)臅r(shí)候刪除每一個(gè)返回的對(duì)象,從而避免內(nèi)存或者其他資源發(fā)生泄漏:
TimeKeeper *ptk = getTimeKeeper(); // 從TimeKeeper層取得
// 一個(gè)動(dòng)態(tài)分配的對(duì)象
... // 使用這個(gè)對(duì)象
delete ptk; // 釋放它,以防資源泄漏
把釋放工作推卸給客戶將會(huì)帶來(lái)出錯(cuò)的隱患,條目13中解釋了這一點(diǎn)。關(guān)于如何修改工廠函數(shù)的接口從而防止一般的客戶端錯(cuò)誤發(fā)生,請(qǐng)參見條目18。但是這些議題在此都不是主要的,這一條目中我們主要討論的是上文中的代碼存在著的一個(gè)更為基本的弱點(diǎn):即使客戶把每一件事都做得很完美,我們?nèi)詿o(wú)法預(yù)知程序會(huì)產(chǎn)生怎樣的行為。
現(xiàn)在的問題是:getTimeKeeper返回一個(gè)指向某個(gè)派生類對(duì)象的指針(比如說AtomicClock),這個(gè)對(duì)象最終會(huì)通過一個(gè)基類指針得到刪除(比如說TimeKeeper*指針),而基類(TimeKeeper)有一個(gè)非虛析構(gòu)函數(shù)。這里埋藏著災(zāi)難,這是因?yàn)镃++有明確的規(guī)則:如果希望通過一個(gè)指向基類的指針來(lái)刪除一個(gè)派生類對(duì)象,并且這一基類有一個(gè)非虛析構(gòu)函數(shù),結(jié)果將是未定義的。典型的后果就是,在運(yùn)行時(shí),派生類中新派生出的部分得不到銷毀。如果getTimeKeeper返回了一個(gè)指向AtomicClock對(duì)象的指針,那么這一對(duì)象中AtomicClock的部分(也就是AtomicClock類中新聲明的數(shù)據(jù)成員)有可能不會(huì)被銷毀掉,AtomicClock的析構(gòu)函數(shù)也可能不會(huì)得到運(yùn)行。然而,這一對(duì)象中基類那一部分(也就是TimeKeeper的部分)很自然的會(huì)被銷毀掉,這樣便會(huì)產(chǎn)生一個(gè)古怪的“部分銷毀”的對(duì)象。用這種方法來(lái)泄漏資源、破壞數(shù)據(jù)結(jié)構(gòu)、浪費(fèi)調(diào)試時(shí)間,實(shí)在是“再好不過”了。
排除這一問題的方法很簡(jiǎn)單:為基類提供一個(gè)虛擬的析構(gòu)函數(shù)。這時(shí)刪除一個(gè)派生類對(duì)象,程序就會(huì)精確地按你的需要運(yùn)行了。整個(gè)對(duì)象都會(huì)得到銷毀,包括所有新派生的部分:
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk; // 現(xiàn)在,程序正常運(yùn)轉(zhuǎn)
通常情況下,像TimeKeeper這樣的基類會(huì)包含除析構(gòu)函數(shù)以外的虛函數(shù),這是因?yàn)樘摵瘮?shù)的目的是允許派生類實(shí)現(xiàn)中對(duì)它們進(jìn)行自定義(參見條目34)。比如說,TimeKeeper類中可能存在一個(gè)虛函數(shù)getCurrentTime,它在不同的派生類中將有不同的實(shí)現(xiàn)方式。任何有虛函數(shù)的類幾乎一定都要包含一個(gè)虛析構(gòu)函數(shù)。
如果一個(gè)類不包含虛函數(shù),通常情況下意味著它將不作為基類使用。當(dāng)一個(gè)類不作為基類時(shí),將它的析構(gòu)函數(shù)其聲明為虛擬的通常情況下不是個(gè)好主意。請(qǐng)看下面的示例,這個(gè)類代表二維空間中的點(diǎn):
class Point { // 2D的點(diǎn)
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
在一般情況下,如果一個(gè)int占用32比特,一個(gè)Point對(duì)象便可以置入一個(gè)64位的寄存器中。而且,這樣的一個(gè)Point對(duì)象可以以一個(gè)64位數(shù)值的形式傳給其他語(yǔ)言編寫的函數(shù),比如C或者FORTRAN。然而如果Point的析構(gòu)函數(shù)是虛擬的,那么就是另一種情況了。
虛函數(shù)的實(shí)現(xiàn)需要它所在的對(duì)象包含額外的信息,這一信息用來(lái)在運(yùn)行時(shí)確定本對(duì)象需要調(diào)用哪個(gè)虛函數(shù)。通常,這一信息采取一個(gè)指針的形式,這個(gè)指針被稱為“vptr”(“虛函數(shù)表指針”,virtual table pointer)。vptr指向一個(gè)包含函數(shù)指針的數(shù)組,這一數(shù)組稱為“vtbl”(“虛函數(shù)表”,virtual table),每個(gè)包含虛函數(shù)的類都有一個(gè)與之相關(guān)的vtbl。當(dāng)一個(gè)虛函數(shù)被一個(gè)對(duì)象調(diào)用時(shí),就用到了該對(duì)象的vptr所指向的vtbl,在vtbl中查找一個(gè)合適的函數(shù)指針,然后調(diào)用相應(yīng)的實(shí)函數(shù)。
虛函數(shù)實(shí)現(xiàn)的細(xì)節(jié)并不重要。重要的僅僅是,如果Point類包含一個(gè)虛函數(shù),這一類型的對(duì)象將會(huì)變大。在一個(gè)32位的架構(gòu)中,Point對(duì)象將會(huì)由64位(兩個(gè)int大小)增長(zhǎng)至96位(兩個(gè)int加一個(gè)vptr);在64位架構(gòu)中,Point對(duì)象將由64位增長(zhǎng)至128位。這是因?yàn)橹赶?4位架構(gòu)的指針有64位大小。可以看到,為Point添加一個(gè)vptr將會(huì)使對(duì)象增大50-100%!這樣,一個(gè)64位的寄存器便容不下一個(gè)Point對(duì)象了。而且,此時(shí)C++版本的Point對(duì)象便不再與其它語(yǔ)言(比如C語(yǔ)言)有同樣的結(jié)構(gòu),這是因?yàn)槠渌Z(yǔ)言很可能沒有vptr的概念。于是,除非你顯式增補(bǔ)一個(gè)vptr的等價(jià)物(但這是這種語(yǔ)言的實(shí)現(xiàn)細(xì)節(jié),而且不具備可移植性),否則Point對(duì)象便無(wú)法與其它語(yǔ)言編寫的函數(shù)互通。
不得不承認(rèn),無(wú)故將所有的析構(gòu)函數(shù)聲明為虛擬的,與從不將它們聲明為虛函數(shù)一樣糟糕,這一點(diǎn)至關(guān)重要。實(shí)際上,許多人總結(jié)出一條解決途徑:當(dāng)且僅當(dāng)類中至少包含一個(gè)虛函數(shù)時(shí),要聲明一個(gè)虛析構(gòu)函數(shù)。
甚至在完全沒有虛函數(shù)的類里,你也可能會(huì)被非虛擬的構(gòu)造函數(shù)所糾纏。比如說,雖然標(biāo)準(zhǔn)的string類型不包含虛函數(shù),但是誤入歧途的程序員有些時(shí)候還是會(huì)將其作為基類:
class SpecialString: public std::string {
// 這不是個(gè)好主意!
// std::string 有一個(gè)非虛擬的析構(gòu)函數(shù)
...
};
乍一看,這樣的代碼似乎沒什么問題,但是如果在某應(yīng)用程序里,你不知出于什么原因希望將一個(gè)SpecialString指針轉(zhuǎn)型為string指針,同時(shí)你又對(duì)這個(gè)string指針使用了delete,你的程序會(huì)立刻陷入未定義行為:
SpecialString *pss = new SpecialString("Impending Doom");
std::string *ps;
...
ps = pss; // SpecialString* ⇒ std::string*
...
delete ps; // 未定義行為!在實(shí)踐中*ps的SpecialString
// 部分資源將會(huì)泄漏,這是因?yàn)?/span>SpecialString
// 的析構(gòu)函數(shù)沒有被調(diào)用。
對(duì)于任意沒有虛析構(gòu)函數(shù)的類而言,上面的分析都成立,包括所有的STL容器類型(比如vector、list、set、tr1::unordered_map(參見條目54),等等)。如果你曾經(jīng)繼承過一個(gè)標(biāo)準(zhǔn)容器或者其他任何包含非虛析構(gòu)函數(shù)的類,一定要打消這種想法!(遺憾的是,C++沒有提供類似Java中的final類或C#中的sealed類那種防止繼承的機(jī)制)
在個(gè)別情況下,為一個(gè)類提供一個(gè)純虛析構(gòu)函數(shù)是十分方便的。你可以回憶一下,純虛函數(shù)會(huì)使其所在的類成為抽象類——這種類不可以實(shí)例化(也就是說,你無(wú)法創(chuàng)建這種類型的對(duì)象)。然而某些時(shí)刻,你希望一個(gè)類成為一個(gè)抽象類,但是你有沒有任何純虛函數(shù),這時(shí)候要怎么辦呢?因?yàn)槌橄箢悜?yīng)該作為基類來(lái)使用,而基類應(yīng)該有虛析構(gòu)函數(shù),又因?yàn)榧兲摵瘮?shù)可以造就一個(gè)抽象類,那么解決方案就顯而易見了:如果你希望一個(gè)類成為一個(gè)抽象類,那么在其中聲明一個(gè)純虛析構(gòu)函數(shù)。下邊是示例:
class AWOV { // AWOV = "Abstract w/o Virtuals"
public:
virtual ~AWOV() = 0; // 聲明純虛析構(gòu)函數(shù)
};
這個(gè)類有一個(gè)純虛函數(shù),所以它是一個(gè)抽象類,同時(shí)它擁有一個(gè)虛析構(gòu)函數(shù),所以你不需要擔(dān)心析構(gòu)函數(shù)的問題。然而這里還是有一個(gè)別扭的地方:你必須為純虛析構(gòu)函數(shù)提供一個(gè)定義:
AWOV::~AWOV() {} // 純虛析構(gòu)函數(shù)的定義
析構(gòu)函數(shù)的工作方式是這樣的:首先調(diào)用最后派生出的類的析構(gòu)函數(shù),然后依次調(diào)用上一層基類的析構(gòu)函數(shù)。由于當(dāng)調(diào)用一個(gè)AWOV的派生類的析構(gòu)函數(shù)時(shí),編譯器會(huì)自動(dòng)調(diào)用~AWOV,因此你必須為~AWOV提供一個(gè)函數(shù)體。否則連接器將會(huì)報(bào)錯(cuò)。
為基類提供虛析構(gòu)函數(shù)的原則僅對(duì)多態(tài)基類(這種基類允許通過其接口來(lái)操控派生類的類型)有效。我們說TimeKeeper是一個(gè)多態(tài)基類,這是由于即使我們手頭只有TimeKeeper指向它們的指針,我們?nèi)钥梢詫?duì)AtomicClock和WaterClock進(jìn)行操控。
并不是所有的基類都要具有多態(tài)性。比如說,標(biāo)準(zhǔn)string類型、STL容器都不用作基類,因此它們都不具備多態(tài)性。另外有一些類是設(shè)計(jì)用作基類的,但是它們并未被設(shè)計(jì)成多態(tài)類。這些類(例如條目6中的Uncopyable和標(biāo)準(zhǔn)類中的input_iterator_tag(參見條目47))不允許通過其接口來(lái)操控它的派生類。因此,它們并不需要虛析構(gòu)函數(shù)。
時(shí)刻牢記
l 應(yīng)該為多態(tài)基類聲明虛析構(gòu)函數(shù)。一旦一個(gè)類包含虛函數(shù),它就應(yīng)該包含一個(gè)虛析構(gòu)函數(shù)。
l 如果一個(gè)類不用作基類或者不需具有多態(tài)性,便不應(yīng)該為它聲明虛析構(gòu)函數(shù)。