一個class的data members,通常而言,能夠表現這個class在程序執行時的某種狀態。Nonstatic data members放置的是「個別的class object」感興趣的數據,static data members則放置的是「整個class」感興趣的數據。ios
C++對象模型儘可能以空間優化和存取速度優化的考慮來表現nonstatic data members,而且保持和C語言struct數據配置的兼容性。它們把數據直接存放在每個class object之中。對於繼承而來的nonstatic data members(無論是virtual仍是nonvirtual base class)也是如此。不過沒有強制定義其間的排列順序。
至於static data members,則被放置在程序的一個global data segment中,不會影響個別class object的大小。在程序之中,無論該class被產生出多少個objects(經由直接產生或間接派生),static data members永遠只存在一份實例(甚至即便該class沒有任何object實例,其static data members也已存在)。可是一個template class的static data members的行爲稍有不一樣。程序員
C++ Standard以「member scope resolution rules」來精煉這個「rewriting rule」,其效果是,若是一個inline函數在class聲明以後當即被定義的話,那麼就仍是對齊評估求值(evaluae)。也就是說,當一我的寫下這樣的代碼:算法
extern int x; class Point3d{ public: //對於函數自己的分析將延遲直至class聲明的右大括號出現纔開始 float X() const { return x; } //... private: float x; }; //事實上,分析在這裏運行
時,對於member functions自己的分析,會直到整個class的聲明都出現了纔開始。
所以,在一個inline member function軀體以內的一個data member綁定操做,會在整個class聲明以後才發生。ide
已知下面一組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,如freeList和chunkSize都不會被放進對象佈局之中。在上述例子中,每個Point3d對象是由三個float組成,次序是x,y,z。static data members存放在程序的data segment中,和個別的class objects無關。工具
C++ Standard要求,在同一個access section(也就是private、public、protected等區段)中,members的排列只需符合「較晚出現的members在class object中有較高的地址」這一條件便可。也就是說,各個members並不必定連續排列。什麼東西可能會介於被聲明的members之間呢?members的邊界調整(alignment)可能就須要填補一些bytes。對於C和C++ 而言這的確是真的,對目前的C++編譯器實現狀況而言,這也是真的。佈局
編譯器還可能會合成一些內部使用的data members,以支持整個對象模型,vptr就是這樣的東西,當前全部的編譯器都把它安插在每個「內含virtual function之class」的object內。測試
static data members,按其字面意義,被編譯器提出於class以外,並被視爲一個global變量(但只在class生命範圍內可見)。每個member的存取許可(譯註:private、protected或public),以及與class的關聯,並不會招致任何空間上或執行時間上的額外負擔——不管是在個別的class objects仍是在static data member自己優化
每個static data member只有一個實例,存放在程序的data segment之中。每次程序參閱(取用)static member時,就會被內部轉化爲對該惟一extern實例的直接參考操做。例如:this
Point3d origin, *pt; //origin.chunkSize = 250 Point3d::chunkSize = 250; //pt->chunkSize = 250 Point3d::chunkSize = 250;
從指令執行的觀點來看,這是C++語言中「經過一個指針和經過一個對象來存取member,結論完成相同」的惟一一種狀況,這是由於「經由member selection operators(也就是'.'運算符)對一個static data member進行存取操做」只是語法上的一種便宜行事而已,member其實並不在class object之中,所以存取static membeers並不須要經過class object。
若取一個static data member的地址,會獲得一個指向其數據類型的指針,而不是一個指向其class member的指針,由於static member並不內含在一個class object之中。例如:
&Point3d::chunkSize;
會得到類型以下的內存地址:
const int*
若是有兩個classes,每個都聲明瞭一個static member freeList,那麼當它們都被放在程序的data segment時,就會致使名稱衝突。編譯器的解決方法是暗中對每個static data member編碼(對於這種手法有個很美的名稱:name-mangling),以得到一個獨一無的程序識別代碼。有多少個編譯器,就有多少中name-manglint作法。任何name-mangling作法都有兩個要點:
Nonstatic data members直接存放在每個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; }
欲對一個nonstatic data member進行存取操做,編譯器須要把class object的起始地址加上data member的偏移位置(offset)。若是:
origin._y = 0.0;
那麼地址&origin._y
將等於:
&origin + (&Point3d::_y - 1);
請注意其中的 -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 class的member是同樣的。
在C++ 繼承模型中,一個derived class object所表現出來的東西,是其本身的members加上其base class members的總和。至於derived class members和base class members的排列順序,則未在C++ standard中強制指定:理論上編譯器能夠自由安排之。在大部分編譯器上頭,base class members老是先出現,但屬於virtual base class的除外。(通常而言,任何一條通則一旦碰上virtual base class就沒有轍了,這裏亦不例外)
通常而言,具體繼承(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+=(const 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+=(const Point3d& rhs){ Point2d::operator(rhs); _z += rhs.z(); } protected: float _z; };
這樣設計的好處就是能夠把管理x和y座標的程序代碼局部化。此外這個設計明顯表現出兩個抽象類之間的緊密關係。當這兩個classes獨立的時候,Point2d object和Point3d object的聲明和使用都不會有所改變,因此這兩個抽象類的使用者不須要知道objects是否爲獨立的classes類型,或是彼此之間有繼承的關係。
把兩個本來不想幹的classes湊出一對「type/subtype」,並帶有繼承關係,會有什麼易犯的錯誤呢?經驗不足的人可能會重複設計一些相同操做的函數。第二個易犯的錯誤是,把一個class分解爲兩層或更多層,有可能會爲了「表現class體系之抽象化」而膨脹所需空間。C++ 語言保證「出如今derived class中的base class subobject有其完整原樣性」。舉例以下:
class Concrete{ public: //... private: int val; char c1; char c2; char c3; };
在一部32位機器中,每個Concrete class object的大小都是8bytes,細分以下:
如今假設,通過某些分析以後,咱們決定了一個更邏輯的表達方式,把Concrete分裂成三層結構:
class Concrete1{ public: //.. private: int val; char bit1; }; class Concrete2 : public Concrete1{ public: //... private: char bit2; }; class Concrete3 : public Concrete2{ public: //... private: char bit3; };
從設計的觀點來看,這個結構可能更合理。可是從效率的觀點來看,咱們可能會受困於一個事實:如今Concrete3 object的大小是16bytes,比原先的設計多了一倍。
怎麼回事?還記得「base class subobject在derived class中的原樣性」嗎?
Concrete1內含兩個members: val和bit1, 加起來是5bytes。而一個Concrete1 object實際用掉8bytes,包括填補用的3bytes,以使object可以符合一個機器的word邊界。Concrete2加了惟一一個nonstatic data member bit2,數據類型爲char,輕率的程序員會認爲它會和Concrete1捆綁在一塊兒,佔用本來用來填補的1bytes。然而Concrete2的bit2實際上倒是被放在填補的3bytes以後,因而大小變成12bytes,而不是8bytes。其中有6bytes浪費在填補空間上。相同的道理是Concrete3 object的大小是16bytes,其中9bytes用於填補空間。
聲明以下:
Concrete2 *pc2; Concrete1 *pc1_1, *pc1_2;
其中pc1_1
和pc1_2
二者均可以指向前述三種class objects。下面這個指定操做:
*pc1_2 = *pc1_1;
應該執行一個默認「memberwise」複製操做(複製一個個的members),對象是被指的object的Concrete1那一部分。若是pc1_1實際指向一個Concrete2 object或Concrete3 object,則上述操做應該將複製內容指定給其Concrete1 subobject。
然而,若是C++ 語言把derived class members(也就是Concrete2::bit2 或Concrete3::bit3)和Concrete subobject捆綁在一塊兒,去除填補空間,上面那些語意就沒法保留了,以下:
pc1_1 = pc2; //令pc1_1指向Concrete2對象 //derived class subobject被覆蓋掉,因而bit2 member現有一個並不是預期的數值 *pc1_2 = *pc1_1;
class Point2d{ public: Point2d(float x = 0.0, float y = 0.0) : _x(x), _y(y) { }; //x和y的存取函數與前一版相同 //因爲對不一樣維度的點,這些函數的操做固定不變,因此沒必要設計爲virtual //加上z的保留空間(當前什麼也不作) virtual float z() { return 0.0; } virtual void z(float){ }; //設定如下的運算符爲virtual virtual void operator+=(const Point2d& rhs){ _x += rhs.x(); _y += rhs.y(); } protected: float _x, _y; };
這樣的設計,給Point2d class帶來空間和存儲時間的額外負擔:
單一繼承提供了一種「天然多態(natural polymorphism)」形式,是關於classed體系中的base type和derived type之間的轉換。
多重繼承既不像單一繼承,也不容易模塑出其模型。多重繼承的複雜度在於derived class和其上一個base class乃至於上上一個base class....之間的「非天然」關係。
考慮下面這個多重繼承所得到的class Vertex3d
class Point2d{ public: //...擁有virtual接口,因此,Point2d對象中會有vptr protected: float _x, _y; }; class Point3d : public Point2d{ public: //... protected: 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 object之間的轉換。不管是直接轉換以下:
extern void mumble(const Vertex&); Vertex3d v; ... //將一個Vertex3d轉換爲一個Vertex,這是「不天然的」 mumble(v);
或是經由其所支持的virtual function機制作轉換。
對一個多重派生對象,將其地址指定給「最左端(也就是第一個)base class的指針」,狀況將和單一繼承時相同,由於兩者都指向相同的起始地址。需付出的成本只有地址的指定操做而已。至於第二個或後繼的base class的地址指定操做,則須要將地址修改過:加上(或減去,若是downcast的話)介於中間的base class subobjects大小。例如:
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;
不可以只是簡單地被轉換爲:
pv = (Vertex*)((char*)pv3d) + sizeof(Point3d);
由於若是pv3d爲0,pv將得到sizeof(Point3d)的值,這是錯誤的。因此,對於指針,內部轉換操做須要一個條件測試:
pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof(Point3d) : 0 ;
至於reference,則不須要針對可能的0值作防衛,由於reference不可能參考到「無物」
若是要存取第二個(或後繼)base class中的一個data member會是怎樣的狀況?須要付出額外的成本嗎? 不,members的位置在編譯期就固定了,所以,存取members只是一個簡單的offset運算,就像單一繼承同樣簡單——無論是經由一個指針,一個reference或是一個object來存取。
多重繼承的一個語意上的反作用就是,它必須支持某種形式的「shared subobject繼承」。一個典型的例子就是最先的iostream library:
不管是istream或ostream都內含一個ios subobject,然而在iostream的對象佈局中,咱們只須要一份ios subobject就好。語言層面的解決辦法是導入所謂的虛擬繼承。
通常的實現方法以下所述:Class若是內含一個或多個virtual base class subobjects,像istream那樣,將被分割爲兩部分:一個不變局部和一個共享局部。不變局部中的數據,無論後繼如何衍化,老是擁有固定的offset(從object的開頭算起),因此這一部分數據能夠被直接存取。至於共享局部,所表現的就是virtual base class subobject。這一部分的數據,其位置會由於每次的派生操做而有變化,因此它們只能夠被間接存取。各家編譯器實現技術之間的差別就在於間接存取的方法不一樣。
如下說明三種主流策略,下面是Vertex3d虛擬繼承的層次結構:
通常的佈局策略是先安排好derived class的不變部分,而後再創建其共享部分。
如何可以存取class的共享部分呢?
cfont編譯器會在每個derived class object中安插一些指針,每一個指針指向一個virtual base class。要存取繼承得來的virtual base class members,可使用相關指針間接完成。舉例以下:
void Point3d::operator+=(const Point3d &rhs){ _x += rhs._x; _y += rhs._y; _z += rhs._z; }
在cfront策略下,這個運算會被內部轉換爲:
_vbcPoint2d->_x += rhs._vbcPoint2d->_x; // vbc意指virtual base class _vbcPoint2d->_y += rhs._vbcPoint2d->_y; _z += rhs._z;
而一個derived class和一個base class的實例之間的轉換,如
Point2d *p2d = pv3d;
在cfront實現模型下,會變成:
Point2d *p2d = pv3d ? pv3d->_vbcPoint2d : 0;
這樣的實現模型有兩個主要的缺點:
MetaWare和其餘編譯器使用cfront的原始模型來解決第二個問題,它們經由拷貝操做去的全部的nested virtual base class指針,放到derived class object中,這就解決了"固定存儲時間"的問題。雖然付出了一些空間上的代價。下圖說明了這種「以指針指向base class」的實現模型。
對於第一個問題,通常有兩個解決辦法。Microsoft編譯器引入所謂的virtual base class table。每個class object若是有一個或多個virtual base classes,就會由編譯器安插一個指針,指向virtual base class table。至於真正的virtual base class指針,固然是被放在表格中。
第二個解決辦法是在virtual function table中放置virtual base class的offset。下圖顯示了base class offset實現模型。
在新近的Sun編譯器中,virtual functon table可經由正值或負值來索引,若是是正值,很顯然就是索引到virtual function;若爲負值,則是索引到virtual base class offsets。在這樣的策略下,Point3d的operator+=運算符必須被轉換爲如下形式:
(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;
上述的每一種方法都是一種實現模型,而不是一種標準,每一種模型都是用來解決「存取shared subobject內的數據(其位置會由於每次派生操做而有變化)」所引起的問題。因爲對virtual base class的支持帶來額外的負擔以及高度的複雜性,每一種模型多少有點不一樣,並且還會隨着時間而進化。
通常而言,virtual base class最有效的一種運用形式就是:一個抽象的virtual base class,沒有任何data members
考慮下面的Point3d聲明,其中有一個virtual function, 一個static data member,以及三個座標值:
class Point3d{ public: virtual ~Point3d(); //... protected: static Point3d origin; float x, y, z; };
取某個座標成員的地址,表明什麼意思? 以下:
&Point3d::z;
上述操做將獲得z座標的class object中的偏移量(offset),最低限度其值將是x和y的大小總和,覺得C++ 語言要求同一個access level中的members的排列次序應該和其聲明次序相同。
然而vptr的位置就沒有限制,實際上vptr不是放在對象的頭部,就是放在對象的尾部。每個float是4bytes,因此咱們應該指望剛纔得到的值要不是8,就是12(在32位機器上一個vptr是4bytes)
然而,這樣的指望還少1bytes。
若是vptr放在對象的尾端,則三個座標值在對象佈局中的offset分別是0,4,8。若是vptr放在對象的起頭,則三個座標值在對象佈局中的offset分別是4,8,12。然而你若去取data members的地址,傳回的值老是多1, 也就是1,5,9或9,5,13等等。
如何區分一個「沒有指向任何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
認識「指向data members的指針」以後,咱們發現,要解釋:
&Point3d::z; &origin.z;
之間的差別,就很是明確了。鑑於「取一個nonstatic data member的地址,將會獲得它在class中的offset」,取一個「綁定於真正class object身上的data member」的地址,將會獲得該member在內存中的真正地址。把
&origin.z
所得結果減z的偏移量(相對於origin的起始地址),並加1,就會獲得origin的起始地址。上一行的返回值類型應該是float*
,而不是float Point3d::*
。
因爲上述操做所參考的是一個特定實例,因此取一個static data member的地址,意義也相同。
在多重繼承中,若要將第二個(或後繼)base class的指針,和一個「與derived class object綁定」的member結合起來,那麼將會由於「須要加入offset值」而變得至關複雜。例如:
struct Base1{ int val1; } struct Base2{ int val2; } struct Derived : Base1, Base2{ ... } void fun1(int Derived::*dmp, Derived *pd){ //指望第一個參數獲得一個「指向derived class之member」的指針 //若是傳進來的倒是一個"指向base class之member"的指針,會怎樣 pd->*dmp; } void fun2(Derived *pd){ //bmp將成爲1 int Base2::*bmp = &Base2::val2; //bmp = 1 //可是在Derived中,val2 = 5 fun1(bmp, pd); }
當bmp被做爲fun1()的第一個參數時,它的值就必須因介入的Base1 class的大小而調整,不然fun1()中這樣的操做:
pd->*dmp;
將存取Base1::val1,而非程序員因此爲的Base2::val2。要解決這個問題,必須
//經由編譯器內部轉換 fun1(bmp + sizeof(Base1), pd);
然而,通常而言,咱們不能保證bmp不是0,所以必須特別留意之:
//內部轉換 //防範bmp == 0 fun1(bmp ? bmp + sizeof(Base1) : 0, pd);