參考博客:https://blog.csdn.net/songguangfan/article/details/87898915
C++中 的虛函數的作用主要是實現了多態的機制。關於多態,簡而言之就是用父類型別的指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數。這種技術 可以讓父類的指針有“多種形態”,這是一種泛型技術。
虛函數表
每個含有虛函數的類都有一個虛函數表(Virtual Table)來實現的。簡稱為V-Table。 C++的編譯器應該是保證虛函數表的指針存在於對象實例中最前面的位置(這是為了保證取到虛函數表的有最高的性能——如果有多層繼承或是多重繼承的情況下)。 這意味着我們通過對象實例的地址得到這張虛函數表,然后就可以遍歷其中函數指針,並調用相應的函數。
1、 每一個類都有虛函數列表。
2、 虛表可以繼承,如果子類沒有重寫虛函數,那么子類虛表中仍然會有該函數的地址,只不過這個地址指向的是基類的虛函數實現。如果基類3個虛函數,那么基類的虛表中就有三項(虛函數地址),派生類也會有虛表,至少有三項,如果重寫了相應的虛函數,那么虛表中的地址就會改變,指向自身的虛函數實現。如果派生類有自己的虛函數,那么虛表中就會添加該項。
3、 派生類的虛表中虛函數地址的排列順序和基類的虛表中虛函數地址排列順序相同,子類獨有的虛函數放在后面。
當定義一個有虛函數類的對象時,對象的第一塊的內存空間就是一個指向虛函數列表的指針。
在這舉個例子
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
然后定義變量 Base b;
看這幅圖:
注意:虛函數表在最后會有一個結束標志,為1說明還有虛表,為0表示沒有虛表了 。(編譯器不同,結束標志可能存在差異)。
(一)無虛函數覆蓋
沒有任何的繼承,虛函數表如下圖
(二)一般繼承(有虛函數覆蓋)
如果子類中有虛函數重載了父類的虛函數,會是一個什么樣子?假設,我們有下面這樣的一個繼承關系。如圖所示:
在這個類的設計中,只覆蓋了父類的一個函數:f()。那么,對於派生類的實例,其虛函數表會是下面的一個樣子:
從表中可以看到下面幾點,
1)覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。
2)沒有被覆蓋的函數依舊。
這樣就會出現
Base *b = new Derive();
b->f();
由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,於是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。
(三)多重繼承(無虛函數覆蓋)
下面我們再看看多重繼承的情況:
對於子類實例中的虛函數表,是下面這個樣子:
從圖上我們可以看到
1)每個父類都有自己的虛表。
2) 子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)
這樣做就是為了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。
(四)多重繼承(有虛函數覆蓋)
下面我們再來看看,如果發生虛函數覆蓋的情況。
下圖中,我們在子類中覆蓋了父類的f()函數。
子類虛函數列表如圖所示:
三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,我們就可以任一靜態類型的父類來指向子類,並調用子類的f()了。