- 問題發現?
在維護一份原有代碼時,看到類似下面的代碼:(模型簡化版)
#include<iostream>
using namespace std;
//定義一個純虛基類
class Base
{
public:
Base()
{
cout<< "Construct Base!"<< endl;
};
~Base() //NOTE: non-virtual
{
cout<< "Destruct Base!"<< endl;
};
virtual void DoSomething() = 0;
};
//實現派生類
class Derived : public Base
{
public:
Derived()
{
cout<< "Construct Derived!" <<endl;
};
~Derived()
{
cout<< "Destruct Derived!" <<endl;
};
void DoSomething()
{
cout<< "Do whatever in Derived class!" <<endl;
};
};
這個實現有問題嗎? 讓我們簡單運行一下看看:
int main()
{
//case1: 使用基類指針指向派生類對象
Base *pBase = new Derived;
pBase->DoSomething();
delete pBase;
pBase = NULL;
//case2: 使用派生類指針指向派生類對象
Derived* pDerived = new Derived;
pDerived = dynamic_cast<Base*>(pDerived);
pDerived ->DoSomething();
delete pDerived ;
pDerived = NULL;
return 0;
}
下面是運行結果:
結果出現了詭異的“局部銷燬”現象,case1中的對象在delete 時沒有調用派生類Derived的析構函數,這將很可能造成內存資源泄露。
問題出現在Derived 對象被基類Base 指針刪除,而目前的基類Base有個non-virtual 析構函數,這將引起一個災難性結果,因爲C++明確指出:當Derived class對象經由一個Base class 指針被刪除,而該Base class 帶着non-virtual 析構函數時,其結果是未定義的,實際執行時通常發生對象的Derived成分沒有被銷燬。
- 怎麼解決呢?
給Base class 一個virtual 的析構函數,此後刪除Derived class 對象時就按照預期銷燬整個對象,包括所有Derived class。可以更改後自行測試一下,此處不再粘貼結果。
像Base class 這樣除了析構函數之外通常還有其他virtual函數,此處virtual目的是允許Derived class 的實現得以具體化,在不同的Derived class中有不同的實現代碼。任何class 只要帶有virtual函數基本都應該明確也應有一個virtual 析構函數。
- 問題延伸
某些情況下,欲使一個類成爲抽象類,但剛好又沒有任何純虛函數。怎麼辦? 實際上,抽象類的產生也可由純虛析構函數實現。
抽象類是準備被用做基類的,如上面所講,基類必須要有一個virtual析構函數,在沒有純虛函數會產生抽象類的情況下,可以藉助於聲明一個純虛析構函數實現:
class AbstractBase
{
public:
virtual ~AbstractBase () = 0; // 聲明一個純虛析構函數
};
該AbstractBase 類有一個純虛函數,是抽象類,而且有一個virtual析構函數,因此不會產生析構函數導致對象釋放不全的問題。
除此聲明之外,因爲要實例化其派生類對象,還必須提供純虛析構函數的定義, 即:
AbstractBase::~AbstractBase () {} // 純虛析構函數的定義
而且該定義是必需的,因爲虛析構函數工作的方式是:最底層的派生類的析構函數最先被調用,然後各個基類的析構函數被調用。也就是說,即使是抽象類,編譯器也要產生對~AbstractBase() 的調用,因此必須要保證爲它提供實現函數體,否則,編譯連接時就會報錯,直到實現該函數體爲止,示例代碼如下:
class AbstractBase
{
public:
virtual ~AbstractBase() = 0;
};
class ConcreteDerived :public AbstractBase
{
public:
virtual ~ConcreteDerived(){};
};
//AbstractBase的純虛函數實現體,此時AbstractBase仍爲抽象類
AbstractBase::~AbstractBase(){}; //必須提供,否則編譯報錯