天天看點

C++虛基類、虛函數、虛析構函數、純虛函數

什麼時多态

多态,即多種形态,是一種“泛型技術”,它企圖使用不變的模闆代碼來實作可變的算法。在C++中,多态分為兩種:

  1. 靜态多态,就是說在編譯時就能确定函數位址,通過複用函數名實作:如函數重載、運算符重載。
  2. 動态多态,就是能夠在運作時确定函數位址,通過派生類和虛函數一起在運作時實作。

它們兩者的差別就在于函數位址綁定的時間不同。函數重載和運算符載比較好了解。我們接下來主要了解派生類與虛函數一起是如何實作多态的。

虛函數

首先,我們要區分一下虛基類與虛函數,它們是不同的。基類是使用基類唯一化,虛函數則是能夠調用派生類的函數,自身的函數實作被隐藏。

什麼是虛基類

舉個例子來說明一下什麼是虛基類吧。

#include <iostream>

using namespace std;

class Base {
public:
    Base(){
        cout<< "Base" << endl;
    }
};

class DerivedA:  public Base{
public:
    DerivedA(){
        cout<< "Derived A" << endl;
    }
};

class DerivedB:  public Base{
public:
    DerivedB(){
        cout<< "Derived B"<<endl;
    }
};

class DerivedAll: public DerivedA,public DerivedB{
public:
    DerivedAll(){
        cout<< "Derived All"<<endl;
    }
};


int main()
{
   DerivedAll a;

    return 0;
}
           

列印結果:

Base
Derived A
Base
Derived B
Derived All
           

從上面列印出來的結果可以看出Base在記憶體中有兩個副本。但實際上隻需要一個Base副本就可以了。此時在繼承類前加上關鍵字virtual,即:

class DerivedA:  virtual public Base{
public:
    DerivedA(){
        cout<< "Derived A" << endl;
    }
};

class DerivedB: virtual public Base{
public:
    DerivedB(){
        cout<< "Derived B"<<endl;
    }
};
           

再次運作結果:

Base
Derived A
Derived B
Derived All
           

此時Base在記憶體隻有一份了。是以虛基類就是讓基類在記憶體中唯一化。

什麼是虛函數

虛函數是指一個類中要重載的成員函數,當一個基類指針或引用指向一個繼承類對象的時候,調用一個虛函數,實際調用的是派生類中的。否則,調用的就是基類中的。

#include <iostream>

using namespace std;

class Base {
public:
    void func(){
        cout<< "Base"<<endl;
    }
};

class DerivedA: public Base{
public:
    void func(){
        cout<< "Derived A"<<endl;
    }
};

int main()
{
   Base * pb = new DerivedA();
   pb->func();
    return 0;
}
           

列印結果:

Base
           

本來我們想通過基類指針調用派生類中的func方法,現在去調用了基類的。其實隻需将基類中的要重載的方法前加上關鍵字virtual,使其成為虛函數,就可以實作用基類指針或引用來調用派生類中重載了的方法:

class Base {
public:
    virtual void func(){
        cout<< "Base"<<endl;
    }
};
           

再次運作的結果:

Derived A
           

編譯器給每個對象和虛函數添加了一個隐形的成員:指向虛函數表的指針。虛函數表包含了虛函數的位址,由所有虛函數對象共用。

當派生類重新定義虛函數時,則将該函數的位址添加到虛函數表中。當一個基類指針指向一個派生類對象時,虛函數表指針指向派生類對象的虛函數表。

當調用虛函數時,由于派生類對象重寫了派生類對應的虛函數表項,基類在調用時會調用派生類的虛函數,進而産生多态。

請記住,無論對象中定義了多少個虛函數,虛函數表指針隻有一個,相應地,每個對象在記憶體中的大小要比沒有虛函數大8B(64位機)或4B(32位機)。這是指針的大小。

派生類繼承了基類的虛函數表指針,是以大小與基類一緻。如果多重繼承的另一個類也包括了虛函數的基類,那麼隐藏成員就包括了兩個虛函數表指針。例如:

#include <iostream>

using namespace std;

class Base {
public:
    void func1(){
        cout<< "Base func1"<<endl;
    }
    virtual void func2(){
        cout<< "Base func2"<<endl;
    }
    virtual void func3(){
        cout<< "Base func3"<<endl;
    }
};

class Derived: public Base{
public:
    virtual void func2(){
        cout<< "Derived func2"<<endl;
    }
};

int main()
{
   Base * pb = new Derived();
   pb->func1();
   pb->func2();
   pb->func3();
    return 0;
}


           
Base func1
Derived func2
Base func3
           
  1. 首先,使用new關鍵字建立一個Derived對象,pb指針指向它。調用派生類構造函數會先調用基類的構造函數,然後再調用派生類的構造函數。
  2. 由于Derived繼承了Base,是以Derived擁有Base的屬性與方法,是以,對于pb->func1()時會調用Base的func1()函數。
  3. 由于Derived重寫了func2()函數,在Derived對象中的虛函數表項中指向func2()函數的指針被修改為Derived::func2(),由于虛函數表指針為類對象的第一個字段,即基類指針指向派生類對象時,仍然會擷取到派生類的虛函數表指針。是以pb->func2()時,程式會先通過派生類的虛函數表指針擷取func2()的入口。
  4. 當進行pb->func3()時,由于派生類沒有重寫它,是以派生類的虛函數表裡的func3()的入口仍然是Base的func3()函數。

虛析構函數

一個基類指針可以通過new産生一個派生類對象,如果delete關鍵字去删除這個指針時,僅僅會調用基類的析構函數,而派生類的空間沒有被釋放,這樣會造成記憶體洩露。

為了防止記憶體洩露,當派生類中有指針成員變量時,才會使用到虛析構函數。虛析構函數使在删除指向派生類對象的基類指針時,可以通過調用派生類的析構函數來實作釋放派生類所占記憶體,進而防止記憶體洩露。

#include <iostream>

using namespace std;

class Base {
public:
    virtual ~ Base(){
        cout<< "delete Base" << endl;
    }
    virtual void func(){
        cout<< "Base func"<<endl;
    }
};

class Derived: public Base{
public:
    ~ Derived(){
        cout << "delete Derived"<< endl;
    }
    void func(){
        cout<< "Derived func2"<<endl;
    }
};

int main()
{
   Base * pb = new Derived();
   pb->func();
   delete pb;
    return 0;
}

           
Derived func2
delete Derived
delete Base
           

如果基類的析構函數不加virtual關鍵字,那它就是一個普通的析構函數。如果沒有将基類的析構函數聲明為虛析構函數,那麼删除基類指針時,隻會調用基類的析構函數,而不會調用派生類的析構函數。如果基類的析構函數聲明為虛析構函數,那麼删除基類指針時,會先調用派生類的析構函數,再調用基類的析構函數。

純虛函數

virtual 傳回類型 函數名(參數清單)=0;
           
class Base {
public:
    virtual void func() = 0;
};

class Derived: public Base{
public:
    virtual void func(){
        cout<< "Derived func"<<endl;
    }
};
           

繼續閱讀