筆記:深度探索C++物件模型 第三章

The Semantics of Data

摘要

 

static data member

static data member 永遠在 global data segment, 不影響 class object 的大小.

 

物件的大小

class object 的大小有可能因為 compiler 實作物件模型的方式不同而有不同,原因:

  1. compiler自動加上額外 data member, 以支援某些語言特性.
  2. alignment 邊界調整需要(比如: char 為配合機器特性而以 long 存放)

雖然目前 C++ compiler 已經非常進步,但早期會有兩種程式寫作防禦方格,以防止資料繫結錯誤.

  1. 把 data member 寫在 class 的開頭.
  2. inline function 移到 class 宣告之後,而不在宣告區裡面實作, 如:
extern int x;
class Point3D {
private:
float x,y,z;
public:
float X() const {return x;} 	//到底傳回哪個 x 呢?
float getX() const; 			//早期若不這樣寫,會造成資料繫結到上面那個 x
};
inline float
Point3D::
getX() const {
return x;
}

 

data member 的佈局

data member 實際的存放(佈局)

注意:各 data member 再實際存放時不一定連續.

如:

class Point3D {
float x;
float y;
float z;
};

x,y,z 不一定是連續的,有可能為了要補 alignment 或因compiler 的調整為 y,x,z, 主要原因是 C++ Standard 對此採放任態度.一般而言,仍是連續的.

 

data member 的存取

當 member 被宣告為 static 時,實際上和一般變數存取一樣,因為 static member 存放在 class 之外,不需要再透過 class
去存取. 若不是宣告為 static, 事實上,都會透過一個隱含的 class object (this) 完成,也就是類似這樣

Point3D
Point3D:translate( Point3D* this, const Point3D &pt) {
this->x+=pt.x;
this->y+=pt.y;
this->z+=pt.z;
}

若在程式中對 data member 做存取,如:

origin.y=0.0;

那麼實際上將等於

&origin+(&Point3D::y-1)

指向 data member 的指標,其 offset 值總是被加上 1, 這樣子 compiler就能需分出"一個指向 data member
的指標,用以指出 class 的第一個 member"和"一個指向 data member 的指標,未指向任何 member"的情況.(那就是上面為什麼要
-1的原因).

 

繼承與 data member

在沒有 virtual function 的狀況下,和 struct 相同.原本是獨立不相干的 class 湊成 type / subtype, 並有繼承關係(如
Point2D -> Point3D), 經驗不足的人可能會重複設計一些相同動作的函式,以 constructor 和 operator += 為例,
可以做成 inline. 另外把一個 class 分解為二層或更多層,有可能會為了"表現 class 體系之抽象化"而膨脹所需空間.如:
parent 為兩個 int, child 為一個 char, grandchild 為一個 char, compiler 會為了 alignment
而填補空間.

加上 virtual function 後,將需要導入 vptr, 並在每個函式做 vptr 的處理, 解構時也要把 vptr 抹消,一般而言, vptr
放在 class 的最後面,但 visual c++ 放在最前面,主要是為了繼承的效率問題.

class 若內含一個或多個 virtual base class subobjects, 將會分割為二部分:一個不變區域和一個共享區域,不變區域的資料,不管後繼如何衍化,總擁有固定的
offset(自 object 的起頭算起), 所以可以直接存取,至於共享區域,表現的是 virtual base class subobject, 其位置會因為每次衍生動作而有變化,只能被間接存取,
compiler 會在子類別中安插一些指標,每個指標指向一個 virtual base class.這樣的做法有兩個缺點:

  1. 每個物件必須針對其每個 virtual base class 背負一個額外指標(空間增加).
  2. 由於虛擬繼承串鏈的加長,導致間接存取層次增加.(時間增加)

大部分編譯器到今天仍使用"經由拷貝動作取得所有的 nested virtual base class 指標,放到 derived class
object 之中"來解決第二個問題.

第一個問題一般有兩個解法. Microsoft compiler 引入 virtual base class table, 每一個 class object
如果有一個或多個 virtual base class, 就會由 compiler 安插一個 pointer, 指向 virtual base class
table. 第二個方法是在 virtual function table 中放置 virtual base class 的 offset.

 

指向 data members 的指標

可用以決定 vptr 放在 class 的 begin 或 end, 另一個用途可用來決定 class 中的 access sections.

usage:

	printf("&Point3d::x=%p\n",&Point3d::x);

如果 vptr 在物件尾巴,則 offset 為 0

如果 vptr 在物件起頭,則 offset 為 4

但為何在尾巴時,傳回值總是多 1? 意即 1 主要是用以區分"沒有指向任何 data member" 的指標和 "指向第一個
data member" 的指標.

注意,此種額外的間接性會降低"把所有處理都搬移到暫存器中執行"最佳能力.