引用《深度探索C++對象模型》這本書中的話:
有兩個概念可以解釋C++對象模型:
- 語言中直接支持面向對象程序設計的部分。
- 對于各種支持的底層實現機制。
直接支持面向對象程序設計,包括了構造函數、析構函數、多態(tài)、虛函數等等,這些內容在很多書籍上都有討論,也是C++最被人熟知的地方(特性)。而對象模型的底層實現機制卻是很少有書籍討論的。對象模型的底層實現機制并未標準化,不同的編譯器有一定的自由來設計對象模型的實現細節(jié)。在我看來,對象模型研究的是對象在存儲上的空間與時間上的更優(yōu),并對C++面向對象技術加以支持,如以虛指針、虛表機制支持多態(tài)特性。
這篇文章主要來討論C++對象在內存中的布局,屬于第二個概念的研究范疇。而C++直接支持面向對象程序設計部分則不多講。文章主要內容如下:
繼承下的C++對象模型。分析C++類對象在下面情形中的內存布局:
理解對象的內存布局之后,我們可以分析一些問題:
至于其他與內存有關的知識,我假設大家都有一定的了解,如內存對齊,指針操作等。本文初看可能晦澀難懂,要求讀者有一定的C++基礎,對概念一有一定的掌握。
C++中虛函數的作用主要是為了實現多態(tài)機制。多態(tài),簡單來說,是指在繼承層次中,父類的指針可以具有多種形態(tài)——當它指向某個子類對象時,通過它能夠調用到子類的函數,而非父類的函數。
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(); //調用Base::print()prt2->print();//調用Drive1::print()prt3->print();//調用Drive2::print()
這是一種運行期多態(tài),即父類指針唯有在程序運行時才能知道所指的真正類型是什么。這種運行期決議,是通過虛函數表來實現的。
如果我們豐富我們的Base類,使其擁有多個virtual函數:
class Base{public: Base(int i) :baseI(i){}; virtual void print(void){ cout << "調用了虛函數Base::print()"; } virtual void setI(){cout<<"調用了虛函數Base::setI()";} virtual ~Base(){} private: int baseI;};
當一個類本身定義了虛函數,或其父類有虛函數時,為了支持多態(tài)機制,編譯器將為該類添加一個虛函數指針(vptr)。虛函數指針一般都放在對象內存布局的第一個位置上,這是為了保證在多層繼承或多重繼承的情況下能以最高效率取到虛函數表。
當vprt位于對象內存最前面時,對象的地址即為虛函數指針地址。我們可以取得虛函數指針的地址:
Base b(1000);int * vptrAdree = (int *)(&b); cout << "虛函數指針(vprt)的地址是:\t"<<vptrAdree << endl;
我們運行代碼出結果:
我們強行把類對象的地址轉換為 int* 類型,取得了虛函數指針的地址。虛函數指針指向虛函數表,虛函數表中存儲的是一系列虛函數的地址,虛函數地址出現的順序與類中虛函數聲明的順序一致。對虛函數指針地址值,可以得到虛函數表的地址,也即是虛函數表第一個虛函數的地址:
typedef void(*Fun)(void); Fun vfunc = (Fun)*( (int *)*(int*)(&b)); cout << "第一個虛函數的地址是:" << (int *)*(int*)(&b) << endl; cout << "通過地址,調用虛函數Base::print():"; vfunc();
這樣,我們就取得了類中的第一個虛函數,我們可以通過函數指針訪問它。
運行結果:
同理,第二個虛函數setI()的地址為:
(int * )(*(int*)(&b)+1)
同樣可以通過函數指針訪問它,這里留給讀者自己試驗。
到目前為止,我們知道了類中虛表指針vprt的由來,知道了虛函數表中的內容,以及如何通過指針訪問虛函數表。下面的文章中將常使用指針訪問對象內存來驗證我們的C++對象模型,以及討論在各種繼承情況下虛表指針的變化,先把這部分的內容消化完再接著看下面的內容。
在C++中,有兩種數據成員(class data members):static 和nonstatic,以及三種類成員函數(class member functions):static、nonstatic和virtual:
現在我們有一個類Base,它包含了上面這5中類型的數據或函數:
class Base{public: Base(int i) :baseI(i){}; int getI(){ return baseI; } static void countI(){}; virtual void print(void){ cout << "Base::print()"; } virtual ~Base(){} private: int baseI; static int baseS;};
那么,這個類在內存中將被如何表示?5種數據都是連續(xù)存放的嗎?如何布局才能支持C++多態(tài)? 我們的C++標準與編譯器將如何塑造出各種數據成員與成員函數呢?
說明:在下面出現的圖中,用藍色邊框框起來的內容在內存上是連續(xù)的。
這個模型非常地簡單粗暴。在該模型下,對象由一系列的指針組成,每一個指針都指向一個數據成員或成員函數,也即是說,每個數據成員和成員函數在類中所占的大小是相同的,都為一個指針的大小。這樣有個好處——很容易算出對象的大小,不過賠上的是空間和執(zhí)行期效率。想象一下,如果我們的Point3d類是這種模型,將會比C語言的struct多了許多空間來存放指向函數的指針,而且每次讀取類的數據成員,都需要通過再一次尋址——又是時間上的消耗。
所以這種對象模型并沒有被用于實際產品上。
這個模型在簡單對象模型的基礎上又添加一個間接層,它把類中的數據分成了兩個部分:數據部分與函數部分,并使用兩張表格,一張存放數據本身,一張存放函數的地址(也即函數比成員多一次尋址),而類對象僅僅含有兩個指針,分別指向上面這兩個表。這樣看來,對象的大小是固定為兩個指針大小。這個模型也沒有用于實際應用于真正的C++編譯器上。
概述:在此模型下,nonstatic 數據成員被置于每一個類對象中,而static數據成員被置于類對象之外。static與nonstatic函數也都放在類對象之外,而對于virtual 函數,則通過虛函數表+虛指針來支持,具體如下:
在此模型下,Base的對象模型如圖:
先在VS上驗證類對象的布局:
Base b(1000);
可見對象b含有一個vfptr,即vprt。并且只有nonstatic數據成員被放置于對象內。我們展開vfprt:
vfptr中有兩個指針類型的數據(地址),第一個指向了Base類的析構函數,第二個指向了Base的虛函數print,順序與聲明順序相同。
這與上述的C++對象模型相符合。也可以通過代碼來進行驗證:
void testBase( Base&p){ cout << "對象的內存起始地址:" << &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 << "根據type_info信息輸出類名:"<< classname << endl; cout << "虛函數表地址:" << (int *)(&p) << endl; //驗證虛表 cout << "虛函數表第一個函數的地址:" << (int *)*((int*)(&p)) << endl; cout << "析構函數的地址:" << (int* )*(int *)*((int*)(&p)) << endl; cout << "虛函數表中,第二個虛函數即print()的地址:" << ((int*)*(int*)(&p) + 1) << endl; //通過地址調用虛函數print() typedef void(*Fun)(void); Fun IsPrint=(Fun)* ((int*)*(int*)(&p) + 1); cout << endl; cout<<"調用了虛函數"; IsPrint(); //若地址正確,則調用了Base類的虛函數print() cout << endl; //輸入static函數的地址 p.countI();//先調用函數以產生一個實例 cout << "static函數countI()的地址:" << p.countI << endl; //驗證nonstatic數據成員 cout << "推測nonstatic數據成員baseI的地址:" << (int *)(&p) + 1 << endl; cout << "根據推測出的地址,輸出該地址的值:" << *((int *)(&p) + 1) << endl; cout << "Base::getI():" << p.getI() << endl; }
Base b(1000);testBase(b);
結果分析:
好的,至此我們了解了非繼承下類對象五種數據在內存上的布局,也知道了在每一個虛函數表前都有一個指針指向type_info,負責對RTTI的支持。而加入繼承后類對象在內存中該如何表示呢?
如果我們定義了派生類
class Derive : public Base{public: Derive(int d) :Base(1000), DeriveI(d){}; //overwrite父類虛函數 virtual void print(void){ cout << "Drive::Drive_print()" ; } // Derive聲明的新的虛函數 virtual void Drive_print(){ cout << "Drive::Drive_print()" ; } virtual ~Derive(){}private: int DeriveI;};
繼承類圖為:
一個派生類如何在機器層面上塑造其父類的實例呢?在簡單對象模型中,可以在子類對象中為每個基類子對象分配一個指針。如下圖:
簡單對象模型的缺點就是因間接性導致的空間存取時間上的額外負擔,優(yōu)點則是類的大小是固定的,基類的改動不會影響子類對象的大小。
在表格驅動對象模型中,我們可以為子類對象增加第三個指針:基類指針(bptr),基類指針指向指向一個基類表(base class table),同樣的,由于間接性導致了空間和存取時間上的額外負擔,優(yōu)點則是無須改變子類對象本身就可以更改基類。表格驅動模型的圖就不再貼出來了。
在C++對象模型中,對于一般繼承(這個一般是相對于虛擬繼承而言),若子類重寫(overwrite)了父類的虛函數,則子類虛函數將覆蓋父類虛表中對應的函數;若子類并無overwrite父類虛函數,而是聲明了自己新的虛函數,則該虛函數地址將擴充到父類虛函數表最后(在vs中無法通過監(jiān)視看到擴充的結果,不過我們通過取地址的方法可以做到,子類新的虛函數確實在父類子物體的虛函數表末端)。而對于虛繼承,若子類overwrite父類虛函數,同樣地將覆蓋父類子物體中的虛函數表對應位置,而若子類聲明了自己新的虛函數,則編譯器將為子類增加一個新的虛表指針vptr,這與一般繼承不同,在后面再討論。
我們使用代碼來驗證以上模型
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]析構函數無法通過地址調用,故手動輸出 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();}
運行結果:
這個結果與我們的對象模型符合。
單繼承中(一般繼承),子類會擴展父類的虛函數表。在多繼承中,子類含有多個父類的子對象,該往哪個父類的虛函數表擴展呢?當子類overwrite了父類的函數,需要覆蓋多個父類的虛函數表嗎?
其中第二點保證了父類指針指向子類對象時,總是能夠調用到真正的函數。
為了方便查看,我們把代碼都粘貼過來
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;};
繼承類圖為:
此時Drive_multyBase 的對象模型是這樣的:
我們使用代碼驗證:
typedef void(*Fun)(void); int main(){ Drive_multyBase d(3000); //[0] cout << "[0]Base::vptr"; cout << "\t地址:" << (int *)(&d) << endl; //vprt[0]析構函數無法通過地址調用,故手動輸出 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]析構函數無法通過地址調用,故手動輸出 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();}
運行結果:
菱形繼承也稱為鉆石型繼承或重復繼承,它指的是基類被某個派生類簡介重復繼承了多次。這樣,派生類對象中擁有多份基類實例(這會帶來一些問題)。為了方便敘述,我們不使用上面的代碼了,而重新寫一個重復繼承的繼承層次:
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; } };
這時,根據單繼承,我們可以分析出B1,B2類繼承于B類時的內存布局。又根據一般多繼承,我們可以分析出D類的內存布局。我們可以得出D類子對象的內存布局如下圖:
D類對象內存布局中,圖中青色表示b1類子對象實例,黃色表示b2類子對象實例,灰色表示D類子對象實例。從圖中可以看到,由于D類間接繼承了B類兩次,導致D類對象中含有兩個B類的數據成員ib,一個屬于來源B1類,一個來源B2類。這樣不僅增大了空間,更重要的是引起了程序歧義:
D d; d.ib =1 ; //二義性錯誤,調用的是B1的ib還是B2的ib? d.B1::ib = 1; //正確 d.B2::ib = 1; //正確
盡管我們可以通過明確指明調用路徑以消除二義性,但二義性的潛在性還沒有消除,我們可以通過虛繼承來使D類只擁有一個ib實體。
虛繼承解決了菱形繼承中最派生類擁有多個間接父類實例的情況。虛繼承的派生類的內存布局與普通繼承很多不同,主要體現在:
虛繼承的子類,如果本身定義了新的虛函數,則編譯器為其生成一個虛函數指針(vptr)以及一張?zhí)摵瘮当怼T搗ptr位于對象內存最前面。
虛繼承的子類對象中,含有四字節(jié)的虛表指針偏移值。
為了分析最后的菱形繼承,我們還是先從單虛繼承繼承開始。
在C++對象模型中,虛繼承而來的子類會生成一個隱藏的虛基類指針(vbptr),在Microsoft Visual C++中,虛基類表指針總是在虛函數表指針之后,因而,對某個類實例來說,如果它有虛基類指針,那么虛基類指針可能在實例的0字節(jié)偏移處(該類沒有vptr時,vbptr就處于類實例內存布局的最前面,否則vptr處于類實例內存布局的最前面),也可能在類實例的4字節(jié)偏移處。
一個類的虛基類指針指向的虛基類表,與虛函數表一樣,虛基類表也由多個條目組成,條目中存放的是偏移值。第一個條目存放虛基類表指針(vbptr)所在地址到該類內存首地址的偏移值,由第一段的分析我們知道,這個偏移值為0(類沒有vptr)或者-4(類有虛函數,此時有vptr)。我們通過一張圖來更好地理解。
虛基類表的第二、第三...個條目依次為該類的最左虛繼承父類、次左虛繼承父類...的內存地址相對于虛基類表指針的偏移值,這點我們在下面會驗證。
如果我們的B1類虛繼承于B類:
//類的內容與前面相同class B{...}class B1 : virtual public B
根據我們前面對虛繼承的派生類的內存布局的分析,B1類的對象模型應該是這樣的:
我們通過指針訪問B1類對象的內存,以驗證上面的C++對象模型:
int main(){B1 a; cout <<"B1對象內存大小為:"<< sizeof(a) << endl; //取得B1的虛函數表 cout << "[0]B1::vptr"; cout << "\t地址:" << (int *)(&a)<< endl; //輸出虛表B1::vptr中的函數 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; //虛表指針的地址 //輸出虛基類指針條目所指的內容 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中的虛函數 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;
運行結果:
這個結果與我們的C++對象模型圖完全符合。這時我們可以來分析一下虛表指針的第二個條目值12的具體來源了,回憶上文講到的:
第二、第三...個條目依次為該類的最左虛繼承父類、次左虛繼承父類...的內存地址相對于虛基類表指針的偏移值。
在我們的例子中,也就是B類實例內存地址相對于vbptr的偏移值,也即是:[4]-[1]的偏移值,結果即為12,從地址上也可以計算出來:007CFDFC-007CFDF4結果的十進制數正是12。現在,我們對虛基類表的構成應該有了一個更好的理解。
如果我們有如下繼承層次:
class B{...}class B1: virtual public B{...}class B2: virtual public B{...}class D : public B1,public B2{...}
類圖如下所示:
菱形虛擬繼承下,最派生類D類的對象模型又有不同的構成了。在D類對象的內存構成上,有以下幾點:
菱形虛擬繼承下的C++對象模型為:
下面使用代碼加以驗證:
int main(){ D d; cout << "D對象內存大小為:" << sizeof(d) << endl; //取得B1的虛函數表 cout << "[0]B1::vptr"; cout << "\t地址:" << (int *)(&d) << endl; //輸出虛表B1::vptr中的函數 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; //虛表指針的地址 //輸出虛基類指針條目所指的內容 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中的虛函數 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; //虛表指針的地址 //輸出虛基類指針條目所指的內容 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中的虛函數 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();}
查看運行結果:
在C語言中,“數據”和“處理數據的操作(函數)”是分開來聲明的,也就是說,語言本身并沒有支持“數據和函數”之間的關聯性。
在C++中,我們通過類來將屬性與操作綁定在一起,稱為ADT,抽象數據結構。
C語言中使用struct(結構體)來封裝數據,使用函數來處理數據。舉個例子,如果我們定義了一個struct Point3如下:
typedef struct Point3{ float x; float y; float z;} Point3;
為了打印這個Point3d,我們可以定義一個函數:
void Point3d_print(const Point3d *pd){ printf("(%f,%f,%f)",pd->x,pd->y,pd_z);}
而在C++中,我們更傾向于定義一個Point3d類,以ADT來實現上面的操作:
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()<<")"; }
看到這段代碼,很多人第一個疑問可能是:加上了封裝,布局成本增加了多少?答案是class Point3d并沒有增加成本。學過了C++對象模型,我們知道,Point3d類對象的內存中,只有三個數據成員。
上面的類聲明中,三個數據成員直接內含在每一個Point3d對象中,而成員函數雖然在類中聲明,卻不出現在類對象(object)之中,這些函數(non-inline)屬于類而不屬于類對象,只會為類產生唯一的函數實例。
所以,Point3d的封裝并沒有帶來任何空間或執(zhí)行期的效率影響。而在下面這種情況下,C++的封裝額外成本才會顯示出來:
不僅如此,Point3d類數據成員的內存布局與c語言的結構體Point3d成員內存布局是相同的。C++中處在同一個訪問標識符(指public、private、protected)下的聲明的數據成員,在內存中必定保證以其聲明順序出現。而處于不同訪問標識符聲明下的成員則無此規(guī)定。對于Point3類來說,它的三個數據成員都處于private下,在內存中一起聲明順序出現。我們可以做下實驗:
void TestPoint3Member(const Point3d& p){ cout << "推測_x的地址是:" << (float *) (&p) << endl; cout << "推測_y的地址是:" << (float *) (&p) + 1 << endl; cout << "推測_z的地址是:" << (float *) (&p) + 2 << endl; cout << "根據推測出的地址輸出_x的值:" << *((float *)(&p)) << endl; cout << "根據推測出的地址輸出_y的值:" << *((float *)(&p)+1) << endl; cout << "根據推測出的地址輸出_z的值:" << *((float *)(&p)+2) << endl; }
//測試代碼 Point3d a(1,2,3); TestPoint3Member(a);
運行結果:
從結果可以看到,x,y,_z三個數據成員在內存中緊挨著。
總結一下:
不考慮虛函數與虛繼承,當數據都在同一個訪問標識符下,C++的類與C語言的結構體在對象大小和內存布局上是一致的,C++的封裝并沒有帶來空間時間上的影響。
今有類如下:
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();}
輸出結果是:
解析: