【C++】C++对象模型:对象内存布局详解(C#实例)
C++對(duì)象模型:對(duì)象內(nèi)存布局詳解
0.前言
C++對(duì)象的內(nèi)存布局、虛表指針、虛基類指針解的探討,參考。
1.何為C++對(duì)象模型?
引用《深度探索C++對(duì)象模型》這本書中的話:
有兩個(gè)概念可以解釋C++對(duì)象模型:
語(yǔ)言中直接支持面向?qū)ο蟪绦蛟O(shè)計(jì)的部分。
對(duì)于各種支持的底層實(shí)現(xiàn)機(jī)制。
直接支持面向?qū)ο蟪绦蛟O(shè)計(jì),包括了構(gòu)造函數(shù)、析構(gòu)函數(shù)、多態(tài)、虛函數(shù)等等,這些內(nèi)容在很多書籍上都有討論,也是C++最被人熟知的地方(特性)。而對(duì)象模型的底層實(shí)現(xiàn)機(jī)制卻是很少有書籍討論的。對(duì)象模型的底層實(shí)現(xiàn)機(jī)制并未標(biāo)準(zhǔn)化,不同的編譯器有一定的自由來(lái)設(shè)計(jì)對(duì)象模型的實(shí)現(xiàn)細(xì)節(jié)。在我看來(lái),對(duì)象模型研究的是對(duì)象在存儲(chǔ)上的空間與時(shí)間上的更優(yōu)化的調(diào)整,并對(duì)C++面向?qū)ο蠹夹g(shù)加以支持,如以虛指針、虛表機(jī)制支持多態(tài)特性。
2.文章內(nèi)容簡(jiǎn)介
本篇主要內(nèi)容如下:
-
虛函數(shù)表解析。含有虛函數(shù)或其父類含有虛函數(shù)的類,編譯器都會(huì)為其添加一個(gè)虛函數(shù)表,
vptr,了解虛函數(shù)表的構(gòu)成,有助對(duì)C++對(duì)象模型的理解。 -
虛基類表解析。虛繼承產(chǎn)生虛基類表(
vbptr),虛基類表的內(nèi)容與虛函數(shù)表完全不同,我們將在講解虛繼承時(shí)介紹虛函數(shù)表。 -
對(duì)象模型概述:介紹簡(jiǎn)單對(duì)象模型、表格驅(qū)動(dòng)對(duì)象模型,以及非繼承情況下的C++對(duì)象模型。
-
繼承下的C++對(duì)象模型。分析C++類對(duì)象在下面情形中的內(nèi)存布局:
-
單繼承:子類單一繼承自父類,分析子類重寫父類虛函數(shù)、子類定義新的虛函數(shù)情況下子類對(duì)象內(nèi)存布局。
-
多繼承:子類繼承于多個(gè)父類,分析子類重寫父類虛函數(shù)、子類定義新的虛函數(shù)情況下子類對(duì)象內(nèi)存布局,同時(shí)分析了非虛繼承下的菱形繼承。
-
虛繼承:分析了單一繼承下的虛繼承、多重基層下的虛繼承、重復(fù)繼承下的虛繼承。
-
-
理解對(duì)象的內(nèi)存布局之后,我們可以分析一些問題:
-
C++封裝帶來(lái)的布局成本是多大?見第三部分
-
由空類組成的繼承層次中,每個(gè)類對(duì)象的大小是多大?
-
至于其他與內(nèi)存有關(guān)的知識(shí),我假設(shè)大家都有一定的了解,如內(nèi)存對(duì)齊,指針操作等。
3.理解虛函數(shù)表
3.1.多態(tài)與虛表
C++中虛函數(shù)的作用主要是為了實(shí)現(xiàn)多態(tài)機(jī)制。多態(tài),簡(jiǎn)單來(lái)說(shuō),是指在繼承層次中,父類的指針可以具有多種形態(tài)——當(dāng)它指向某個(gè)子類對(duì)象時(shí),通過(guò)它能夠調(diào)用到子類的函數(shù),而非父類的函數(shù)。
class Base {virtual void print(void);}
class Drive1 :public Base{virtual void print(void);}
class Drive2 :public Base{virtual void print(void);}
Base * ptr1 = new Base;
Base * ptr2 = new Drive1; ?
Base * ptr3 = new Drive2;
ptr1->print(); //調(diào)用Base::print()
prt2->print(); //調(diào)用Drive1::print()
prt3->print(); //調(diào)用Drive2::print()
這是一種運(yùn)行期多態(tài),即父類指針唯有在程序運(yùn)行時(shí)才能知道所指的真正類型是什么。這種運(yùn)行期決議,是通過(guò)虛函數(shù)表來(lái)實(shí)現(xiàn)的,也稱動(dòng)態(tài)綁定。
3.2.使用指針訪問虛表
如果豐富Base類,使其擁有多個(gè)virtual函數(shù),如下:
class Base
{
public:Base(int i):baseI(i){};virtual void print(void){ cout << "調(diào)用了虛函數(shù)Base::print()"; }virtual void setI(){cout<<"調(diào)用了虛函數(shù)Base::setI()";}virtual ~Base(){}
private:int baseI;
};
當(dāng)一個(gè)類本身定義了虛函數(shù),或其父類有虛函數(shù)時(shí),為了支持多態(tài)機(jī)制,編譯器將為該類添加一個(gè)虛函數(shù)指針(vptr)。虛函數(shù)指針一般都放在對(duì)象內(nèi)存布局的第一個(gè)位置上,這是為了保證在多層繼承或多重繼承的情況下能以最高效率取到虛函數(shù)表。
當(dāng)vprt位于對(duì)象內(nèi)存最前面時(shí),對(duì)象的地址即為虛函數(shù)指針地址。我們可以取得虛函數(shù)指針的地址:
Base b(1000);
int * vptrAdree = (int *)(&b); ?
cout << "虛函數(shù)指針(vprt)的地址是:\t"<<vptrAdree << endl;
我們運(yùn)行代碼出結(jié)果:
強(qiáng)行把類對(duì)象的地址轉(zhuǎn)換為 int* 類型,取得了虛函數(shù)指針的地址。虛函數(shù)指針指向虛函數(shù)表,虛函數(shù)表中存儲(chǔ)的是一系列虛函數(shù)的地址,虛函數(shù)地址出現(xiàn)的順序與類中虛函數(shù)聲明的順序一致。對(duì)虛函數(shù)指針地址值,可以得到虛函數(shù)表的地址,也即是虛函數(shù)表第一個(gè)虛函數(shù)的地址:
? ?typedef void(*Fun)(void);Fun vfunc = (Fun)*( (int *)*(int*)(&b));cout << "第一個(gè)虛函數(shù)的地址是:" << (int *)*(int*)(&b) << endl;cout << "通過(guò)地址,調(diào)用虛函數(shù)Base::print():";vfunc();
-
我們把虛表指針的值取出來(lái): *(int*)(&b),它是一個(gè)地址,虛函數(shù)表的地址
-
把虛函數(shù)表的地址強(qiáng)制轉(zhuǎn)換成 int* : ( int *) *( int* )( &b )
-
再把它轉(zhuǎn)化成我們Fun指針類型 : (Fun )*(int *)*(int*)(&b)
這樣,我們就取得了類中的第一個(gè)虛函數(shù),我們可以通過(guò)函數(shù)指針訪問它。 運(yùn)行結(jié)果:
同理,第二個(gè)虛函數(shù)setI()的地址為:
(int * )(*(int*)(&b)+1)
同樣可以通過(guò)函數(shù)指針訪問它,這里留給讀者自己試驗(yàn)。
到目前為止,我們知道了類中虛表指針vprt的由來(lái),知道了虛函數(shù)表中的內(nèi)容,以及如何通過(guò)指針訪問虛函數(shù)表。下面的文章中將常使用指針訪問對(duì)象內(nèi)存來(lái)驗(yàn)證我們的C++對(duì)象模型,以及討論在各種繼承情況下虛表指針的變化,先把這部分的內(nèi)容消化完再接著看下面的內(nèi)容。
4.對(duì)象模型概述
在C++中,有兩種數(shù)據(jù)成員(class data members):static 和nonstatic,以及三種類成員函數(shù)(class member functions):static、nonstatic和virtual:
現(xiàn)在我們有一個(gè)類Base,它包含了上面這5中類型的數(shù)據(jù)或函數(shù):
class Base
{
public:Base(int i) :baseI(i){};int getI(){ return baseI; }static void countI(){};virtual ~Base(){}
?virtual void print(void){ cout << "Base::print()"; }
?private:int baseI;static int baseS;
};
那么,這個(gè)類在內(nèi)存中將被如何表示?5種數(shù)據(jù)都是連續(xù)存放的嗎?如何布局才能支持C++多態(tài)? 我們的C++標(biāo)準(zhǔn)與編譯器將如何塑造出各種數(shù)據(jù)成員與成員函數(shù)呢?
4.1.簡(jiǎn)單對(duì)象模型
說(shuō)明:在下面出現(xiàn)的圖中,用藍(lán)色邊框框起來(lái)的內(nèi)容在內(nèi)存上是連續(xù)的。 這個(gè)模型非常地簡(jiǎn)單粗暴。在該模型下,對(duì)象由一系列的指針組成,每一個(gè)指針都指向一個(gè)數(shù)據(jù)成員或成員函數(shù),也即是說(shuō),每個(gè)數(shù)據(jù)成員和成員函數(shù)在類中所占的大小是相同的,都為一個(gè)指針的大小。這樣有個(gè)好處——很容易算出對(duì)象的大小,不過(guò)賠上的是空間和執(zhí)行期效率。想象一下,如果我們的Point3d類是這種模型,將會(huì)比C語(yǔ)言的struct多了許多空間來(lái)存放指向函數(shù)的指針,而且每次讀取類的數(shù)據(jù)成員,都需要通過(guò)再一次尋址——又是時(shí)間上的消耗。 所以這種對(duì)象模型并沒有被用于實(shí)際產(chǎn)品上。
4.2.表格驅(qū)動(dòng)模型
這個(gè)模型在簡(jiǎn)單對(duì)象模型的基礎(chǔ)上又添加一個(gè)間接層,它把類中的數(shù)據(jù)分成了兩個(gè)部分:數(shù)據(jù)部分與函數(shù)部分,并使用兩張表格,一張存放數(shù)據(jù)本身,一張存放函數(shù)的地址(也即函數(shù)比成員多一次尋址),而類對(duì)象僅僅含有兩個(gè)指針,分別指向上面這兩個(gè)表。這樣看來(lái),對(duì)象的大小是固定為兩個(gè)指針大小。這個(gè)模型也沒有用于實(shí)際應(yīng)用于真正的C++編譯器上。
4.3.非繼承下的C++對(duì)象模型
概述:在此模型下,nonstatic 數(shù)據(jù)成員被置于每一個(gè)類對(duì)象中,而static數(shù)據(jù)成員被置于類對(duì)象之外。static與nonstatic函數(shù)也都放在類對(duì)象之外,而對(duì)于virtual 函數(shù),則通過(guò)虛函數(shù)表+虛指針來(lái)支持,具體如下:
-
每個(gè)類生成一個(gè)表格,稱為虛表(virtual table,簡(jiǎn)稱
vtbl)。虛表中存放著一堆指針,這些指針指向該類每一個(gè)虛函數(shù)。虛表中的函數(shù)地址將按聲明時(shí)的順序排列,不過(guò)當(dāng)子類有多個(gè)重載函數(shù)時(shí)例外,后面會(huì)討論。 -
每個(gè)類對(duì)象都擁有一個(gè)虛表指針(
vptr),由編譯器為其生成。虛表指針的設(shè)定與重置皆由類的復(fù)制控制(也即是構(gòu)造函數(shù)、析構(gòu)函數(shù)、賦值操作符)來(lái)完成。vptr的位置為編譯器決定,傳統(tǒng)上它被放在所有顯示聲明的成員之后,不過(guò)現(xiàn)在許多編譯器把vptr放在一個(gè)類*對(duì)象的最前端*。關(guān)于數(shù)據(jù)成員布局的內(nèi)容,在后面會(huì)詳細(xì)分析。 另外,虛函數(shù)表的前面設(shè)置了一個(gè)指向type_info的指針,用以支持RTTI(Run Time Type Identification,運(yùn)行時(shí)類型識(shí)別)。RTTI是為多態(tài)而生成的信息,包括對(duì)象繼承關(guān)系,對(duì)象本身的描述等,只有具有虛函數(shù)的對(duì)象在會(huì)生成。
在此模型下,Base的對(duì)象模型如圖:
先在VS上驗(yàn)證類對(duì)象的布局:
Base b(1000);
可見對(duì)象b含有一個(gè)vfptr,即vprt。并且只有nonstatic數(shù)據(jù)成員被放置于對(duì)象內(nèi)。我們展開vfprt:
vfptr中有兩個(gè)指針類型的數(shù)據(jù)(地址),第一個(gè)指向了Base類的析構(gòu)函數(shù),第二個(gè)指向了Base的虛函數(shù)print,順序與聲明順序相同。 這與上述的C++對(duì)象模型相符合。也可以通過(guò)代碼來(lái)進(jìn)行驗(yàn)證:
void testBase( Base&p)
{cout << "對(duì)象的內(nèi)存起始地址:" << &p << endl;cout << "type_info信息:" << endl;RTTICompleteObjectLocator str = *((RTTICompleteObjectLocator*)*((int*)*(int*)(&p) - 1));string classname(str.pTypeDescriptor->name);classname = classname.substr(4, classname.find("@@") - 4);cout << ?"根據(jù)type_info信息輸出類名:"<< classname << endl;cout << "虛函數(shù)表地址:" << (int *)(&p) << endl;//驗(yàn)證虛表cout << "虛函數(shù)表第一個(gè)函數(shù)的地址:" << (int *)*((int*)(&p)) << endl;cout << "析構(gòu)函數(shù)的地址:" << (int* )*(int *)*((int*)(&p)) << endl;cout << "虛函數(shù)表中,第二個(gè)虛函數(shù)即print()的地址:" << ((int*)*(int*)(&p) + 1) << endl;//通過(guò)地址調(diào)用虛函數(shù)print()typedef void(*Fun)(void);Fun IsPrint=(Fun)* ((int*)*(int*)(&p) + 1);cout << endl;cout<<"調(diào)用了虛函數(shù)";IsPrint(); //若地址正確,則調(diào)用了Base類的虛函數(shù)print()cout << endl;//輸入static函數(shù)的地址p.countI();//先調(diào)用函數(shù)以產(chǎn)生一個(gè)實(shí)例cout << "static函數(shù)countI()的地址:" << p.countI << endl;//驗(yàn)證nonstatic數(shù)據(jù)成員cout << "推測(cè)nonstatic數(shù)據(jù)成員baseI的地址:" << (int *)(&p) + 1 << endl;cout << "根據(jù)推測(cè)出的地址,輸出該地址的值:" << *((int *)(&p) + 1) << endl;cout << "Base::getI():" << p.getI() << endl;}
Base b(1000);
testBase(b);
結(jié)果分析:
-
通過(guò) (int *)(&p)取得虛函數(shù)表的地址
-
type_info信息的確存在于虛表的前一個(gè)位置。通過(guò)((int)(int*)(&p) - 1))取得type_infn信息,并成功獲得類的名稱的Base
-
虛函數(shù)表的第一個(gè)函數(shù)是析構(gòu)函數(shù)。
-
虛函數(shù)表的第二個(gè)函數(shù)是虛函數(shù)print(),取得地址后通過(guò)地址調(diào)用它(而非通過(guò)對(duì)象),驗(yàn)證正確
-
虛表指針的下一個(gè)位置為nonstatic數(shù)據(jù)成員baseI。
-
可以看到,static成員函數(shù)的地址段位與虛表指針、baseI的地址段位不同。
好的,至此我們了解了非繼承下類對(duì)象五種數(shù)據(jù)在內(nèi)存上的布局,也知道了在每一個(gè)虛函數(shù)表前都有一個(gè)指針指向type_info,負(fù)責(zé)對(duì)RTTI的支持。而加入繼承后類對(duì)象在內(nèi)存中該如何表示呢?
5.繼承下的C++對(duì)象模型
5.1.單繼承
如果我們定義了派生類
class Derive : public Base
{
public:Derive(int d) :Base(1000), DeriveI(d){};//overwrite父類虛函數(shù)virtual void print(void){ cout << "Drive::Drive_print()" ; }// Derive聲明的新的虛函數(shù)virtual void Drive_print(){ cout << "Drive::Drive_print()" ; }virtual ~Derive(){}
private:int DeriveI;
};
繼承類圖為:
一個(gè)派生類如何在機(jī)器層面上塑造其父類的實(shí)例呢?在簡(jiǎn)單對(duì)象模型中,可以在子類對(duì)象中為每個(gè)基類子對(duì)象分配一個(gè)指針。如下圖:
簡(jiǎn)單對(duì)象模型的缺點(diǎn)就是因間接性導(dǎo)致的空間存取時(shí)間上的額外負(fù)擔(dān),優(yōu)點(diǎn)則是類的大小是固定的,基類的改動(dòng)不會(huì)影響子類對(duì)象的大小。
在表格驅(qū)動(dòng)對(duì)象模型中,我們可以為子類對(duì)象增加第三個(gè)指針:基類指針(bptr),基類指針指向指向一個(gè)基類表(base class table),同樣的,由于間接性導(dǎo)致了空間和存取時(shí)間上的額外負(fù)擔(dān),優(yōu)點(diǎn)則是無(wú)須改變子類對(duì)象本身就可以更改基類。表格驅(qū)動(dòng)模型的圖就不再貼出來(lái)了。
在C++對(duì)象模型中,對(duì)于一般繼承(這個(gè)一般是相對(duì)于虛擬繼承而言),若子類重寫(overwrite)了父類的虛函數(shù),則子類虛函數(shù)將覆蓋虛表中對(duì)應(yīng)的父類虛函數(shù)(注意子類與父類擁有各自的一個(gè)虛函數(shù)表);若子類并無(wú)overwrite父類虛函數(shù),而是聲明了自己新的虛函數(shù),則該虛函數(shù)地址將擴(kuò)充到虛函數(shù)表最后(在vs中無(wú)法通過(guò)監(jiān)視看到擴(kuò)充的結(jié)果,不過(guò)我們通過(guò)取地址的方法可以做到,子類新的虛函數(shù)確實(shí)在父類子物體的虛函數(shù)表末端)。而對(duì)于虛繼承,若子類overwrite父類虛函數(shù),同樣地將覆蓋父類子物體中的虛函數(shù)表對(duì)應(yīng)位置,而若子類聲明了自己新的虛函數(shù),則編譯器將為子類增加一個(gè)新的虛表指針vptr,這與一般繼承不同,在后面再討論。
我們使用代碼來(lái)驗(yàn)證以上模型
typedef void(*Fun)(void);int main()
{Derive d(2000);//[0]cout << "[0]Base::vptr";cout << "\t地址:" << (int *)(&d) << endl;//vprt[0]cout << " [0]";Fun fun1 = (Fun)*((int *)*((int *)(&d)));fun1();cout << "\t地址:\t" << *((int *)*((int *)(&d))) << endl;//vprt[1]析構(gòu)函數(shù)無(wú)法通過(guò)地址調(diào)用,故手動(dòng)輸出cout << " [1]" << "Derive::~Derive" << endl;//vprt[2]cout << " [2]";Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2);fun2();cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 2) << endl;//[1]cout << "[2]Base::baseI=" << *(int*)((int *)(&d) + 1);cout << "\t地址:" << (int *)(&d) + 1;cout << endl;//[2]cout << "[2]Derive::DeriveI=" << *(int*)((int *)(&d) + 2);cout << "\t地址:" << (int *)(&d) + 2;cout << endl;getchar();
}
運(yùn)行結(jié)果:
這個(gè)結(jié)果與我們的對(duì)象模型符合。
5.2.多繼承
5.2.1一般的多重繼承(非菱形繼承)
單繼承中(一般繼承),子類會(huì)擴(kuò)展父類的虛函數(shù)表。在多繼承中,子類含有多個(gè)父類的子對(duì)象,該往哪個(gè)父類的虛函數(shù)表擴(kuò)展呢?當(dāng)子類overwrite了父類的函數(shù),需要覆蓋多個(gè)父類的虛函數(shù)表嗎?
-
子類的虛函數(shù)被放在聲明的第一個(gè)基類的虛函數(shù)表中。
-
overwrite時(shí),所有基類的print()函數(shù)都被子類的print()函數(shù)覆蓋。
-
內(nèi)存布局中,父類按照其聲明順序排列。
其中第二點(diǎn)保證了父類指針指向子類對(duì)象時(shí),總是能夠調(diào)用到真正的函數(shù)。
為了方便查看,我們把代碼都粘貼過(guò)來(lái)
class Base
{
public:Base(int i) :baseI(i){};virtual ~Base(){}int getI(){ return baseI; }static void countI(){};virtual void print(void){ cout << "Base::print()"; }private:int baseI;static int baseS;
};
class Base_2
{
public:Base_2(int i) :base2I(i){};virtual ~Base_2(){}int getI(){ return base2I; }static void countI(){};virtual void print(void){ cout << "Base_2::print()"; }private:int base2I;static int base2S;
};class Drive_multyBase :public Base, public Base_2
{
public:Drive_multyBase(int d) :Base(1000), Base_2(2000) ,Drive_multyBaseI(d){};virtual void print(void){ cout << "Drive_multyBase::print" ; }virtual void Drive_print(){ cout << "Drive_multyBase::Drive_print" ; }private:int Drive_multyBaseI;
};
繼承類圖為:
此時(shí)Drive_multyBase 的對(duì)象模型是這樣的:
我們使用代碼驗(yàn)證:
typedef void(*Fun)(void);int main()
{Drive_multyBase d(3000);//[0]cout << "[0]Base::vptr";cout << "\t地址:" << (int *)(&d) << endl;//vprt[0]析構(gòu)函數(shù)無(wú)法通過(guò)地址調(diào)用,故手動(dòng)輸出cout << " [0]" << "Derive::~Derive" << endl;//vprt[1]cout << " [1]";Fun fun1 = (Fun)*((int *)*((int *)(&d))+1);fun1();cout << "\t地址:\t" << *((int *)*((int *)(&d))+1) << endl;//vprt[2]cout << " [2]";Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2);fun2();cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 2) << endl;//[1]cout << "[1]Base::baseI=" << *(int*)((int *)(&d) + 1);cout << "\t地址:" << (int *)(&d) + 1;cout << endl;//[2]cout << "[2]Base_::vptr";cout << "\t地址:" << (int *)(&d)+2 << endl;//vprt[0]析構(gòu)函數(shù)無(wú)法通過(guò)地址調(diào)用,故手動(dòng)輸出cout << " [0]" << "Drive_multyBase::~Derive" << endl;//vprt[1]cout << " [1]";Fun fun4 = (Fun)*((int *)*((int *)(&d))+1);fun4();cout << "\t地址:\t" << *((int *)*((int *)(&d))+1) << endl;//[3]cout << "[3]Base_2::base2I=" << *(int*)((int *)(&d) + 3);cout << "\t地址:" << (int *)(&d) + 3;cout << endl;//[4]cout << "[4]Drive_multyBase::Drive_multyBaseI=" << *(int*)((int *)(&d) + 4);cout << "\t地址:" << (int *)(&d) + 4;cout << endl;getchar();
}
運(yùn)行結(jié)果:
5.2.2 菱形繼承
菱形繼承也稱為鉆石型繼承或重復(fù)繼承,它指的是基類被某個(gè)派生類簡(jiǎn)單重復(fù)繼承了多次。這樣,派生類對(duì)象中擁有多份基類實(shí)例(這會(huì)帶來(lái)一些問題)。為了方便敘述,我們不使用上面的代碼了,而重新寫一個(gè)重復(fù)繼承的繼承層次:
class B{public:int ib;public:B(int i=1) :ib(i){}virtual void f() { cout << "B::f()" << endl; }virtual void Bf() { cout << "B::Bf()" << endl; }};class B1 : public B{public:int ib1;public:B1(int i = 100 ) :ib1(i) {}virtual void f() { cout << "B1::f()" << endl; }virtual void f1() { cout << "B1::f1()" << endl; }virtual void Bf1() { cout << "B1::Bf1()" << endl; }};class B2 : public B{public:int ib2;public:B2(int i = 1000) :ib2(i) {}virtual void f() { cout << "B2::f()" << endl; }virtual void f2() { cout << "B2::f2()" << endl; }virtual void Bf2() { cout << "B2::Bf2()" << endl; }};class D : public B1, public B2{public:int id;public:D(int i= 10000) :id(i){}virtual void f() { cout << "D::f()" << endl; }virtual void f1() { cout << "D::f1()" << endl; }virtual void f2() { cout << "D::f2()" << endl; }virtual void Df() { cout << "D::Df()" << endl; }};
這時(shí),根據(jù)單繼承,我們可以分析出B1,B2類繼承于B類時(shí)的內(nèi)存布局。又根據(jù)一般多繼承,我們可以分析出D類的內(nèi)存布局。我們可以得出D類子對(duì)象的內(nèi)存布局如下圖:
D類對(duì)象內(nèi)存布局中,圖中青色表示b1類子對(duì)象實(shí)例,黃色表示b2類子對(duì)象實(shí)例,灰色表示D類子對(duì)象實(shí)例。從圖中可以看到,由于D類間接繼承了B類兩次,導(dǎo)致D類對(duì)象中含有兩個(gè)B類的數(shù)據(jù)成員ib,一個(gè)屬于來(lái)源B1類,一個(gè)來(lái)源B2類。這樣不僅增大了空間,更重要的是引起了程序歧義:
D d;d.ib =1 ; //二義性錯(cuò)誤,調(diào)用的是B1的ib還是B2的ib?d.B1::ib = 1; //正確d.B2::ib = 1; //正確
盡管我們可以通過(guò)明確指明調(diào)用路徑以消除二義性,但二義性的潛在性還沒有消除,我們可以通過(guò)虛繼承來(lái)使D類只擁有一個(gè)ib實(shí)體。
6.虛繼承
虛繼承解決了菱形繼承中最派生類擁有多個(gè)間接父類實(shí)例的情況。虛繼承的派生類的內(nèi)存布局與普通繼承很多不同,主要體現(xiàn)在:
-
虛繼承的子類,如果本身定義了新的虛函數(shù),則編譯器為其生成一個(gè)虛函數(shù)指針(vptr)以及一張?zhí)摵瘮?shù)表。該vptr位于對(duì)象內(nèi)存最前面。
-
vs非虛繼承:直接擴(kuò)展父類虛函數(shù)表。
-
-
虛繼承的子類也單獨(dú)保留了父類的vprt與虛函數(shù)表。這部分內(nèi)容接與子類內(nèi)容以一個(gè)四字節(jié)的0來(lái)分界。
-
虛繼承的子類對(duì)象中,含有四字節(jié)的虛表指針偏移值。
為了分析最后的菱形繼承,我們還是先從單虛繼承繼承開始。
6.1.虛基類表解析
在C++對(duì)象模型中,虛繼承而來(lái)的子類會(huì)生成一個(gè)隱藏的虛基類指針(vbptr),在Microsoft Visual C++中,虛基類表指針總是在虛函數(shù)表指針之后,因而,對(duì)某個(gè)類實(shí)例來(lái)說(shuō),如果它有虛基類指針,那么虛基類指針可能在實(shí)例的0字節(jié)偏移處(該類沒有vptr時(shí),vbptr就處于類實(shí)例內(nèi)存布局的最前面,否則vptr處于類實(shí)例內(nèi)存布局的最前面),也可能在類實(shí)例的4字節(jié)偏移處。 一個(gè)類的虛基類指針指向的虛基類表,與虛函數(shù)表一樣,虛基類表也由多個(gè)條目組成,條目中存放的是偏移值。第一個(gè)條目存放虛基類表指針(vbptr)所在地址到該類內(nèi)存首地址的偏移值,由第一段的分析我們知道,這個(gè)偏移值為0(類沒有vptr)或者-4(類有虛函數(shù),此時(shí)有vptr)。我們通過(guò)一張圖來(lái)更好地理解。
虛基類表的第二、第三...個(gè)條目依次為該類的最左虛繼承父類、次左虛繼承父類...的內(nèi)存地址相對(duì)于虛基類表指針的偏移值,這點(diǎn)我們?cè)谙旅鏁?huì)驗(yàn)證。
6.2.簡(jiǎn)單虛繼承
如果我們的B1類虛繼承于B類:
//類的內(nèi)容與前面相同
class B{...}
class B1 : virtual public B
根據(jù)我們前面對(duì)虛繼承的派生類的內(nèi)存布局的分析,B1類的對(duì)象模型應(yīng)該是這樣的:
我們通過(guò)指針訪問B1類對(duì)象的內(nèi)存,以驗(yàn)證上面的C++對(duì)象模型:
int main()
{
B1 a;cout <<"B1對(duì)象內(nèi)存大小為:"<< sizeof(a) << endl;//取得B1的虛函數(shù)表cout << "[0]B1::vptr";cout << "\t地址:" << (int *)(&a)<< endl;//輸出虛表B1::vptr中的函數(shù)for (int i = 0; i<2;++ i){cout << " [" << i << "]";Fun fun1 = (Fun)*((int *)*(int *)(&a) + i);fun1();cout << "\t地址:\t" << *((int *)*(int *)(&a) + i) << endl;}//[1]cout << "[1]vbptr " ;cout<<"\t地址:" << (int *)(&a) + 1<<endl; //虛表指針的地址//輸出虛基類指針條目所指的內(nèi)容for (int i = 0; i < 2; i++){cout << " [" << i << "]";cout << *(int *)((int *)*((int *)(&a) + 1) + i);cout << endl;}//[2]cout << "[2]B1::ib1=" << *(int*)((int *)(&a) + 2);cout << "\t地址:" << (int *)(&a) + 2;cout << endl;//[3]cout << "[3]值=" << *(int*)((int *)(&a) + 3);cout << "\t\t地址:" << (int *)(&a) + 3;cout << endl;//[4]cout << "[4]B::vptr";cout << "\t地址:" << (int *)(&a) +3<< endl;//輸出B::vptr中的虛函數(shù)for (int i = 0; i<2; ++i){cout << " [" << i << "]";Fun fun1 = (Fun)*((int *)*((int *)(&a) + 4) + i);fun1();cout << "\t地址:\t" << *((int *)*((int *)(&a) + 4) + i) << endl;}//[5]cout << "[5]B::ib=" << *(int*)((int *)(&a) + 5);cout << "\t地址: " << (int *)(&a) + 5;cout << endl;
運(yùn)行結(jié)果:
這個(gè)結(jié)果與我們的C++對(duì)象模型圖完全符合。這時(shí)我們可以來(lái)分析一下虛表指針的第二個(gè)條目值12的具體來(lái)源了,回憶上文講到的:
第二、第三...個(gè)條目依次為該類的最左虛繼承父類、次左虛繼承父類...的內(nèi)存地址相對(duì)于虛基類表指針的偏移值。
在我們的例子中,也就是B類實(shí)例內(nèi)存地址相對(duì)于vbptr的偏移值,也即是:[4]-[1]的偏移值,結(jié)果即為12,從地址上也可以計(jì)算出來(lái):007CFDFC-007CFDF4結(jié)果的十進(jìn)制數(shù)正是12。現(xiàn)在,我們對(duì)虛基類表的構(gòu)成應(yīng)該有了一個(gè)更好的理解。
6.3.虛擬菱形繼承
如果我們有如下繼承層次:
class B{...}
class B1: virtual public B{...}
class B2: virtual public B{...}
class D : public B1,public B2{...}
類圖如下所示:
菱形虛擬繼承下,最派生類D類的對(duì)象模型又有不同的構(gòu)成了。在D類對(duì)象的內(nèi)存構(gòu)成上,有以下幾點(diǎn):
-
在D類對(duì)象內(nèi)存中,基類出現(xiàn)的順序是:先是B1(最左父類),然后是B2(次左父類),最后是B(虛祖父類)
-
D類對(duì)象的數(shù)據(jù)成員id放在B類前面,兩部分?jǐn)?shù)據(jù)依舊以0來(lái)分隔。
-
編譯器沒有為D類生成一個(gè)它自己的vptr,而是覆蓋并擴(kuò)展了最左父類的虛基類表,與簡(jiǎn)單繼承的對(duì)象模型相同。
-
超類B的內(nèi)容放到了D類對(duì)象內(nèi)存布局的最后。
菱形虛擬繼承下的C++對(duì)象模型為:
下面使用代碼加以驗(yàn)證:
int main()
{D d;cout << "D對(duì)象內(nèi)存大小為:" << sizeof(d) << endl;//取得B1的虛函數(shù)表cout << "[0]B1::vptr";cout << "\t地址:" << (int *)(&d) << endl;//輸出虛表B1::vptr中的函數(shù)for (int i = 0; i<3; ++i){cout << " [" << i << "]";Fun fun1 = (Fun)*((int *)*(int *)(&d) + i);fun1();cout << "\t地址:\t" << *((int *)*(int *)(&d) + i) << endl;}//[1]cout << "[1]B1::vbptr ";cout << "\t地址:" << (int *)(&d) + 1 << endl; //虛表指針的地址//輸出虛基類指針條目所指的內(nèi)容for (int i = 0; i < 2; i++){cout << " [" << i << "]";cout << *(int *)((int *)*((int *)(&d) + 1) + i);cout << endl;}//[2]cout << "[2]B1::ib1=" << *(int*)((int *)(&d) + 2);cout << "\t地址:" << (int *)(&d) + 2;cout << endl;//[3]cout << "[3]B2::vptr";cout << "\t地址:" << (int *)(&d) + 3 << endl;//輸出B2::vptr中的虛函數(shù)for (int i = 0; i<2; ++i){cout << " [" << i << "]";Fun fun1 = (Fun)*((int *)*((int *)(&d) + 3) + i);fun1();cout << "\t地址:\t" << *((int *)*((int *)(&d) + 3) + i) << endl;}//[4]cout << "[4]B2::vbptr ";cout << "\t地址:" << (int *)(&d) + 4 << endl; //虛表指針的地址//輸出虛基類指針條目所指的內(nèi)容for (int i = 0; i < 2; i++){cout << " [" << i << "]";cout << *(int *)((int *)*((int *)(&d) + 4) + i);cout << endl;}//[5]cout << "[5]B2::ib2=" << *(int*)((int *)(&d) + 5);cout << "\t地址: " << (int *)(&d) + 5;cout << endl;//[6]cout << "[6]D::id=" << *(int*)((int *)(&d) + 6);cout << "\t地址: " << (int *)(&d) + 6;cout << endl;//[7]cout << "[7]值=" << *(int*)((int *)(&d) + 7);cout << "\t\t地址:" << (int *)(&d) + 7;cout << endl;//間接父類//[8]cout << "[8]B::vptr";cout << "\t地址:" << (int *)(&d) + 8 << endl;//輸出B::vptr中的虛函數(shù)for (int i = 0; i<2; ++i){cout << " [" << i << "]";Fun fun1 = (Fun)*((int *)*((int *)(&d) + 8) + i);fun1();cout << "\t地址:\t" << *((int *)*((int *)(&d) + 8) + i) << endl;}//[9]cout << "[9]B::id=" << *(int*)((int *)(&d) + 9);cout << "\t地址: " << (int *)(&d) +9;cout << endl;getchar();
}
查看運(yùn)行結(jié)果:
7.一些問題解答
7.1.C++封裝帶來(lái)的布局成本是多大?
在C語(yǔ)言中,“數(shù)據(jù)”和“處理數(shù)據(jù)的操作(函數(shù))”是分開來(lái)聲明的,也就是說(shuō),語(yǔ)言本身并沒有支持“數(shù)據(jù)和函數(shù)”之間的關(guān)聯(lián)性。 在C++中,我們通過(guò)類來(lái)將屬性與操作綁定在一起,稱為ADT,抽象數(shù)據(jù)結(jié)構(gòu)。
C語(yǔ)言中使用struct(結(jié)構(gòu)體)來(lái)封裝數(shù)據(jù),使用函數(shù)來(lái)處理數(shù)據(jù)。舉個(gè)例子,如果我們定義了一個(gè)struct Point3如下:
typedef struct Point3
{float x;float y;float z;
} Point3;
為了打印這個(gè)Point3d,我們可以定義一個(gè)函數(shù):
void Point3d_print(const Point3d *pd)
{printf("(%f,%f,%f)",pd->x,pd->y,pd_z);
}
而在C++中,我們更傾向于定義一個(gè)Point3d類,以ADT來(lái)實(shí)現(xiàn)上面的操作:
class Point3d
{public:point3d (float x = 0.0,float y = 0.0,float z = 0.0): _x(x), _y(y), _z(z){}float x() const {return _x;}float y() const {return _y;}float z() const {return _z;}private:float _x;float _y;float _z;
};inline ostream&operator<<(ostream &os, const Point3d &pt){os<<"("<<pr.x()<<","<<pt.y()<<","<<pt.z()<<")";}
看到這段代碼,很多人第一個(gè)疑問可能是:加上了封裝,布局成本增加了多少?答案是class Point3d并沒有增加成本。學(xué)過(guò)了C++對(duì)象模型,我們知道,Point3d類對(duì)象的內(nèi)存中,只有三個(gè)數(shù)據(jù)成員。
上面的類聲明中,三個(gè)數(shù)據(jù)成員直接內(nèi)含在每一個(gè)Point3d對(duì)象中,而成員函數(shù)雖然在類中聲明,卻不出現(xiàn)在類對(duì)象(object)之中,這些函數(shù)(non-inline)屬于類而不屬于類對(duì)象,只會(huì)為類產(chǎn)生唯一的函數(shù)實(shí)例。
所以,Point3d的封裝并沒有帶來(lái)任何空間或執(zhí)行期的效率影響。而在下面這種情況下,C++的封裝額外成本才會(huì)顯示出來(lái):
-
虛函數(shù)機(jī)制(virtual function) , 用以支持執(zhí)行期綁定,實(shí)現(xiàn)多態(tài)。
-
虛基類 (virtual base class) ,虛繼承關(guān)系產(chǎn)生虛基類,用于在多重繼承下保證基類在子類中擁有唯一實(shí)例。
不僅如此,Point3d類數(shù)據(jù)成員的內(nèi)存布局與c語(yǔ)言的結(jié)構(gòu)體Point3d成員內(nèi)存布局是相同的。C++中處在同一個(gè)訪問標(biāo)識(shí)符(指public、private、protected)下的聲明的數(shù)據(jù)成員,在內(nèi)存中必定保證以其聲明順序出現(xiàn)。而處于不同訪問標(biāo)識(shí)符聲明下的成員則無(wú)此規(guī)定。對(duì)于Point3類來(lái)說(shuō),它的三個(gè)數(shù)據(jù)成員都處于private下,在內(nèi)存中一起聲明順序出現(xiàn)。我們可以做下實(shí)驗(yàn):
void TestPoint3Member(const Point3d& p)
{cout << "推測(cè)_x的地址是:" << (float *) (&p) << endl;cout << "推測(cè)_y的地址是:" << (float *) (&p) + 1 << endl;cout << "推測(cè)_z的地址是:" << (float *) (&p) + 2 << endl;cout << "根據(jù)推測(cè)出的地址輸出_x的值:" << *((float *)(&p)) << endl;cout << "根據(jù)推測(cè)出的地址輸出_y的值:" << *((float *)(&p)+1) << endl;cout << "根據(jù)推測(cè)出的地址輸出_z的值:" << *((float *)(&p)+2) << endl;}//測(cè)試代碼Point3d a(1,2,3);TestPoint3Member(a);
運(yùn)行結(jié)果:
從結(jié)果可以看到,x,y,_z三個(gè)數(shù)據(jù)成員在內(nèi)存中緊挨著。
總結(jié)一下: 不考慮虛函數(shù)與虛繼承,當(dāng)數(shù)據(jù)都在同一個(gè)訪問標(biāo)識(shí)符下,C++的類與C語(yǔ)言的結(jié)構(gòu)體在對(duì)象大小和內(nèi)存布局上是一致的,C++的封裝并沒有帶來(lái)空間時(shí)間上的影響。
7.2.下面這個(gè)空類構(gòu)成的繼承層次中,每個(gè)類的大小是多少?
今有類如下:
class B{};
class B1 :public virtual B{};
class B2 :public virtual B{};
class D : public B1, public B2{};int main()
{B b;B1 b1;B2 b2;D d;cout << "sizeof(b)=" << sizeof(b)<<endl;cout << "sizeof(b1)=" << sizeof(b1) << endl;cout << "sizeof(b2)=" << sizeof(b2) << endl;cout << "sizeof(d)=" << sizeof(d) << endl;getchar();
}
輸出結(jié)果是:
解析:
-
編譯器為空類安插1字節(jié)的char,以使該類對(duì)象在內(nèi)存得以配置一個(gè)地址。
-
b1虛繼承于b,編譯器為其安插一個(gè)4字節(jié)的虛基類表指針(32為機(jī)器),此時(shí)b1已不為空,編譯器不再為其安插1字節(jié)的char(優(yōu)化)。
-
b2同理。
-
d含有來(lái)自b1與b2兩個(gè)父類的兩個(gè)虛基類表指針。大小為8字節(jié)。
轉(zhuǎn)載請(qǐng)注明原出處:http://www.cnblogs.com/QG-whz/p/4909359.html
總結(jié)
以上是生活随笔為你收集整理的【C++】C++对象模型:对象内存布局详解(C#实例)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 成都女司机被打,我突然间产生好多问题啊,
- 下一篇: 【Smart_Point】C/C++ 中