《深度探索C++物件模型》讀書筆記(3)

來源:互聯網
上載者:User
《深度探索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++物件模型》讀書筆記 最後一記

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.