天天看點

虛函數的工作原理

通常,編譯器處理虛函數的方法是:給每個對象添加一個隐藏成員,隐藏成員中儲存了一個指向函數位址數組(虛函數表)的指針,虛函數表中存儲了為類對象進行聲明的虛函數的位址,例如:

class Scientist {
    ...
    char name[40];
public: 
    virtual void show_name();
    virtual void show_all();
};

class Physicist : public Scientist
{
  ...
  char field[40];
public:
    void show_all();  //重寫父類方法
    virtual void show_field();
    ...
};      

類​

​Scientist​

​​有一個隐藏的資料成員,裡面存儲了一個指向一張虛函數表的指針,該虛函數表記錄了類​

​Scientist​

​​定義的虛函數的位址;同理類​

​Physicist​

​​也同樣有一個隐藏的資料成員,裡面存儲了一個指向一張虛函數表的指針,該虛函數表記錄了類​

​Physicist​

​​繼承自​

​Scientist​

​的虛函數的位址和自己新定義虛函數的位址。

調用虛函數時,程式将檢視存儲在對象中的虛函數表位址,然後通過查詢虛函數表轉向相應的虛函數,是以虛函數相比于非虛函數存在一些缺點(凡是都具有兩面性):

  • 每個對象都将增大,增大量為存儲位址的空間;
  • 對于每個類,編譯器都建立一個虛函數位址表(數組);
  • 對于每個函數調用,都需要執行一項額外的操作,即到表中查找位址

雖然非虛函數的效率比虛函數稍高,但不具備動态聯編功能。

虛函數注意事項:

  • 在基類方法的聲明中使用關鍵字​

    ​virtual​

    ​可使該方法在基類以及所有的派生類(包括派生類派生出來的類)中是虛的;
  • 如果使用指向對象的引用或指針來調用虛方法,程式将使用對象類型定義的方法,而不使用為引用或指針類型定義的方法,即非虛看引用或指針類型,虛看對象類型;
  • 如果定義的類被用作基類,則應将那些要在派生類中重新定義的類方法聲明為虛的;
  • 構造函數不能是虛函數;
  • 析構函數應當是虛函數,除非類不用做基類
class Singer : public Employee
{
    ...
}

Employee * pe = new Singer;
delete pe;      

如果使用靜态聯編 ,​

​delete pe;​

​​将根據指針類型調用類​

​Employee​

​​的析構函數​

​~Employee()​

​​,這樣隻會釋放類​

​Singer​

​​從類​

​Employee​

​中繼承過來的那一部分,自身的那一部分因為沒有調用自身的析構函數而不會被釋放;

如果使用動态聯編,​

​delete pe;​

​​将根據對象類型先調用類​

​Singer​

​​的析構函數​

​~Singer()​

​​,釋放屬于類​

​Singer​

​​的那一部分,然後再調用類​

​Employee​

​​的析構函數​

​~Employee()​

​​,釋放屬于類​

​Employee​

​的那一部分;

  • 友元函數不能是虛函數,因為友元函數不是類成員,而隻有成員才能是虛函數,如果因為這個原因引起的設計問題,可以通過讓友元函數使用虛成員函數來解決;
  • 如果派生類沒有重新定義函數,将使用該函數的基類版本。如果派生類位于派生鍊中,則将使用最新的虛函數版本,例外的情況是基類版本是隐藏的;
  • 重新定義将隐藏方法
class Dweling
{
public: 
    virtual void showperks(int a) const;
    ...
};

class Hovel : public Dwelling
{
public:
    virtual void showperks() const;
}

Hovel trump;
trump.showperks();   //valid
trump.showperks(5);  //invalid      

新定義将​

​showperks()​

​定義為一個不接受任何參數的函數,重新定義不會生成函數的兩個重載版本,而是隐藏了接受一個int參數的基類版本。

  • 如果重新定義繼承的方法,應確定與原來的原型完全相同,但如果傳回類型是基類引用或指針,則可以修改為指向派生類的引用或指針(注意:這種例外隻适應于傳回值,而不适用于參數)
class Dweling
{
public: 
    virtual Dweling & showperks(int a) const;
    ...
};

class Hovel : public Dwelling
{
public:
    virtual Hovel &  showperks(int a) const;
}      
  • 如果基類聲明被重載了,則應在派生類中重新定義所有的基類版本
class Dweling
{
public: 
    virtual void showperks(int a) const;
    virtual void showperks(double x) const;
    virtual void showperks() const;
    ...
};

class Hovel : public Dwelling
{
public:
    virtual void showperks(int a) const;
    virtual void showperks(double x) const;
    virtual void showperks() const;
}