深度探索C++對象模型筆記(三)

Data 語意學

class X { };

class Y : public virtual X { };

class Z : public virtual X { };

class A : public Y, public Z{ };

sizeof X 的結果爲1 //翻譯者在visual C++ 5.0上的執行結果 1

sizeof  Y的結果爲8 //4

sizeof Z 的結果爲8 //4

sizeof A 的結果爲12 //8

一個空class如:

class X{};//sizeof X == 1

事實上並不是空的,它有一個隱含的1byte,是被編譯器添加進去的char,使得這個class的兩個object得以在內存中配置獨一無二的地址:

X a, b:

if(&a == &b)

事實上,Y和Z的大小收到三個因素的影響:

  1. 語言本身所造成的額外負擔 當語言支持virtual base class時,在derived class中,此負擔即爲某種形式上的指針,它指向virtual base class subobject,或指向一個相關表格(視編譯器實現而定)。
  2. 編譯器對於特殊情況所提供的優化處理 Virtual base class X subobject的1bytes大小也會出現在class Y和Z身上,傳統上它被放在derived class的固定部分的尾部。
  3. Alignment的限制 class Y和Z的大小截至目前爲5bytes,在大部分的機器上,羣聚的結構體大小會受到alignment的限制,使它們更有效率地在內存中被存取。(alignment是將數值調整到某數的整數倍,在32位機器上,通常alignment爲4bytes(32位),以使bus的“運輸量”達到最高效率
Empty virtual base class已經成爲C++OO設計的一個特有屬於,它提供一個virtual interface,沒有定義任何數據,某些編譯器對此提供特殊處理,一個empty virtual class被視爲derived class object最開頭的一部分,也就是說沒有花費任何額外空間,這就節省空class所謂的1bytes(因爲有個成員,就不需要爲空class安插一個char),因此Y和Z的大小是4而不是8(VC++就是這一類型的編譯器)

class A的大小是什麼呢?很明顯,某種程度上必須視你所使用的編譯器而定。按第一種情況(沒有特殊處理Empty virtual base class),我們可能會回答16,畢竟Y和Z都是8,但事實是12.
記住,一個virtual base class subobject只會在derived class中存在一份實體,Class A的大小由下列幾點決定:
  • 被大家共享的唯一一個class X實體,大小爲1byte。
  • Base class Y的大小,減去“因virtual base class X”而配置的大小,結果是4bytes,Base class Z的算法亦同,加起來是8bytes
  • class A自己的大小:0byte
  • class A的alignment數量,前述三項總和是9bytes,class A必須調整至4bytes邊界,所以要填補3bytes,結果是12bytes。
C++Standard並不強制規定如“base class subobject”的排列次序或“不同存取層級的data member的排列次序”(如public和private誰先誰後等)這種瑣碎細節,它也不規定virtual functions或virtual base classes的實現細節。C++Standard只說:那些細節由各家廠商自定。

1、Data Member的綁定

瞭解即可,目前已無意義。

2、Data Member的佈局

Nonstatic data member在classobject中的排列順序將和其被聲明的順序一樣。
其他的情況,如與access sections,vptr等相關的,C++Standard並未做過多要求,vptr有放最後的,也有放最前的。

3、Data Member的存取

已知程序:Point3d origin; origin.x = 0.0;

x的存取成本是什麼?答案視x和Point3d的如何聲明而定,x可能是static 或nonstatic,Point3d可能是獨立類,也可能從其他類繼承而來。

Static Data Member

每一個static data member只有一個實體,存放在程序的data segment中

如果有兩個classes,都聲明瞭一個static member freeList,會產生命名衝突,編譯的解決辦法(name-mangling);

  1. 一種算法,推導出獨一無二的名稱。
  2. 萬一編譯系統(或環境工具)必須和使用者交談,那些獨一無二的名稱可以輕易被推導回到原來的名稱。

Nonstatic Data Member

直接存放在每一個class object中。
Point3d Point3d::translate(const Point3d &pt)
{
x += pt.x;
y += pt.y;
z += pt.z;
}
x,y,z的直接存取,實際上是由一個“implicit class object”(this指針)完成的:
Point3d Point3d::translate(Point3d *const this, const Point3d &pt)
{
this->x += pt.x;
this->y += pt.y;
this->z += pt.z;
}

要對nonstatic data member進行操作,編譯器需要把class object的起始地址加上data member的偏移量,如:
origin._y = 0.0;
那麼地址&origin._y等於:
&origin + (&Point3d::_y - 1);
-1操作原因:由於指向data member的指針其offset總是被加上1,這樣編譯系統就能區分“一個指向data member的指針,指出class的第一個member”和“一個指向data member的指針,但沒有指出任何member”兩種情況。
每一個nonstatic data member的偏移量在編譯時期即可獲知,因此,存取效率與C struct member是一樣的。

虛擬繼承會爲“base class subobject”存取class memeber導入一層新的間接層,如:
Point3d *pt3d;
pt3d->_x = 0.0;
其效率在_x是一個struct member,class member,單一繼承,多重繼承的情況下完全一致。但如果_x是一個virtual base class 的member,存取速度會慢一點。

origin.x = 0.0;
pt->x = 0.0;
從origin和pt存取有什麼重大差異?答案是“當Point3d是一個derived class,而在其繼承結構中有一個virtual base class,並且被存取的x是一個從該virtual base class 繼承而來的member時,就會有重大差異”。這時候我們不能說pt必然指向哪一種class type(因此我們也不知道編譯時期這個member真正的offset位置),所以這個賦值操作必須延遲至執行期。但如果使用origin,就不會有這個問題,其類型無疑是Point3d class。

4、繼承與Data Member

在C++繼承模型中,派生類成員和基類成員的排列次序,理論上編譯器可以自由安排,在大部分編譯器中,基類成員總是先出現,但屬於virtual base class 的除外。

只要繼承不要多態

有一個設計,就是從Point2d派生一個Point3d,於是3d將繼承2d數據和操作方法。一般而言,具體繼承,相對於虛擬繼承並不會增加空間或存取時間上的額外負擔。把兩個獨立不想幹的classes湊成一對“type/subtype”,並帶有繼承關係,會有什麼易犯的錯誤呢?

  • 經驗不足的人可能會重複設計一些相同操作的函數如operator+=
  • 把一個class分解爲兩層或多層,有可能會爲了“表現class體系的抽象化”而膨脹所需空間
C++語言保證“出現在derived class中的base class subobject有其完整原樣性”:
class C
{
private:
int val;
char c1;
char c2;
char c3;
};
在一部32爲機器中,每一個C object的大小都是8bytes;val 4bytes, c1,c2,c3各1bytes,alignment 1 bytes;
如果把C分爲三層結構:
class C1
{
private:
int val;
char bit1;
};

class C2 : public C1
{
private:
char bit2;
};

class C3 : public C2
{
private:
char bit3;
};
從設計觀點來看,這個結構可能更合理,從效率觀點來看,現在C3的大小是16bytes,比原先設計多了一倍。

加上多態

而外的負擔:

  • 導入一個有關的virtual table,用來存放聲明的每一個virtual functions的地址,在加上一個或兩個slots用以支持runtime type identification
  • 在每一個class object中導入一個vptr
  • 加強constructor,使它能夠爲vptr賦初值,讓它指向class所對應的virtual table
  • 加強destructor,使它能夠清除指向class相關virtual table的vptr

多重繼承

單一繼承提供了一種“自然多態”形式,是關於classed體系中的base type和derived type之間的轉換。base class 和derived class的object都是從相同的地址開始的,期間差異只在於derived object比較大。

多重繼承的問題主要發生於derived class object和其第二或後繼base class objects之間的轉換:

對一個多重派生對象,將其地址指定給“最左端(也就是第一個)base class的指針”,情況將和單一繼承時相同,因爲二者都指向相同的起始地址,至於第二個或後繼base class的地址操作,則需要將地址修改過:

//Point3d繼承Point2d,Vertex3d多重繼承Point3d和Vertex

Vertex3d v3d;

Vertex *pv;

Point2d *p2d;

Point3d *p3d;

那麼 pv = &v3d;需要如下的內部轉換:

pv = (Vertex*)( ( (char*)&v3d ) + sizeof(Point3d) );

而:

p2d = &v3d;

p3d = &v3d;

都只需要簡單的地址拷貝就行了。如果有兩個指針如下:

Vertex3d *pv3d;

Vertex *pv;

那麼 pv = pv3d;

不能只是簡單的地址轉換,因爲如果pv3d爲0,pv將獲得sizeof(Point3d)的值,這是錯誤的,因爲,內部需要一個條件測試:

pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof(Point3d) : 0;

C++Standar 並未要求Vertexd中base class Point3d和Vertex有特定的排列次序。

虛擬繼承

istream繼承iosostream繼承ios,iostream多重繼承istream和ostream,不論是istream還是ostream都內含一個ios subobject,然而在iostream的對象佈局中,我們只需要一份ios subobject就好,解決辦法是導入所謂的虛擬繼承。(對於更深層次的問題,如固定部分和共享部分數據的存儲問題等,暫不記錄,有需要可回頭重看

5、對象成員的效率

書中列舉幾個測試表明,如果編譯器將優化開關打開的情況下,C++的封裝就不會帶來執行期的效率成本(Data 成員的存取),使用inline調用函數也一樣。
單一繼承也不會影響效率,因爲members被連續存儲在derived class object中,並且其offset在編譯期就已知了。而虛擬繼承效率則令人失望。

6、指向Data Members的指針

指向data members的指針,可以用來調查vptr是放在class起始處還是尾端,也可以用來決定class中access sections的次序。
class Point3d
{
public:
virtual ~Point3d();
protected:
static Point3d origin;
float x, y, z;
};

唯一可能因編譯器不同而不同的是vptr的位置。
那麼存取某個座標成員的地址,代表什麼意思?
& Point3d::z;
上述操作將得到z座標在class object中的偏移量,最低限度其值是x和y的大小總和,然而vptr的位置沒有限制。在一部32位機器上,每一個float是4bytes,所以其值要麼是8,要麼是12;
如果vptr放在對象的尾端,則三個座標值在對象佈局中的offset分別是0,4,8,如果vptr放在對象的起頭,則是1,5,9或5,9,13.總是多1,爲什麼呢?
問題在於,如何區分一個“沒有指向任何data member”的指針和一個指向“第一個data member”的指針?
float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::x;
//Point3d::*的意思是指向Point3d data member的指針類型
//如何區分
if(p1 == p2)
爲了區分p1和p2,每一個真正的member offset都被加上1.
認識指向data member的指針之後,要解釋:
& Point3d::z;
和 & origin.z;
之間的差異,就非常明確了,一個是它在class 中的offset,一個是真正的class object的data member在內存中的地址。

指向members的指針的效率問題

未優化的情況下,要比直接存取多出一倍不止。(因爲指針多了一層間接層)
單一繼承不影響效率,虛擬繼承妨礙了優化的有效性,每一層虛擬繼承都導入一個額外層次的間接性。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章