代碼與可執行文件
代碼段,數據段,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++ 對象與存儲
虛指針理解為一個 int64_t* 的數組,每個數組成員都是函數指針
先說明幾點:
- 空類大小不為 0
- 靜態類成員函數與類綁定,並不會被注入 this 指針
因此靜態成員函數可以通過類名直接調用,不需要創建類對象。同樣因為沒有 this 指針,所以靜態成員函數也不能調用普通成員函數,只能訪問靜態成員變量。 - 普通成員函數為類全局共享,不與類實例綁定
但是普通成員函數的調用需要綁定實例,這是因為普通成員函數 this 指針的存在; 也因為普通成員函數綁定的不是類實例,所以普通繼承關系不具有多態,而是由指針決定。 - 虛表為類全局共享,不與類實例綁定; 虛指針與類實例綁定
虛表是全局存在的,相當於一個全局變量,而不是每個類實例都創建一個虛表,虛表只能通過虛指針來訪問。
虛指針在Linux G++/Clang++ 實現是放在類的最前面。須指針指向虛表的操作由構造函數初始化。 - 成員函數不占用類的內存空間
即 new 一個對象只是創建了對象的數據部分,並不包含函數部分
類的實際內存結構如下:
虛表與虛指針
說明:
-
虛表指針總是存在在類的頭部,並按類的繼承順序排放。一個子類可以有多個虛表指針,且虛指針個數和具有虛函數的基類個數相同。
-
虛成員函數總是按照聲明順序存在於虛表中。
-
如果存在同名函數,子類虛函數會覆蓋每一個父類的每一個同名虛函數。
-
子類獨有的虛函數填入第一個虛函數表中,且用父類指針是不能調用。
-
父類獨有的虛函數不會被覆蓋覆蓋。僅子類和該父類指針能調用。
如下圖類的內存結構圖
無虛函數
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 的虛函數,所以可以知道編譯器在這里做了處理,避免了菱形繼承中尷尬的情況。