什麼時多态
多态,即多種形态,是一種“泛型技術”,它企圖使用不變的模闆代碼來實作可變的算法。在C++中,多态分為兩種:
- 靜态多态,就是說在編譯時就能确定函數位址,通過複用函數名實作:如函數重載、運算符重載。
- 動态多态,就是能夠在運作時确定函數位址,通過派生類和虛函數一起在運作時實作。
它們兩者的差別就在于函數位址綁定的時間不同。函數重載和運算符載比較好了解。我們接下來主要了解派生類與虛函數一起是如何實作多态的。
虛函數
首先,我們要區分一下虛基類與虛函數,它們是不同的。基類是使用基類唯一化,虛函數則是能夠調用派生類的函數,自身的函數實作被隐藏。
什麼是虛基類
舉個例子來說明一下什麼是虛基類吧。
#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
- 首先,使用new關鍵字建立一個Derived對象,pb指針指向它。調用派生類構造函數會先調用基類的構造函數,然後再調用派生類的構造函數。
- 由于Derived繼承了Base,是以Derived擁有Base的屬性與方法,是以,對于pb->func1()時會調用Base的func1()函數。
- 由于Derived重寫了func2()函數,在Derived對象中的虛函數表項中指向func2()函數的指針被修改為Derived::func2(),由于虛函數表指針為類對象的第一個字段,即基類指針指向派生類對象時,仍然會擷取到派生類的虛函數表指針。是以pb->func2()時,程式會先通過派生類的虛函數表指針擷取func2()的入口。
- 當進行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;
}
};