關於虛函數的背景知識
- 用virtual關鍵字申明的函數叫做虛函數,虛函數肯定是類的成員函數。
- 存在虛函數的類都有一個一維的虛函數表叫做虛表。類的對象有一個指向虛表開始的虛指針。虛表是和類對應的,虛表指針是和對象對應的。
- 多態性是一個接口多種實現,是面向對象的核心。分爲類的多態性和函數的多態性。
- 多態用虛函數來實現,結合動態綁定。
- 純虛函數是虛函數再加上= 0。並且該函數只有聲明,沒有實現。
- 抽象類是指包括至少一個純虛函數的類。
那虛函數是如何運行的呢?
- class Base
- {
- public:
- virtual void func() {}
- }
- class Derive : public Base
- {
- public:
- void func() {}
- }
- void main()
- {
- Derive d;
- Base *pb = &d;
- b->func();
- }
class Base
{
public:
virtual void func() {}
}
class Derive : public Base
{
public:
void func() {}
}
void main()
{
Derive d;
Base *pb = &d;
b->func();
}
編譯器在編譯的時候,發現Base類中有虛函數,此時編譯器會爲每個包含虛函數的類創建一個虛表(即vtable),該表是一個一維數組,在這個數組中存放每個虛函數的地址。由於Base類和Derive類都包含了一個虛函數func(),編譯器會爲這兩個類都建立一個虛表,(即使子類裏面沒有virtual函數,但是其父類裏面有,所以子類中也有了)
那麼如何定位虛表呢?編譯器另外還爲每個類的對象提供了一個虛表指針(即vptr),這個指針指向了對象所屬類的虛表。在程序運行時,根據對象的類型去初始化vptr,從而讓vptr正確的指向所屬類的虛表。所以在調用虛函數時,就能夠找到正確的函數。
對於上述程序,由於pb實際指向的對象類型是Derive,因此vptr指向的Derive類的vtable,當調用pb->func()時,根據虛表中的函數地址找到的就是Derive類的func()函數。
正是由於每個對象調用的虛函數都是通過虛表指針來索引的,也就決定了虛表指針的正確初始化是非常重要的。換句話說,在虛表指針沒有正確初始化之前,我們不能夠去調用虛函數。那麼虛表指針在什麼時候,或者說在什麼地方初始化呢?
答案是在構造函數中進行虛表的創建和虛表指針的初始化。
還記得構造函數的調用順序嗎,在構造子類對象時,要先調用父類的構造函數,此時編譯器只“看到了”父類,並不知道後面是否後還有繼承者,它初始化父類對象的虛表指針,該虛表指針指向父類的虛表。當執行子類的構造函數時,子類對象的虛表指針被初始化,指向自身的虛表。對於以上的例子,當Derive類的d對象構造完畢後,其內部的虛表指針也就被初始化爲指向Derive類的虛表。在類型轉換後,調用pb->func(),由於pb實際指向的是Derive類的對象,該對象內部的虛表指針指向的是Derive類的虛表,因此最終調用的是Derive類的func()函數。
要注意:對於虛函數調用來說,每一個對象內部都有一個虛表指針,該虛表指針被初始化爲本類的虛表。所以在程序中,不管你的對象類型如何轉換,但該對象內部的虛表指針是固定的,所以呢,才能實現動態的對象函數調用,這就是C++多態性實現的原理。
總結(基類有虛函數):
- 每一個類都有虛表。
- 虛表可以繼承,如果子類沒有重寫虛函數,那麼子類虛表中仍然會有該函數的地址,只不過這個地址指向的是基類的虛函數實現。如果基類有3個虛函數,那麼基類的虛表中就有三項(虛函數地址),派生類也會有虛表,至少有三項,如果重寫了相應的虛函數,那麼虛表中的地址就會改變,指向自身的虛函數實現。如果派生類有自己的虛函數,那麼虛表中就會添加該項。
- 派生類的虛表中虛函數地址的排列順序和基類的虛表中虛函數地址排列順序相同。