C 虛函式表原理

2021-10-03 05:56:07 字數 4458 閱讀 4499

一、概述

為了實現c++的多型,c++使用了一種動態繫結的技術。這個技術的核心是虛函式表(下文簡稱虛表)。本文介紹虛函式表是如何實現動態繫結的。

二、類的虛表

每個包含了虛函式的類都包含乙個虛表。

我們知道,當乙個類(a)繼承另乙個類(b)時,類a會繼承類b的函式的呼叫權。所以如果乙個基類包含了虛函式,那麼其繼承類也可呼叫這些虛函式,換句話說,乙個類繼承了包含虛函式的基類,那麼這個類也擁有自己的虛表。

我們來看以下的**。類a包含虛函式vfunc1,vfunc2,由於類a包含虛函式,故類a擁有乙個虛表。

class a ;

類a的虛表如圖1所示

圖1:類a的虛表示意圖

虛表是乙個指標陣列,其元素是虛函式的指標,每個元素對應乙個虛函式的函式指標。需要指出的是,普通的函式即非虛函式,其呼叫並不需要經過虛表,所以虛表的元素並不包括普通函式的函式指標。

虛表內的條目,即虛函式指標的賦值發生在編譯器的編譯階段,也就是說在**的編譯階段,虛表就可以構造出來了。

三、虛表指標

虛表是屬於類的,而不是屬於某個具體的物件,乙個類只需要乙個虛表即可。同乙個類的所有物件都使用同乙個虛表。

為了指定物件的虛表,物件內部包含乙個虛表的指標,來指向自己所使用的虛表。為了讓每個包含虛表的類的物件都擁有乙個虛表指標,編譯器在類中新增了乙個指標,*__vptr,用來指向虛表。這樣,當類的物件在建立時便擁有了這個指標,且這個指標的值會自動被設定為指向類的虛表。

圖2:物件與它的虛表

上面指出,乙個繼承類的基類如果包含虛函式,那個這個繼承類也有擁有自己的虛表,故這個繼承類的物件也包含乙個虛表指標,用來指向它的虛表。

四、動態繫結

說到這裡,大家一定會好奇c++是如何利用虛表和虛表指標來實現動態繫結的。我們先看下面的**。

class a ;

class b : public a ;

class c: public b ;

類a是基類,類b繼承類a,類c又繼承類b。類a,類b,類c,其物件模型如下圖3所示。

圖3:類a,類b,類c的物件模型

由於這三個類都有虛函式,故編譯器為每個類都建立了乙個虛表,即類a的虛表(a vtbl),類b的虛表(b vtbl),類c的虛表(c vtbl)。類a,類b,類c的物件都擁有乙個虛表指標,*__vptr,用來指向自己所屬類的虛表。

類a包括兩個虛函式,故a vtbl包含兩個指標,分別指向a::vfunc1()和a::vfunc2()。

類b繼承於類a,故類b可以呼叫類a的函式,但由於類b重寫了b::vfunc1()函式,故b vtbl的兩個指標分別指向b::vfunc1()和a::vfunc2()。

類c繼承於類b,故類c可以呼叫類b的函式,但由於類c重寫了c::vfunc2()函式,故c vtbl的兩個指標分別指向b::vfunc1()(指向繼承的最近的乙個類的函式)和c::vfunc2()。

雖然圖3看起來有點複雜,但是只要抓住「物件的虛表指標用來指向自己所屬類的虛表,虛表中的指標會指向其繼承的最近的乙個類的虛函式」這個特點,便可以快速將這幾個類的物件模型在自己的腦海中描繪出來。

非虛函式的呼叫不用經過虛表,故不需要虛表中的指標指向這些函式。

假設我們定義乙個類b的物件。由於bobject是類b的乙個物件,故bobject包含乙個虛表指標,指向類b的虛表。

int main()

現在,我們宣告乙個類a的指標p來指向物件bobject。雖然p是基類的指標只能指向基類的部分,但是虛表指標亦屬於基類部分,所以p可以訪問到物件bobject的虛表指標。bobject的虛表指標指向類b的虛表,所以p可以訪問到b vtbl。如圖3所示。

int main()

當我們使用p來呼叫vfunc1()函式時,會發生什麼現象?

int main()

程式在執行p->vfunc1()時,會發現p是個指標,且呼叫的函式是虛函式,接下來便會進行以下的步驟。

首先,根據虛表指標p->__vptr來訪問物件bobject對應的虛表。雖然指標p是基類a*型別,但是*__vptr也是基類的一部分,所以可以通過p->__vptr可以訪問到物件對應的虛表。

然後,在虛表中查詢所呼叫的函式對應的條目。由於虛表在編譯階段就可以構造出來了,所以可以根據所呼叫的函式定位到虛表中的對應條目。對於 p->vfunc1()的呼叫,b vtbl的第一項即是vfunc1對應的條目。

最後,根據虛表中找到的函式指標,呼叫函式。從圖3可以看到,b vtbl的第一項指向b::vfunc1(),所以 p->vfunc1()實質會呼叫b::vfunc1()函式。

如果p指向類a的物件,情況又是怎麼樣?

int main()

當aobject在建立時,它的虛表指標__vptr已設定為指向a vtbl,這樣p->__vptr就指向a vtbl。vfunc1在a vtbl對應在條目指向了a::vfunc1()函式,所以 p->vfunc1()實質會呼叫a::vfunc1()函式。

可以把以上三個呼叫函式的步驟用以下表示式來表示:

(*(p->__vptr)[n])(p)

可以看到,通過使用這些虛函式表,即使使用的是基類的指標來呼叫函式,也可以達到正確呼叫執行中實際物件的虛函式。

我們把經過虛表呼叫虛函式的過程稱為動態繫結,其表現出來的現象稱為執行時多型。動態繫結區別於傳統的函式呼叫,傳統的函式呼叫我們稱之為靜態繫結,即函式的呼叫在編譯階段就可以確定下來了。

那麼,什麼時候會執行函式的動態繫結?這需要符合以下三個條件。

通過指標來呼叫函式

指標upcast向上轉型(繼承類向基類的轉換稱為upcast,關於什麼是upcast,可以參考本文的參考資料)

呼叫的是虛函式

如果乙個函式呼叫符合以上三個條件,編譯器就會把該函式呼叫編譯成動態繫結,其函式的呼叫過程走的是上述通過虛表的機制。

五、總結

封裝,繼承,多型是物件導向設計的三個特徵,而多型可以說是物件導向設計的關鍵。c++通過虛函式表,實現了虛函式與物件的動態繫結,從而構建了c++物件導向程式設計的基石。

#include using namespace std;

class a

virtual void vfunc1() ;

virtual void vfunc2() ;

void func1() ;

void func2() ;

private:

int m_data1, m_data2;

};class b : public a

virtual void vfunc1() ;

virtual void vfunc2() ;

void func1() ;

void func2() ;

private:

int m_data3;

};class c: public b

virtual void vfunc1() ;

virtual void vfunc2() ;

void func1() ;

void func2() ;

private:

int m_data1, m_data4;

};int main()

結論;這裡使用b類做基類,指向c類,會發現只要定義virturl虛函式,虛函式不停向子類傳導而呼叫子類的虛函式,如果是普通函式,則呼叫基類的。

舉例:class a;

class b :class a;

class c : class b;

如果b *b = new c();

b->vfunc1();//這裡實際呼叫c類的虛函式,因為b類沒有實現;如果c也沒有實現,就呼叫a的虛函式.

c 虛函式與虛函式表原理

目錄 用virtual修飾的成員函式叫虛函式 小知識 沒有虛建構函式 不寫虛函式,沒有預設的虛函式 普通函式不影響類的記憶體 class mm protected void testvirtual int main 輸出 1 增加乙個指標的記憶體,32位作業系統多4個位元組 64位作業系統多8個位元...

C 原理剖析之虛函式表

最近在看c 的一些相關的機制,再加上剛看了陳皓大神的早期關於虛函式表的部落格,便自己動手通過程式設計了解了下虛函式表的原理。c 是通過虛函式來實現多型的的機制。我們可以通過將父類的指標指向子類的例項,如base b new derive 如此一來,如果子類derive中過載了父類中的乙個函式h 那麼...

C 虛函式表及虛函式執行原理詳解

為了實現虛函式,c 使用了虛函式表來達到延遲繫結的目的。虛函式表在動態 延遲繫結行為中用於查詢呼叫的函式。儘管要描述清楚虛函式表的機制會多費點口舌,但其實其本身還是比較簡單的。首先,每個包含虛函式的類 或者繼承自的類包含了虛函式 都有乙個自己的虛函式表。這個表是乙個在編譯時確定的靜態陣列。虛函式表包...