- 當類中包含虛函數時,則該類每個對象中在內存分配中除去數據外還包含了一個虛函數表指針(vfptr),指向虛函數表(vftable),虛函數表中存放了該類包含的虛函數的地址。
- 當子類通過虛繼承的方式從父類中派生出來,此時稱父類為子類的虛基類。子類中將包含虛基表指針(vbptr),指向虛基類表(vbtable)
- 在單繼承形式下,子類將完全獲得父類的虛函數表和數據(假入父類中有虛函數的話)。如果子類中重寫了父類的虛函數,就會在虛函數表中原本記錄父類中虛函數的地址覆蓋為子類中對應的重定義后的該函數地址,否則不做變動。如果在子類中定義了新的虛函數,則虛函數表中會追加一條記錄,記錄該函數的地址(虛函數表中是順序存放虛函數地址的,記住,雖然虛函數表中添加了內容,但是此時對於該類的大小來說並未發生改變,因為始終只有一個指向虛函數表的指針vfptr)。
- 如果在派生類對象直接訪問自身中的重定義的虛函數是不會觸發多態機制的,因為這個函數調用在編譯時期是可以確定的,編譯器只需要直接調用即可;
- 當對父類指針賦予不同的子類指針時,在調用子類中重定義的虛函數時才會觸發多態機制,即動態的在運行期間調用屬於子類的該函數。(可適度地了解下覆蓋override和重載overload的區別)
- 在多重繼承形式下,派生類會把所有的父類按繼承時的順序包含在自身內部。每個父類對應一個單獨的虛函數表。多重繼承下,子類不再具有自身的虛函數表,它的虛函數表與第一個父類的虛函數表合並了。同樣的,如果子類重寫了任意父類的虛函數,都會覆蓋對應的函數地址記錄。如果如果兩個虛基類都有該函數那么兩個虛函數表的記錄都需要被覆蓋!
- 此時由於多重繼承下將存在對公共基類的多份拷貝問題,為了節省內存空間,故而出現了虛擬繼承,那么將只生成一個共享的基類。
- 虛繼承的引入把對象的模型變得復雜,除了每個基類和公共基類的虛函數表指針vfptr需要記錄外,每個虛擬繼承了的父類還需要記錄一個虛基類表vbtable的指針vbptr。
- 虛基類表每項記錄了被繼承的虛基類子對象相對於虛基類表指針的偏
來自:https://blog.csdn.net/u012209626/article/details/48682555
虛函數開銷
- 每一個包含虛函數的類都需要專門的空間存放這個類的虛函數表。
- 需要在包含虛函數的類的每個對象中放置一個額外的指針。
- 必須放棄內聯
特性 | 增加對象大小 | 增加類的大小 | 減少內聯 |
虛函數 | 是 | 是 | 是 |
多繼承 | 是 | 是 | 否 |
虛基類 | 經常 | 有時 | 否 |
RTTI | 否 | 是 | 否 |
虛函數與虛繼承的具體實現
一、基本對象模型
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::$vftable@: 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::$vftable@: 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::$vftable@MyClassA@: 1> | &MyClassC_meta 1> | 0 1> 0 | &MyClassA::fun 1> 1 | &MyClassA::funA 1> 2 | &MyClassC::funC 1> 1> MyClassC::$vftable@MyClassB@: 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對象內的偏移量。
虛函數表,虛函數繼承實現:https://blog.csdn.net/best_fiends_zxh/article/details/59111761
虛擬繼承是為了解決多重繼承下公共基類的多份拷貝問題。比如上邊的例子中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::$vftable@MyClassA@: 1> | &MyClassC_meta 1> | 0 1> 0 | &MyClassA::funA 1> 1 | &MyClassC::funC 1> 1> MyClassC::$vftable@MyClassB@: 1> | -12 1> 0 | &MyClassC::funB 1> 1> MyClassC::$vbtable@MyClassA@: 1> 0 | -4 1> 1 | 24 (MyClassCd(MyClassA+4)MyClass) 1> 1> MyClassC::$vbtable@MyClassB@: 1> 0 | -4 1> 1 | 12 (MyClassCd(MyClassB+4)MyClass) 1> 1> MyClassC::$vftable@MyClass@: 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對象模型
注意:對於在對象中存取虛基類的問題,虛基類表僅是Microsoft編譯器的解決辦法。在其他編譯器中,一般采用在虛函數表中放置虛基類的偏移量的方式。
一般編譯器實現動態多態方法:
1、通過vbptr找到對象的vtbl(this指針的調整量);
2、找到vfptr中對應函數的指針(虛函數表中記錄的個數);
3、調用對象中指針指向的函數
*(this指針+調整量)[虛函數在vftable內的偏移]()
MyClassC中的fun()函數直接繼承與基類!!!
虛基類表每項記錄了被繼承的虛基類子對象相對於虛基類表指針的偏移量。比如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++虛擬機制的文章有所不同。
來自http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064.html
注意;兩個函數的返回類型,參數類型,參數個數都得相同,不然就起不到多態的作用
#include<iostream> #include<cmath> using namespace std; class A { public : virtual void fun(int x) { cout << "A: " << x << endl; } }; class B :public A { public : virtual void fun(float x) { cout << "B: " << x << endl; } }; void test(A & x) { int i = 1; x.fun(i); float a = 2.0; x.fun(a); } int main() { A a; B b; test(a); test(b); return 0; }
但是有一種特殊的情況,那就是如果基類中虛函數返回一個基類指針或引用,派生類中返回一個派生類的指針或引用,則c++將其視為同名虛函數而進行遲后聯編。
#include<iostream> #include<cmath> using namespace std; class A { public : virtual A * fun() { cout << "A: " << endl; return this; } }; class B :public A { public : virtual B * fun() { cout << "B: " << endl; return this; } }; void test(A & x) { x.fun(); } int main() { A a; B b; test(a); test(b); return 0; }
虛函數在g++中的實現
因為vptr明確的屬於一個實例,所以vptr的賦值理應放在類的構造函數中。 g++為每個有虛函數的類在構造函數末尾中隱式的添加了為vptr賦值的操作,vtbl的生成並不是運行時的,而是在編譯期就已經確定了存放在這兩個地址上的,這個地址屬於.rodata(只讀數據段)。所以g++在編譯期就為每個類確定了vtbl的內容,並且在構造函數中添加相應代碼使vptr能夠指向已經填好的vtbl的地址。
來自:https://www.tuicool.com/articles/iUB3Ebi
繼承是如何實現的:https://blog.csdn.net/dream_1996/article/details/68931347