多重繼承,菱形繼承下的虛擬函式表和虛基類表
虛擬函式與虛繼承尋蹤
封裝、繼承、多型是面嚮物件語言的三大特性,熟悉C++的人對此應該不會有太多異議。C語言提供的struct,頂多算得上對資料的簡單封裝,而C++的引入把struct“升級”為class,使得面向物件的概念更加強大。繼承機制解決了物件複用的問題,然而多重繼承又會產生成員衝突的問題,虛繼承在我看來更像是一種“不得已”的解決方案。多型讓物件具有了執行時特性,並且它是軟體設計複用的本質,虛擬函式的出現為多型性質提供了實現手段。
如果說C語言的struct相當於對資料成員簡單的排列(可能有對齊問題),那麼C++的class讓物件的資料的封裝變得更加複雜。所有的這些問題來源於C++的一個關鍵字——virtual!virtual在C++中最大的功能就是宣告虛擬函式和虛基類,有了這種機制,C++物件的機制究竟發生了怎樣的變化,讓我們一起探尋之。
為了檢視物件的結構模型,我們需要在編譯器配置時做一些初始化。在VS2010中,在專案——屬性——配置屬性——C/C++——命令列——其他選項中新增選項“/d1reportAllClassLayout”。再次編譯時候,編譯器會輸出所有定義類的物件模型。由於輸出的資訊過多,我們可以使用“Ctrl+F”查詢命令,找到物件模型的輸出。
一、基本物件模型
首先,我們定義一個簡單的類,它含有一個數據成員和一個虛擬函式。
class MyClass
{
int var;
public:
virtual void fun()
{}
};
編譯輸出的MyClass物件結構如下:
1> class MyClass size(8):
1> +---
1> 0 | {vfptr}
1> 4 | var
1> +---
1>
1> MyClass::[email protected]:
1> | &MyClass_meta
1> | 0
1> 0 | &MyClass::fun
1>
1> MyClass::fun this adjustor: 0
從這段資訊中我們看出,MyClass物件大小是8個位元組。前四個位元組儲存的是虛擬函式表的指標vfptr,後四個位元組儲存物件成員var的值。虛擬函式表的大小為4位元組,就一條函式地址,即虛擬函式fun的地址,它在虛擬函式表vftable的偏移是0。因此,MyClass物件模型的結果如圖1所示。
圖1 MyClass物件模型
MyClass的虛擬函式表雖然只有一條函式記錄,但是它的結尾處是由4位元組的0作為結束標記的。
adjust表示虛擬函式機制執行時,this指標的調整量,假如fun被多型呼叫的話,那麼它的形式如下:
*(this+0)[0]()
總結虛擬函式呼叫形式,應該是:
*(this指標+調整量)[虛擬函式在vftable內的偏移]()
二、單重繼承物件模型
我們定義一個繼承於MyClass類的子類MyClassA,它重寫了fun函式,並且提供了一個新的虛擬函式funA。
class MyClassA:public MyClass
{
int varA;
public:
virtual void fun()
{}
virtual void funA()
{}
};
它的物件模型為:
1> class MyClassA size(12):
1> +---
1> | +--- (base class MyClass)
1> 0 | | {vfptr}
1> 4 | | var
1> | +---
1> 8 | varA
1> +---
1>
1> MyClassA::[email protected]:
1> | &MyClassA_meta
1> | 0
1> 0 | &MyClassA::fun
1> 1 | &MyClassA::funA
1>
1> MyClassA::fun this adjustor: 0
1> MyClassA::funA this adjustor: 0
可以看出,MyClassA將基類MyClass完全包含在自己內部,包括vfptr和var。並且虛擬函式表內的記錄多了一條——MyClassA自己定義的虛擬函式funA。它的物件模型如圖2所示。
圖2 MyClassA物件模型
我們可以得出結論:在單繼承形式下,子類的完全獲得父類的虛擬函式表和資料。子類如果重寫了父類的虛擬函式(如fun),就會把虛擬函式表原本fun對應的記錄(內容MyClass::fun)覆蓋為新的函式地址(內容MyClassA::fun),否則繼續保持原本的函式地址記錄。如果子類定義了新的虛擬函式,虛擬函式表內會追加一條記錄,記錄該函式的地址(如MyClassA::funA)。
使用這種方式,就可以實現多型的特性。假設我們使用如下語句:
MyClass*pc=new MyClassA;
pc->fun();
編譯器在處理第二條語句時,發現這是一個多型的呼叫,那麼就會按照上邊我們對虛擬函式的多型訪問機制呼叫函式fun。
*(pc+0)[0]()
因為虛擬函式表內的函式地址已經被子類重寫的fun函式地址覆蓋了,因此該處呼叫的函式正是MyClassA::fun,而不是基類的MyClass::fun。
如果使用MyClassA物件直接訪問fun,則不會出發多型機制,因為這個函式呼叫在編譯時期是可以確定的,編譯器只需要直接呼叫MyClassA::fun即可。
三、多重繼承物件模型
和前邊MyClassA類似,我們也定義一個類MyClassB。
class MyClassB:public MyClass
{
int varB;
public:
virtual void fun()
{}
virtual void funB()
{}
};
它的物件模型和MyClassA完全類似,這裡就不再贅述了。
為了實現多重繼承,我們再定義一個類MyClassC。
class MyClassC:public MyClassA,public MyClassB
{
int varC;
public:
virtual void funB()
{}
virtual void funC()
{}
};
為了簡化,我們讓MyClassC只重寫父類MyClassB的虛擬函式funB,它的物件模型如下:
1> class MyClassC size(28):
1> +---
1> | +--- (base class MyClassA)
1> | | +--- (base class MyClass)
1> 0 | | | {vfptr}
1> 4 | | | var
1> | | +---
1> 8 | | varA
1> | +---
1> | +--- (base class MyClassB)
1> | | +--- (base class MyClass)
1> 12 | | | {vfptr}
1> 16 | | | var
1> | | +---
1> 20 | | varB
1> | +---
1> 24 | varC
1> +---
1>
1> MyClassC::[email protected]@:
1> | &MyClassC_meta
1> | 0
1> 0 | &MyClassA::fun
1> 1 | &MyClassA::funA
1> 2 | &MyClassC::funC
1>
1> MyClassC::[email protected]@:
1> | -12
1> 0 | &MyClassB::fun
1> 1 | &MyClassC::funB
1>
1> MyClassC::funB this adjustor: 12
1> MyClassC::funC this adjustor: 0
和單重繼承類似,多重繼承時MyClassC會把所有的父類全部按序包含在自身內部。而且每一個父類都對應一個單獨的虛擬函式表。MyClassC的物件模型如圖3所示。
圖3 MyClassC物件模型
多重繼承下,子類不再具有自身的虛擬函式表,它的虛擬函式表與第一個父類的虛擬函式表合併了。同樣的,如果子類重寫了任意父類的虛擬函式,都會覆蓋對應的函式地址記錄。如果MyClassC重寫了fun函式(兩個父類都有該函式),那麼兩個虛擬函式表的記錄都需要被覆蓋!在這裡我們發現MyClassC::funB的函式對應的adjust值是12,按照我們前邊的規則,可以發現該函式的多型呼叫形式為:
*(this+12)[1]()
此處的調整量12正好是MyClassB的vfptr在MyClassC物件內的偏移量。
四、虛擬繼承物件模型
虛擬繼承是為了解決多重繼承下公共基類的多份拷貝問題。比如上邊的例子中MyClassC的物件內包含MyClassA和MyClassB子物件,但是MyClassA和MyClassB內含有共同的基類MyClass。為了消除MyClass子物件的多份存在,我們需要讓MyClassA和MyClassB都虛擬繼承於MyClass,然後再讓MyClassC多重繼承於這兩個父類。相對於上邊的例子,類內的設計不做任何改動,先修改MyClassA和MyClassB的繼承方式:
class MyClassA:virtual public MyClass
class MyClassB:virtual public MyClass
class MyClassC:public MyClassA,public MyClassB
由於虛繼承的本身語義,MyClassC內必須重寫fun函式,因此我們需要再重寫fun函式。這種情況下,MyClassC的物件模型如下:
1> class MyClassC size(36):
1> +---
1> | +--- (base class MyClassA)
1> 0 | | {vfptr}
1> 4 | | {vbptr}
1> 8 | | varA
1> | +---
1> | +--- (base class MyClassB)
1> 12 | | {vfptr}
1> 16 | | {vbptr}
1> 20 | | varB
1> | +---
1> 24 | varC
1> +---
1> +--- (virtual base MyClass)
1> 28 | {vfptr}
1> 32 | var
1> +---
1>
1> MyClassC::[email protected]@:
1> | &MyClassC_meta
1> | 0
1> 0 | &MyClassA::funA
1> 1 | &MyClassC::funC
1>
1> MyClassC::[email protected]@:
1> | -12
1> 0 | &MyClassC::funB
1>
1> MyClassC::[email protected]@:
1> 0 | -4
1> 1 | 24 (MyClassCd(MyClassA+4)MyClass)
1>
1> MyClassC::[email protected]@:
1> 0 | -4
1> 1 | 12 (MyClassCd(MyClassB+4)MyClass)
1>
1> MyClassC::[email protected]@:
1> | -28
1> 0 | &MyClassC::fun
1>
1> MyClassC::fun this adjustor: 28
1> MyClassC::funB this adjustor: 12
1> MyClassC::funC this adjustor: 0
1>
1> vbi: class offset o.vbptr o.vbte fVtorDisp
1> MyClass 28 4 4 0
虛繼承的引入把物件的模型變得十分複雜,除了每個基類(MyClassA和MyClassB)和公共基類(MyClass)的虛擬函式表指標需要記錄外,每個虛擬繼承了MyClass的父類還需要記錄一個虛基類表vbtable的指標vbptr。MyClassC的物件模型如圖4所示。
圖4 MyClassC物件模型
虛基類表每項記錄了被繼承的虛基類子物件相對於虛基類表指標的偏移量。比如MyClassA的虛基類表第二項記錄值為24,正是MyClass::vfptr相對於MyClassA::vbptr的偏移量,同理MyClassB的虛基類表第二項記錄值12也正是MyClass::vfptr相對於MyClassA::vbptr的偏移量。
和虛擬函式表不同的是,虛基類表的第一項記錄著當前子物件相對與虛基類表指標的偏移。MyClassA和MyClassB子物件內的虛表指標都是儲存在相對於自身的4位元組偏移處,因此該值是-4。假定MyClassA和MyClassC或者MyClassB內沒有定義新的虛擬函式,即不會產生虛擬函式表,那麼虛基類表第一項欄位的值應該是0。
通過以上的物件組織形式,編譯器解決了公共虛基類的多份拷貝的問題。通過每個父類的虛基類表指標,都能找到被公共使用的虛基類的子物件的位置,並依次訪問虛基類子物件的資料。至於虛基類定義的虛擬函式,它和其他的虛擬函式的訪問形式相同,本例中,如果使用虛基類指標MyClass*pc訪問MyClassC物件的fun,將會被轉化為如下形式:
*(pc+28)[0]()
通過以上的描述,我們基本認清了C++的物件模型。尤其是在多重、虛擬繼承下的複雜結構。通過這些真實的例子,使得我們認清C++內class的本質,以此指導我們更好的書寫我們的程式。本文從物件結構的角度結合圖例為大家闡述物件的基本模型,和一般描述C++虛擬機制的文章有所不同。作者只希望藉助於圖表能把C++物件以更好理解的形式為大家展現出來,希望本文對你有所幫助。
相關推薦
多重繼承,菱形繼承下的虛擬函式表和虛基類表
虛擬函式與虛繼承尋蹤 封裝、繼承、多型是面嚮物件語言的三大特性,熟悉C++的人對此應該不會有太多異議。C語言提供的struct,頂多算得上對資料的簡單封裝,而C++的引入把struct“升級”為cla
C++虛繼承和虛基類;虛擬函式與繼承
定義:在C++中,在定義公共基類的派生類的時候,如果在繼承方式前使用關鍵字virtual對繼承方式限定,這樣的繼承方式就是虛擬繼承,公共的基類成為虛基類。這樣,在具有公共基類的、使用了虛擬繼承方式的多個派生類的公共派生類中,該基類的成員就只有一份拷貝
【c++】深入剖析虛擬繼承與各種繼承關係中派生類內成員記憶體分佈情況及虛基類表的內容
概要 本文講述在VS2012環境下,採用程式碼和圖結合的方法,分析C++程式碼中不同繼承方式的物件模型,以及從彙編角度分析虛擬繼承編譯器生成的虛基類表裡的內容,不涉及虛擬函式。 繼承分類: 1.單繼承 一個子類只有一個直接父類 // 單繼承 工人類 繼承 人類 cl
鉆石(菱形)繼承和虛基類
pan 由於 test 聲明 16px pri 技術分享 color protect 鉆石(菱形)繼承 如圖,B,C繼承了A,D繼承了B,C 在這種情況下,如果D類的對象通過B、C兩個不同的作用域調用A的數據成員,將會產生兩個 不同的A的數據成員值 如下(Grandfat
C++繼承和派生——派生類成員的標識與訪問(作用域分辨符和虛基類技術)
在派生類中,成員可以按訪問屬性劃分為以下四種: 不可訪問成員 準確說是不可以直接訪問。這種成員是從基類私有成員繼承而來,派生類或者派生類物件的模組都無法訪問這些成員,當然,派生類繼續派生的新類也是無法訪問它們的。 私有成員 這個可以是從
C++ 虛基類表指針字節對齊
小結 整數 影響 div 軟件測試 而已 焦點 測試 內存布局 下面博客轉載自別人的,我也是被這個問題坑了快兩天了,關於各種虛基類,虛繼承,虛函數以及數據成員等引發的一系列內存對齊的問題再次詳細描述 先看下面這片代碼。在這裏我使用了一個空類K,不要被這個東西所迷
【C++】c++單繼承、多繼承、菱形繼承記憶體佈局(虛擬函式表結構)
單繼承:只有一個基類和一個派生類 class Base { public: virtual void fun1() { cout << "Base::func1()" << endl;
c++單繼承、多繼承、菱形繼承記憶體佈局(虛擬函式表結構)
單繼承:只有一個基類和一個派生類 class Base { public: virtual void fun1() { cout << "Base::func1()" << endl; } vir
解析虛擬函式表和虛繼承
之前大二在學C++的時候一直對虛擬函式和虛繼承有些暈(其實好像就是對virtual這個關鍵字不太熟悉)現在又學習到了一些,對虛擬函式表和虛繼承的機制有了一點更深入的瞭解。 關於虛擬函式以及虛繼承的基礎知識,我自己也總結了一下,點選淺談C++多型和C++繼承可檢
繼承關係中的虛擬函式表
首先我們從一道筆試題開始: class ClassA { public: virtual ~ ClassA(){}; virtual void FunctionA(){}; }; cl
13.多重繼承(菱形繼承),虛繼承,虛基類
多重繼承: 就是一個派生類多個基類,幾乎與單繼承是一致的。 唯一考點: 菱形繼承結構(B類C類繼承A類,D類繼承A類。),產生問題,派生類有多份基類的資料。 解決辦法:B C都採用虛繼承,只有一
16 More Effective C++ —— 條款23/24 (虛擬函式、虛基類、多繼承、RTTI)
0 前序 由於條款23只是針對iostream和stdio.h之間,進行執行效率的對比,此處不會詳細展開。其宗旨是儘量使用C++的庫,可以提高程式的執行效率和安全性。 此篇將著重討論條論24的內容。 1 多型 C++中,多型是指使用基類指標、引用指向派生類,若基類和派生類中,
C++類的繼承關係——多繼承(未重寫虛擬函式)
首先,在介紹有虛擬函式的多繼承(未重寫虛擬函式),先介紹一下以下概念。 多型:多種形態,簡單地說父類的指標或引用呼叫重寫的虛擬函式,當父類的指標或引用指向父類物件呼叫的就是父類的虛擬函式,指向子類物件時呼叫的就是是子類的虛擬函式 虛擬
三種繼承、多型-虛擬函式
總結一下最近學到的類繼承的知識,包括三種繼承方式、多型的實現、動態聯編和靜態聯編。 歡迎各位指正其中的錯誤。 以後的理解更加深刻了回來更新和修改。 三種繼承 從一個類,派生出另一個類時,原始類稱為基類(父類),繼承類稱為派生類(子類) 派生類物
繼承多型與虛擬函式及對類的理解
B是A的子類,子類會繼承父類的public方法,子類物件可以呼叫父類的public介面,但是子類和父類函式重名時,父類的方法會被子類覆蓋(隱藏),子類呼叫和父類同名同參函式或者過載父類的函式後不可以直接呼叫父類函式,需要加A::fun()呼叫,父類的protect方
繼承*菱形繼承與菱形虛擬繼承(上)
面向物件程式設計的核心思想是封裝(資料抽象)、繼承和多型(動態繫結)。 通過使用資料抽象,我們可以將類的介面與實現分離; 使用繼承,可以定義相似的型別並對其相似關係建模; 使用動態繫結,可以在一定程度上忽略相似型別的區別,而用統一方式使用它們的物件。 簡
多重繼承的建構函式和解構函式的執行順序(包含虛基類)
C++程式碼: #include <iostream> using namespace std; class A { public: A() { co
c++筆記之虛基類&菱形繼承
簡單例子: #include <iostream> #include <string> using namespace std; class Person { public: Person(string nam, char s, int a) { n
第四章——64位軟體逆向技術-基本語法(下 虛擬函式)
虛擬函式 VC++實現虛擬函式的方式就是虛表,如果一個類至少要有一個虛擬函式,編譯器會為這個類產生一個虛表。不同的類虛表就不同,相同的類虛表就會共享 識別構造和析構 如果在函式入口有 lea reg,off_xxxx
C++ 虛擬函式 獲取C++虛表地址和虛擬函式地址
學過C++的應該都對虛表有所耳聞,在此就不過多介紹概念了,通過實 例來演示一下如何獲取虛表地址和虛擬函式地址。 簡單說一下虛表的概念:在一個類中如果有虛擬函式,那麼此類的例項中就有一個虛表指標指向虛表,這個虛表是一塊兒專門存放類的虛擬函式地址的記憶體。 圖示說