在C++中,多態是利用虛函數來實現的。比如說,有如下代碼:
#includeusing namespace std; class Animal { public: void Cry() { cout << "Animal cry!" << endl; } }; class Dog :public Animal { public: void Cry() { cout << "Wang wang!" << endl; } }; void MakeAnimalCry(Animal& animal) { animal.Cry(); } int main() { Dog dog; dog.Cry(); MakeAnimalCry(dog); return 0; }
輸出如下圖:
這裡定義了一個Animal類,Dog類繼承該類,並覆蓋了它的Cry方法。有一個MakeAnimalCry方法,傳入了Animal的引用,傳入了dog對象,但是輸出確是Animal的輸出。理想的情況下,用戶希望傳入的是dog對象,就該調用dog的Cry方法。要實現這種多態行為,需要將Animal::Cry()聲明為虛函數。可以通過Animal指針或者Animal引用來訪問Animal對象,這種指針或者引用可以指向Animal、Dog、Cat對象,而不需要關心它們具體指向的是哪種對象。修改代碼如下:
#includeusing namespace std; class Animal { public: virtual void Cry() { cout << "Animal cry!" << endl; } }; class Dog :public Animal { public: void Cry() { cout << "Wang wang!" << endl; } }; class Cat:public Animal { public: void Cry() { cout << "Meow meow" << endl; } }; void MakeAnimalCry(Animal& animal) { animal.Cry(); } int main() { Dog dog; Cat cat; //dog.Cry(); MakeAnimalCry(dog); MakeAnimalCry(cat); return 0; }
修改後的輸出如下:
這就是多態的效果,將派生類對象視為基類對象,並執行派生類的Cry實現。如果基類指針指向的是派生類對象,通過該指針調用運算符delete時,即對於使用new在自由存儲區中實例化的派生類對象,如果將其賦給基類指針,並通過該指針調用delete,將不會調用派生類的析構函數。這可能會導致資源未釋放、內存洩露等問題,為了避免這種問題,可以將基類的析構函數聲明為虛函數。
在上面的程序中,演示了多態的效果,即在函數MakeAnimalCry中,雖然通過Animal引用調用Cry方法,但是實際調用的確是Dog::Cry或者Cat::Cry方法。在編譯階段,編譯器並不知道將要傳遞給該函數的是哪種對象,無法確保在不同的情況下執行不同的Cry方法。應該調用哪個Cry方法顯然是在運行階段決定的。這是使用多態的不可見邏輯實現的,而這種邏輯是編譯器在編譯階段提供的。下面詳細地說明一下虛函數的底層實現原理。
比如說有下面的基類Base,它聲明了N個虛函數:
class Base { public: virtual void Func1() { //Func1的實現代碼 } virtual void Func2() { //Func2的實現代碼 } //Func3、Func4等虛函數的實現 virtual void FuncN() { //FuncN的實現代碼 } };
下面的Derived類繼承了Base類,並且覆蓋了除Func2之外的其他所有虛函數,
class Derived:public Base { public: virtual void Func1() { //Func2覆蓋Base類的Func1代碼 } //除去Func2的其他所有虛函數的實現代碼 virtual void FuncN() { //FuncN覆蓋Base類的FuncN代碼 } };
編譯器見到這種繼承層次結構後,知道Base定義了虛函數,並且在Derived類中覆蓋了這些函數。在這種情況下,編譯器將為實現了虛函數的基類和覆蓋了虛函數的派生類分別創建一個虛函數表(Virtual Function Table,VFT)。也就是說Base和Derived類都將有自己的虛函數表。實例化這些類的對象時,將創建一個隱藏的指針VFT*,它指向相應的VFT。可將VFT視為一個包含函數指針的靜態數組,其中每個指針都指向相應的虛函數。Base類和Derived類的虛函數表如下圖所示:
每個虛函數表都由函數指針組成,其中每個指針都指向相應虛函數的實現。在類Derived的虛函數表中,除一個函數指針外,其他所有的函數指針都指向本地的虛函數實現。Derived沒有覆蓋Base::Func2,因此相應的虛函數指針指向Base類的Func2的實現。這就意味著,當執行下面的代碼時,編譯器將查找Derived類的VFT,確保調用Base::Func2的實現:
Derived objDerived; objDerived.Func2();
調用被覆蓋的方法時,也是這樣:
void DoSomething(Base& objBase) { objBase.Func1(); } int main() { Derived objDerived; DoSomething(objDerived); }
在這種情況下,雖然將objDerived傳遞給了objBase,進而被解讀成一個Base實例,但該實例的VFT指針仍然指向Derived類的虛函數表,因此通過該VFT執行的是Derived::Func1.虛函數表就是通過上面的方式來實現C++的多態。
要驗證虛函數表的存在其實也很簡單,可以通過比較同一個類,一個包含虛函數,一個不包含,對比其大小就知道了。
#includeusing namespace std; class Test { public: int a,b; void DoSomething() { } }; class Base { public: int a,b; virtual void DoSomething() { } }; int main() { cout<<"sizeof(Test):"< 執行輸出如下: 雖然兩個類幾乎相同,因為Base中的DoSomething方法是一個虛函數,編譯器為Base類生成了一個虛函數表,並為其虛函數表指針預留空間,所以Base類占用的內存空間比Test類多了8個字節。