1. 程式人生 > >虛擬函式,虛指標和虛表詳解

虛擬函式,虛指標和虛表詳解

關於虛擬函式的背景知識
1. 用virtual關鍵字申明的函式叫做虛擬函式,虛擬函式肯定是類的成員函式。
2. 存在虛擬函式的類都有一個一維的虛擬函式表叫做虛表。每一個類的物件都有一個指向虛表開始的虛指標。虛表是和類對應的,虛表指標是和物件對應的。
3. 多型性是一個介面多種實現,是面向物件的核心。分為編譯多型性和執行多型性。
4. 執行多型用虛擬函式來實現,結合動態繫結。
5. 純虛擬函式是虛擬函式再加上=0。並且該函式只有宣告,沒有實現。
6. 抽象類是指包括至少一個純虛擬函式的類。

詳細原理:
編譯器在編譯的時候,發現Base類中有虛擬函式,此時編譯器會為每個包含虛擬函式的類建立一個虛表(即vtable),該表是一個一維陣列(而不是一個連結串列),在這個陣列中存放每個虛擬函式的地址。由於Base類和Derive類都包含了一個虛擬函式func(),編譯器會為這兩個類都建立一個虛表。

那麼如何定位虛表呢?編譯器另外還為每個帶有虛擬函式的類的物件自動建立一個虛表指標(即vptr),這個指標指向了物件所屬類的虛表。在程式執行時,根據物件的型別去初始化vptr,從而讓vptr正確的指向所屬類的虛表。所以在呼叫虛擬函式時,就能夠找到正確的函式。

對於上述程式,由於實際指向的物件型別是Derive,因此vptr指向的Derive類的vtable,當呼叫func()時,根據虛表中的函式地址找到的就是Derive類的func()函式。

正是由於每個物件呼叫的虛擬函式都是通過虛表指標來索引的,也就決定了虛表指標的正確初始化是非常重要的。換句話說,在虛表指標沒有正確初始化之前,我們不能夠去呼叫虛擬函式。那麼虛表指標在什麼時候,或者說在什麼地方初始化呢?
答案是在建構函式中進行虛表的建立和虛表指標的初始化。

建構函式的呼叫順序,在構造子類物件時,要先呼叫父類的建構函式,之後再完成子類的構造。在呼叫父類的建構函式時,編譯器只“看到了”父類,並不知道後面是否後還有繼承者,它初始化虛表指標,將該虛表指標指向父類的虛表。當執行子類的建構函式時,虛表指標被重新賦值,指向自身的虛表。對於以上的例子,當Derive類的物件構造完畢後,其內部的虛表指標也就被初始化為指向Derive類的虛表。在型別轉換後,呼叫func(),由於實際指向的是Derive類的物件,該物件內部的虛表指標指向的是Derive類的虛表,因此最終呼叫的是Derive類的func()函式。

要注意:對於虛擬函式呼叫來說,每一個物件內部都有一個虛表指標,該虛表指標被初始化為本類的虛表。所以在程式中,不管你的物件型別如何轉換,但該物件內部的虛表指標是固定的,所以呢,才能實現動態的物件函式呼叫,這就是C++多型性實現的原理。

總結(基類有虛擬函式的情況下):
1. 每一個派生類都有虛表。
2. 虛表可以繼承,如果子類沒有重寫虛擬函式,那麼子類虛表中仍然會有該函式的地址,只不過這個地址指向的是基類的虛擬函式實現。如果基類有3個虛擬函式,那麼基類的虛表中就有三項(虛擬函式地址),派生類也會有虛表,至少有三項,如果重寫了相應的虛擬函式,那麼虛表中的地址就會改變,指向自身的虛擬函式實現。如果派生類有自己的虛擬函式,那麼虛表中就會新增該項。
3. 派生類的虛表中虛擬函式地址的排列順序和基類的虛表中虛擬函式地址排列順序相同。