C++中基類的解構函式為什麼要用virtual虛解構函式【轉】
(轉自:https://blog.csdn.net/iicy266/article/details/11906457)
知識背景
要弄明白這個問題,首先要了解下C++中的動態繫結。
關於動態繫結的講解,請參閱: C++中的動態型別與動態繫結、虛擬函式、多型實現
正題
直接的講,C++中基類採用virtual虛解構函式是為了防止記憶體洩漏。
示例程式碼講解
現有Base基類,其解構函式為非虛解構函式。Derived1和Derived2為Base的派生類,這兩個派生類中均有以string* 指向儲存其name的地址空間,name物件是通過new建立在堆上的物件,因此在析構時,需要顯式呼叫delete刪除指標歸還記憶體,否則就會造成記憶體洩漏。
class Base {
public:
~Base() {
cout << "~Base()" << endl;
}
};
class Derived1 : public Base { public: Derived1():name_(new string("NULL")) {} Derived1(const string& n):name_(new string(n)) {} ~Derived1() { delete name_; cout << "~Derived1(): name_ has been deleted." << endl; } private: string* name_; }; class Derived2 : public Base { public: Derived2():name_(new string("NULL")) {} Derived2(const string& n):name_(new string(n)) {} ~Derived2() { delete name_; cout << "~Derived2(): name_ has been deleted." << endl; } private: string* name_; };
我們看下面對其析構情況進行測試:
int main() {
Derived1* d1 = new Derived1();
Derived2 d2 = Derived2("Bob");
delete d1;
return 0;
}
d1為Derived1類的指標(派生類指標),它指向一個在堆上建立的Derived1的物件;d2為一個在棧上建立的物件。其中d1所指的物件需要我們顯式的用delete呼叫其解構函式;d2物件在其生命週期結束時,系統會自動呼叫其解構函式。看下其執行結果:
剛才我們說,Base基類的解構函式並不是虛解構函式,現在結果顯示,派生類的解構函式被呼叫了,正常的釋放了其申請的記憶體資源。這兩者並不矛盾(取決於是使用 基類指標/引用,還是 派生類指標/引用),因為無論是d1還是d2,兩者都屬於靜態繫結,而且其靜態型別恰好都是派生類,因此,在析構的時候,即使基類的解構函式為非虛解構函式,也會呼叫相應派生類的解構函式。
下面我們來看下,當發生動態繫結時,也就是當用基類指標指向派生類,這時候採用delete顯式刪除指標所指物件時,如果Base基類的解構函式沒有virtual,會發生什麼情況?
int main() {
Base* base[2] = {
new Derived1(),
new Derived2("Bob")
};
for (int i = 0; i != 2; ++i) {
delete base[i];
}
return 0;
}
從上面結果我們看到,儘管派生類中定義了解構函式來釋放其申請的資源,但是並沒有得到呼叫。原因是基類指標指向了派生類物件,而基類中的解構函式卻是非virtual的,之前講過,虛擬函式是動態繫結的基礎。現在解構函式不是virtual的,因此不會發生動態繫結,而是靜態繫結,指標的靜態型別為基類指標,因此在delete時候只會呼叫基類的解構函式,而不會呼叫派生類的解構函式。這樣,在派生類中申請的資源就不會得到釋放,就會造成記憶體洩漏,這是相當危險的:如果系統中有大量的派生類物件被這樣建立和銷燬,就會有記憶體不斷的洩漏,久而久之,系統就會因為缺少記憶體而崩潰。
也就是說,在基類的解構函式為非虛解構函式的時候,並不一定會造成記憶體洩漏;當派生類物件的解構函式中有記憶體需要收回,並且在程式設計過程中採用了基類指標指向派生類物件,如為了實現多型,並且通過基類指標將該物件銷燬,這時,就會因為基類的解構函式為非虛解構函式而不觸發動態繫結,從而沒有呼叫派生類的解構函式而導致記憶體洩漏。
因此,為了防止這種情況下記憶體洩漏的發生,最好將基類的解構函式寫成virtual虛解構函式。
下面把Base基類的解構函式改為虛解構函式:
class Base {
public:
virtual ~Base() {
cout << "~Base()" << endl;
}
};
再看下其執行結果:
這樣就會實現動態繫結,派生類的解構函式就會得到呼叫,從而避免了記憶體洩漏。