1. 程式人生 > >C++ 虛解構函式的用法

C++ 虛解構函式的用法

C++中虛解構函式的作用

我們知道,用C++開發的時候,用來做基類的類的解構函式一般都是虛擬函式。可是,為什麼要這樣做呢?下面用一個小例子來說明: 

有下面的兩個類:

  1. #include <iostream>
  2. usingnamespace std;  
  3. class ClxBase  
  4. {  
  5. public:  
  6.     ClxBase() {};  
  7.     virtual ~ClxBase() {cout<<"AAA"<<endl;};  
  8.     virtualvoid DoSomething() { cout << "Do something in class ClxBase!"
     << endl; };  
  9. };  
  10. class ClxDerived : public ClxBase  
  11. {  
  12. public:  
  13.     ClxDerived() {};  
  14.     ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };  
  15.     void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };  
  16. };  
  17. int main()  
  18. {  
  19.     ClxBase *pTest = new ClxDerived;  
  20.     pTest->DoSomething();  
  21.     delete pTest;  
  22. }  

輸出結果:

Do something in class ClxDerived!

Output from the destructor of class ClxDerived!

AAA

這個很簡單,非常好理解。
但是,如果把類ClxBase解構函式前的virtual去掉,那輸出結果就是下面的樣子了:

Do something in class ClxDerived!

AAA

也就是說,類ClxDerived的解構函式根本沒有被呼叫!一般情況下類的解構函式裡面都是釋放記憶體資源,而解構函式不被呼叫的話就會造成記憶體洩漏。我想所有的C++程式設計師都知道這樣的危險性。當然,如果在解構函式中做了其他工作的話,那你的所有努力也都是白費力氣。
    所以,文章開頭的那個問題的答案就是--這樣做是為了當用一個基類的指標刪除一個派生類的物件時,派生類的解構函式會被呼叫。
    當然,並不是要把所有類的解構函式都寫成虛擬函式。因為當類裡面有虛擬函式的時候,編譯器會給類新增一個虛擬函式表,裡面來存放虛擬函式指標,這樣就會增加類的儲存空間。所以,只有當一個類被用來作為基類的時候,才把解構函式寫成虛擬函式。

你需要virtual解構函式嗎

使用VC的class wizard自動生成一個類,會得到兩個空的函式:建構函式和virtual解構函式。為什麼解構函式要宣告成virtual呢?

如果一個類要被使用成多型(polymorphic)的,那麼這個virtual是必須的。比如:

  1. #include <iostream>
  2. class Animal  
  3. {  
  4.   char* ap;  
  5. public:  
  6.   Animal()  
  7.   {  
  8.     ap = newchar;  
  9.     std::cout << "Animal ctor" << std::endl;  
  10.   }  
  11.   virtualvoid foo()  
  12.   {  
  13.     std::cout << "Animal::foo" << std::endl;  
  14.   }  
  15.   virtual ~Animal()  
  16.   {  
  17.     std::cout << "Animal dtor" << std::endl;  
  18.     delete ap;  
  19.   }  
  20. };  
  21. class Dog : public Animal  
  22. {  
  23.   char* dp;  
  24. public:  
  25.   Dog()  
  26.   {  
  27.     dp = newchar;  
  28.     std::cout << "Dog ctor" << std::endl;  
  29.   }  
  30.   virtualvoid foo()  
  31.   {  
  32.     std::cout << "Dog::foo" << std::endl;  
  33.   }  
  34.   virtual ~Dog()  
  35.   {  
  36.     delete dp;  
  37.     std::cout << "Dog dtor" << std::endl;  
  38.   }  
  39. };  
  40. int main()  
  41. {  
  42.   Animal* pa = new Dog();  
  43.   pa->foo();  
  44.   delete pa;  
  45.   return 0;  
  46. }  
 

delete pa 實際上相當於:
 pa->~Animal();
 釋放pa所指向的記憶體(或許是free(pa))。
在 這裡,因為~Animal()是virtual的,儘管是通過Animal型別的指標呼叫的,根據v-table的資訊,~Dog()被正確呼叫到。如果 把virtual屬性去掉,那麼被呼叫的是~Animal(),Dog類的建構函式被呼叫而解構函式未被呼叫,建構函式中分配的資源沒有釋放,從而產生了 記憶體洩漏。解構函式預設宣告為virtual,就可以避免這一問題。

可另一個問題是,有時virtual是不需要的。如果一個類不會被繼承,比如一個utility類,該類完全是靜態方法;或者一些類儘管可能會被繼承,但不會被使用成多型的,即除了解構函式外,沒有其他的方法是virtual的,這時就可以把virtual屬性去掉。

去掉解構函式的virtual屬性後,因為該類中沒有其他的virtual函式,所以編譯時不會生成v-table,這樣就節省了編譯時間,並減少了最終生成的程式的大小。更重要的是,遵從這一規則,給該類的維護者一個資訊,即該類不應被當作多型類使用。

同樣,當作一個抽象時,如果你模仿Java的interface,聲明瞭如下的虛基類:

  1. class AbstractBase  
  2. {  
  3.  virtual method1() = 0;  
  4.  virtual method2() = 0;  
  5. };  
 

那麼應該給它增加一個空的virtual解構函式:
 virtual ~AbstractBase(){}

如果你對COM比較熟悉,可能會注意到,COM interface中並沒有這個virutal建構函式。這是因為,COM通過使用引用計數的機制來維護物件。當你使用完一個COM物件,呼叫Release()時,COM的內部實現檢查引用技術是否為零。如果是,則呼叫
 delete this;
因為Release()是virtual的,所以該COM物件對應的正確的派生類被呼叫,delete this會呼叫正確的解構函式,達到了使用virtual解構函式的效果。

純虛成員函式通常沒有定義;它們是在抽象類中宣告,然後在派生類中實現。比如說下面的例子:

  1. class File //an abstract class
  2. {  
  3. public:  
  4.  virtualint open(const string & path, int mode=0x666)=0;  
  5.  virtualint close()=0;  
  6. //...
  7. };   
 

但是,在某些情況下,我們卻需要定義一個純虛成員函式,而不僅僅是宣告它。最常見的例子是純虛解構函式。在宣告純虛解構函式時,不要忘了同時還要定義它。

  1. class File //abstract class
  2. {  
  3. public:  
  4.  virtual ~File()=0; //declaration of a pure virtual dtor
  5. };  
  6. File::~File() {} //definition of dtor 
 
為什麼說定義純虛解構函式是非常重要的

派生類的解構函式會自動呼叫其基類的解構函式。這個過程是遞迴的,最終,抽象類的純虛解構函式也會被呼叫。

如果純虛解構函式只被宣告而沒有定義,那麼就會造成執行時(runtime)崩潰。(在很多情況下,這個錯誤會出現在編譯期,但誰也不擔保一定會是這樣。)純虛解構函式的啞元實現(dummy implementation,即空實現)能夠保證這樣的程式碼的安全性。

  1. class DiskFile : public File  
  2. {  
  3. public:  
  4.  int open(const string & pathname, int mode);  
  5.  int close();  
  6.  ~DiskFile();  
  7. };  
  8. File * pf = new DiskFile;  
  9. //. . .
  10. delete pf; //OK, ultimately invokes File::~File() 

在某些情況下定義其它純虛成員函式可能也是非常有用的(比如說在除錯應用程式以及記錄應用程式的日誌時)。例如,在一個不應該被呼叫,但是由於一個缺陷而被呼叫的基類中,如果有一個純虛成員函式,那麼我們可以為它提供一個定義。

  1. class Abstract  
  2. {  
  3. public:  
  4.  virtualint func()=0;  
  5. //..
  6. };  
  7. int Abstract::func()  
  8. {  
  9. std::cerr<<"got called from thread " << thread_id<<  
  10.              "at: "<<gettimeofday()<<std::endl;  
  11. }   

這樣,我們就可以記錄所有對純虛擬函式的呼叫,並且還可以定位錯誤程式碼;不為純虛擬函式提供定義將會導致整個程式無條件地終止。

虛建構函式(virtual constructor)

C++不支援直接的虛建構函式。虛 擬機制的設計目的是使程式設計師在不完全瞭解細節(比如只知該類實現了某個介面,而不知該類確切是什麼東東)的情況下也能使用物件。但是,要建立一個物件,可 不能只知道“這大體上是什麼”就完事——你必須完全瞭解全部細節,清楚地知道你要建立的物件是究竟什麼。所以,建構函式當然不能是虛的了。但是,可通過虛擬函式 virtual clone()(對於拷貝建構函式)或虛擬函式 virtual create()(對於預設建構函式),得到虛建構函式產生的效果。

注意:子類成員函式clone()的返回值型別故意與父類成員函式clone()的不同。這種特徵被稱為“協變的返回型別”(Covariant Return Types),該特徵最初並不是C++語言的一部分,VC6.0以下版本編譯器不支援這樣的寫法。

虛解構函式(virtual destructor)

當你可能通過基類指標刪除派生類物件時,建議使用虛解構函式。虛擬函式繫結到物件的類的程式碼,而不是指標/引用的類。如果基類有虛解構函式,delete basePtr(基類指標)時,*basePtr 的物件型別的解構函式被呼叫,而不是該指標的型別的解構函式。

簡單講,這個類有虛擬函式就應該有虛解構函式。一旦你在類中加上了一個虛擬函式,你就已經需要為每一個物件支付空間代價(每個物件一個指標),所以這時使解構函式成為虛擬的通常不會額外付出什麼。

對於那些trivial且沒有子類的類,虛解構函式只會增加開銷,不要使用。