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