C++中, 爲什麼需要定義析構函數爲虛函數

先構造一個類,如下所示:

 

[plain] view plaincopy

  1. #include   
  2. #include   
  3. using namespace std;  
  4. #include    
  5.   
  6. using namespace std;  
  7.   
  8. class Base  
  9. {  
  10.     public:  
  11.         Base(){ cout<<"Constructing Base\n";}  
  12.   
  13.      // this is a destructor:  
  14.   
  15.         ~Base(){ cout<<"Destroying Base\n";}  
  16. };  
  17.   
  18. class Derive: public Base  
  19. {  
  20.         public:  
  21.         Derive(){ cout<<"Constructing Derive\n";}  
  22.   
  23.         ~Derive(){ cout<<"Destroying Derive\n";}  
  24.  };  
  25.   
  26. int main()  
  27. {  
  28.         Derive s;  
  29.   
  30.   
  31.         return 0;  
  32. }  

上面代碼在運行的時候,生成Derive的對象的時候調用的構造函數,會首先調用基類的構造函數,所以當函數執行完畢,撤銷s 的時候,也會調用Derive的析構函數之後調用Base 的析構函數,也就是說,不管析構函數是不虛函數,派生類對象在撤銷的時候,肯定會一次調用基類的析構函數。那我們爲什麼要將析構函數定義爲虛函數呢?

 

      原因是因爲多態的存在。

         我們大家都知道,在C++ 中,當一個對象銷燬時,析構函數是用來對類對象和對象成員進行釋放內存和做一些其他的cleanup操作。析構函數靠~符號來區分,~ 出現在 析構函數名字的前面,  當我們去定義一個 虛析構函數時,你只需要簡單的的在~符號前面 加一個  virtual標誌就可以了。

         爲什麼需要將析構函數聲明爲 虛函數,我們最好用幾個例子來驗證一下,我們首先以一個 不使用虛析構函數的例子開始,然後我們使用一個使用析構函數的例子。一旦看到了其中的區別,你就會明白 爲什麼需要將析構函數聲明爲虛函數。讓我們開始看一下代碼。

         Example  without a Virtual Destructor:

         

[python] view plaincopy

  1. "font-size:14px;">#include    
  2.   
  3. using namespace std;  
  4.   
  5. class Base  
  6. {  
  7.     public:  
  8.         Base(){ cout<<"Constructing Base\n";}  
  9.   
  10.      // this is a destructor:  
  11.   
  12.         ~Base(){ cout<<"Destroying Base\n";}  
  13. };  
  14.   
  15. class Derive: public Base  
  16. {  
  17.         public:  
  18.         Derive(){ cout<<"Constructing Derive\n";}  
  19.   
  20.         ~Derive(){ cout<<"Destroying Derive\n";}  
  21.  };  
  22.   
  23. int main()  
  24. {  
  25.         Base *basePtr = new Derive();  
  26.   
  27.         delete basePtr;  
  28.         return 0;  
  29. }  
  30.   

運行 輸出 是下面這樣的:

 

根據上面的輸出,我們可以看到當我們新建一個指向Deriver類型對象指針的時候,構造函數按照正適當的順序依次調用,但是這裏有一個主要問題

當我們刪除指向Deriver 的基類指針時, Deriver類中的析構函數沒有調用。這裏調用的是Base的析構函數(靜態聯編)這裏設計

那何爲動態聯編和  靜態聯編?下面講解一下:

聯編是一個計算機程序彼此相關連的過程。按照聯編所進行的階段不同,可分爲兩種不同的聯編方法:靜態聯編和動態聯編。

       將源代碼中的函數調用解釋爲執行特定的函數代碼被稱爲函數名聯編(binding).

         在編譯過程中進行聯編被稱爲靜態聯編(static binding),又稱早期聯編(early binding).它對函數的選擇是根據基於對象的 指針或者引用來確定的。(即指針或者引用 指向哪個對象就調用哪個對象的相應的函數)
         編譯器必須生成能夠在程序運行時選擇正確的虛方法的代碼,這被稱爲動態聯編(dynamic binding), 又稱爲晚期聯編(late binding).

 

 

 1.爲什麼有兩種類型的聯編以及爲什麼默認爲靜態聯編?
      如果動態聯編讓您能夠重新定義類方法,而靜態聯編在這方面很差,爲何摒棄靜態聯編呢?原因有兩個-----效率
      首先來看效率.爲使程序能夠在運行階段進行決策,必須採取一些方法來跟蹤實際運行中基類指針或引用指向的對象類型,這增加了額外的開銷.例如,如果類不會用作基類,則不需要動態聯編.同樣,如果派生類不重新定義基類的任何方法,也不需要使用動態聯編.在這些情況下,使用靜態聯編更合理,效率也更高.由於靜態聯編的效率更高,因此被設置爲C++的默認選擇.Strousstrup說,C++的指導原則之一是, 不要爲不使用的我付出代價(內存或者處理時間).僅當程序設計確實 需要虛函數時,才使用它們.
      接下來看概念模型.在設計類時,可能包含一些不在派生類重新定義的成員函數.不將訪函數設置爲虛函數有兩方面好處:首先效率高;其次,指出不要重新定義該函數.這表明,僅將那些預期將被重新定義的方法聲明爲虛擬的.

  2.虛函數的工作原理
      通常,編譯器處理虛函數的方法是:給每一個對象添加一個隱藏成員.隱藏成員中保存了一個指向函數地址數組的指針.這種數組稱爲虛函數表(virtual function table, vtbl).虛函數表中存儲了爲類對象進行聲明的虛函數的地址.例如,基類對象包含一個指針,訪指針指向基類中所有虛函數地址表.派生類對象將包含一個指向獨立地址表的指針.如果派生類提供了虛歲函數的新定義,訪虛函數表將保存新函數的地址;如果派生類沒有重新定義虛函數.該vtbl將保存函數原始版本的地址.如果派生類定義了新的虛函數,則該函數的地址也將被添加到vtbl中.注意,無論類中包含的虛函數是1還是10 個,都只需要在對象中添加1個地址成員,只是表的大小不同而已.


      調用虛函數時,程序將查看存儲在對象中的vtbl地址,然後轉向相應的函數地址表.如果使用類聲明中定義的第一個虛函數,則程序將使用數組中的第一個函數地址,並執行具有該地址的函數.如果使用類聲明中的第三個虛函數,程序將使用地址爲數組中第三個元素的函數.


      簡而言之,使用虛函數時, 在內存和執行速度方面有一定成本,包括:
            每個對象都將增大,增大量爲存儲地址的空間.
            對每個類,編譯器都創建一個虛歲函數地址表.
            每個函數調用都有需要執行一步額外的操作,即到表中查找地址.
      雖然非虛函數的效率比虛函數稍高,但不具備動態聯編功能.


 

 

那我們瞭解了動態聯編和靜態聯編,如何解決這個問題呢,使用虛函數來 轉到動態聯編。

我們用基類指針 去指向一個派生類對象,然後釋放該指針,在沒有將基類析構函數聲明爲 虛函數的時候,只調用的基類的析構函數,而沒有去調用派生類的析構函數。(因爲這裏是使用的基類指針)

如果我們修改一下代碼,使用派生類指針,並且釋放派生類指針的話結果:

這裏會首先調用派生類析構函數,然後 在調用基類的析構函數。

解決: 將 基類析構函數生命爲虛析構函數,

 

           Example with a Virtual Destructor:

          我們需要做的就是修改Base 類中的析構函數,在~前面加上virtual ,關鍵字爲紅色。

 

[plain] view plaincopy

  1. class Base  
  2. {  
  3.     public:  
  4.         Base(){ cout<<"Constructing Base";}  
  5.   
  6.     // this is a virtual destructor:  
  7.     virtual ~Base(){ cout<<"Destroying Base";}  
  8. };  


改變之後,運行輸出爲:

 

 

我們在基類中將析構函數標明爲虛函數,就表示在使用析構函數時,是採用動態聯編的。那麼delete basePtr的時候不再是採用靜態聯編直接在編譯的時候確定basePtr指向的析構函數,而是在運行的時候根據 指向的類型來調用析構函數。(如有錯誤,請指出。謝謝~)

現在你知道我們爲什麼需要 虛析構函數和 他們是怎麼工作的了把?

 

 

 

如果你在 派生類中 分配了 內存空間的話,沒有將基類的析構函數聲明爲虛析構函數,很容易發生內存泄漏事件。

例子:

 

[plain] view plaincopy

  1. #include    
  2.   
  3. using namespace std;  
  4.   
  5. class Base  
  6. {  
  7.     public:  
  8.         Base(){ data = new char[10];}  
  9.   
  10.      // this is a destructor:  
  11.   
  12.         ~Base(){ cout << "destroying Base data[]";delete []data;}  
  13.     private:  
  14.         char *data;  
  15. };  
  16.   
  17. class Derive: public Base  
  18. {  
  19.     public:  
  20.         Derive(){ D_data = new char[10];}  
  21.   
  22.         ~Derive(){ cout << "destroying Derive data[]";delete []D_data;}  
  23.     private:  
  24.         char *D_data;  
  25.  };  
  26.   
  27. int main()  
  28. {  
  29.         Base *basePtr = new Derive();  
  30.   
  31.         delete basePtr;  
  32.         return 0;  
  33. }  
  34.   


首先我們在基類,和派生類中都個分配了 10個 字節的空間,這些空間應該由程序員來釋放,如果沒有釋放掉的話,就會造成內存泄漏。

 

來看一下運行結果:

我們可以看到只刪除了基類的分配的空間,這個時候派生類的對象的空間沒有刪除,內存泄漏。

這一部分摘自:http://blog.csdn.net/searchlife/article/details/3985341

另外一個例子:

 

[plain] view plaincopy

  1. #include   
  2. class CBase  
  3. {  
  4. public:  
  5.     CBase(){data = new char[64];}  
  6.     ~CBase(){delete [] data;}  
  7. private:  
  8.     char *data;  
  9. };  
  10. class CFunction  
  11. {  
  12. public:  
  13.     CFunction(){};  
  14.     ~CFunction(){};  
  15. };  
  16. class CFunctionEx : public CFunction  
  17. {  
  18. public:  
  19.     CFunctionEx(){};  
  20.     ~CFunctionEx(){};  
  21. private:  
  22.     CBase m_cbase;  
  23. };  
  24. void main()  
  25. {  
  26. CFunction *pCFun = new CFunctionEx;  
  27. delete pCFun;  
  28. }  


這裏CfunctionEx和Cfunction中本身並沒有分配內存,應該不會有內存泄漏。和上例一樣當刪除pCFun時,它只調用了Cfunction的析構函數而沒調用CfunctionEx的析構函數,但CfunctionEx本身並沒分配內存。所以發生內存泄露的地方是m_cbase,因爲它是CBase的實例且是CfunctionEx成員變量,當CfunctionEx的析構函數沒有被調用時,當然m_cbase的析構函數也沒有被調用,所以CBase中分配的內存被泄漏。
解決以上問題的方法很簡單,就是使基類Cfunction的析構函數爲虛函數就可以了。

        這樣就得出一個結論,當你的基類的析構函數不爲虛的話,其子類中所有的成員變量的類中分配的內存也將可能泄漏。

        這裏說的可能是因爲,如果程序中沒有以上示例類似寫法(指用基類指針指向子類實例,虛函數是C++的精華,很少有人不用的,由其是在大中型軟件項目中),就不會出現本文所說的內存泄漏。看來在基類中使析構函數爲虛函數是如此的重要。所以強烈建議在基類中把析構函數聲明爲虛函數,但是隻有你寫的類並不做爲基類時例外。

 

 

 

 

將基類的析構函數設爲virtual型,則所有的基類的派生類對象的析構函數都會自動設置爲virtual型,這保證了任何情況下,不會出現由於析構函數沒有被調用而導致的內存泄漏。 這是MFC將基類的析構函數設置爲虛函數的真正原因。

 

 

 

 

爲什麼 構造函數不能爲虛函數。原文地址:http://blog.sina.com.cn/s/blog_620882f401016ri2.html

         1.從存儲空間角度

             虛函數對應一個vtale,這個表的地址是存儲在對象的內存空間的。如果將構造函數設置爲虛函數,就需要到vtable中調用,可是對象還沒有實例化,沒有內存空間分配,如何調用。(悖論)

         2.從使用角度

 

        虛函數主要用於在信息不全的情況下,能使重載的函數得到對應的調用。構造函數本身就是要初始化實例,那使用虛函數也沒有實際意義呀。所以構造函數沒有必要是虛函數。虛函數的作用在於通過父類的指針或者引用來調用它的時候能夠變成調用子類的那個成員函數。而構造函數是在創建對象時自動調用的,不可能通過父類的指針或者引用去調用,因此也就規定構造函數不能是虛函數。

          構造函數不需要是虛函數,也不允許是虛函數,因爲創建一個對象時我們總是要明確指定對象的類型,儘管我們可能通過實驗室的基類的指針或引用去訪問它。但析構卻不一定,我們往往通過基類的指針來銷燬對象。這時候如果析構函數不是虛函數,就不能正確識別對象類型從而不能正確調用析構函數。

        3、從實現上看,vbtl在構造函數調用後才建立,因而構造函數不可能成爲虛函數  

  從實際含義上看,在調用構造函數時還不能確定對象的真實類型(因爲子類會調父類的構造函數);而且構造函數的作用是提供初始化,在對象生命期只執行一次,不是對象的動態行爲,也沒有太大的必要成爲虛函數

        4、當一個構造函數被調用時,它做的首要的事情之一是初始化它的V P T R。因此,它只能知道它是“當前”類的,而完全忽視這個對象後面是否還有繼承者。 當編譯器爲這個構造函數產生代碼時,它是爲這個類的構造函數產生代碼- -既不是爲基類,也不是爲它的派生類(因爲類不知道誰繼承它)。

         所以它使用的V P T R必須是對於這個類的V TA B L E。而且,只要它是最後的構造函數調用,那麼在這個對象的生命期內, V P T R將 保持被初始化爲指向這個V TA B L E, 但如果接着還有一個更晚派生的構造函數被調用,這個構造函數又將設置V P T R指向它的 V TA B L E,等.直到最後的構造函數結束。V P T R的狀態是由被最後調用的構造函數確定的。這就是爲什麼構造函數調用是從基類到更加派生 類順序的另一個理由。

        但是,當這一系列構造函數調用正發生時,每個構造函數都已經設置V P T R指向它自己的 V TA B L E。如果函數調用使用虛機制,它將只產生通過它自己的V TA B L E的調用,而不是最後的V TA B L E(所有構造函數被 調用後纔會有最後的V TA B L E)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章