有很多方法可以跟蹤時間的軌跡,所以有必要建立一個 TimeKeeper 基類,并為不同的計時方法建立派生類:
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper { ... };
class WaterClock: public TimeKeeper { ... };
class WristWatch: public TimeKeeper { ... };
很多客戶只是想簡單地取得時間而不關心如何計算的細節(jié),所以一個 factory 函數——返回一個指向新建派生類對象的基類指針的函數——被用來返回一個指向計時對象的指針:
TimeKeeper* getTimeKeeper(); // returns a pointer to a dynamic-
// ally allocated object of a class
// derived from TimeKeeper
按照 factory 函數的慣例,getTimeKeeper 返回的對象是建立在堆上的,所以為了避免泄漏內存和其他資源,最重要的就是要讓每一個返回的對象都可以被完全刪除。
TimeKeeper *ptk = getTimeKeeper(); // get dynamically allocated object
// from TimeKeeper hierarchy
... // use it
delete ptk; // release it to avoid resource leak
現在我們精力集中于上面的代碼中一個更基本的缺陷:即使客戶做對了每一件事,也無法預知程序將如何運轉。
問題在于 getTimeKeeper 返回一個指向派生類對象的指針(比如 AtomicClock),那個對象通過一個基類指針(也就是一個 TimeKeeper* 指針)被刪除,而且這個基類(TimeKeeper)有一個非虛的析構函數。禍端就在這里,因為 C++ 指出:當一個派生類對象通過使用一個基類指針刪除,而這個基類有一個非虛的析構函數,則結果是未定義的。運行時比較有代表性的后果是對象的派生部分不會被銷毀。如果 getTimeKeeper 返回一個指向 AtomicClock 對象的指針,則對象的 AtomicClock 部分(也就是在 AtomicClock 類中聲明的數據成員)很可能不會被銷毀,AtomicClock 的析構函數也不會運行。然而,基類部分(也就是 TimeKeeper 部分)很可能已被銷毀,這就導致了一個古怪的“部分析構”對象。這是一個泄漏資源,破壞數據結構以及消耗大量調試時間的絕妙方法。 排除這個問題非常簡單:給基類一個虛析構函數。于是,刪除一個派生類對象的時候就有了你所期望的正確行為。將銷毀整個對象,包括全部的派生類部分:
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk; // now behaves correctly
類似 TimeKeeper 的基類一般都包含除了析構函數以外的其它虛函數,因為虛函數的目的就是允許派生類定制實現(參見 Item 34)。例如,TimeKeeper 可能有一個虛函數 getCurrentTime,在各種不同的派生類中有不同的實現。幾乎所有擁有虛函數的類差不多都應該有虛析構函數。
如果一個類不包含虛函數,這經常預示不打算將它作為基類使用。當一個類不打算作為基類時,將析構函數聲明為虛擬通常是個壞主意。考慮一個表現二維空間中的點的類:
class Point { // a 2D point
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
如果一個 int 占 32 位,一個 Point 對象正好適用于 64 位的寄存器。而且,這樣一個 Point 對象可以被作為一個 64 位的量傳遞給其它語言寫的函數,比如 C 或者 FORTRAN。如果 Point 的析構函數是虛擬的,情況就完全不一樣了。
虛函數的實現要求對象攜帶額外的信息,這些信息用于在運行時確定該對象應該調用哪一個虛函數。典型情況下,這一信息具有一種被稱為 vptr(virtual table pointer,虛函數表指針)的指針的形式。vptr 指向一個被稱為 vtbl(virtual table,虛函數表)的函數指針數組,每一個包含虛函數的類都關聯到 vtbl。當一個對象調用了虛函數,實際的被調用函數通過下面的步驟確定:找到對象的 vptr 指向的 vtbl,然后在 vtbl 中尋找合適的函數指針。
虛函數如何被實現的細節(jié)是不重要的。重要的是如果 Point 類包含一個虛函數,這個類型的對象的大小就會增加。在一個 32 位架構中,它們將從 64 位(相當于兩個 int)長到 96 位(兩個 int 加上 vptr);在一個 64 位架構中,他們可能從 64 位長到 128 位,因為在這樣的架構中指針的大小是 64 位的。為 Point 加上 vptr 將會使它的大小增長 50-100%!Point 對象不再適合 64 位寄存器。而且,Point 對象在 C++ 和其他語言(比如 C)中,看起來不再具有相同的結構,因為其它語言缺乏 vptr 的對應物。結果,Points 不再可能傳入其它語言寫成的函數或從其中傳出,除非你為 vptr 做出明確的對應,而這是它自己的實現細節(jié)并因此失去可移植性。
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper { ... };
class WaterClock: public TimeKeeper { ... };
class WristWatch: public TimeKeeper { ... };
很多客戶只是想簡單地取得時間而不關心如何計算的細節(jié),所以一個 factory 函數——返回一個指向新建派生類對象的基類指針的函數——被用來返回一個指向計時對象的指針:
TimeKeeper* getTimeKeeper(); // returns a pointer to a dynamic-
// ally allocated object of a class
// derived from TimeKeeper
按照 factory 函數的慣例,getTimeKeeper 返回的對象是建立在堆上的,所以為了避免泄漏內存和其他資源,最重要的就是要讓每一個返回的對象都可以被完全刪除。
TimeKeeper *ptk = getTimeKeeper(); // get dynamically allocated object
// from TimeKeeper hierarchy
... // use it
delete ptk; // release it to avoid resource leak
現在我們精力集中于上面的代碼中一個更基本的缺陷:即使客戶做對了每一件事,也無法預知程序將如何運轉。
問題在于 getTimeKeeper 返回一個指向派生類對象的指針(比如 AtomicClock),那個對象通過一個基類指針(也就是一個 TimeKeeper* 指針)被刪除,而且這個基類(TimeKeeper)有一個非虛的析構函數。禍端就在這里,因為 C++ 指出:當一個派生類對象通過使用一個基類指針刪除,而這個基類有一個非虛的析構函數,則結果是未定義的。運行時比較有代表性的后果是對象的派生部分不會被銷毀。如果 getTimeKeeper 返回一個指向 AtomicClock 對象的指針,則對象的 AtomicClock 部分(也就是在 AtomicClock 類中聲明的數據成員)很可能不會被銷毀,AtomicClock 的析構函數也不會運行。然而,基類部分(也就是 TimeKeeper 部分)很可能已被銷毀,這就導致了一個古怪的“部分析構”對象。這是一個泄漏資源,破壞數據結構以及消耗大量調試時間的絕妙方法。 排除這個問題非常簡單:給基類一個虛析構函數。于是,刪除一個派生類對象的時候就有了你所期望的正確行為。將銷毀整個對象,包括全部的派生類部分:
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk; // now behaves correctly
類似 TimeKeeper 的基類一般都包含除了析構函數以外的其它虛函數,因為虛函數的目的就是允許派生類定制實現(參見 Item 34)。例如,TimeKeeper 可能有一個虛函數 getCurrentTime,在各種不同的派生類中有不同的實現。幾乎所有擁有虛函數的類差不多都應該有虛析構函數。
如果一個類不包含虛函數,這經常預示不打算將它作為基類使用。當一個類不打算作為基類時,將析構函數聲明為虛擬通常是個壞主意。考慮一個表現二維空間中的點的類:
class Point { // a 2D point
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
如果一個 int 占 32 位,一個 Point 對象正好適用于 64 位的寄存器。而且,這樣一個 Point 對象可以被作為一個 64 位的量傳遞給其它語言寫的函數,比如 C 或者 FORTRAN。如果 Point 的析構函數是虛擬的,情況就完全不一樣了。
虛函數的實現要求對象攜帶額外的信息,這些信息用于在運行時確定該對象應該調用哪一個虛函數。典型情況下,這一信息具有一種被稱為 vptr(virtual table pointer,虛函數表指針)的指針的形式。vptr 指向一個被稱為 vtbl(virtual table,虛函數表)的函數指針數組,每一個包含虛函數的類都關聯到 vtbl。當一個對象調用了虛函數,實際的被調用函數通過下面的步驟確定:找到對象的 vptr 指向的 vtbl,然后在 vtbl 中尋找合適的函數指針。
虛函數如何被實現的細節(jié)是不重要的。重要的是如果 Point 類包含一個虛函數,這個類型的對象的大小就會增加。在一個 32 位架構中,它們將從 64 位(相當于兩個 int)長到 96 位(兩個 int 加上 vptr);在一個 64 位架構中,他們可能從 64 位長到 128 位,因為在這樣的架構中指針的大小是 64 位的。為 Point 加上 vptr 將會使它的大小增長 50-100%!Point 對象不再適合 64 位寄存器。而且,Point 對象在 C++ 和其他語言(比如 C)中,看起來不再具有相同的結構,因為其它語言缺乏 vptr 的對應物。結果,Points 不再可能傳入其它語言寫成的函數或從其中傳出,除非你為 vptr 做出明確的對應,而這是它自己的實現細節(jié)并因此失去可移植性。