在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.
(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;
// ...
};
由於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實體的直接參考操作。
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開始:
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.
(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;
};
對一個多重繼承對象,將其地址指定給“第一個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之中。
// 在該策略下,這 個程序片段會被轉換為
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在編譯時期就已知了,故單一繼承不 會影響效率。對於多重繼承,這一點應該也是相同的。虛擬繼承的效率令人失望。
***指向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);