1 前言
在C++中類的數據成員有兩種:static和nonstatic,類的函數成員由三種:static,nonstatic和virtual。上篇我們盡量說一些宏觀上的東西,數據成員與函數成員在類中的布局將在微觀篇中詳細討論。
每當我們聲明一個類,定義一個對象,調用一個函數.....的時候,不知道你有沒有一些疑惑--編譯器私底下都干了些什麼?普通函數,成員函數都是怎麼調用的?static成員又是個什麼玩意。如果你對這些東西也感興趣,那麼好,我們一起將class的底層翻個底朝天。修煉好底層的內功,我想對於上層的提供,幫助可不止一點點吧?
2 class整體布局
C語言中“數據“與函數式分開聲明的,也就是說C語言並不支持”數據“與函數之間的關聯性。
我們來看下面的例子。
typedefstructpoint3d{//數據 floatx; floaty; floatz; }Point3d; voidPoint3d_print(constPoint3d*pd{ printf("%g,%g,%g",pd->x,pd->y,pd->z); }
我們再來看看C++中的做法。
classPoint3d{ float_x; float_y; float_z; public: voidpoint3d_print(){ printf("%g,%g,%g",_x,_y,_z); } };
在Point3d轉換到C++之後,我們可能會問加上封裝之後,成本會增加多少?
答案是class Point3d並沒有增加成本。三個數據成員(_x,_y,_z)直接內含在每一個對象之中,而成員函數雖在類中聲明,卻不出現在對象之中。如下圖所示:
凡事沒有絕對,virtual看起來就會增加C++在布局及存取時間上的額外負擔。稍後討論。
好吧,我承認光說面上(宏觀上)的東西東西大家都懂,而且底層的東西注定不會太宏觀。那麼下面我們舉例子來證明上述的討論。
需要說明的是以下是在vs2010下的運行結果,若是gcc,可能某些地方會有所差異。
classA{};//sizeof(A)=1有木有很奇怪?稍後說明 classB{intx;};//sizeof(B)=4 classC{ intx; public: intget(){returnx;} };//sizeof(C)=4;是不是驗證了我們上述的論述?
很奇怪sizeof(A) = 1而不是0吧?
事實上A並不是空的,他有一個隱藏的1byte大小,那是被編譯器安插進去的一個char。這樣做使得用同一個空類定義兩個對象的時候得以在內存中配置獨一無二的地址。
例如:
Aa,b; if(&a==&b)cout<<"error"<
我們都知道在C語言中struct優化的時候會進行內存對齊,那麼我們來看看class中有沒有這個優化。
classA{ charx; inty; charz; };//sizeof(A)==12; classB{ charx; chary; intz; };//sizeof(B)=8; classC{ intx; chary; charz; };//sizeof(C)=8; classD{ longlongx; chary; charz; };//sizeof(D)=16;由於longlong為8字節大小,此處以8字節對齊
顯然編譯器進行類內存對齊的優化。
接著上文,我們知道stroustrup老大的設計(目前仍在使用)是:nonstatic data members 被置於每一個對象之中,static data member則被置於對象之外。static和nonstatic function members 則被放在對象之外。
classA{ staticintx; };//sizeof(A)=1; classB{ intx; public: intget(){ returnx; } };//sizeof(B)=4 classC{ intx; public: virtualintget(){ returnx; } };//sizeof(C)=8;
顯然驗證了上述我所說的。
classA{ void(*pf)();//函數指針 };//sizeof(A)=4; classB{ int*p;//指針 };//sizeof(B)=4;
所以含有虛函數的時候,object中會包含一個虛表指針。我們知道指針一邊占用4個字節,上面的sizeof(C)就好解釋了。
3 虛函數
我們都知道虛函數是下面這個樣子。
classX{ inta; intb; public: virtualvoidfoo1(){cout<<"X::foo1"< 內存布局如下:
下面我們來證明這種布局。
#include usingnamespacestd; classX{ int_a; int_b; public: virtualvoidfoo1(){cout<<"X::foo1"<
那麼,我們繼續往下看。
4 繼承
當涉及到繼承的時候,情況又會怎樣呢?
classA{ intx; }; classB:publicA{ inty; };//sizeof(B)=8; 我們來看看涉及到繼承的時候內存的布局情況。
我們繼續,若基類中包含有虛函數,這時候又會如何呢?
classC{ public: virtualvoidfooC(){ cout<<"C::fooC()"<
下面我們來驗證這種布局:
typedefvoid(*pf)(); intmain(){ Ca; Db; int**tmpc=(int**)&a; int**tmpb=(int**)&b; pfptf; ptf=(pf)tmpc[0][0]; ptf(); ptf=(pf)tmpb[0][0]; ptf(); ptf=(pf)tmpb[0][1]; ptf(); } 運行結果:
顯然上述的布局是對的。這個時候需要注意的是:C::fooC()在前,D::fooD()在後,若出現函數覆蓋,則D中的函數會覆蓋掉繼承過來的同名函數,而對於沒有覆蓋的虛函數則追加在虛表的最後。
我們再來看看下面的涉及到虛函數的多重繼承。
classA{ int_a; public: virtualvoidfooA(){ cout<<"A::fooA()"<
有了上面的布局信息,我們可以推測類C的布局如下:
下面我們來驗證這種推測。
typedefvoid(*pf)(); intmain(){ Ca; int**tmp=(int**)&a; pfptf; for(inti=0;i<3;++i){ ptf=(pf)tmp[0][i]; ptf(); } cout<<"-----------"< 運行結果:
顯然與我們的猜測一致。
最後,我們再來看看菱形繼承的情況。
classA{ int_a1; int_a2; };//sizeof(A)=8; classB:virtualpublicA{ intb; };//sizeof(B)=16; classC:virtualpublicA{ intc; };//sizeof(C)=16; classD:publicB,publicC{ intd; };//sizeof(D)=28;
我們來看看這時候的內存布局:
我們來驗證這種布局:
intmain(){ Dd; A*pta=&d; B*ptb=&d; C*ptc=&d; cout<<"D:"<<&d<
你在嘗試的時候地址可能會有所差異,但是偏移量應該會保持一致。至於不同的編譯器是否布局都一樣,我也不得而知。至於那兩個虛指針所指虛表提供的也就是虛基類的成員偏移量信息,大家如果感興趣,可以自己驗證。
至此,宏觀布局部分大致說完,欲知後事如何請轉至“成員篇”。