class object layout //64位系統 class A{ }; //sizeof(A)為1 class B : virtual public A{ }; //sizeof(B)為8 class C : virtual public A{ }; //sizeof(C)為8 class ...
class object layout
//64位系統
class A{ }; //sizeof(A)為1
class B : virtual public A{ }; //sizeof(B)為8
class C : virtual public A{ }; //sizeof(C)為8
class D : public B, public C{ }; //sizeof(D)為16
//sizeof(A)為1是因為編譯器會安插一個char,使得多個object會有不同的地址
-
記憶體佈局:
-
造成B和C大小為8的原因如下:
- 語言本身造成的額外負擔。若derived class派生自virtual base class,則derived class中含有一個vbptr指針,此指針指向virtual base class subobject或一個相關表格vbtable,而vbtable存放virtual base class subobject地址或編譯位置(offset)
- 註:derived class中包含本身和base class組成了對象,而屬於某個基類的對象就是base class subobject
- 編譯器對特殊情況的優化處理。virtual base class A subobject的1 bytes一般放於derived class的固定部分的末端,某些編譯器會對empty virtual base class提供特殊支持
- empty virtual base class不定義任何數據,提供一個virtual interface。某些編譯器處理下,一個empty virtual base class被視為derived class object最開始的那一部分,並沒有使用任何的額外空間。因為含有member,所以也沒有必要安插char
- Alignment padding的限制。聚合的結構體大小收alignment限制,使其在記憶體更有效率地被存取
- 語言本身造成的額外負擔。若derived class派生自virtual base class,則derived class中含有一個vbptr指針,此指針指向virtual base class subobject或一個相關表格vbtable,而vbtable存放virtual base class subobject地址或編譯位置(offset)
-
nonstatic data members和virtual nonstatic data members都存與class object中,且沒有強制定義其排列順序;static data members存於global data segment,不影響class object大小
-
nonstatic data members在class object中同一個access level的記憶體排列順序應和被聲明的順序相同,不受static data members影響
-
class object的同一個access section中members不一定非得連續排列,member的alignment和內部使用的data members可能會介於聲明的members間;且多個access section中data members可以自由排序,不用考慮聲明順序
-
access sections的多少並不影響記憶體大小
class A { public: ... private: float x; static int y; private: float z; static int i; private: float j; }
the binding of a data member
- 現有以下代碼:
extern float x;
class A
{
public:
A(float, float, float);
float X() const { return x; };
private:
float x, y, z;
}
-
放在現在,X()的返回值肯定是class內部那個,但在以前的編譯器,此操作會返回extern那個。因此,這也就產生了兩種防禦性程式風格:
-
將所有data member放於class聲明最開始處
class A { private: float x, y, z; public: //這樣將保證class內部 float X() const { return x; }; }
-
將所有inline member functions,放於class外。inline函數實體,在整個class聲明完全看見後,綁定操作才會進行
class A { public: A(); private: float x,y,z; }; inline float A::X() const { return x; }
-
-
請思考如下代碼:
typedef int length; class A { public: //length被判定為int類型 //_val 判定為A::_val void do1( length val ) { _val = val; }; length do1() { return _val; }; private: //這裡length必須在"本class對它的第一個操作前"被看見.否則先前的判定操作不合法 typedef float length; length _val; }
-
對於member function的argument list來說,argument list中的名稱會在它們第一次遭遇時被適當判斷完成。因此,需要將nested type聲明放於判斷前
data member 的存取
現有如下代碼:
A a;
//x的存取成本?
a.x = 0.0;
A* ot = &A;
//通過指針的x的存取成本?
pt->x = 0.0
- 用指針進行存取:若A為derived class且繼承體系中含有virtual base class,且存取的member從virtual base class繼承而來,和單一繼承、多重繼承這樣的就有很大差距,因為這個存取操作需要延遲至執行器,經由一個額外的間接導引解決
static data members
-
class object里的static data member,對於class objects和其本身,都不會產生額外負擔
-
無論是複雜的繼承關係還是單一的class object,static data member永遠只有一個實例
-
static data member每次被取用時,編譯器都會對其進行轉化
//a.i = 0; A::i = 0; //pt->i = 0; A::i = 0;
-
多個相同的classs都聲明相同的static member,在data segment中這肯定會導致名稱衝突,但編譯器對其進行name-mangling,也就是暗中對每一個衝突的static data member編碼,如此即可獲得獨一無二的識別代碼
- 不同的編譯器有不同的name-mangling,但都包含兩點:
- 運用一個演算法推導識別代碼
- 若編譯系統必須和使用者交談,這是識別代碼可以被輕易地推導回原來的名稱
- 不同的編譯器有不同的name-mangling,但都包含兩點:
-
對於以上代碼,雖然使用的member selection operators對static data member進行存取操作,但這隻是圖方便,實際上static data member並不在class object中,因此也並沒有通過class object
若由A中的一函數調用static data member,會發生如下轉化:
//do為A中的函數
do().i = 0;
//轉化求值
(void) do();
A.i = 0;
若取static data member地址,也只會得到指向其類型的指針,並不會指向其class member
&A::i;
//轉化
const int*
nonstatic data members
-
nonstatic data members存放在class object中,需經過explict或implicit class object進行存取,且進行存取操作時,編譯器還需要把class object的起始地址加上data member的offset
A A::do1( const A& pt ) { x += pt.x; y += pt.y; z += pt.z; } //轉化 A A::do1( A* const this, const A& pt ) { this->x += pt.x; this->y += pt.y; this->z += pt.z; } --------------------------------------------分割線-------------------------------------------------------- a.y = 0.0; //起始地址+offset &a + (A::y - 1);
-
這裡的"-1"操作是因為指向data member的指針的offset總是被加上1,如此編譯系統即可區分"指向data member的指針,用以指出class的第一個member"和"指向data member的指針,沒有指向任何member"兩種情況
- 取一個nonstatic dat member的地址,會得到它在class中的offset;而取一個綁定在class object上的data member的地址,會得到他在記憶體中的真實地址
class B { public: virtual ~B(); protected: static B origin; float x,y,z; } //&origin: 當前地址減去offset並加一 float B::* p1 = &origin.y; //最終得到val:offset + 1 float B::* p2 = &B::x; //B::* 是指向B data member的指針
-
因為offset的值於編譯期即可得出,因此存取一個nonstatic data member其實效率和c struct member一樣,派不派生也是如此
-
data member的繼承
單一繼承
- 對於derived class object,編譯器可以自由其derived class member 和 base class member的排列順序,但大部分編譯器中,base class members會先出現(以上virtual base class除外)
現有以下代碼:
class Point2d
{
public:
float x() { return _x; }
float y() { return _y; }
void operation+=( const Point2d& rhs )
{
_x += rhs.x();
_y += rhs.y();
}
... //constructor
private:
float _x, _y;
}
class Point3d
{
public:
float z() { return _z; }
void operation+=( const Point3d& rhs )
{
Point2d::operator+=( rhs );
_z += rhs.z();
}
...//constructor
private:
float _z;
}
-
以上這種繼承被稱為具體繼承(concrete inheritance),derived class繼承base class的data member和 member function,將之局部化,但這種行為並不會增加空間和時間上的額外負擔。沒有virtual function時,佈局其實和c struct一樣
-
對於具體繼承,需要註意因alignment padding膨脹的空間
//32位 //A大小 4 + 1 + alignment 3 class A { private: int val; char c1; } //B大小由8 + 1 + aligment3 class B : public A { private: char c2; } //根據B的意思,C也就是16 class C : public B { private: char c3; }
也許你會認為,這不是浪費很多空間嗎,為什麼不讓derived class member直接填上base class aligment那一部分?
如果是以上佈局,又會產生一個問題:繼承而得的members會被覆蓋
B* pb; A* pa1, pa2; //可指向ABC pa1 = pb; //這將導致c2的值被覆蓋掉 *pa2 = *pa1;
多態(單一)繼承
現有如下代碼:
class Point2d
{
public:
float x() { return _x; }
float y() { return _y; }
virtual void operation+=( const Point2d& rhs )
{
_x += rhs.x();
_y += rhs.y();
}
virtual float z() { return 0.0; }
virtual void z(float) { }
... //constructor
private:
float _x, _y;
}
class Point3d
{
public:
float z() { return _z; }
void operation+=( const Point2d& rhs )
{
Point2d::operator+=( rhs );
_z += rhs.z();
}
...//constructor
private:
float _z;
}
//p1和p2可能為Point2d類型,也可能為Point3d類型
void do( Point2d& p1, Point2d& p2 )
{
p1 += p2;
}
- 支持多態繼承會造成空間和時間上的負擔:
- virtual table,存放virtual functions地址和slots(支持runtime type identification)
- 每個class object導入一個vptr
- 優化constructor,在其中設定vptr的初值,使其指向class應對應的virtual table
- 優化destructor,在其中抹去vptr
- 對於編譯器來說,vptr一般放於class object尾端,如此可以保留base class C對象佈局,放在c中亦可使用
多重繼承
- 對於單一繼承這種形式,base class object 和 derived class object都是從相同地址開始(例如先前實例中),因此將derived class object指定給base class的指針或引用,編譯器不需要針對其修改地址,執行效率很高
現有如下代碼:
class Point2d
{
public:
... //含有virtual函數
protected:
float _x, _y;
}
class Point3d : public Point2d
{
...
protected:
float _z;
}
class Vertex
{
public:
... //含有virtual函數
protected:
Vertex* next;
}
class Vertex3d : public point3d, public Vertex
{
...
protected:
float mumble;
}
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 ? (Vertex*)((char*)pv3d) + sizeof( Point3d ); //pv3d可能為野指針
記憶體佈局:
- c++並未要求多重derived class object中,base class objects有特定的排列順序
- 對於多重派生對象,例如Vertex3d,將地址指定給最左端base class(point3d)時,無需修改地址,因為兩者起始地址相同;但往後的base class,需要修改地址,加上或減去介於其中的base class subobjects大小。若存取往後的base class data members,也並不需要付出額外成本,members的位置在編譯期已固定,通過offset運算即可得出
虛擬繼承
iostram library:
//對應如下左圖
class ios {...};
class istream : public ios {...};
class ostream : public ios {...};
class iostream : public istream, public ostream {...};
//對應如下右圖
class ios {...};
class istream : virtual public ios {...};
class ostream : virtual public ios {...};
class iostream : public istream, public ostream {...};
根據如上可知,虛擬繼承可以解決存儲多個同一base class的問題(ios),那麼這是如何實現的呢?
- class內若內含virtual base class subobjects,會被分割為兩部分:一個不變區域和一個共用區域
- 不變區域:含有固定的offset,不受影響,可以直接存取
- 共用區域:也就是virtual base class subobjects,這一區域會受每次派生操作影響而變化,只可以被簡介存取
編譯期實現策略:
class Point2d
{
...
protected:
float _x, _y;
}
class Point3d : virtual public Point2d
{
...
protected:
float _z;
}
class Vertex : virtual public Point2d
{
...
protected:
Vertex* next;
}
class Vertex3d : public Vertex, public Point3d
{
...
protected:
float mumble;
}
-
一般的佈局策略是先安排derived class不變部分,隨後建立共用部分
-
存取class的共用部分:在每一個derived class object中安插一些指針,每個指針指向一個virtual base class
void Point3d::operator+=( const Point3d& rhs )
{
_x += rhs._x;
_y += rhs._y;
_z += rhs._z;
}
//進行如下轉換
__vbcPoint2d->_x += rhs.__vbcPoint2d->_x;
_z += rhs._z;
----------------------------------分割線-------------------------
Point2d* p2d = pv3d;
//進行如下轉換
Point2d* p2d = pv3d ? pv3d->__vbcPoint2d : 0;
然而,這種實現模型卻存在兩個缺點:
- 每個對象針對每一個virtual base class含有一個指向其class的指針
- 隨著虛擬繼承串鏈的變長,間接存取層次也會增加。(如三層虛擬派生,則有三次間接存取,也就是三個virtual base class指針)
解決:
-
對於第一個,引入virtual base class table,virtual base class指針放在table中,編譯期會安插一個指針指向virtual base class table
-
對於第二個,拷貝取得所有的nested virtual base class指針
-
virtual base class最有效的形式:一個抽象virtual base class,不含data member
對象成員和指向data member的指針效率
- 對於對象成員,在編譯期未優化時聚合、封裝、繼承方式在存取方面都有效率上的差異;優化後都是相同的,且封裝並不會帶來執行器的效率成本。其中聚合和封裝、單一繼承效率高,因為單一繼承中members被連續存儲在derived class中,且offset於編譯期就計算出了;但虛擬繼承的效率很低
- 對於指向data member的指針,在編譯期未優化時,通過指針間接存取效率相對於直接存取會更低,但優化後都是一樣的;單一繼承並不會降低效率,但虛擬繼承中,因每一層都導入一個額外層次的間接性,因此效率較差