繼我的上一篇文章:淺談學習C++時用到的【封裝繼承多態】三個概念
此篇我們從C++對象內存布局和構造過程來具體分析C++中的封裝、繼承、多態。
一、封裝模型的內存布局
常見類對象的成員可能包含以下元素:內建類型、指針、引用、組合對象、虛函數。
另一個角度的分類:
數據成員:靜態、非靜態
成員函數:靜態、非靜態、虛函數
1.僅包含內建類型的場合:
class T { int data1; char data2; double data3; };
類中的內建類型按照聲明的順序在內存中連續存儲,並且分配的大小由內建類型本身的大小決定(依賴機器),布局受字節對齊影響(本篇不討論字節對齊)。
2.包含指針和引用的場合:
class T { int data1; char data2; double data3; int& ri1;//需要構造函數 int* rp1; int (*pf)(); };
存儲方式同1的場合,不同點為指針和引用通常為固定大小(32位機器4字節、64位機器8字節)。
有關引用:個人理解的引用就是懶人專用指針,取地址又間地址是很麻煩的操作,於是出現了自動取址又間址的指向常量的常指針。
在類中聲明可以測出固定字節大小,所以也是占用固定的字節大小。
3.包含組合對象的場合:
class Q { int a; int b; }; class T {int data1; Q q; double data2; };
內存布局圖示(本篇以及后續篇使用的環境為 32位Win7, VS2008):

再來看一下地址:

結論:(顯而易見就不解釋了)
類對象最終被解釋成內建類型,布局依然按照聲明的順序,並且對象布局在內存中依然是連續的
4.在3的場合添加虛函數的場合
class Q { virtual void fun(){} int a; int b; }; class T { virtual void fun(){} int data1; Q q; double data2; };
內存布局圖示

通過程序輸出看一下
typedef void (*PF)(); int main() { T t; PF pf1, pf2; cout<<"vfptr的地址"<<(int*)&t<<endl; cout<<"vftable的地址"<<(int*)*(int*)&t<<endl; cout<<"通過vftable調用類T的fun函數: "; pf1 = (PF)*(int*)*(int*)&t; pf1(); cout<<"通過vftable調用類Q的fun函數: "; pf2 = (PF)*(int*)*(int*)&t.q; pf2(); return 0; }
輸出圖示

推理證明:
1.取t的地址強轉成(int*)類型輸出以后得到的地址 == 取t的vfptr的地址(調試窗口第一行): 虛函數指針被放在對象布局的首地址位置
2.因為(int*)&t == vfptr,那么*vfptr得到的是虛函數表的首地址。
(int*)*vfptr,把虛函數表的首地址強轉成(int*)的地址 == t對象的__vftable的虛函數表的地址(調試窗口第四行行):虛函數指針指向虛函數表
3.vftable的首地址到vftable的第一個函數的地址中間相差很多空間:虛函數表還承擔了虛函數以外的內容
什么內容也會放在虛函數表中呢?
虛函數表用來實現多態,多態意味着類型上的模糊,模糊以后必須有東西來記錄自己的老本,否則無法實現另外一個東西——RTTI。
結論:
在包含虛函數的場合多了一個vfptr,它是一個const指針,位於類布局中的首位置,指向了虛函數表,虛函數表包含了虛函數地址,通過虛函數地址訪問虛函數。
並且虛函數表的首地址存在了本類的類型信息,用於實現RTTI。
5.包含了static的場合
static的特性眾所周知,從調試窗口觀察變量並不能得出什么結論,我們先列出幾條特性:
1.static成員為整個類共有的屬性
2.static函數不包含this指針
3.static成員不能訪問nonstatic成員
初步結論:
內存對象模型中對static作了隔離處理(不是所有對象具有的),static自己獨霸一方。
通過以上5條現在來構建C++的封裝模型:

有關普通的成員函數
所謂類,就是自己圈定了一個域名,所以在內存中的代碼區也圈定了自己的域,普通的成員函數放在那里。
有關靜態成員函數
在代碼區中圈定的類域名中的圈定一個static區域,思路依然是獨霸一方。
有關構造函數
由於構造函數的特殊性,所以在代碼區擁有一個自己的構造代碼區域。
現在又有了一個更完整的模型:
假定讀者已經了解堆/棧/靜態區和常量區/代碼區

根據上圖我們得到一些結論
1.類最終被解釋內建類型(內建類型過了編譯期以后,都不復存在,只是編譯期的解讀方式而已)
2.內建類型按照聲明的次序順序存儲
3.存在虛函數的場合,會生成vfptr,並且vfptr->vtable->function()
4.靜態成員被單獨對待、數據只有一份拷貝,函數被放到static區域。
5.Type Infomation被放到vftable中
二、封裝模型的構造過程
1.靜態是編譯期決定的,所有對象共有的數據拷貝,優先創建。
2.進入構造函數,優先創建vfptr和vftable,也就是優先構造虛函數部分
3.其次按照聲明的順序構造數據成員。
我們可以使用逗號表達式來干一些有意思的事情。
事先我們需要定義
typedef void (*PF)();
PF pf = NULL;
class Q { public: Q():b((cout<<"b constructing\n", 1)), a((cout<<"a constructing\n", 2)){}//組合對象的初始化順序,注意初始化列表寫的順序是和聲明的順序相反的 virtual void fun(){cout<<"Q::f"<<endl;} int a; int b; }; class T { public: T():data1(((pf =(PF)*(int*)*(int*)this, pf()), cout<<"data1 constructing\n", 1)), data2((cout<<"data2 constructing\n", 2)){}//data2的構造使用了簡單的逗號表達式 //data1的初始化嵌套了一層逗號表達式,結構其實是data1((為函數指針pf賦值, 調用pf), 打印data1構造中, 數值) virtual void fun(){cout<<"T::f"<<endl;} int data1; Q q; double data2; static int sdata1; }; int T::sdata1 = (cout<<"sdata1 constructing\n", 10);//用來指定靜態變量的初始化順序
以下是程序運行的結果:

靜態--虛函數表--聲明次序初始化。
文章不免有疏忽和不足的地方,歡迎大家批評指正。郵箱【betachen@yeah.net】
下一篇重點講繼承時和多態時的內存布局。
