C++內存中的封裝、繼承、多態(上)


繼我的上一篇文章:淺談學習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】

下一篇重點講繼承時和多態時的內存布局。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM