問題聚焦:
已經對一個對象執行了delete語句,還會發生內存洩漏嗎?
先來看個demo:
// 計時器類
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
};
class AtomicClock: public TimeKeeper { ...... }; // 原子鐘
class WaterClock: public TimeKeeper { ...... }; // 水表
class WristWatch: public TimeKeeper { ...... }; // 腕表
// 設計工廠函數以供用戶使用
TimeKeeper* ptk = getTimeKeeper(); // Factory函數會“返回一個父類的指針,指向新生成的子類對象”
......
delete ptk; // Point! 釋放它,避免資源洩漏
上面的這個demo有什麼問題呢?
內存洩漏?後面已經delete掉這個對象了,還會內存洩漏嗎?答案是肯定的。
讓我們分析一下。
問題描述:getTimeKeeper()函數返回的指針指向一個derived class對象,而那個子類對象經由它的父類指針被釋放,而它的父類有個non-virtual析構函數。
導致結果:詭異的“局部銷毀”
C++指出,當子類對象經由一個它的父類對象指針被刪除,而該父類對象的析構函數為non-virtual,其結果是:通常情況下,該對象的父類部分被銷毀,而子類部分沒有被銷毀。
解決方案:父類的析構函數聲明為virtual函數。
Demo:
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
......
};
// 使用
TimeKeeper* ptk = getTimeKeeper();
....
delete ptk;
這樣看來,以後我們定義一個類的時候,就把它的析構函數全部聲明為virtual函數,可以避免“局部銷毀”問題。
但是這更不是一個好主意。(PS: 感謝我的老師讓我知道了虛函數表這個東東.....)
還是先來看一個demo.
class Point {
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
如果int占用32bits,那麼Point對象可塞入一個64bit緩存器中。這樣一個Point對象可被當作一個“64bit量”傳給以其他語言如C或Fortran撰寫的函數。
但是如果這裡的析構函數被聲明為virtual,會引起什麼影響呢?
virtual關鍵字可以在運行期決定哪一個virtual函數被調用,這個強大的功能顯然要付出代價的。這個代價就是需要額外的空間存儲虛函數表——編譯器在其中尋找適當的函數指針,以及指向其中的指針(存儲在對象中)。(這裡不討論虛函數表的實現細節)
所以,如果將析構函數聲明為virtual,Point對象的體積就會增大:在32bit計算機體系結構中將占用64bits到96bits(加上虛函數指針32bits)。因此,添加一個虛函數會使得這個對象增大50%~100%。C++的該對象也就無法和C裡的該對象兼容了,如果不明確補償,那麼兩者就無法兼容了。
總結一句話就是:盲目地將所有類的析構函數聲明為virtual,或者non-virtual都是錯誤的。
需要格外注意的一點是:不要企圖繼承一個標准容器或者其他“帶有non-virtual析構函數”,雖然看起來很方便。就像下面做的這樣:
class SpecialString: public std::string {
......
};
// 如果你有一段代碼這樣寫,絕對是你悲劇的開始
SpecialString* pss = new SpecialString("Hello world!");
std::string* ps;
......
ps = pss;
......
delete ps; // 局部銷毀,發生了資源洩漏
如果你確定這個類是當作一個父類來使用的話,聲明一個抽象類或許是一個不錯的主意。
來看一個demo
class AWOV {
public:
virtual ~AWOV() = 0;
};
AWOV::~AWOV() {} //純虛函數的定義
這裡有一個需要注意的地方是,這個析構函數的定義是必須的,不然編譯器會報錯。(因為編譯不會再為你默默的生成一個了)
小結:
帶有多態性質的父類應該聲明一個virtual析構函數,如果class帶有任何virtual函數,它就應該擁有一個virtual析構函數。如果一個類的不是設計為一個父類來使用,或不是為了具備多態性,就不應該聲明virtual析構函數,當然,不要有繼承它的類出現。