前述
本章的的主題是Data語意學(xué)呻拌,主要是探究編譯器對class中的Data member的綁定、布局和存儲等操作亚茬,最后探究Data member存取和多種繼承方式之間的效率關(guān)系铸题,以及指向Data member的指針的效率問題。
參考書籍及鏈接:《深度探索c++對象模型》
0翰萨、本章基礎(chǔ)
1. 空類對象的大小是多少脏答?
class X { };//空類
對于空類,它有一個隱藏的1byte大小亩鬼,那個被編譯器安插進(jìn)去的一個char,這使得這一class的兩個objects得以在內(nèi)存中配置獨(dú)一無二的地址殖告。
2. class object的size會受到哪些因素的影響?
會影響class object的size的因素有如下三個雳锋,編譯器:
- 語言本身所造成的額外負(fù)擔(dān):當(dāng)語言支持virtual base classes時丛肮,就會導(dǎo)致一些額外負(fù)擔(dān)。需要一個指針魄缚,它或者指向virtual base class subobject,或者指向一個相關(guān)的表格宝与,表格用于存儲subobject地址或偏移值。
- 編譯器對于特殊情況所提供的優(yōu)化處理:Virtual base class subobject的1 byte大小也會出現(xiàn)在derived class上冶匹。
- Alignment(邊界對齊)的限制:在大部分的機(jī)器上习劫,聚合的結(jié)構(gòu)體大小會受到alignment的限制,使他們能夠更有效率地在內(nèi)存中被存取嚼隘。比如32機(jī)器字上就是4的整數(shù)倍诽里。
3. 各種類型data member的存放。
nonstatic直接放在class object之中飞蛹。static data member放置在程序的一個global data segment中,不會影響個別class object的大小席镀。無論class產(chǎn)生多少個object,甚至是0個祭犯,其static data members永遠(yuǎn)也只存在一份實(shí)例。但是一個template classs的static data members的行為稍有不同焰宣。
一、Data member的綁定
1. member function取用的是global還是local data member?
當(dāng)member funtion取用Data時捕仔,優(yōu)先考慮member data,人們稱這種情況為“member rewriting rule”匕积,意思是對于member functions本身的分析,會直到整個class的聲明都出現(xiàn)了才開始榜跌。在一個inline member function軀體之內(nèi)的一個data member綁定操作闪唆,會在整個class聲明之后才發(fā)生。
以前人們提倡兩種程序設(shè)計(jì)風(fēng)格钓葫,即將所有的data members放在class聲明起始處悄蕾,或者把所有的inline function都放在class聲明之外。就是為解決綁定問題础浮,但這種情況在c++ 2.0之后已經(jīng)解決了帆调。
2. member function的argument list的情況又是怎么樣的呢?
與取用data member不同的是,argument list中的名稱還是會在它們第一次 遭遇時被適當(dāng)?shù)貨Q議(resolved)完成霸旗。
typedef int length;
class Point3d{
public:
void mumble(length val) { _val=val;} //length被決議為global
length mumble() {return val;}
// ...
private:
typedef float length;//這樣的聲明將使先前的參考操作不合法
length _val;
// ...
};
雖然編譯器能處理贷帮,但還是提倡一種防御性程序風(fēng)格:即總是把“nested(嵌套的) type聲明”放在class的起始處。
二诱告、Data member的布局
1. Data member是怎樣被放置的撵枢?
關(guān)于data member的布局,記住以下三點(diǎn):
- nonstatic data members在class object中的排列順序和其被聲明的順序一樣精居,任何中間介入的static data members都不會被放進(jìn)對象布局之中锄禽。
- C++ standard允許編譯器將多個access sections(也就是private、public靴姿、protected等區(qū)段)之中的data members整體自由排列沃但,不必在乎他們的出現(xiàn)在class中的聲明順序(連續(xù)的兩個privata也算兩個section)。
- 編譯器還可能會合成一些內(nèi)部使用的data members佛吓,以支持整個對象模型宵晚,vptr就是這樣的東西,當(dāng)前所有的編譯器都把它安插在每一個“內(nèi)含virtual function之class”的object內(nèi)维雇。
三淤刃、Data member的存取
1. 經(jīng)由一個class object和一個指針存取data member,有重大差異嗎吱型?
答案是顯然的逸贾,這跟data member的類型和class的繼承等都有關(guān)系,分如下兩種情況討論:
-
data member 為 static
static data members會被編譯器提出于class之外,并被視為一個global變量(但只在class生命范圍內(nèi)可見)铝侵。每一個static data member只有一個實(shí)例灼伤,存放在程序的data segment之中,通過一個指針和通過一個對象來存取data member都是一樣的咪鲜。若取一個static data member的地址狐赡,會得到一個指向其數(shù)據(jù)類型的指針,而不是一個指向其class member的指針嗜诀,因?yàn)閟tatic member并不內(nèi)含在一個class object之中猾警。
如果有兩個classes孔祸,每一個都聲明了一個同名的static member隆敢,編譯器就會暗中對每一個static data member編碼(對于這種手法有個很美的名稱:name-mangling),以獲得一個獨(dú)一無的程序識別代碼崔慧。
-
data member 為 nonstatic
Nonstatic data members直接存放在每一個class object之中拂蝎。只有經(jīng)過class object才能存取它們(implicit 存取如this指針)。欲對一個nonstatic data member進(jìn)行存取操作惶室,編譯器需要把class object的起始地址加上data member的偏移位置(offset)温自。每一個nonstatic data member的偏移位置(offset)在編譯時期即可獲知,甚至如果member屬于一個base class subobject(派生自單一或多重繼承串鏈)也是一樣的皇钞。因此悼泌,存取一個nonstatic data member,其效率和存取一個C struct member或一個nonderived class的member是一樣的夹界。
但是如果該data member是一個virtual base class 的member,那么通過指針的存取速度會稍慢一點(diǎn)馆里。(指針的真正class type 只有在執(zhí)行器才真正確定)。
四可柿、“繼承”與Data Member
C++ standard未強(qiáng)制指定derived class members和base class members的排列順序鸠踪,理論上編譯器可以自由安排之。在大部分編譯器上頭复斥,base class members總是先出現(xiàn)营密,但屬于virtual base class的除外∧慷В“繼承”會對Data Member的布局有什么影響评汰?接下來分四種情況進(jìn)行討論。
1. 第一種情況:只要繼承不要多態(tài)痢虹。
這種情況不會存儲時間上的額外負(fù)擔(dān)被去,由于base class和derived class的objects都是從相同的地址開始,其差異只在于derived object 比較大世分,用以容納自建的nonstatic data members编振,把一個derived class object指定給base class 的指針或引用,并不需要編譯器去調(diào)停或修改地址踪央,可以提供了最佳執(zhí)行效率臀玄。
2. 第二種情況:加上多態(tài)。
加上virtual function接口后畅蹂,彈性增加了健无,但也同時增加了空間和存取時間上的額外負(fù)擔(dān),如何取舍液斜,視多態(tài)程序所帶來的利益累贤。可能帶來的額外負(fù)擔(dān)如下:
- 導(dǎo)入一個和virtual table 少漆,用來存儲它所聲明的每一個virtual functions的地址臼膏。再加上一兩個slots(type_info)。
- 在每一個class object中導(dǎo)入一個vptr,提供執(zhí)行期的鏈接示损,使每一個object能夠找到相應(yīng)的virtual table渗磅。
- 加強(qiáng)constructor,使它能夠?yàn)関ptr設(shè)定初始值检访,讓它指向class所對應(yīng)的virtual table始鱼。
- 加強(qiáng)destructor,使它能夠消抹“指向class 相關(guān)virtual table”的vptr脆贵。
3. 第三種情況:多重繼承医清。
對于單一繼承,如果沒有virtual function卖氨,那么編譯器就不需要做其他工作;但如果base class沒有virtual function而derived class有会烙,并且vptr放在object首部,那么當(dāng)把一個derived object轉(zhuǎn)換為其base object時双泪,就需要編譯器對vptr進(jìn)行調(diào)整持搜。在既是多重繼承又是虛擬繼承的情況下,編譯器的需要做的會更多焙矛。
對一個多重派生對象葫盼,將其地址指定給“最左端(也就是第一個)base class的指針”,情況將和單一繼承時相同村斟,因?yàn)槎叨贾赶蛳嗤钠鹗嫉刂菲兜肌V劣诘诙€或后繼的base class的地址指定操作,則需要將地址修改為:加上(或減去)介于中間的base class subobjects大小蟆盹。比較需要注意的是孩灯,如果在取drived class object的地址時進(jìn)行偏移計(jì)算時,若其為指針逾滥,就需要判斷其是否為0峰档,若為0則基類object的地址也應(yīng)為0。當(dāng)然,這些都是編譯器的工作讥巡,我們需要了解掀亩,但不需要自己去實(shí)現(xiàn)。
如果要存取第二個(或后繼)base class中的一個data member會是怎樣的情況欢顷?需要付出額外的成本嗎槽棍? 不,members的位置在編譯期就固定了抬驴,因此炼七,存取members只是一個簡單的offset運(yùn)算,就像單一繼承一樣簡單布持,不管是經(jīng)由一個指針豌拙,一個reference或是一個object來存取。
4. 第四種情況:虛擬繼承鳖链。
虛擬繼承的出現(xiàn)是為了避免多個相同base class subobject的出現(xiàn)姆蘸,將其只保留一份墩莫,從而減少空間浪費(fèi)芙委。
class如果含有一個或多個virtual base class subobjects將被分割為兩部分:一個不變區(qū)域和一個共享區(qū)域。不變區(qū)域中的數(shù)據(jù)狂秦,總是能有固定的offset灌侣,這部分可以被直接存取,至于共享部分裂问,所表現(xiàn)的就是virtual base class subobject 侧啼,這個部分?jǐn)?shù)據(jù),其位置因?yàn)槊看闻缮僮鞫凶兓安荆灾荒荛g接存取痊乾。
一般而言,virtual base class最有效的一種運(yùn)用形式就是:一個抽象的virtual base class椭更,沒有任何data members哪审。
五、對象成員的效率
程序員如果只關(guān)心起程序效率虑瀑,應(yīng)該實(shí)際測試湿滓,不能光憑推論、常識判斷或假設(shè)舌狗。
參考書籍作者所做的測試表明叽奥,虛擬繼承所造成確實(shí)會嚴(yán)重影響data member的存取效率。
五痛侍、指向Data members的指針(Pointer to Data Members)
1. 如果獲取Data member的偏移值朝氓?偏移值應(yīng)該為多少?
通過如(&Point3d::z)這樣的操作可以獲得data member的偏移值。實(shí)際測試表明所獲得的offset比預(yù)想大1赵哲,這是為什么嘹狞?實(shí)際上這樣做的目的是為了區(qū)分一個“沒有指向任何data member”的指針,和一個指向“第一個data member”的指針的情況誓竿。比如:
float Point3d::*p1 = 0;//“沒有指向任何data member”的指針
float Point3d::*p2 = &Point3d::x;//指向“第一個data member”的指針
if(p1 == p2) //如何區(qū)分?
{
cout << "p1 & p2 contain the same value --" ;
cout << " they must address the same member!" << endl;
}
因此磅网,不論編譯器或使用者都必須記住,在真正使用該值以指出一個member之前筷屡,請先減掉1涧偷。
2.“指向Member的指針”對數(shù)據(jù)的存取有什么影響?
無繼承時毙死,指向member的指針對數(shù)據(jù)的存取操作燎潮,首先需要計(jì)算offset-1,其次具體的object需要用offset計(jì)算地址,會極大地降低效率扼倘,但目前的一些編譯器提供了對應(yīng)的優(yōu)化确封,可以使其像直接通過對象取值一下快速。
有繼承時再菊,data member是直接放在class object中的爪喘,理論上不會影響代碼的效率,但繼承的使用會妨礙優(yōu)化的效果纠拔。