程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> [Effective C++系列]-為多態基類聲明Virtual析構函數,effectivevirtual

[Effective C++系列]-為多態基類聲明Virtual析構函數,effectivevirtual

編輯:C++入門知識

[Effective C++系列]-為多態基類聲明Virtual析構函數,effectivevirtual


Declare destructors virtual in polymorphic base classes.  
  • [原理]
C++指出,當derived class對象經由一個由base class類型的指針刪除時,如果這個base class 擁有一個non-virtual的析構函數,那個析構的結果將是未定義的。即通常情況下是該對象的base class成分會被析構掉,但是其derived class成分沒有被銷毀,甚至連derived class的析構函數也不會被調用。 於是形成一個被“局部銷毀”的對象,造成資源洩漏。  
  • [示例]
例如:
class car
{
public:
     car();
     ~car();
     ...   
};
 
class diesel_car : public car {…};
class solar_car: public car {…};
class electric_car : public car {…};
當客戶代碼中使用汽車對象時,如果他不關心使用的是具體哪一類汽車這個細節,那麼我們可以設計一個工廠函數(或者工廠類)負責創建一個汽車對象,該工廠函數返回一個base class指針或者引用,指向新生成的derived class對象:
car* get_car();

為遵守工廠函數的規矩,返回的對象必須位於heap(否則函數返回的指針在函數返回後將指向一個非法的位置,因為位於stack的對象的生命周期為函數域),因此為了避免內存洩漏,需要客戶代碼將工廠函數返回的對象適當地delete掉:

car* p_car = get_car();      // 從car繼承體系中獲得一個動態分配對象
…                            // 使用這個對象
delete p_car;                // 釋放這個對象以避免內存洩漏

首先需要說明,上述做法已經存在兩個缺陷:

1.依賴客戶代碼執行delete操作,帶有錯誤傾向,客戶可能會忘記做這件事。 2.工廠函數結構應該考慮預防常見的客戶代碼錯誤。   但是最根本的弱點在於:客戶代碼根本無法將返回的derived class對象徹底銷毀。   簡單的做法便是:為base class定義一個virtual析構函數。此後刪除derived class對象就會銷毀這個對象,包括所有的derived class成分。
class car
{
public:
     car();
     virtual ~car();
     ...   
};

 

  • [引申1]
當一個類需要被用作多態(Polymorphism)時,就應該為該類聲明一個virtual析構函數,即任何class只要帶有virtual函數都幾乎確定應該也有一個virtual析構函數。   但是,如果class沒有virtual函數,即不被用作多態用途,通常意味著它並不意圖被用作一個base class(除了某些特殊情況,如noncopyable類)。當class不被用作base class時,最好不要為其定義一個析構函數。 因為C++中將函數定義為virtual是有代價的,這個代價就是虛表指針virtual table pointer。   欲實現virtual函數,對象必須攜帶某種信息,用於在運行期決定調用哪一個virtual函數。這份信息通常是由一個所謂的vptr(virtual table pointer)指針指出。vptr指向一個有函數指針構成的數組,成為vtbl(virtual table);每一個帶有virtual函數的class都有一個相應的vtbl。當對對象調用某一virtual函數,是及被調用的函數取決於該對象的vptr所指向的那個vtbl——編譯器在其中尋找適當的函數指針。   因此,每一個定義了virtual函數的class的對象都包含一個vptr。這樣一來,對象的體積會因為virtual函數的存在而增加。   例如:
class point
{
public:
     point(int coord_x, int coord_y);
     ~point();
private:
     int x, y;
};
32位系統中,int類型占32bits,因此point對象一共占64bits,可以被塞入一個64bit緩存器中,甚至可以被當作一個“64bit 量”傳給其他語言如C活著FORTRAN編寫的函數。 但是如果point內含析構函數時,point對象占用的空間將是96bits,(2個ints加1個vptr)。對象體積從64bits增加到96bits。 而在64bit計算機體系結構中,point對象將占用128bits(因為指針類型占用64bits)。對象體積從64bits增加到128bits。   這樣的對象將無法被塞入一個64-bit緩存器中,而C++的point對象也不再和其他語言(如C)內的相同聲明有著一樣的結構,因此也就無法將其傳遞到其他語言編寫的函數中,因此不再有移植性。 因此,將不用作多態用途的class的析構函數聲明為virtual是不合理的。只有當class內至少含有一個virtual函數時才應該將其析構函數聲明為virtual。  
  • [引申2]
不要試圖繼承任何帶有non-virtual析構函數的類,包括所有STL容器如vector,list, set, unordered_map, string等等。因為這會導致資源洩漏! 不幸的是C++中沒有提供類似java的final classes或者c#中的sealed classes那樣的“禁止派生”機制。  
  • [引申3]
當希望將一個class定義為抽象class(pure virtual class),但有沒有任何pure virtual函數時,為這個class聲明一個pure virtual析構函數是很便利的。
class abstract_class
{
public:
     virtual ~abstract_class() = 0;
};
但是要注意:必須為這個pure virtual析構函數提供一份定義:
abstract_class::~abstract_class(){}
  因為析構函數的運作方式是:最深層派生(most derived)的那個class的析構函數最先被調用,然後是其每一個base class的析構函數被調用。編譯器會在 abstract_class的derived classes中創建一個對~abstract_class的調用動作,所以必須為~abstract_class提供定義,否則鏈接器會報錯。  
  • [總結]
1.polymorphic (帶多態性質的)base classes 應該聲明一個virtual析構函數。如果class 帶有任何virtual函數,就應該為其聲明一個virtual析構函數。因為這樣的base class設計出來的目的就是用來“通過base class 接口處理derived class對象”。 2.有些class原本就不是設計作為base class使用,或者就算是作為base class 也不具備多態性,這樣的class就不應該聲明為virtual析構函數。  
  • [補充]
默認生成的析構函數是public且non-virtual的。  

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved