《深度探索C++物件模型》讀書筆記(3)。
在visual C++ 6.0中測試如下代碼:
#include "iostream"
using namespace std;
class X {};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y,public Z {};
int main()
{
cout<<"sizeof(X): "<<sizeof(X)<<endl;
cout<<"sizeof(Y): "<<sizeof(Y)<<endl;
cout<<"sizeof(Z): "<<sizeof(Z)<<endl;
cout<<"sizeof(A): "<<sizeof(A)<<endl;
return 0;
}
得出的結果也許會令你毫無頭緒
sizeof(X): 1
sizeof(Y): 4
sizeof(Z): 4
sizeof(A): 8
下面一一闡釋原因:
(1)對於一個class X這樣的空的class,由於需要使得這個class的兩個objects得以在記憶體中配置獨一無二的地址,故編譯器會在其中安插進一個char.因而class X的大小為1.
(2)由於class Y虛擬繼承於class X,而在derived class中,會包含指向visual base class subobject的指標(4 bytes),而由於需要區分這個class的不同對象,因而virtual base class X subobject的1 bytes也出現在class Y中(1 bytes),此外由於Alignment的限制,class Y必須填補3bytes(3 bytes),這樣一來,class Y的大小為8.
需要注意的是,由於Empty virtual base class已經成為C++ OO設計的一個特有術語,它提供一個virtual interface,沒有定義任何資料。visual C++ 6.0的編譯器將一個empty virtual base class視為derived class object最開頭的一部分,因而省去了其後的1 bytes,自然也不存在後面Alignment的問題,故實際的執行結果為4.
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(3)。
(3)不管它在class繼承體系中出現了多少次,一個virtual base class subobject只會在derived class中存在一份實體。因此,class A的大小有以下幾點決定:(a)被大家共用的唯一一個class X實體(1 byte);(b)Base class Y的大小,減去“因virtual base class X而配置”的大小,結果是4 bytes.Base class Z的演算法亦同。(8bytes)(c)classs A的alignment數量,前述總和為9 bytes,需要填補3 bytes,結果是12 bytes.
考慮到visual C++ 6.0對empty virtual base class所做的處理,class X實體的那1 byte將被拿掉,於是額外的3 bytes填補額也不必了,故實際的執行結果為8.
不管是自身class的還是繼承於virtual或nonvirtual base class的nonstatic data members,其都是直接存放在每個class object之中的。至於static data members,則被放置在程式的一個global data segment中,不會影響個別的class object的大小,並永遠只存在一份實體。
***Data Member的綁定***
早期C++的兩種防禦性程式設計風格的由來:
(1)把所有的data members放在class聲明起頭處,以確保正確的綁定:
class Point3d
{
// 在class聲明起頭處先放置所有的data member
float x,y,z;
public:
float X() const { return x; }
// ...
};
這個風格是為了防止以下現象的發生:
typedef int length;
class Point3d
{
public:
// length被決議為global
// _val被決議為Point3d::_val
void mumble(length val) { _val = val; }
length mumble() ...{ return _val; }
// ...
private:
// length必須在“本class對它的第一個參考操作”之前被看見
// 這樣的聲明將使先前的參考操作不合法
typedef float length;
length _val;
// ...
};
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(3)。
由於member function的argument list中的名稱會在它們第一次遭遇時被適當地決議完成,因而,對於上述程式片段,length的類型在兩個member function中都被決議為global typedef,當後續再有length的nested typedef聲明出現時,C++ Standard就把稍早的綁定標示為非法。
(2)把所有的inline functions,不管大小都放在class聲明之外:
class Point3d
{
public:
// 把所有的inline都移到class之外
Point3d();
float X() const;
void X(float) const;
// ...
};
inline float Point3d::X() const
{
return x;
}
這個風格的大意就是“一個inline函數實體,在整個class聲明未被完全看見之前,是不會被評估求值的”,即便使用者將inline函數也在class聲明中,對該member function的分析也會到整個class聲明都出現了才開始。
***Data Member的布局***
同一個access section中的nonstatic data member在class object中的排列順序和其被聲明的順序一致,而多個access sections中的data members可以自由排列。(雖然當前沒有任何編譯器會這麼做)
編譯器還可能會合成一些內部使用的data members(例如vptr,編譯器會把它安插在每一個“內含virtual function之class”的object內),以支援整個物件模型。
***Data Member的存取***
(1)Static Data Members
需要注意以下幾點:
(a)每一個static data member只有一個實體,存放在程式的data segment之中,每次程式取用static member,就會被內部轉化為對該唯一的extern實體的直接參考操作。
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(3)。
Point3d origin, *pt = &origin;
// origin.chunkSize = 250;
Point::chunkSize = 250;
// pt->chunkSize = 250;
Point3d::chunkSize = 250;
(b)若取一個static data member的地址,會得到一個指向其資料類型的指標,而不是一個指向其class member的指標,因為static member並不內含在一個class object之中。
&Point3d::chunkSize會獲得類型如下的記憶體位址:const int*
(c)如果有兩個classes,每一個都聲明了一個static member freeList,那麼編譯器會採用name-mangling對每一個static data member編碼,以獲得一個獨一無二的程式識別代碼。
(2)Nonstatic Data Members以兩種方法存取x座標,像這樣:
origin.x = 0.0;
pt->x = 0.0;
“從origin存取”和“從pt存取”有什麼重大的差異嗎?
答案是“當Point3d是一個derived class,而在其繼承結構中有一個virtual base class,並且並存取的member(如本例的x)是一個從該virtual base class繼承而來的member時,就會有重大的差異”。這時候我們不能夠說pt必然指向哪一種 class type(因此我們也就不知道編譯期間這個member真正的offset位置),所以這個存取操作必須延遲到執行期,經由一個額外的簡潔導引,才能夠解決。但如果使用origin,就不會有這些問題,其類型無疑是Point3d class,而即使它繼承自virtual base class,members的offset位置也在編譯時間期就固定了。
***繼承與Data Member***
(1)只要繼承不要多態(Inheritance without Polymorphism)
讓我們從一個具體的class開始:
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(3)。
class Concrete{
public:
// ...
private:
int val;
char c1;
char c2;
char c3;
};
每一個Concrete class object的大小都是8 bytes,細分如下:(a)val佔用4 bytes;(b)c1、c2、c3各佔用1 byte;(c)alignment需要1 byte.
現在假設,經過某些分析之後,我們決定採用一個更邏輯的表達方式,把Concrete分裂為三層結構:
class Concrete {
private:
int val;
char bit1;
};
class Concrete2 : public Concrete1 {
private:
char bit2;
};
class Concrete3 : public Concrete2 {
private:
char bit3;
};
現在Concrete3 object的大小為16 bytes,細分如下:(a)Concrete1內含兩個members:val和bit1,加起來是5 bytes,再填補3 bytes,故一個Concrete1 object實際用掉8 bytes;(b)需要注意的是,Concrete2的bit2實際上是被放在填補空間之後的,於是一個Concrete2 object的大小變成12 bytes;(c)依次類推,一個Concrete3 object的大小為16 bytes.
為什麼不採用那樣的布局(int佔用4 bytes,bit1、bit2、bit3各佔用1 byte,填補1 byte)?
下面舉一個簡單的例子:
Concrete2 *pc2;
Concrete1 *pc1_1, *pc1_2;
pc1_1 = pc2; // 令pc1_1指向Concrete2對象
// derived class subobject被覆蓋掉
// 於是其bit2 member現在有了一個並非預期的數值
*pc1_2 = *pc1_1;
pc1_1實際指向一個Concrete2 object,而複製內容限定在其Concrete subobject,如果將derived class members和Concrete1 subobject捆綁在一起,去除填補空間,上述語意就無法保留了。在pc1_1將其Concrete1 subobject的內容複寫給pc1_2時,同時將其bit2的值也複製給了pc1_1.
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(3)。
(2)加上多態(Adding Polymorphism)
為了以多態的方式處理2d或3d座標點,我們需要在繼承關係中提供virtual function介面。改動過的class聲明如下:
class Point2d {
public:
Point2d(float x = 0.0, float y = 0.0) : _x(x),_y(y) {};
virtual float z() ...{ return 0.0; } // 2d座標點的z為0.0是合理的
virtual void operator+=(const Point2d& rhs) {
_x += rhs.x();
_y += rhs.y();
}
protected:
float _x,_y;
};
virtual function給Point2d帶來的額外負擔:
(a)匯入一個和Point2d有關的virtual table,用來存放它聲明的每一個virtual function的地址;
(b)在每一個class object中匯入一個vptr;(c)加強constructor和destructor,使它們能設定和抹消vptr.
class Point3d : public Point2d {
public:
Point3d(float x = 0.0, float y = 0.0,float z = 0.0) : Point2d(x,y),_z(z) {};
float z() { return _z; }
void z(float newZ) { _z = newZ; }
void operator+=(const Point2d& rhs) { //注意參數是Point2d&,而非Point3d&
Point2d::operator+=(rhs);
_z += rhs.z();
}
protected:
float _z;
};
自此,你就可以把operator+=運用到一個Point3d對象和一個Point2d對象身上了。
(3)多重繼承(Multiple Inheritance)
請看以下的多重繼承關係:
class Point2d {
public:
// ... // 擁有virtual介面
protected:
float _x,_y;
};
class Point3d : public Point2d {
public:
// ...
protected:
float _z;
};
class Vertex {
public:
// ... // 擁有virtual介面
protected:
Vertex *next;
};
class Vertex3d : public Point3d,public Vertex {
public:
// ...
protected:
float mumble;
};
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(3)。
對一個多重繼承對象,將其地址指定給“第一個base class的指標”,情況將和單一繼承時相同,因為二者都指向相同的起始地址,需付出的成本只有地址的指定操作而已。至於第二個或後繼的base class的地址指定操作,則需要將地址修改過,加上(或減去,如果downcast的話)介於中間的base class subobjects的大小。
Vertex3d v3d;
Vertex3d *pv3d;
Vertex *pv;
pv = &v3d;
// 上一行需要內部轉化為
pv = (Vertex*)((char*)&v3d) + sizeof(Point3d));
pv = pv3d;
// 上一行需要內部轉化為
pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof(Point3d)) : 0; // 防止可能的0值
(4)虛擬繼承(Virtual Inheritance)
class如果內含一個或多個virtual base class subobject,將被分隔為兩部分:一個不變局部和一個共用局部。不變局部中的資料,不管後繼如何衍化,總是擁有固定的offset,所以這一部分資料可以被直接存取。至於共用局部,所表現的就是virtual base class subobject.這一部分的資料,其位置會因為每次的派生操作而變化,所以它們只可以被間接存取。
以下均以下述程式片段為例:
void Point3d::operator+=(const Point3d& rhs)
{
_x += rhs._x;
_y += rhs._y;
_z += rhs._z;
}
間接存取主要有以下三種主流策略:
(a)在每一個derived class object中安插一些指標,每個指標指向一個virtual base class.要存取繼承得來的virtual base class members,可以使用相關指標間接完成。
由於虛擬繼承串鏈得加長,導致間接存取層次的增加。然而理想上我們希望有固定的存取時間,不因為虛擬衍化的深度而改變。具體的做法是經由拷貝操作取得所有的nested virtual base class指標,放到derived class object之中。
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(3)。
// 在該策略下,這個程式片段會被轉換為
void Point3d::operator+=(const Point3d& rhs)
{
_vbcPoint2d->_x += rhs._vbcPoint2d->_x;
_vbcPoint2d->_y += rhs._vbcPoint2d->_y;
_z += rhs._z;
}
(b)在(a)的基礎上,為瞭解決每一個對象必須針對每一個virtual base class背負一個額外的指標的問題,Micorsoft編譯器引入所謂的virtual base class table.
每一個class object如果有一個或多個virtual base classes,就會由編譯器安插一個指標,指向virtual base class table.這樣一來,就可以保證class object有固定的負擔,不因為其virtual base classes的數目而有所變化。
(c)在(a)的基礎上,同樣為瞭解決(b)中面臨的問題,Foundation項目採取的做法是在virtual function table中放置virtual base class的offset.
新近的Sun編譯器採取這樣的索引方法,若為正值,就索引到virtual functions,若為負值,則索引到virtual base class offsets.
// 在該策略下,這個程式片段會被轉換為
void Point3d::operator+=(const Point3d& rhs)
{
(this + _vptr_Point3d[-1])->_x += (&rhs + rhs._vptr_Point3d[-1])->_x;
(this + _vptr_Point3d[-1])->_y += (&rhs + rhs._vptr_Point3d[-1])->_y;
_z += rhs._z;
}
小結:一般而言,virtual base class最有效一種運用方式就是:一個抽象的virtual base class,沒有任何data members.
***對象成員的效率***
如果沒有把最佳化開關開啟就很難猜測一個程式的效率表現,因為程式碼潛在性地受到專家所謂的與特定編譯器有關的奇行怪癖。由於members被連續儲存於derived class object中,並且其offset在編譯時間期就已知了,故單一繼承不會影響效率。對於多重繼承,這一點應該也是相同的。虛擬繼承的效率令人失望。
關鍵字: malloc wxWidgets OpenGL 多態性 doxygen 《深度探索C++物件模型》讀書筆記(3)。
***指向Data Members的指標***
如果你去取class中某個data member的地址時,得到的都是data member在class object中的實際位移量加1.為什麼要這麼做呢?主要是為了區分一個“沒有指向任何data member”的指標和一個指向“的第一個data member”的指標。
考慮這樣的例子:
float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::x;
// Point3d::*的意思是:“指向Point3d data member”的指標類型
if( p1 == p2) {
cout<<" p1 & p2 contain the same value ";
cout<<" they must address the same member "<<endl;
}
為了區分p1和p2每一個真正的member offset值都被加上1.因此,無論編譯器或使用者都必須記住,在真正使用該值以指出一個member之前,請先減掉1.
正確區分& Point3d::z和&origin.z:取一個nonstatic data member的地址將會得到它在class中的offset,取一個綁定於真正class object身上的data member的地址將會得到該member在記憶體中的真正地址。
在多重繼承之下,若要將第二個(或後繼)base class的指標和一個與derived class object綁定之member結合起來那麼將會因為需要加入offset值而變得相當複雜。
struct Base1 { int val1; };
struct Base2 { int val2; };
struct Derived : Base1, Base2 { ... };
void func1(int Derived::*dmp, Derived *pd)
{
// 期望第一個參數得到的是一個“指向derived class之member”的指標
// 如果傳來的卻是一個“指向base class之member”的指標,會怎樣呢
pd->*dmp;
}
void func2(Derived *pd)
{
// bmp將成為1
int Base2::*bmp = &Base2::val2;
// bmp == 1
// 但是在Derived中,val2 == 5
func1(bmp,pd);
}
也就是說pd->*dmp將存取到Base1::val1,為解決這個問題,當bmp被作為func1()的第一個參數時,它的值必須因介入的Base1 class的大小而調整:
// 內部轉換,防止bmp == 0
func1(bmp ? bmp + sizeof(Base1) : 0, pd);
系列文章:
《深度探索C++物件模型》讀書筆記(1)
《深度探索C++物件模型》讀書筆記(2)
《深度探索C++物件模型》讀書筆記(4)
《深度探索C++物件模型》讀書筆記(5)
《深度探索C++物件模型》讀書筆記(6)
《深度探索C++物件模型》讀書筆記(7)
《深度探索C++物件模型》讀書筆記 最後一記