本文首發於我的博客:劉衝的博客
在閱讀C++項目(caffe)源碼時,發現不少基類不僅把常規的成員函數定義成虛函數(virtual),也會把析構函數定義爲虛函數,結合前面幾節的介紹,稍稍思考下,這樣做的確是有原因的,本文將結合C++代碼實例嘗試探討下。
常規
隨便寫一段C++代碼作爲實例,在這個例子中,我們先不把析構函數定義爲虛函數:
class Base {
public:
Base () {
cout << "Base construct\n";
}
~Base() {
cout << "Base deconstruct\n";
}
virtual void foo() {
cout << "Base::foo\n";
}
char *buf;
};
class Child: public Base {
public:
Child() {
cout << "Child construct\n";
buf = new char[16];
}
~Child() {
delete[] buf;
cout << "Child deconstruct, delete buf\n";
}
void foo() {
buf[0] = 3;
cout << "Child::foo\n";
}
};
這段代碼的邏輯很簡單,無非就是定義了兩個類:類 Base 的成員函數 foo() 爲虛函數,構造函數和析構函數都是常規函數,此外它還有個 public 的成員變量 buf。類 Child 則公開繼承了 Base,因此它可以直接使用 Base::buf——在構造函數中 new
了一段內存,並且在析構函數 delete
掉它。
Child c;
c.foo();
我們直接使用 Child 實例化一個對象 c,調用 c.foo(),此時得到如下輸出:
Base construct
Child construct
Child::foo
Child deconstruct, delete buf
Base deconstruct
一切盡在預料中。
不安全的問題
雖說對象 c 調用 foo() 的輸出完全符合預計,但像上面那樣定義類仍然是非常危險的做法。在這一節我們曾討論過,父類指針可以調用派生類的重寫函數,因此下面這兩行C++代碼也是合法的,請看:
Base *pb = new Child();
pb->foo();
delete pb;
編譯這段C++代碼完全沒有問題,運行也不會報錯,輸出如下:
Base construct
Child construct
Child::foo
Base deconstruct
可是,從輸出信息能夠看出,派生類 Child 的析構函數沒有被調用,對於本例而言,new
出來的 buf 沒有對應的 delete
,勢必會造成內存泄漏。
解決問題
要解決所謂的“不安全問題”,其實很簡單,按照題目說的做——將基類的析構函數也定義爲虛函數就可以了,請看修改後的C++代碼:
class Base {
public:
Base () {
cout << "Base construct\n";
}
virtual ~Base() {
cout << "Base deconstruct\n";
}
...
也即盡在基類 Base 的析構函數前加上 virtual 關鍵字,其他的所有代碼都無需改動。現在再執行下面的這幾行C++代碼:
Base *pb = new Child();
pb->foo();
delete pb;
輸出如下:
Base construct
Child construct
Child::foo
Child deconstruct, delete buf
Base deconstruct
顯然,此時派生類 Child 的析構函數也會被調用了,內存泄漏的問題倍解決了。
小結
C++ 中的 virtual 關鍵字是非常好用,也是C++程序員必須掌握的關鍵字,其實,“不安全問題”出現的原因也是簡單的:我們在靜態類型與動態綁定一節中提到過,基本上只有涉及到 virtual 函數時,纔會發生動態綁定,此時通過對象指針(pb)調用的函數由它指向的類(Child)決定,所以此時派生類 Child 的析構函數會被調用。如果基類 Base 的析構函數不是虛函數,那麼對象指針(pb)調用的函數由其靜態類型(Base)決定,也即調用的其實只是基類 Base 的析構函數而已。