C++ 類的內存結構


代碼與可執行文件

代碼段,數據段,BSS段,堆、棧

數據段、代碼段、堆棧段、BSS段的區別
程序內存空間(代碼段、數據段、堆棧段)

  • 代碼段:簡單說就是存儲函數與常量的地方。C/C++寫的成員函數,非成員函數都是在這里。
  • 數據段:初始化的全局變量,初始化的靜態變量 被編譯器放在這里
  • BSS 段: 這里存放未初始化的全局變量,未初始化的靜態變量。BSS 部分並不占據存儲空間,編譯器只是把這些為初始化的全局/靜態變量記錄在這里。內存空間要等到執行階段由系統分配,並完成初始化!這也是為什么內存結構上BSS 部分在棧空間下面的原因。
  • 堆、棧:這個比較好理解,不做介紹了

一個小c 程序demo

#include <stdio.h>

int global_init = 1;
int global_uinit;
static int global_static_init = 1;
static int global_static_uinit;
int main()
{
    int local_init = 1;
    int local_uinit;
    static int local_static_init = 1;
    static int local_static_uinit;

    return 0;
}

用 nm 查看結果:其中 D、d 都是表示 data 段; B、b 表示BSS 段。詳細介紹參考 man nm

C++ 對象與存儲

C++成員函數在內存中的存儲方式

虛指針理解為一個 int64_t* 的數組,每個數組成員都是函數指針

先說明幾點:

  • 空類大小不為 0
  • 靜態類成員函數與類綁定,並不會被注入 this 指針
    因此靜態成員函數可以通過類名直接調用,不需要創建類對象。同樣因為沒有 this 指針,所以靜態成員函數也不能調用普通成員函數,只能訪問靜態成員變量。
  • 普通成員函數為類全局共享,不與類實例綁定
    但是普通成員函數的調用需要綁定實例,這是因為普通成員函數 this 指針的存在; 也因為普通成員函數綁定的不是類實例,所以普通繼承關系不具有多態,而是由指針決定。
  • 虛表為類全局共享,不與類實例綁定; 虛指針與類實例綁定
    虛表是全局存在的,相當於一個全局變量,而不是每個類實例都創建一個虛表,虛表只能通過虛指針來訪問。
    虛指針在Linux G++/Clang++ 實現是放在類的最前面。須指針指向虛表的操作由構造函數初始化。
  • 成員函數不占用類的內存空間
    即 new 一個對象只是創建了對象的數據部分,並不包含函數部分

類的實際內存結構如下:

虛表與虛指針

說明:

  1. 虛表指針總是存在在類的頭部,並按類的繼承順序排放。一個子類可以有多個虛表指針,且虛指針個數和具有虛函數的基類個數相同。

  2. 虛成員函數總是按照聲明順序存在於虛表中。

  3. 如果存在同名函數,子類虛函數會覆蓋每一個父類的每一個同名虛函數。

  4. 子類獨有的虛函數填入第一個虛函數表中,且用父類指針是不能調用。

  5. 父類獨有的虛函數不會被覆蓋覆蓋。僅子類和該父類指針能調用。

如下圖類的內存結構圖


圖中代碼參考鏈接

無虛函數

class Drive
{
public:
    void f() {}
};

int main()
{
    Drive d;

    cout << sizeof(d) << endl;
    return 0;
}

如下,類中沒有虛函數,只有一個成員函數,以及其他默認構造析構函數。類似於空類,類的大小為1(注意空類大小不為0,因為為0的話,實例化后沒法區分)。因此可以的出結論類的非虛成員函數信息不存在於對象實例中!

無繼承

代碼:

class Drive
{
public:
    virtual void vf() {}
    void f() {}
};

int main()
{
    Drive d;

    return 0;
}

如下,子類經過強制類型轉換,得到虛表指針,並提取虛表指針的內容,經過轉換可以得到第一個虛函數。虛表中只有一個虛函數。

單繼承

class Base1
{
public:
    virtual void vb1f() {}
    virtual void vf() {}
};

class Drive : public Base1
{
public:
    virtual void vdf() {}
    virtual void vf() {}
    void f() {}
};

int main()
{
    Drive d;

    return 0;
}

虛表中只有多個虛函數。順序是父類,子類的順序。其中注意到雙方共有的虛函數 “vf”, 在虛表中子類的虛函數覆蓋了父類的需函數。

多繼承

class Base1
{
public:
    virtual void vb1f() {}
    virtual void vf() {}
};

class Base2
{
public:
    virtual void vb2f() {}
    virtual void vf() {}
};

class Drive : public Base1, Base2
{
public:
    virtual void vdf() {}
    virtual void vf() {}
    void f() {}
};

int main()
{
    Drive d;

    return 0;
}

虛表中只有多個虛函數。順序是父類Base1, 父類Base2,子類。

查看第一個虛表:其中注意到雙方共有的虛函數 “vf”, 在虛表中子類的虛函數覆蓋了父類的需函數。另外子類的虛函數 ”vdf“ 被放在了第一個虛表的后面。

查看第二個虛表:第二個虛表指針在地一個虛表指針后面。同樣方式可以看到第二個虛表只有父類 Base2 的虛成員函數,而且共有的虛函數被子類的虛函數 vf 覆蓋。

虛繼承(菱形繼承)

單虛繼承情況和單繼承完全一樣,這里忽略,直接描述虛繼承的菱形繼承情況
注:虛繼承在虛根基類初始化不一樣,這也是為什么“用虛繼承實現不能繼承的類”的原理

class Base
{
public:
   virtual void vbbf() {}
   virtual void vbf() {}
};

class Base1 : virtual public Base
{
public:
   virtual void vb1f() {}
   virtual void vf() {}
};

class Base2 : virtual public Base
{
public:
   virtual void vb2f() {}
   virtual void vf() {}
};

class Drive : virtual public Base1, virtual public Base2
{
public:
   virtual void vdf() {}
   virtual void vf() {}
   void f() {}
};

int main()
{
   Base1 b1;
   Base2 b2;
   Drive d;

   return 0;
}

如上可以看到地一個虛表和預期完全一樣,按照繼承的順序,虛函數的順序存在虛表中。
但是第二個虛表就不一樣了,第二個虛表的前兩個成員都是空的,並不是指向 Base 的虛函數,所以可以知道編譯器在這里做了處理,避免了菱形繼承中尷尬的情況。


免責聲明!

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



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