c/c++对象模型大总结:第5-8章、数据成员的存取与布局
深度探索c++對象模型大總結、中
--第五~八章
?
?
作者:July、吳黎明。
聲明:版權所有,侵權必究。
二零一一年三月十八日。
?
?
本文接上一篇 c++對象模型大總結:第1-4章、對象初探與構造函數,而寫。
?
?
第二部分
第五章、數據成員的布局
??
????? 已知下面一組數據成員:
class Point3d{
public:
?//…
private:
?float x;
?static List<Point3d*> *freeList;
?float y;
?static const int chunkSize = 250;
?float z;
}
??? 非靜態數據成員在class object中的排列順序將和其被聲明的順序一樣,任何中間介入的靜態數據成員如freeList和chunkSize都不會被放進對象布局中。在上述例子中,每一個Point3d對象由三個float組成,次序是x、y、z。靜態數據成員存放在程序的data segment中,和個別的class object無關。
??? C++標準要求,在同一個access section(也就是private、public、protected等區段)中,members的排列只須符合“較晚出現的members在class object中有較高的地址”這一條即可。也就是說,各個members并不一定得連續排列。什么東西可能會介于被聲明的members之間呢?比如說members的邊界調整時需要填充的一些字節等等。
??? 同時,編譯器還可能會合成一些內部使用的data members,以支持整個對象模型。vptr就是這樣的東西,當前所有的編譯器都把它安插在每一個“內含virtual function的class”的object內。vptr會被放在什么位置呢?傳統上它被放在所有明確聲明的members的最后,不過如今也有一些編譯器把vptr放在class object的最前端。C++ standard允許編譯器把這些內部產生出來的members自由放在任何位置上。
??? C++標準也允許編譯器將多個access sections之中的data members自由排列,不必在乎它們出現在class聲明中的次序。也就是說,下面這樣的聲明中:
class Point3d{
public:
?//…
private:
?float x;
?static List<Point3d*> *freeList;
private:
?float y;
?static const int chunkSize = 250;
private:
?float z;
}
??? 其class object的大小和組成和我們先前聲明的那個相同,但是members的排列次序則視編譯器而定。編譯器可以隨意把y或z或其它什么東西放在第一個,不過大部分的編譯器都沒有這樣做。當前各家編譯器都是把一個以上的access sections連鎖在一起,依照聲明次序,成為一個連續的區塊。access sections的多少,不會招來額外的負擔。例如,在一個section中聲明8個members,或是在8個sections中總共聲明8個members,得到的object大小是一樣的。
?
第六章、靜態數據成員的存取
??? Static data members,按照其字面意思,被編譯器提出到class之外,并被視為一個global變量(但只在class生命范圍之內可見)。每一個member的存取許可(private或protected或public),以及與class的關聯,并不會導致任何空間上或執行時間上的額外負擔。
??? 每一個static data member只有一個實體,存放在程序的data segment之中。每次程序取用static data 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還是只有唯一的一個實體,而其存取路徑依然是那么直接。
??? 若取一個靜態數據成員的地址,會得到一個指向其數據類型的指針,而不是一個指向其class member的指針,因為static member并不內含在一個class object之中。例如:
????? &Point3d::chunkSize;
會獲得類型如下都內存地址:
????? const int*
??? 如果有兩個classes,每一個都聲明了一個static member freeList,那么當它們都被放在程序的data segment時,就會導致名稱沖突。編譯器的解決辦法是暗中對每一個static data member編碼(這種手法被稱為:name-mangling),以獲得一個獨一無二的程序識別代碼。
?
第七章、非靜態數據成員的存取
??? 非靜態數據成員直接存放在每一個class object之中。除非經由明確的(explicit)或暗喻的(implicit)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;
}
??? 欲對一個非靜態數據成員進行存取操作,編譯器需要把class object的起始地址加上data member的偏移量。比如:
origin._y = 0.0;
地址&origin._y將等于:
&origin + (&Point3d::_y - 1);
??? 要注意這里都有減1的操作。指向數據成員的指針,其offset值總是被加上1,這樣可以使編譯系統區分出“沒有指向任何數據成員的指針”和“指向第一個數據成員的指針”這兩種情況。
每一個非靜態數據成員的偏移量(offset)在編譯時期即可獲知,甚至如果member屬于一個base class subobject也是一樣,因此,存取一個非靜態數據成員,其效率和存取一個C struct member或一個nonderived class的member也是一樣的。
??? 現在我們看看虛擬繼承。虛擬繼承將為“經由base class subobject“存取class members導入一層新的間接性,譬如:
Point3d *pt3d;
Pt3d->_x = 0.0;
??? 其執行效率在_x是一個struct member、一個class member、單一繼承、多重繼承的情況下都完全相同。但如果_x是一個virtual base class的member,存取速度會慢一些。
?
第八章、“繼承“與數據成員
??? 只要繼承不要多態(Inheritance without Polymorphism)
假設有如下三個類及其繼承關系:
class Concreate1{
public:
?//...
private:
?int val;
?char bit1;
};
class Concreate2 : public Concrete1{
public:
?//...
private:
?char bit2;
}
class Concreate3 : public Concrete2{
public:
?//...
private:
?char bit3;
}
??? Concreate1、Concreate2、Concreate3的對象布局情況:
??? C++語言保證”出現在派生類中的base class subobject有其完整原樣性“。Concrete1內含兩個members:val和bit1,加起來5bytes。而一個Concreate1 object實際上用掉8bytes,包括填充用的3bytes,以使object能夠符合一部機器的word邊界。一般而言,邊界調整(alignment)是由處理器來決定的。
??? 然而,Concreate2的bit2實際上卻是被放在填補空間所用的3bytes之后,于是其大小變成12bytes,而不是8bytes,其中6bytes浪費在填補空間上。相同的道理使得Concreate3 object的大小是16bytes,其中9bytes用于填補空間。
加上多態(adding Polymorphism)
??? 假設我們要處理一個坐標點,而不打算在乎它是一個Point2d或Point3d實例,那么我們需要在繼承關系中提供一個virtual function接口:
class Point2d{
public:
?Point2d(float x=0.0, float y=0.0) : _x(x),_y(y){};
?virtual float z(){return 0.0;}
?virtual void z(float){}
?operate+=(const Point2d &rhs){
?_x += rhs.x();
?_y += rhs.y();
?}
protected:
?float _x;
?float _y;
}
class Point3d : public Point2d{
public:
?Point3d(float x=0.0, float y=0.0, float z=0.0) : Point2d(x,y),_z(z){};
?virtual float z(){ return _z; }
?virtual void z(float newZ){ _z = newZ; }
?operate+=(const Point2d &rhs){
?Point2d::operator+=(rhs);
?_z += rhs.z();
?}
protected:
?float _z;
}
??? 只有當我們以多態的方式來處理2d或3d坐標點時,在設計之中導入一個virtual接口才顯得合理。也就是說,寫下這樣的代碼:
void foo( Point2d &p1, Point2d &p2 ){
?//…
?P1 += p2;
?//…
}
??? 其中p1和p2可能是2d也可能是3d坐標點,這并不是以前任何設計所能支持的。這樣的彈性,當然正是面向對象程序設計的中心。同時,支持這樣的彈性,也給我們的Point2d class帶來空間和存取時間的額外負擔:
導入一個和Point2d有關的virtual table,用來存放它所聲明的每一個virtual functions的地址。
在每一個class object中導入一個vptr,提供執行期的鏈接,使每一個object能夠找到相應的virtual table。
??? 加強constructor,使它能夠為vptr設定初值,讓它指向class所對應的virtual table。這可能意味著在derived class和每一個base class的constructor中,重新設定vptr的值。其情況視編譯器的優化的積極性而定。
??? 加強destructor,使它能夠抹消“指向class之相關virtual table“的vptr。要知道,vptr很可能已經在derived class destructor中被設定為derived class的virtual table地址。記住,desturctor的調用次序是反向的:從derived class到base class。
下圖顯示了Point2d和Point3d加上了virtual function之后的繼承布局。此圖把vptr放在base class的尾端:
?????
多重繼承(Multiple Inheritance)
??? 多重繼承不像單一繼承,不容易模塑出其模型。多重繼承的復雜度在于derived class和其上一個base class乃至于上上一個base class…之間的“非自然“關系。例如,考慮下面這個多重繼承所獲得的class Vertex3d:
class Point2d{
public:
?//...
protected:
?float _x;
?float _y;
}
class Point3d : public Point2d{
public:
?//...
ptotected:
?float _z;
}
class Vertex{
public:
?//...
protected:
?Vertex *next;
}
class Vertex3d : public Point3d, public Vertex{
public:
?//...
protected:
?float mumble;
}
至此,Point2d、Point3d、Vertex、Vertex3d的繼承關系如下圖所示:??
?????
??? 多重繼承的主要問題發生于derived class objects和其第二或后繼的base class objects之間的轉換。對于一個多重派生對象,將其地址指定給“最左端base class的指針”,情況將和單一繼承時相同,因為二者都指向相同的起始地址。至于第二個或后繼的base class的地址指定操作,則需要對地址進行調整:加上(或減去,如果downcast的話)介于中間的base class subobject大小,例如:
Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;
那么下面這個指定操作:
?????? pv = &v3d;
需要這樣的內部轉化:
//虛擬C++碼
????? pv = (Vertex *)( ((char *)&v3d) + sizeof( Point3d ) )
而下面都指定操作:
????? p2d = &v3d;
????? p3d = &v3d;
都只需要簡單地拷貝其地址就可以了。下面是該多重繼承的數據布局示意圖:
虛擬繼承(Virtual Inheritance)
??? 下圖是Point2d、Point3d、Vertex、Vertex3d的繼承體系:?
?????
class Point2d{
public:
?//...
protected:
?float _x;
?float _y;
};
class Vertex : public virtual Point2d{
public:
?//...
protected:
?Vertex *next;
};
class Point3d : public virtual Point2d{
public:
?//...
protected:
?float _z;
};
class Vertex3d: public Vertex, public Point3d{
public:
?//...
protected:
?float mumble;
};
??? 在存取派生類的共有的虛擬基類的時候,cfront編譯器會在每一個派生類對象中安插一些指針,每個指針指向一個virtual base class。要存取繼承得來的virtual base class members,可以使用相關指針間接完成。
?????
??? 上面這種實現方式的一個缺點是:每一個對象必須針對每一個virtual base class背負一個額外的指針。然而理想情況下我們希望class object有固定的負擔,不因為其virtual base classes的數目而有所變化。該如何解決這個問題呢?virtual table offset strategy采用了另外一種實現策略:在virtual function table中放置virtual base class的offset(而不是地址),將virtual base class offset和virtual function entries混在一起。virtual function table可經由正值或負值來索引:如果是正值,很顯然就索引到virtual functions;如果是負值,則索引到virtual base class offsets。
??? 一般而言,虛基類最有效的一種運用形式就是:一個抽象的虛基類,其中沒有任何數據成員。
參考資料:
《深度探索C++對象模型》
?
版權歸本人、吳黎明、CSDN共同所有,任何人,任何網站,在未經本人書面許可的情況下,嚴禁轉載。
否則,盡我一切,永久追究法律責任的權利。July、二零一一年三月十八日聲明。?
轉載于:https://www.cnblogs.com/v-July-v/archive/2011/03/18/2009184.html
總結
以上是生活随笔為你收集整理的c/c++对象模型大总结:第5-8章、数据成员的存取与布局的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 去除HTML
- 下一篇: 【OpenCV学习笔记4】OpenCV