一、為什么基類中的析構函數要聲明為虛析構函數?
直接的講,C++中基類采用virtual虛析構函數是為了防止內存泄漏。具體地說,如果派生類中申請了內存空間,并在其析構函數中對這些內存空間進行釋放。假設基類中采用的是非虛析構函數,當刪除基類指針指向的派生類對象時就不會觸發動態綁定,因而只會調用基類的析構函數,而不會調用派生類的析構函數。那么在這種情況下,派生類中申請的空間就得不到釋放從而產生內存泄漏。所以,為了防止這種情況的發生,C++中基類的析構函數應采用virtual虛析構函數。?
1. 測試代碼:?
#include <iostream>
#include <memory>
using namespace std;class Base {
public:Base() { cout << "Base Constructor" << endl; }~Base() { cout << "Base Destructor" << endl; }
};class Derived : public Base {
public:Derived() { cout << "Derived Constructor" << endl; }~Derived() { cout << "Derived Destructor" << endl; }
};int main()
{Base* p = new Derived();delete p;return 0;
}
輸出結果:
?
2. 測試代碼:?
#include <iostream>
#include <memory>
using namespace std;class Base {
public:Base() { cout << "Base Constructor" << endl; }virtual ~Base() { cout << "Base Destructor" << endl; }
};class Derived : public Base {
public:Derived() { cout << "Derived Constructor" << endl; }virtual ~Derived() { cout << "Derived Destructor" << endl; }
};int main()
{Base* p = new Derived();delete p;return 0;
}
輸出結果:
?
二、構造函數可以調用虛函數嗎?語法上通過嗎?語義上可以通過嗎?
#include<string>
#include<iostream>
using namespace std;class B {
public:B(const string& ss) { cout << "B constructor\n"; f(ss); }virtual void f(const string&) { cout << "B::f\n"; }
};class D : public B {
public:D(const string& ss) :B(ss) { cout << "D constructor\n"; }void f(const string& ss) { cout << "D::f\n"; s = ss; }
private:string s;
};int main()
{D d("Hello");
}
輸出結果:
分析:
注意,輸出不是D::f 。 究竟發生了什么?f()是在B::B()中調用的。如果構造函數中調用虛函數的規則不是如前文所述那樣, 而是如一些人希望的那樣去調用D::f()。那么因為構造函數D::D()尚未運行,字符串s還未初始化,所以當D::f()試圖將參數 賦給s時,結果多半是——立馬當機。析構則正相反,遵循從繼承類到基類的順序(拆房子總得從上往下拆吧?),所以其調用虛函數的行為和在構造函數中一樣:虛函數此時此刻被綁定到哪里(當然應該是基類啦——因為繼承類已經被“拆”了——析構了!),調用的就是哪個函數。
有時,這條規則被解釋為是由于編譯器的實作造成的。[譯注:從實作角度可以這樣解釋:在許多編譯器中,直到構造函數調用完畢,vtable才被建立,此時虛函數才被動態綁定至繼承類的同名函數。] 但事實上不是這么一回事——讓編譯器實作成“構造函數中調用虛函數也和從其他函數中調用一樣”是很簡單的[譯注:只要把vtable的建立移至構造函數調用之前即可]。關鍵還在于語言設計時的考量——讓虛函數可以求助于基類提供的通用代碼。[譯注:先有雞還是先有蛋?Bjarne實際上是在告訴你,不是“先有實作再有規則”,而是“如此實作,因為規則如此”。]