構造函數為什么不能為虛函數 & 基類的析構函數為什么要為虛函數


一、構造函數為什么不能為虛函數
1. 從存儲空間角度,虛函數相應一個指向vtable虛函數表的指針,這大家都知道,但是這個指向vtable的指針事實上是存儲在對象的內存空間的。問題出來了,假設構造函數是虛的,就須要通過 vtable來調用,但是對象還沒有實例化,也就是內存空間還沒有,怎么找vtable呢?所以構造函數不能是虛函數。
2. 從使用角度,虛函數主要用於在信息不全的情況下,能使重載的函數得到相應的調用。構造函數本身就是要初始化實例,那使用虛函數也沒有實際意義呀。所以構造函數沒有必要是虛函數。虛函數的作用在於通過父類的指針或者引用來調用它的時候可以變成調用子類的那個成員函數。而構造函數是在創建對象時自己主動調用的,不可能通過父類的指針或者引用去調用,因此也就規定構造函數不能是虛函數。
3. 構造函數不須要是虛函數,也不同意是虛函數,由於創建一個對象時我們總是要明白指定對象的類型,雖然我們可能通過實驗室的基類的指針或引用去訪問它但析構卻不一定,我們往往通過基類的指針來銷毀對象。這時候假設析構函數不是虛函數,就不能正確識別對象類型從而不能正確調用析構函數。
4. 從實現上看,vbtl在構造函數調用后才建立,因而構造函數不可能成為虛函數從實際含義上看,在調用構造函數時還不能確定對象的真實類型(由於子類會調父類的構造函數);並且構造函數的作用是提供初始化,在對象生命期僅僅運行一次,不是對象的動態行為,也沒有必要成為虛函數。
5. 當一個構造函數被調用時,它做的首要的事情之中的一個是初始化它的VPTR。因此,它僅僅能知道它是“當前”類的,而全然忽視這個對象后面是否還有繼承者。當編譯器為這個構造函數產生代碼時,它是為這個類的構造函數產生代碼——既不是為基類,也不是為它的派生類(由於類不知道誰繼承它)。所以它使用的VPTR必須是對於這個類的VTABLE。並且,僅僅要它是最后的構造函數調用,那么在這個對象的生命期內,VPTR將保持被初始化為指向這個VTABLE, 但假設接着另一個更晚派生的構造函數被調用,這個構造函數又將設置VPTR指向它的 VTABLE,等.直到最后的構造函數結束。VPTR的狀態是由被最后調用的構造函數確定的。這就是為什么構造函數調用是從基類到更加派生類順序的還有一個理由。可是,當這一系列構造函數調用正發生時,每一個構造函數都已經設置VPTR指向它自己的VTABLE。假設函數調用使用虛機制,它將僅僅產生通過它自己的VTABLE的調用,而不是最后的VTABLE(全部構造函數被調用后才會有最后的VTABLE)。

虛函數與非虛函數對照
  使用虛函數是有代價的,在內存和運行速度方面是有一定成本的,包含:

    l  每一個對象都將增大,增大量為存儲虛函數表指針的大小;

    l  對於每一個類,編譯器都創建一個虛函數地址表;

    l  對於每一個函數調用,都須要運行一項額外的操作,即到虛函數表中查找地址。

  盡管非虛函數比虛函數效率稍高,單不具備動態聯編能力


二、為什么基類的析構函數是虛函數?

  在實現多態時,當用基類操作派生類,在析構時防止僅僅析構基類而不析構派生類的狀況發生。

  以下轉自網絡:源地址 http://blog.sina.com.cn/s/blog_7c773cc50100y9hz.html

  a.第一段代碼

  

復制代碼
復制代碼
#include<iostream>
using namespace std;
class ClxBase{
public:
    ClxBase() {};
    ~ClxBase() {cout << "Output from the destructor of class ClxBase!" << endl;};

    void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};

class ClxDerived : public ClxBase{
public:
    ClxDerived() {};
    ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };

    void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
};
  int   main(){  
  ClxDerived *p =  new ClxDerived;
  p->DoSomething();
  delete p;
  return 0;
  }
復制代碼
復制代碼

 

  執行結果:

  Do something in class ClxDerived!            

  Output from the destructor of class ClxDerived!

  Output from the destructor of class ClxBase!  

  這段代碼中基類的析構函數不是虛函數,在main函數中用繼承類的指針去操作繼承類的成員,釋放指針P的過程是:先釋放繼承類的資源,再釋放基類資源. 

 

  b.第二段代碼

  

復制代碼
復制代碼
#include<iostream>
using namespace std;
class ClxBase{
public:
    ClxBase() {};
    ~ClxBase() {cout << "Output from the destructor of class ClxBase!" << endl;};

    void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};

class ClxDerived : public ClxBase{
public:
    ClxDerived() {};
    ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };

    void DoSomething() { cout << "Do something in class ClxDerived!" << endl; }
};
  int   main(){  
  ClxBase *p =  new ClxDerived;
  p->DoSomething();
  delete p;
  return 0;
  } 
復制代碼
復制代碼

 

  輸出結果:

  Do something in class ClxBase!
  Output from the destructor of class ClxBase!

    這段代碼中基類的析構函數相同不是虛函數,不同的是在main函數中用基類的指針去操作繼承類的成員,釋放指針P的過程是:僅僅是釋放了基類的資源,而沒有調用繼承類的析構函數.調用  dosomething()函數運行的也是基類定義的函數.

    普通情況下,這種刪除僅僅可以刪除基類對象,而不能刪除子類對象,形成了刪除一半形象,造成內存泄漏.

    在公有繼承中,基類對派生類及其對象的操作,僅僅能影響到那些從基類繼承下來的成員.假設想要用基類對非繼承成員進行操作,則要把基類的這個函數定義為虛函數.

    析構函數自然也應該如此:假設它想析構子類中的又一次定義或新的成員及對象,當然也應該聲明為虛的. 

 

  c.第三段代碼:

  

復制代碼
復制代碼
#include<iostream>
using namespace std;
class ClxBase{
public:
    ClxBase() {};
    virtual ~ClxBase() {cout << "Output from the destructor of class ClxBase!" << endl;};
    virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};

class ClxDerived : public ClxBase{
public:
    ClxDerived() {};
    ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };
    void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
};

  int   main(){  
  ClxBase *p =  new ClxDerived;
  p->DoSomething();
  delete p;
  return 0;
  }  
復制代碼
復制代碼

 

  執行結果:

  Do something in class ClxDerived!
  Output from the destructor of class ClxDerived!
  Output from the destructor of class ClxBase!

    這段代碼中基類的析構函數被定義為虛函數,在main函數中用基類的指針去操作繼承類的成員,釋放指針P的過程是:僅僅是釋放了繼承類的資源,再調用基類的析構函數.調用dosomething()函數運行的也是繼承類定義的函數.  

    假設不須要基類對派生類及對象進行操作,則不能定義虛函數,由於這樣會添加內存開銷.當類里面有定義虛函數的時候,編譯器會給類加入一個虛函數表,里面來存放虛函數指針,這樣就會添加類的存儲空間.所以,僅僅有當一個類被用來作為基類的時候,才把析構函數寫成虛函數.



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM