1. 程式人生 > >基類的解構函式需不需要寫成虛擬函式

基類的解構函式需不需要寫成虛擬函式

 我之前一直認為,基類的解構函式應當是虛擬函式,否則會留下析構時的隱患,並且在沒有其他虛擬函式的時候,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。