C++對象模型——Data Member的綁定(第三章)
3.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;
};
Point3d::X()傳回哪一個x?是 class 內部的那個x,還是外部(extern)的那個x?現在的答案是內部的.但過去的答案並不是這樣.
在C++最早的編譯器上,如果在Point3d::X()的兩個函數實例中對x進行參閱(引用)操作,這操作將會指向global x object.這樣的綁定結果幾乎普遍存在,並因此導出早期C++的兩種防御性程序設計風格:
1. 把所有的data members放在 class 聲明開始處,以確保正確的綁定:
class Pointed
{
// 防御性程序設計風格 #1
// 在class聲明開始先放置所有的data members
float x, y, z;
public:
float X() const { return x; }
// ...etc...
};
2. 把所有的inline functions,不管大小都放在 class 聲明之外:
class Point3d
{
public:
// 防御性程序設計風格 #2
// 把所有的inlines都移到class之外
Pointed();
float X() const;
void X(float) const;
// ...etc...
};
inline float POint3d::X() const {
return x;
}
這些程序設計風格事實上到今天還存在,這個古老的語言規則被稱為member rewriting rule,大意是一個inline函數實體,在整個class聲明未被完全看見之前,是不會被評估求值(evaluated)的.C++ Standard以member scope resolution rules來精煉這個rewriting rule,其效果是,如果一個inline函數在 class 聲明後立刻被定義的話,那麼就還是會對其評估求值(evaluated).也就是說,如果寫下面的代碼:
extern int x;
class Point3d {
public:
// 對於函數本身的分析將延遲直至class聲明的右括號出現才開始
float X()const { return x; }
private:
float x;
};
對於member functions本身的分析,會直到整個 class 的聲明都出現了才開始.因此,在一個 inline member function內的一個data member綁定操作,會在整個 class 聲明完成後才發生.
3.2 Data Member的布局 (Data Member Layout)
已知下面一組data members:
class Point3d {
public:
// ...
private:
float x;
static List *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,vptr會放在什麼位置呢?傳統上它被放在所有明確聲明的members的最後,不過如今也有一些編譯器把vptr放在一個 class object的最前端.C++ Standard對布局持放任態度,允許編譯器把那些內部產生出來的members自由放在任何位置上,甚至放在那些被程序員聲明出來的members之間.
C++ Standard也允許編譯器將多個access sections中的data members自由排列,不必在乎它們出現中在 class 聲明中的次序,也就是說,下面這樣的聲明:
class Point3d {
public:
// ...
private:
float x;
static List *freeList;
private:
float y;
static const int chunkSize = 250;
private:
float z;
};
其 class object的大小和組成都和先前聲明的那個相同,但是members的排列次序則視編譯器而定.
當前編譯器都是把一個以上的access sections連鎖在一起,依照聲明的次序成為一個連續區塊.Access sections的多少並不會導致額外負擔,例如在一個section中聲明8個members,或者在8個sections中總共聲明8個members,得到的object大小是一樣的.
下面這個 template function,接受兩個data members,然後判斷誰先出現在 class object中.如果兩個members都是不同的access sections中的第一個被聲明者,此函數可以用來判斷哪一個section先出現:
template
char * access_order(data_type1 class_type::*mem1, data_type2 class_type::*mem2) {
assert(mem1 != mem2);
return mem1 < mem2 ? member 1 occurs first : member 2 occurs first;
}
上述函數可以這樣被調用:
access_order(&Point3d::z, &Point3d::y);
於是class_type會被綁定為Point3d,而data_type1和data_type2會被綁定為float.