基類的解構函式需不需要寫成虛擬函式
我之前一直認為,基類的解構函式應當是虛擬函式,否則會留下析構時的隱患,並且在沒有其他虛擬函式的時候,dynamic_cast將不能工作。
舉個例子,如下,基類Base僅僅提供一個唯一的ID來標識一個例項化的物件,它沒有其他任何使用虛擬函式的需求。
typedef long ClassID; ClassID gID; class Base { public: Base() { mClassID = gID++; } ~Base() { printf("Base::~Base()\n"); } public: ClassID getClassID() { return mClassID; } private: ClassID mClassID; }; class DerivedA : public Base { public: DerivedA() { mX = mY = 0; } ~DerivedA() { printf("DerivedA::~DerivedA()\n"); } public: int getX() { return mX; } void setX( int x ) { mX = x; } int getY() { return mY; } void setY( int y ) { mY = y; } private: int mX; int mY; }; class DerivedAObj : public DerivedA { public: DerivedAObj() { mRaduis = 0; } ~DerivedAObj() { printf("DerivedAObj::~DerivedAObj()\n"); } public: int getRaduis() { return mRaduis; } void setRaduis( int raduis ) { mRaduis = raduis; } private: int mRaduis; };
首先,一個隱患:
DerivedAObj* pAObj = new DerivedAObj;
DerivedA* pA = pAObj;
delete pA;
會輸出什麼呢?
DerivedA::~DerivedA() Base::~Base() |
而DerivedAObj的解構函式並沒有呼叫。假設該類的解構函式實際上對一些堆分配的記憶體進行了清理工作,那麼這種隱患就會造成記憶體洩漏。
其次假設這個時候,我有個方法:
void printRaduis( DerivedA* pObj ) { DerivedAObj* pAObj = dynamic_cast< DerivedAObj* >( pObj ); if( pAObj != NULL ) { printf( "Raduis: %d\n", pAObj->getRaduis() ); } }
這個時候編譯會有錯誤提示:error C2683: 'dynamic_cast' : 'DerivedA' is not a polymorphic type
這個錯誤是說,DerivedA不是一個多型型別,無法使用dynamic_cast進行動態轉換。我們知道,dynamic_cast的動態轉換依靠的是RTTI,該資訊是存在於虛表中。但這裡繼承中沒有使用任何虛擬函式,因此無法在執行時得到型別資訊。
那麼,是不是一定要給基類,比如Base的解構函式加上virtual呢?
如果Base::~Base()是虛擬函式的話,那麼首先當子類物件的指標轉換為基類指標後再刪除時,依然會正確的進行析構操作。
比如上面的輸出就會變成:
DerivedAObj::~DerivedAObj() DerivedA::~DerivedA() Base::~Base() |
其次,dynamic_cast也會正確操作。
這似乎都是成為基類一定要使用虛解構函式的理由,但是,同樣有一些其他的理由試圖說明,沒有必要為沒有需求的基類設定虛解構函式。
class DerivedB : public Base
{
public:
DerivedB() { }
~DerivedB() { }
//Data block
};
比如Base,它僅僅提供了一個ClassID,如果宣告虛解構函式,則標誌著所有繼承於它的子類都將有著自己的虛表,特別的,繼承於它的DerivedB可能僅僅就是一個數據類,但它仍然要建立自己的虛表,這是顯然都會降低效能。
而多型的需求,可能並不從繼承鏈的頂端開始,比如對於Base的子類DerivedA,從這一層開始,可能會使用到多型的特性,那麼,完全可以從這一節點宣告虛解構函式。而不是Base類。因為繼承於Base類的其他子類(如DerivedB)完全沒有必要消耗效能去建立一張虛表。
既然這樣,為了解決正確析構的問題,我們宣告DerivedA::~DerivedA()為虛解構函式
virtual ~DerivedA() { printf("DerivedA::~DerivedA()\n"); }
則
DerivedAObj* pAObj = new DerivedAObj;
DerivedA* pA = pAObj;
delete pA;
會正確輸出:
DerivedAObj::~DerivedAObj()
DerivedA::~DerivedA()
Base::~Base()
那麼,如果這樣使用呢?
DerivedAObj* pAObj = new DerivedAObj;
Base* pB = pAObj;
delete pB;
遺憾的是,這隻會輸出:
Base::~Base()
為什麼?因為~Base()不是虛擬函式。
糾結麼?其實不糾結。如果從效能上考慮,的確不需要Base使用虛解構函式,那麼就應該將Base的解構函式定義為private,以防止對Base進行delete操作,如果使用者嘗試這樣做,編譯器將會報錯,強制使用者正確的使用物件,防範隱患的產生。
還有問題,dynamic_cast呢?
事實上,dynamic_cast依賴於RTTI,而RTTI並不是在所有的情況下都是開啟的,所以本來安全的轉換其實並不一定安全。假如你寫的是個底層庫,你不知道它會用在什麼地方,那個地方是否開啟或是否支援RTTI,因此,更安全的方法是自己實現一套RTTI的機制。
一般來說,預設開啟RTTI,dynamic_cast依賴於虛表中的RTTI資訊,如果沒有定義虛擬函式,編譯器會報錯。並且dynamic_cast在菱形/cross繼承等方面還存在著問題,所以說,dynamic_cast是“不一定安全”的轉換。
結論:
當然,這是見仁見智的看法,通常情況下,我們還是大量使用到動態轉換,但在寫一些特定的元件或底層時,應當慎重考慮RTTI的實現問題。
對於基類是否定義虛解構函式的問題,在不關注效能的情況下,定義虛解構函式可以避免delete的隱患。同樣,關注效能的情況下,在未定義虛解構函式的層級,就應該注意將解構函式私有化(private),防止造成隱藏的bug。