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的指標,在編譯期未優化時,通過指標間接存取效率相對于直接存取會更低,但優化后都是一樣的;單一繼承并不會降低效率,但虛擬繼承中,因每一層都匯入一個額外層次的間接性,因此效率較差
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/518455.html
標籤:C++
