C++對象模型學習——Data語意學

     對於下面代碼,sizeof的結果:前端

#include <iostream>

class X{ };

class Y : public virtual X { };

class Z : public virtual X { };

class A : public Y, public Z { };

int main()
{
  X x;
  Y y;
  Z z;
  A a;
  
  std::cout << "對於class X的sizeof結果爲:" << sizeof( x ) << std::endl;
  std::cout << "對於class Y的sizeof結果爲:" << sizeof( y ) << std::endl;
  std::cout << "對於class Z的sizeof結果爲:" << sizeof( z ) << std::endl;
  std::cout << "對於class A的sizeof結果爲:" << sizeof( a ) << std::endl;
}

    結果的大小和機器還有編譯器都有關。ios

    從class X並非空,它有一個隱藏的1byte大小,這是編譯器安插進去的一個char。這使得這程序員

一class的兩個objects得以在內存中配置獨一無二的地址:   算法

// #include <iostream>
#include <stdio.h>

class X{ };

int main()
{
  X x1, x2;

//  std::cout << "x1的地址:" << &x1 << std::endl;
//  std::cout << "x2的地址:" << &x2 << std::endl;
  printf( "x1的地址:%x\n", (int)&x1 );
  printf( "x2的地址:%x\n", (int)&x2 );
}

   

        而對於Y和Z的大小受到三個因素的影響:express

        1)語言自己所形成的額外負擔(overhead):當語言支持virtual base classes時,就會導數組

致一些額外的負擔。在derived class中,這個額外負擔反映在某種形式的指針身上,它或者指ide

向virtual base class subobject,或者指向一個相關表格;表格中存放的若不是virtual base class函數

subject的地址,就是其偏移位(offest)。這裏是4bytes。 工具

         2)編譯器對於特殊狀況所提供的優化處理:Virtual base class X subobject的1bytes大小佈局

也出如今class Y和Z身上。傳統上它被放在derived class的固定部分的尾端。某些編譯器會對

empty virtual base class提供特殊支持。 

         3)Alignment的限制:class Y和Z的大小截至目前爲5bytes。在大部分機器上,聚合的結

構體大小會受到alignment的限制,使它們可以更有效率地在內存中被存取。

          Empty virtual base class提供了一個virtual interface,沒有定義任何數據。某些編譯器對

此提供了優化處理。在這個策略之下,一個empty virtual base class被視爲derived class object

最開頭的一部分,也就是說它並無花費任何的額外空間。這就節省了1bytes(由於既然有了

members,就不須要本來爲了empty class而安插的一個char),也就再也不須要第三點所說的

3bytes的填補。只剩下第一點所說的額外負擔。

           一個virtual base class subobject只會在derived class中存在一份實例,無論它在class繼

承體系中出現了多少次!class A的大小由下列幾點決定:

           1)被你們共享的惟一一個class X實例,大小爲1bytes。

           2)Base class Y的大小,減去「因virtual base class X而配置」的大小,結果是4bytes。

Base class Z的算法亦同。加起來8bytes。

           3)class A本身的大小:0 byte。

           4)class A的alignment數量(若是有的話)。前述三項總和,表示調整前大小是9

bytes。class A必須調整至4 bytes邊界,因此須要填補3bytes,結果是12 bytes。

           而對於優化的編譯器,會拿掉class X的那1byte,則3bytes也不用補齊了,則結果是

8bytes。而若是在virtual base class X中放置一個(以上)的data members,兩種編譯器(「有

特殊處理」者和「沒有特殊處理」者)就會產生出徹底相同的對象佈局。

            每一個class object的nonstatic data members大小可能很大,由於:

           1)由編譯器自動加上的額外data members,用以支持某些語言特性(主要是各類virtual

特性)。

           2)由於alignment(邊界調整)的須要。

1、Data Member的綁定(The Binding of a Data Member)

         考慮下面的代碼:

// 某個foo.h頭文件,從某處含入
extern float x;

// 程序員的Point3d.h文件
class Point3d
{
  public:
    Point3d( float, float, float );
    // 問題:被傳回和被設定的x是哪個x呢?
    float X() const { return x; }
    void X( float new_x ) const { x = new_x; }
    // ...
  
  private:
    float x, y, z;
}

         是class內部那個仍是外部那個x?如今的答案是內部那個。

          而在最先的編譯器上,該操做會指向global x object!並所以導出早期C++的兩種防護性

程序設計風格:

         1)把全部的data members放在class聲明起頭處,以確保正確的綁定:

class Point3d
{
  // 防護性程序設計風格 #1
  // 在class聲明起頭處先放置全部data member
  float x, y, z;
  
  public:
    float X() const { return x; }
    // ... etc. ...
};

           2)把全部的inline functions,無論大小都放在class聲明以外:

class Point3d
{
  public:
    // 防護性程序設計風格 #2
    // 把全部的inlines都移到class以外
    Point3d();
    float X() const;
    void X( float ) const;
    // ... etc. ...
};

inline float Point3d::X() const
{
  return x;
}

// ... etc. ...

          這些古老的語言規則被稱爲「member rewriting rule」,大意是「一個inline函數實體,在整個

class聲明未被徹底看見以前,是不會被評估求值(evaluated)的」。C++ Standard以「member

scope resolution rules」來精煉這個「rewriting rule」,其效果是,若是一個inline函數在class聲明

以後馬上被定義的話,那麼就仍是對其評估求值(evaluate)。也就是說,當一我的寫下如下

這樣的代碼:

extern int x;

class Point3d
{
  public:
    ...
    // 對於函數本體的分析將延遲,直至class聲明的右大括號出現纔開始
    float X() const { return x; }
    // ...
  
  private:
    float x;
    ...
};

// 事實上,分析在這裏進行

         然而這對於member function的argument list並不爲真。

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;
    // ...
};

         上面這種語言狀況,仍然須要某種防護性程序風格:總把」nested type聲明「放在class 的起

始處。

2、Data Member的佈局(Data Member Layout)

       已知下面一組data members:

class Point3d
{
  public:
    // ...
  
  private:
    float x;
    static List<Point3d*> *freeList;
    float y;
    static const int chunkSize = 250;
    float z;
};

          Nonstatic data members在class object中的排列順序將和其被聲明的順序同樣static data

members存放在程序的data segment中,和個別的class objects無關。

          C++ Standard要求在同一個access section(也就是private、public、protected等區段)

中,members的排列只需符合「較晚出現的members在class object中有較高的地址」這一條件即

可。也就是說各個members並不必定得連續排列。members的邊界調整(alignment)可能就需

要填補一些bytes。

           編譯器還可能會合成一些內部使用的data members,以支持整個對象模型。vptr就是這

樣的東西,目前全部的編譯器都把它安插在每個「內含virtual function之class」的object內。vptr

傳統上它被放在全部顯示聲明的members的最後。不過現在也有一些編譯器把vptr放在一個

class object的最前端。C++ Standard則秉承「對於佈局所持的聽任態度」。

            C++ Standard也容許編譯器將多個access sections之中的data members自由排列,沒必要

在意它們出如今class聲明中的順序。也就是說:

#include <iostream>
#include <list>

class Point3d
{
  public:
    // ...
  
  public:
    float x;
    static std::list<Point3d*> *freeList;
 
  public:
    float y;
    static const int chunkSize = 250;

  public:
    float z; 
};

int main()
{
  Point3d point;

  std::cout << "point.x的地址:" << &point.x << std::endl;
  std::cout << "point.y的地址:" << &point.y << std::endl;
  std::cout << "point.z的地址:" << &point.z << std::endl;

}

            members的排列順序視編譯器而定。

            目前各家編譯器都是把一個以上的access section連鎖在一塊兒,依照聲明的順序,成爲一

個連續區塊。Access sections的多寡不會招來額外負擔。

3、Data Member的存取

       已知下面這段程序代碼:

Point3d origin;
origin.x = 0.0;

        x的存取成本視x和Point3d如何聲明而定。x多是個static member,也多是個nonstatic

member。Point3d多是個獨立(非派生)的class,也多是從另外一個單一的base class派生

而來,甚至有多是從多重繼承或虛擬繼承而來的。 

        若是有兩個定義,origin和pt:

Point3d origin, *pt = &origin;

       用它們來存取data members,以下:

origin.x = 0.0;
pt->x = 0.0;

       會在後面講解經過origin存取和經過pt存取的重大差別。       

         一、Static Data members

         Static data members按字面意義,被編譯器提出於class以外,並被視爲一個global變量

(但只在class生命範圍內可見)。每個member的存取許可(private、protected或public),

以及與class的關聯,並不會招致任何空間上或執行時間上的額外負擔——不管是在個別的class

object仍是在static data member自己。        

          每一個static data member只有一個實例,存放在程序的data segment之中。每次程序取用

static member時,就會被內部轉化爲對該惟一extern實例的直接參考操做。例如:

// origin.chunkSize = 250
Point3d::chunkSize = 250;

// pt->chunkSize = 250;
Point3d::chunkSize = 250;

         從指令執行的觀點來看,這是C++語言中「經過一個指針和經過一個對象來存取member,

結論徹底相同」的惟一一種狀況。這是由於「經由member selection operators(也就是「.」運算

符)對一個static data member進行存取操做」只是文法上的一種便宜行事而已。member其實並

不在class object之中,所以存取static members並不須要經過class object。

          即使chunkSize是一個從複雜繼承關係中繼承而來的member,或許它是一個「virtual base

class的virtual base class」(或其它同等複雜的繼承結構)的member也說不定。程序中對於

static members仍是隻有惟一一個實例,而其存取路徑仍然是那麼直接。 

         若是static data member是經由函數調用,或其餘某些語法而被存取呢?以下:

foobar().chunkSize = 250;

          cfront的作法是簡單的把foobar()函數扔掉,但C++ Standard明確要求foobar()必須被求值

(evaluated),雖然其結果並沒有用處。下面是一種可能的優化:

// foobar().chunkSize = 250;

// evaluate expression, discarding result
( void ) foobar();
Point3d.chunkSize = 250;

         若取一個static data member的地址,會獲得一個指向其數據類型的指針,而不是一個指向

其class member的指針,由於static member並不內含一個class object之中。例如:

&Point3d::chunkSize;

          會得到類型以下的內存地址:

const int*
#include <iostream>
#include <list>

class Point3d
{
  public:
    // ...
  
  public:
    float x;
    static std::list<Point3d*> *freeList;
 
  public:
    float y;
    static int chunkSize;

  public:
    float z; 
};

int Point3d::chunkSize = 0;

int main()
{
  Point3d point;
  point.chunkSize = 250;

  std::cout << "Point3d::chunkSize的地址:" << &Point3d::chunkSize << std::endl;
}

           而對於直接在類中定義並初始化的static const member ,編譯器不會給其分配地址,直

接用常量代替。以下:

#include <iostream>
#include <list>

class Point3d
{
  public:
    // ...
  
  public:
    float x;
    static std::list<Point3d*> *freeList;
 
  public:
    float y;
    static const int chunkSize = 250;

  public:
    float z; 
};

//int Point3d::chunkSize = 0;

int main()
{
  Point3d point;
 // point.chunkSize = 250;

  std::cout << "Point3d::chunkSize的地址:" << Point3d::chunkSize << std::endl;
}

     若是有兩個classes,每個都聲明一個static member freeList,那麼當它們都被放在程序的

data segment時,會致使名字衝突,編譯器的解決辦法是進行名字編碼(即name-

mangling),以下代碼:

#include <iostream>

class X
{
  public:
    static int free;
};

class Y
{
  public:
    static int free;
};

int X::free = 1;
int Y::free = 2;

int main()
{
  std::cout << X::free << std::endl;
  std::cout << Y::free << std::endl;
}

        對於兩個class中的static member free,轉換而成的彙編代碼結果以下:

.globl	_ZN1X4freeE
	.data
	.align 4
	.type	_ZN1X4freeE, @object
	.size	_ZN1X4freeE, 4
_ZN1X4freeE:
	.long	1
	.globl	_ZN1Y4freeE
	.align 4
	.type	_ZN1Y4freeE, @object
	.size	_ZN1Y4freeE, 4
_ZN1Y4freeE:
	.long	2

          任何name-mangling作法都有兩個重點:

          1)一個算法,推導出獨一無二的名稱。

          2)萬一編譯系統(或環境工具)必須和使用者交談,那些獨一無二的名稱能夠輕易被推

導回到原來的名稱。

         二、Nonstatic Data members

         Nonstatic data members直接存放在每個class object之中。除非經由顯示的(explict)

或隱式的(implict)class object,不然沒有辦法直接存取它們。只要程序員在一個member

function中直接處理一個nonstatic data member,所謂「implicit class object」就會發生。例以下

面這段代碼:

Point3d Point3d::translate( const Point3d &pt )
{
  x += pt.x;
  y += pt.y;
  z += pt.z;
}

        表面上所看到的對於x、y、z的直接存取,事實上是經由一個「implicit class object「(由this

指針表達)完成的。事實上這個函數的參數是:

// member function的內部轉化
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的偏移位置(offset)。例如:

origin._y = 0.0;

         那麼地址&origin._y將等於:

&origin + ( &Point3d::_y - 1 )

         指向data member的指針,其offset值老是被加上1,這樣可使編譯器系統區分出」一個指

向data member的指針,用以指出class的第一個member「和」一個指向data member的指針,沒

有指出任何member「兩種狀況。

         每個nonstatic data member的偏移位置(offset)在編譯時期便可獲知,甚至若是

member屬於一個base class subobject(派生自單一或多重繼承串鏈)也是同樣的。所以,存

取一個nonstatic data member,其效率和存取一個C struct member或一個nonderived的

member是同樣的。

         虛擬繼承將爲」經由base class subobject存取class members「導入一層新的間接性,比

如:

Point3d *pt3d;
pt3d->_x = 0.0;

         其執行效率在_x是一個struct member、一個class member=單一繼承、多重繼承的狀況下

都徹底相同。但若是_x是一個virtual base class的member,存取速度會稍微慢一點。因而回到

先前的一個問題:以兩種方法存取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位置

也在編譯時期就固定了。

4、」繼承「與Data Member

        在C++繼承模型中,一個derived class object所表現出來的東西,是其本身的members加

上其base class(es)members的總和,在大部分編譯器上,base class members老是先出

現,但屬於virtual base class的除外。

        一、只要繼承不要多態(Inheritance without Polymorphism)

        或許程序員但願,不論2D或3D座標點,都可以共享同一個實例,但又可以繼承使用」與類

型性質相關(所謂type-specific)「的實例。咱們有一個設計策略,就是從Point2d派生一個

Point3d,因而Point3d將繼承x和y座標的一切(包括數據實例和操做方法)。帶來的影響則是

能夠共享」數據自己「以及」數據的處理方法「,並將之局部化。通常而言,具體繼承(concrete

inheritance,相對於虛擬繼承virtual inheritance)並不會增長空間或存取時間上的額外負擔。

class Point2d
{
  public:
    Point2d( float x = 0.0, float y = 0.0 )
             : _x( x ), _y( y ){ }

    float x() { return _x; }
    float y() { return _y; }

    void x( float newX ) { _x = newX; }
    void y( float newY ) { _y = newY; }

    void operator+=( Point2d& rhs )
    {
      _x += rhs.x();
      _y += rhs.y();
    }
    // ... more members

  protected:
    float _x, _y;
};

// inheritance from concrete class
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+=( Point3d& rhs )
    {
      Point2d::operator+=( rhs );
      _z += rhs.z();
    }
    // ... more members

  protected:
    float _z;
};

        把兩個本來獨立不相干的classes湊成一對」type/subtype「,並帶有繼承關係,會犯一些錯

誤。可能會重複設計一些相同操做的函數。上面例子中的constructor和operator+=爲例,並無

被作成inline函數。通常而言選擇某些函數作成inline函數,是設計class時的一個重要課題。      

         第二個易犯的錯誤是,把一個class分解爲兩層或多層,有可能會爲了」表現class體系之抽

象化「而膨脹空間。C++語言保證」吹按在derived class中的base class subobject有其完整原樣性

「,正式重點所在。例子以下:

#include <iostream>

class Concrete
{
  public:
    // ...
  
  public:
    int val;
    char c1;
    char c2;
    char c3;
};

class Concrete1
{
  public:
    // ...
  
  public:
    int val;
    char bit1;
};

class Concrete2 : public Concrete1
{
  public:
    // ...
  
  public:
    char bit2;
};

class Concrete3 : public Concrete2
{
  public:
    // ...
  
  public:
    char bit3;
};

int main()
{
  Concrete object;
  Concrete1 object1;
  Concrete2 object2;
  Concrete3 object3;

  std::cout << "Concrete類的大小:" << sizeof( object ) << std::endl;

  std::cout << "Concrete1類的大小:" << sizeof( object1 ) << std::endl;

  std::cout << "Concrete2類的大小:" << sizeof( object2 ) << std::endl;

  std::cout << "Concrete3類的大小:" << sizeof( object3 ) << std::endl;

}

         能夠看到Concrete object的確是8bytes,而Concrete1內含兩個members:val和bit1,加

起來5bytes。而一個Concrete1 object實際用掉8bytes,包括補用的3bytes。通常而言,邊界調

整(alignment)是由處理器(processor)來決定的。

          雖然Concrete2只增長了一個bit2,但它倒是放在填補空間所用的3bytes以後。因而大小變

爲12bytes。這裏Concrete3應該被編譯器作了優化,不是原來的16bytes,而是12bytes。

        二、加上多態(Adding Polymorphism)

        若是要處理一個座標點,而不打算在意它是一個Point2d或Point3d實例,那麼就須要在繼

承關係中提供一個virtual function接口。

class Point2d
{
  public:
    Point2d( float x = 0.0, float y = 0.0 )
             : _x( x ), _y( y ){ }

    float x() { return _x; }
    float y() { return _y; }

    void x( float newX ) { _x = newX; }
    void y( float newY ) { _y = newY; }
  
    // 加上z的保留空間
    virtual float z() { return 0.0; }
    virtual void z( float ) { }

    virtual void operator+=( Point2d& rhs )
    {
      _x += rhs.x();
      _y += rhs.y();
    }
    // ... more members

  protected:
    float _x, _y;
};

        只有當咱們企圖以多態的方式(polymorphically)處理2d或3d座標時,在設計之中導入一

個virtual才顯得合理。也就是說下面代碼:

void foo( Point2d &p1, Point2d &p2 )
{
  // ...
  p1 += p2;
  // ...
}

        其中,p1和p2多是2d,也多是3d座標點。這樣的彈性,正是面向對象程序設計的中

心。代價是空間和存取時間上的額外負擔:

        1)導入一個和Point2d有關的virtual table,用來存放它所聲明的每個virtual functions的

地址。這個table的元素個數通常而言是被聲明的virtualfunctions的個數,再加上一個或者兩個

slots(以支持runtime type identification)。

         2)在每個class object中導入一個vptr,提供執行期的連接,使每個object可以找到相

應的virtual table。

         3)增強constuctor,使它可以爲vptr設定初值,讓它指向class所對應的virtual table。這可

能意味着在derived class和每個base class的constructor中,從新設定vptr的值。其狀況視編

譯器優化的積極性而定。

          4)增強destructor,使它可以抹消」指向class之相關virtual table「的vptr。

          這些額外負擔帶來的衝擊程度視」被處理的Point2d object的個數和生命期「而定,也視」對

這些objects作多臺程序設計所得的利益「而定。若是一個應用程序知道它所能使用的point

objects只限於二維座標點或三維座標點,則這種設計所帶來的額外負擔可能變得使人沒法接

受。

           如下是新的Point3d聲明:

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+=( Point2d& rhs )
    {
      Point2d::operator+=( rhs );
      _z += rhs.z();
    }
    // ... more members

  protected:
    float _z;
};

         最大的好處是能夠把operator+=運用在一個Point3d對象和一個Point2d對象身上:

Point2d p2d( 2.1, 2.2 );
Point3d p3d( 3.1, 3.2, 3.3 );
p3d += p2d;

// 獲得的p3d新值將是(5.2, 5.4, 3.3)

          把vptr放在class object的尾端,能夠保留base class C struct的對象佈局,於是容許在C程

序代碼中也可以使用。這種作法在C++最初問世時,被許多人採用。

           後來到了C++2.0開始支持虛擬繼承以及抽象基類,而且因爲面向對象範式(OO

paradigm)的興起,某些編譯器開始把vptr放到class object的起頭處。

           把vptr放在class object的前端,對於」在多重繼承之下,經過指向class members的指針調

用virtual function「,會帶來一些幫助。代價就是喪失了對C語言的兼容性。

        三、多重繼承(Multiple Inheritance)

         單一繼承提供了一種「天然多態(natural polymorphism)」形式,是關於classes體系中的

base type和derived type之間的轉換。其中base class和derived class的objects都是從相同地址

開始,其間差別只在於derived object比較大,用以多容納它本身的nonstatic data members。

下面的指定操做:

Point3d p3d;
Point2d *p = &p3d;

        把一個derived class object指定給base class(無論繼承深度有多深)的指針或reference。

這個操做並不須要編譯器去調停或修改地址。它很天然地能夠發生,並且提供了最佳執行效

率。

         把vptr放在class object的起始處。若是base class沒有virtual function而derived class有,

那麼單一繼承的天然多態就會被打破。這種狀況下,把一個derived object轉換爲其base類型。

就須要編譯器的介入,用以調整地址(因vptr插入之故)。在既是多重繼承又是虛擬繼承的狀況

下,編譯器的介入更有必要。

class Point2d
{
  public:
    // virtual接口,因此Point2d對象之中會有vptr
  
  protected:
    float _x, _y;
};

class Point3d : public Point2d
{
  public:
    // ...
  
  private:
    float _z;
};

class Vertex
{
  public:
    // virtual接口,因此Vertex對象之中會有vptr
  
  protected:
    Vertex *next;
};

class Vertex3d : public Point3d, public Vertex
{
  public:
    // ...
  
  protected:
    float mumble;
};

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

轉換。不管是直接轉換以下:

extern void mumble( const Vertex& );
Vertex3d v;
...
// 將一個Vertex3d轉換爲一個Vertex。這是「不天然的」
mumble( v );

         對一個多重派生對象,將其地址指定給「最左端(也就是第一個)base class的指針」,狀況

將和單一繼承時相同,由於兩者都指向相同的起始地址。須要付出的成本只是地址的指定操做

而已。至於第二個或者後繼的base class的地址指定操做,則須要將地址修改過:加上(或減

去,若是downcast的話)介於中間的base class subject(s)大小,例如:

Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;

          那麼下面這個指定操做:

pv = &v3d;

           須要這樣的內部轉化:

// 虛擬C++代碼
pv = ( Vertex* )( ( ( char* )&v3d ) + sizeof( Point3d ) );

           而下面的指定操做:

p2d = &v3d;
p3d = &v3d;

           都只須要簡單地拷貝其地址就好。以下:

Vertex3d *pv3d;
Vertex *pv;

           那麼下面的指定操做:

pv = pv3d;

           不可以只是簡單地被轉換爲:

// 虛擬C++代碼
pv = ( Vertex* )( ( ( char* )pv3d ) + sizeof( Point3d ) );

           由於若是pv3d爲0,pv將得到sizeof(Point3d)的值。這是錯誤的!因此,對於指針,內部

轉換操做須要有一個條件測試:

// 虛擬C++代碼
pv = pv3d ? ( Vertex* )( ( char* )pv3d ) + sizeof( Point3d ) : 0;

// pv3d爲0,說明指針爲空

           至於reference,則不須要針對可能的0值作防衛,由於reference不可能參考到「無物「。

           C++ Standard並未要求Vertex3d中的base classes Point3d和Vertex有特定的排列順序。

原始的cfront編譯器是根據聲明順序來排列它們的。目前各編譯器仍然以此方式完成多重base

classes的佈局(但若是加上虛擬繼承,就不同了)。

             若是要存取第二個(或後繼)base class中的一個data member,並不會付出額外的成

本,由於members的位置在編譯時就固定了,所以存取members只是一個簡單的offset運算。

        四、虛擬繼承(Virtual Inheritance)

        多重繼承的一個語意上的反作用就是,它必須支持某種形式上的」shared subobject繼承「。

典型的一個例子是最先的iostream library:

// pre-standard iostream implementation
class ios { ... };
class istream : public ios { ... };
class ostream : public ios { ... };
class iostream : public istream, public ostream { ... };

         istream和ostream都內含一個ios suboject。然而在iostream的對象佈局中,咱們只須要單

一一份ios suboject就好。語言層面的解決辦法是導入所謂的虛擬繼承:

class ios { ... };
class istream : public virtual ios { ... };
class ostream : public virtual ios { ... };
class iostream : public istream, public ostream { ... };

        實現虛擬繼承的難度在於要找到一個足夠有效的方法,將istream和ostream各自維護的一

個ios suboject,摺疊成爲一個由iostream維護的單一ios suboject,而且還能夠保存base class

和derived class的指針(以及references)之間的多態指定操做(polymorphism

assigbments)。

        通常實現方法以下。Class若是內含一個或多個virtual base class subobjects,像istream那

樣,將被分割爲兩部分:一個不變區域和一個共享區域。不變區域中的數據,無論後繼如何衍

化,老是擁有固定的offset(從object的開頭算起),因此這一部分數據能夠被直接存取。至於

共享區域,所表現的就是virtual base class subobject。這一部分數據,其位置會由於每次的派

生操做而有變化,因此它們只能夠被間接存取。編譯器實現技術的差別就在於間接存取的方法

不一樣。下面是3中主流策略。下面是Vertex3d虛擬繼承的層次結構:

class Point2d
{
  public:
    // ...
  
  protected:
    float _x, _y;
};

class Point3d : public Point2d
{
  public:
    // ...
  
  private:
    float _z;
};

class Vertex
{
  public:
    // ...
  
  protected:
    Vertex *next;
};

class Vertex3d : public Vertex, public Point3d
{
  public:
    // ...
  
  protected:
    float mumble;
};

        通常的佈局策略是先安排好derived class的不變部分,而後再創建其共享部分。

        如何可以存取class的共享部分呢?cfront編譯會在每個derived class object中安插一些指

針,每一個指針指向一個virtual base class。要存取繼承得來的virtual base class members,能夠

經過相關指針間接完成。舉個例子:

void Point3d::operator+=( const Point3d &rhs )
{
  _x += rhs._x;
  _y += ths._y;
  _z += ths._z;
};

          在cfront策略之下,這個運算符會被內部轉換爲:

// 虛擬C++代碼
_vbcPoint2d->_x += rhs._vbcPoint2d->_x; // vbc意爲:virtual base class
_vbcPoint2d->_y += rhs._vbcPoint2d->_y;
_z += ths._z;

           而一個derived class和一個base class的實例之間的轉換,像這樣:

Point2d *p2d = pv3d;

           在cfront實現模型之下,會變爲:

// 虛擬C++代碼
Point2d *p2d = pv3d ? pv3d->_vbcPoint2d : 0;

            這樣的實現模型有兩個主要的缺點:

          1)每個對象必須針對每個virtual base class揹負一個額外的指針。然而理想上咱們卻

但願class object有固定的負擔,不由於其virtual base classes的個數而有所變化。

          2)因爲虛擬繼承串鏈的加長,致使簡介存取層次的增長。意思是,若是有三層虛擬派

生,就須要三次間接存取(經由三個virtual base class指針)。然而理想上咱們卻但願有固定的

存取時間,不由於虛擬派生的深度而改變。

          一些編譯器使用cfront的原始實現模型來解決第二個問題,它們經由拷貝操做取得全部的

nested virtual base class指針,放到derived class object之中。這就解決了「固定存取時間」的問

題,雖然付出了一些空間上的代價。

          至於第一個問題,通常而言有兩個解決辦法。Microsoft編譯器引入所謂的virtual base

class table。每個class object若是有一個或多個virtual base classes,就會由編譯器安插一個

指針,指向virtual base class table。

           第二個解決方法,也是Bjarne比較喜歡的方法,是在virtual function table中放置virtual

base class的offset(而不是地址)。這種實現方法將virtual base class offset和virtual function

entries混雜在一塊兒。在Sun編譯器中,virtual function table 可經由正負值來索引。若是是正

值,很明顯就是索引到virtual functions;若是是賦值,則是索引到virtual base class offsets。在

這樣的策略下,Point3d的的operator+=運算符必須被轉換爲如下形式:

// 虛擬C++代碼
( this + _vptr_Point3d[ -1 ] )->_x += 
      ( &rhs + rhs._vptr_Point3d[ -1 ] )->_x;
( this + _vptr_Point3d[ -1 ] )->_y += 
      ( &rhs + rhs._vptr_Point3d[ -1 ] )->_y;
_z += ths._z;

          雖然在此策略之下,對於繼承而來的members作存取操做,成本會比較昂貴,不過成本已

經被分散至「對member的使用」上,屬於局部性成本。Derived class實例和base class實例之間

的轉換操做,例如:

Point2d *p2d = pv3d;

          在上述實現模型下將變成:

// 虛擬C++代碼
Point2d *p2d = pv3d ? pv3d->_vptr_Point3d[ -1 ] : 0;

         經由一個非多態的class object來存取一個繼承而來的virtual base class的member,像這

樣:

Point3d origin;
...
origin._x;

         能夠被優化爲一個直接存取操做。

         通常而言,virtual base class最有效的一種運用形式就是:一個抽象的virtual base class,

沒有任何data members。

5、對象成員的效率(Object Member Efficiency)

        下面幾個測試,意旨在測試聚合(aggregation)、封裝(encapsulation)以及

繼承(inheritance)所引起的額外負荷的程度。全部測試都是以個別局部變量的加

法、減法、賦值(assign)等操做的存取成本爲依據。下面就是個別的局部變量:

float pA_x = 1.725, pA_y = 0.875, pA_z = 0.478;
float pB_x = 0.315, pB_y = 0.317, pB_z = 0.838;

           每次表達式需執行1000萬次,以下所示:

for( int iters = 0; iters < 10000000; iters++ )
{
  pB_x = pA_x - pB_z;
  pB_y = pA_y + pB_x;
  pB_z = pA_z + pB_y;
}

          咱們首先針對三個float元素所組成的局部數組進行測試:

enum fussy { x, y, z };

for( int iters = 0; iters < 10000000; iters++ )
{
  pB[ x ] = pA[ x ] - pB[ z ];
  pB[ y ] = pA[ y ] + pB[ x ];
  pB[ z ] = pA[ z ] + pB[ y ];
}

       未優化:        

        -O1優化:

          第二個測試是把同質的數組元素轉換爲一個C struct數據抽象類型,其中的成員皆爲float,

成員名稱是x、y、z:

for( int iters = 0; iters < 10000000; iters++ )
{
  pB.x = pA.x - pB.z;
  pB.y = pA.y + pB.x;
  pB.z = pA.z + pB.y;
}

         未優化:

 

         -O1優化:

         更深一層的抽象化,是作出數據封裝,並使用inline函數。座標點如今以一個獨立的

Point3d class來表示。兩種不一樣的存取函數。第一,我定義一個inline函數,傳回一個

reference,容許它出如今assignment運算符的兩端:

class Point3d
{
  public:
    Point3d( float xx = 0.0, float yy = 0.0, float zz - 0.0 )
             : _x( xx ), _y( yy ), _z( zz ) { }
    
    float& x() { return _x; }
    float& y() { return _y; }
    float& z() { return _z; }

  private:
    float _x, _y, _z;
};

          那麼真正對每個座標元素的存取操做應該像這樣:

for( int iters = 0; iters < 10000000; iters++ )
{
  pB.x() = pA.x() - pB.z();
  pB.y() = pA.y() + pB.x();
  pB.z() = pA.z() + pb.y();
}

      未優化:

       

      -O1優化:

 

         第二種存取函數形式是,提供一對get/set函數:

float x() { return _x; }             // get函數
void x( float newX ) { _x = newX; }  // set函數

         因而對於每個座標值的存取操做應該像這樣:

pB.x( pA.x() - pB.z() );

    未優化:

     

      -O1優化:

 

     能夠看到封裝帶來的成本在未優化時仍是挺大的,但在-O1優化後,「封裝」就不會帶來執行期

的效率成本。

     下一個測試中,要介紹Point抽象化的的一個三層單一繼承表達法,而後再介紹Point抽象化

的一個虛擬繼承表達法。這裏要測試直接存取和inline存取。

      單一繼承(直接存取):

    

      單一繼承(inline):

       虛擬繼承(雙層,直接存取):

  

       虛擬繼承(雙層,inline):

6、指向Data Member的指針(Pointer to Data Members)

        指向data members的指針,在須要詳細調查class members的底層佈局時特別

有用。這樣的調查可用以決定vptr是放在class的起始處或是尾端。另外一個用途,可用

來決定class中的access sections的順序。

        考慮下面的Point3d聲明。其中有一個virtual function,一個static data

member,以及三個座標值:

class Point3d
{
  public:
    virtual ~Point3d(){ }
    // ...
  
  protected:
    static Point3d origin;
    float x, y, z;
};

    

#include <iostream>
#include <stdio.h>

class Point3d
{
  public:
    virtual ~Point3d(){ }
    // ...
  
  public:
    static Point3d origin;
    float x, y, z;
};

Point3d Point3d::origin;

int main()
{
  Point3d point;
  Point3d point1;
  
  std::cout << "&Point3d::x = " << &Point3d::x << std::endl;
  std::cout << "&Point3d::y = " << &Point3d::y << std::endl;
  std::cout << "&Point3d::z = " << &Point3d::z << std::endl;

  std::cout << "&point = " << &point << std::endl;
  std::cout << "&point.x = " << &point.x << std::endl;
  std::cout << "&point.y = " << &point.y << std::endl;
  std::cout << "&point.z = " << &point.z << std::endl;

  std::cout << "&point.origin = " << &point.origin << std::endl;
  std::cout << "&point1.origin = " << &point1.origin << std::endl;
  std::cout << "&point.origin.x = " << &point.origin.x << std::endl;

  std::cout << "class object的大小: " << sizeof( point ) << std::endl;
}

         能夠從Point3d object的members位置結果看出,vptr是放在對象頭的。靜態對象在程序中

只有1個,而一個指向data member的指針爲1。是爲了區分「沒有指向任何data member」的指針

和一個「指向data member」的指針,以下:

float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::x;

// 如何區分
if( p1 == p2 )
{
  // ...
}

         多重繼承之下,若要將第二個(或後繼)base class的指針,和一個「與derived class

object綁定」的member結合起來,那麼將會由於「須要加入offset值」而變得至關複雜。

#include <iostream>

struct Base1 { int val1; };

struct Base2 { int val2; };

struct Derived : Base1, Base2 { int val3; };

int main()
{
  Derived derive;
  
  std::cout << "&Base1::val1 = " << &Base1::val1 << std::endl;
  std::cout << "&Base2::val2 = " << &Base2::val2 << std::endl;
  std::cout << "&Derived::val1 = " << &Derived::val1 << std::endl;
  std::cout << "&Derived::val2 = " << &Derived::val2 << std::endl;
  std::cout << "&Derived::val3 = " << &Derived::val3 << std::endl;

  std::cout << "&derive = " << &derive << std::endl;
  std::cout << "&derive.val1 = " << &derive.val1 << std::endl;
  std::cout << "&derive.val2 = " << &derive.val2 << std::endl;
  std::cout << "&derive.val3 = " << &derive.val3 << std::endl;

  std::cout << "sizeof( Base1 ) = " << sizeof( Base1 ) << std::endl;
  std::cout << "sizeof( Base2 ) = " << sizeof( Base2 ) << std::endl;
  std::cout << "sizeof( Derived ) = " << sizeof( Derived ) << std::endl;
}

        「指向Members的指針」的效率問題

       

#include <iostream>

class Point3d
{
  public:
    Point3d( float xx = 0.0, float yy = 0.0, float zz = 0.0 )
             : x( xx ), y( yy ), z( zz ) { }

  public:
    float x, y, z;
};

int main()
{

  Point3d pA( 1.725, 0.875, 0.478 );
  Point3d pB( 0.315, 0.317, 0.838 );
  
  float *ax = &pA.x;
  float *ay = &pA.y;
  float *az = &pA.z;

  float *bx = &pB.x;
  float *by = &pB.y;
  float *bz = &pB.z;

  for( int iters = 0; iters < 10000000; iters++ )
  {
    *bx = *ax - *bz;
    *by = *ay + *bx;
    *bz = *az + *by;
  }

  return 0;
}

       

#include <iostream>

class Point3d
{
  public:
    Point3d( float xx = 0.0, float yy = 0.0, float zz = 0.0 )
             : x( xx ), y( yy ), z( zz ) { }

  public:
    float x, y, z;
};

int main()
{

  Point3d pA( 1.725, 0.875, 0.478 );
  Point3d pB( 0.315, 0.317, 0.838 );
  
  float Point3d::*ax = &Point3d::x;
  float Point3d::*ay = &Point3d::y;
  float Point3d::*az = &Point3d::z;

  float Point3d::*bx = &Point3d::x;
  float Point3d::*by = &Point3d::y;
  float Point3d::*bz = &Point3d::z;

  for( int iters = 0; iters < 10000000; iters++ )
  {
    pB.*bx = pA.*ax - pB.*bz;
    pB.*by = pA.*ay + pB.*bx;
    pB.*bz = pA.*az + pB.*by;
  }

  return 0;
}

相關文章
相關標籤/搜索