C++對(duì)象模型--4.Data語(yǔ)義學(xué)
對(duì)象內(nèi)存
有以下類,這些類實(shí)例化的對(duì)象在內(nèi)存中占的大小分別是多少?
class X
{
};
class Y : public virtual X
{
};
class Z : public virtual X
{
};
class A : public Y, public Z
{
};
乍一看這三個(gè)類都沒(méi)有成員變量,對(duì)象所占的內(nèi)存大小應(yīng)該是0,而實(shí)際不是。
? ? ? ? sizeof X的結(jié)果是1
? ? ? ? sizeof Y的結(jié)果是8
? ? ? ? sizeof Z的結(jié)果是8
? ? ? ? sizeof Z的結(jié)果是12
class X不是空的,它有一個(gè)隱晦的1 byte,那是編譯器安插進(jìn)去的一個(gè)char,使得該class對(duì)應(yīng)的object得以在內(nèi)存中配置唯一的地址
class Y和class Z的大小都是8,這個(gè)和機(jī)器以及編譯器有關(guān),受到以下三個(gè)因素影響:
1. 語(yǔ)言本身造成的額外負(fù)擔(dān)
? ? 當(dāng)語(yǔ)言支持virtual base class時(shí),在derived class中,這個(gè)額外負(fù)擔(dān)反應(yīng)在某種形式的指針身上,它或者指向virtual base class subobject或者指向一個(gè)相關(guān)表格,這個(gè)表格里或者virtual base class subobject的地址或者其偏移量。
2. 編譯器對(duì)于特殊情況所提供的優(yōu)化處理
? ? virtual base class X subobject的1 byte大小也出現(xiàn)在class Y和Z身上。傳統(tǒng)上它被放在derived class的固定(不變動(dòng))部分的尾端。某些編譯器會(huì)對(duì)empty virtual base class提供特殊支持(如下面第3點(diǎn))
? ? 對(duì)于后續(xù)編譯器優(yōu)化,這1byte被舍去了,因?yàn)閐erived class object中已經(jīng)不需要這1byte來(lái)申請(qǐng)唯一地址了,object已經(jīng)至少有一個(gè)指針了。
3. Alignment的限制
? ? ?class Y和Z的大小截止當(dāng)前為5 bytes,在大部分機(jī)器上,群聚的結(jié)構(gòu)體大小會(huì)受到alignment的限制,使他們能夠更加有效率地在內(nèi)存中被存取
? ? alignment就是將數(shù)值調(diào)整到某數(shù)的整數(shù)倍,在32位計(jì)算機(jī)上,通常alignment為4bytes(32位),以使bus的“運(yùn)輸量”達(dá)到最高效率。
class的data member一般是可以表現(xiàn)這個(gè)class在程序執(zhí)行時(shí)的某種狀態(tài)。nonstatic data members放置的是“個(gè)別的class object”感興趣的數(shù)據(jù),static data member則放置的是“整個(gè)class”感興趣的數(shù)據(jù)。不管該class被產(chǎn)生出多少個(gè)objects,static data members永遠(yuǎn)只存在一份實(shí)體(即使還沒(méi)產(chǎn)生任何該class的object實(shí)體,其static data member也已經(jīng)存在)。但一個(gè)template class的static
data members的行為有所不同(后續(xù)會(huì)討論)
Data Member的綁定
關(guān)于全局變量和內(nèi)部變量的resolved問(wèn)題
? ? ? 現(xiàn)在編譯器已優(yōu)化,對(duì)于class內(nèi)部和外部同名變量,內(nèi)部function優(yōu)先使用內(nèi)部同名變量。但是對(duì)于typedef定義優(yōu)先使用先出現(xiàn)的,即如果使用點(diǎn)出現(xiàn)時(shí),只有外部typedef可見(jiàn),那么使用外部typedef。因此為避免歧義,需要把內(nèi)部的typedef放在所有數(shù)據(jù)使用之前,如下:
typedef int length;
class Point3D
{
private:
? ? typedef float length;
? ? length _val;
};
Data Member的布局
nonstatic data member在class object中的排列順序和其被聲明的順序一致,中間插入static data members不會(huì)被放進(jìn)對(duì)象布局中。
C++ Standard要求,在同一個(gè)access section(也就是private、public、protected等區(qū)段)中,members的排列只需符合“較晚出現(xiàn)的members在class object中有較高的地址”這一條件即可,也就是說(shuō)各個(gè)members并不一定得連續(xù)排列,中間可能會(huì)被一些字節(jié)介入,如member的邊界調(diào)整(alignment)可能就填補(bǔ)一些bytes。access sections的多寡并不會(huì)招來(lái)額外負(fù)擔(dān),例如在一個(gè)section中聲明8個(gè)members或者在8個(gè)sections中總共聲明8個(gè)members得到的object大小是一樣的。
各類Data Member的存取
static data member
每一個(gè)static data member只有一個(gè)實(shí)體,存放在程序的data segment中,每次程序取用static member就會(huì)被內(nèi)部轉(zhuǎn)化成對(duì)該唯一的extern實(shí)體的直接參考操作。
//origin.chunkSize = 250;
Point3d::chunkSize = 250;
//pt->chunkSize = 250;
Point3D::chunkSize = 250;
不管這個(gè)static data member是通過(guò)繼承還是什么途徑得來(lái)的,存取都是一樣的。取一個(gè)static data member的地址會(huì)得到一個(gè)指向其數(shù)據(jù)類型的指針,而不是一個(gè)指向其class member的指針,因?yàn)閟tatic member并不包含在一個(gè)class object中,在data segment中。兩個(gè)class命名了相同的static變量,那么他們?cè)赿ata segment就會(huì)導(dǎo)致名稱沖突,編譯器解決的辦法是對(duì)他們重新編碼。
Nonstatic Data Member
nonstatic data member的存取是通過(guò)class object來(lái)操作的,通過(guò)明確的或者暗喻的(通過(guò)調(diào)用內(nèi)部方法間接使用nonstatic data member)。對(duì)于nonstatic data member的存取是編譯器把class object的起始地址加上data member的偏移量(offset),如:
? ? origin.y = 0.0
那么地址&origin.y等于&origin+(&Point3d::_y-1),指向data member的指針,其offset值總被加上1,這是用于區(qū)分“一個(gè)指向data member的指針,用以指出class的第一個(gè)member”和“一個(gè)指向data member的指針,沒(méi)有指向任何member”兩種情況,所以這里需要-1
Add Polymorphism
class Point2d
{
? ? ...
? ? virtual function...
};
class Point3d : public Point2d
{
? ?...
};
加入多態(tài)后,程序有了更多彈性,為支持這樣的彈性,勢(shì)必給Point2d帶來(lái)空間和存取時(shí)間的額外負(fù)擔(dān):
1. 導(dǎo)入一個(gè)和Point2d有關(guān)的virtual table,用來(lái)存放它所聲明的每一個(gè)virtual function的地址。這個(gè)table的元素?cái)?shù)目一般而言是被聲明的virtual functions的數(shù)目,再加上一個(gè)或兩個(gè)slots(用以支持runtime type identification)
2. 在每一個(gè)class object中導(dǎo)入一個(gè)vptr,提供執(zhí)行期的鏈接,使每一個(gè)object能夠找到相應(yīng)的virtual table
3. 加強(qiáng)constructor,使它訥訥夠?yàn)関ptr設(shè)定初值,讓它指向class所對(duì)應(yīng)的virtual table。這可能意味著在derived class和每一個(gè)base class的constructor中,重新設(shè)定vptr的值,其情況視編譯器的優(yōu)化的積極性而定。
4. 加強(qiáng)destructor,使它能抹消“指向class之相關(guān)virtual table”的vptr。要知道,vptr很可能已經(jīng)在derived class destructor中被設(shè)定為derived class的virtual table地址。記住,destructor的調(diào)用次序是反向的:從derived class到base class,一個(gè)積極的優(yōu)化編譯器可以壓抑那些大量的指定操作。
每一個(gè)Point3d class object內(nèi)含一個(gè)額外的vptr member(繼承自Point2d);多了一個(gè)Point3d virtual table,此外,每一個(gè)virtual member function的調(diào)用也比以前復(fù)雜了。
多重繼承(Multiple Inheritance)
單一繼承提供一種“自然多態(tài)(natural polymorphism)”形式,base class和derived class的objects都是從相同地址開(kāi)始的,其差異只在于derived object 比較大,用以容納它自己的nonstatic data members。
對(duì)于多重繼承,復(fù)雜度在于derived class和其上一個(gè)base class乃至上上一個(gè) base class之間的“非自然”關(guān)系,如下:
? ? class Point2d
? ? {
? ? public:
? ? ? //...(擁有virtual接口,所以Point2d對(duì)象中會(huì)有vptr)
? ? protected:
? ? ? float _x, _y;
? ? };
? ? class Point3d : public Point2d
? ? {
? ? public:
? ? ? //...
? ? protected:
? ? ? float _z;
? ? };
? ? class Vertex
? ? {
? ? public:
? ? ? //...(擁有virtual接口,所以Vertex對(duì)象之中會(huì)有vptr)
? ? protected:
? ? ? Vertex* next;
? ? };
? ? class Vertex3d : public Point3d, public Vertex
? ? {
? ? public:
? ? ? //...
? ? protected:
? ? ? float mumble;
? ? };
對(duì)于將Vertex3d對(duì)象地址指定給最左端(即第一個(gè))base class類型的指針,情況將和單一繼承時(shí)相同,因?yàn)槎叨贾赶蛳嗤钠鹗嫉刂?。至于第二個(gè)或者后續(xù)的base class的地址指定操作,則需要將地址修改,加上(或減去,如downcast的話)介于中間的base class subobject(s)大小
? ? ? Vertext3d v3d;
? ? ? Vertex *pv;
? ? ? Point2d *p2d;
? ? ? Point3d *p3d;
那么下面這個(gè)指定操作:
? ? ?pv = &v3d;
需要這樣的內(nèi)部轉(zhuǎn)化,虛擬碼:
? ? pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));
下面的指定操作:
? ? p2d = &v3d;
? ? p3d = &v3d;
都只是需要簡(jiǎn)單拷貝地址就行。
如果有下面兩個(gè)指針,且進(jìn)行指定操作:
? ? Vertex3d *pv3d;
? ? Vertex *pv;
? ? pv = pv3d;
不能只是簡(jiǎn)單地被轉(zhuǎn)化為:
? ? pv = (Vertex*)((char*)pv3d + sizeof(Point3d));
這里面還要判斷pv3d是否為0,正確的內(nèi)部轉(zhuǎn)化應(yīng)該再添加一個(gè)條件測(cè)試:
? ? pv = pv3d ? (Vertex*)((char*)pv3d + sizeof(Point3d)):0;
至于reference則不需要針對(duì)可能的0地址做測(cè)試,因?yàn)閞eference不可能參考到no object
虛擬繼承(Virtual Inheritance)
? ? ? ? 在虛擬繼承中,如果繼承有多個(gè)相同的subobject,則在derived class中只需保存一份。一般的實(shí)現(xiàn)方法:Class如果內(nèi)含一個(gè)或多個(gè)virtual base class subobject,像istream那樣,那么將被分割成兩部分:一個(gè)不變局部和一個(gè)共享局部。不變局部中的數(shù)據(jù),不管后續(xù)如何衍化,總擁有固定的offset(從object的開(kāi)頭算起),所以這部分?jǐn)?shù)據(jù)可以被直接存取。至于共享局部,所表現(xiàn)的就是virtual base class subobject。這一部分的數(shù)據(jù),其位置會(huì)因?yàn)槊看闻派喜僮鞫兴兓?,所以他們只能被間接存取。各家編譯器實(shí)現(xiàn)技術(shù)之間的差異在于間接存取的方法不同。有如下虛擬繼承層次結(jié)構(gòu):
? ? class Point2d
? ? {
? ? public:
? ? ? ?...
? ? protected:
? ? ? float _x, _y;
? ? };
? ? class Vertex : public virtual Point2d
? ? {
? ? public:
? ? ? ...
? ? protected:
? ? ? Vertex *next;
? ? };
? ? class Point3d : public virtual Point2d
? ? {
? ? public:
? ? ? ...
? ? protected:
? ? ? float _z;
? ? };
? ? class Vertex3d : public Vertex, public Point3d
? ? {
? ? public:
? ? ? ...
? ? protected:
? ? ? float mumble;
? ? };
一般的布局策略是先安排好derived class的不變部分,然后在簡(jiǎn)歷其共享部分。編譯器在每個(gè)derived class object中安插一些指針,每個(gè)指針指向一個(gè)virtual base class,存取寄存得來(lái)的virtual base class members可以使用相關(guān)指針間接完成(理想狀態(tài)是不在virtual base class中設(shè)置member),如下:
? ? void Point3d::operator += (const Point3d &rhs)
? ? {
? ? ? _x += rhs._x;
? ? ? _y += rhs._y;
? ? ? _x += rhs._z;
? ? };
在cfront編譯器策略里,這個(gè)運(yùn)算符會(huì)被內(nèi)部轉(zhuǎn)換為:
? ? //虛擬碼
? ? __vbcPoint2d->_x += rhs.__vbcPoint2d->_x;//vbc意為virtual base class
? ??__vbcPoint2d->_y += rhs.__vbcPoint2d->_y;
? ? _z += rhs._z;
這里表現(xiàn)出來(lái)2個(gè)主要缺點(diǎn):
1. 每一個(gè)對(duì)象必須對(duì)其每一個(gè)virtual base class背負(fù)一個(gè)額外指針,而我們希望class object有固定的內(nèi)存容量,而不是隨著virtual base class的數(shù)目有所改變
2. 由于虛擬繼承串聯(lián)的加長(zhǎng),導(dǎo)致間接存取的層次增加,從而導(dǎo)致存取時(shí)間變長(zhǎng),而我們希望存取時(shí)間是固定的。
針對(duì)這兩個(gè)問(wèn)題各個(gè)編譯器有各自的做法:
MetaWare和其他編譯器仍使用cfront的原始模型來(lái)解決第2個(gè)問(wèn)題,他們經(jīng)由拷貝操作來(lái)取得所有的nested virtual base class指針,放到derived class object中,用空間上的代價(jià)解決“固定存取時(shí)間”這個(gè)問(wèn)題,如下圖:
另一種解決方案是將virtual base class offset和virtual function entry混在一起,具體做法:在virtual function table中放置virtual base class的offset(而不是地址),如下圖:
以上方法都是實(shí)現(xiàn)模型,而不是一種標(biāo)準(zhǔn),每一種模型都是用來(lái)解決“存取shared subobject內(nèi)的數(shù)據(jù)(其位置會(huì)因每次派生操作而又變化)”所引發(fā)的問(wèn)題。最理想的情況是virtual base class 中不存放members,只是提供接口。
指向Data Member的指針
指向data member的指針是一個(gè)有用的語(yǔ)言特性,特別是要詳細(xì)調(diào)查class members的底層布局的話。這樣的調(diào)查可用以決定vptr是放在class的起始處或是尾端。另一個(gè)用途是決定class的access section次數(shù)。
有以下注意點(diǎn):如何區(qū)分一個(gè)“沒(méi)有指向任何data member”的指針和一個(gè)指向“第一個(gè)datamember”的指針,如下:
? ? float Point3d::*p1 = 0;
? ? float Point3d::*p2 = &Point3d::x;//“指向Point3d data member”的指針類型
? ? if(p1 == p2)
? ? {
? ? ? ...
? ? }
為了區(qū)分p1和p2,每一個(gè)真正的member offset值都被加上1,因此不論編譯器或使用者都必須記住,在真正使用該值以指出一個(gè)member之前,請(qǐng)先減掉1。
? ??





