C++成員變數記憶體模型
0X00.不帶繼承類記憶體佈局
類變數記憶體中有哪些內容
靜態變數:靜態變數被放在全域性區的靜態區中,並不在變數中。 函式(非類成員函式,成員函式):程式碼區
每一個類變數的記憶體佈局中沒有這個類的函式資訊,只包含成員,虛擬函式表指標(vfptr),虛繼承表指標(vtptr)(不同編譯器對虛繼承實現不一致,本篇用微軟的cl編譯器做例項)。
class A{
public:
void print() {
cout << d << endl;
}
int d;
};
類A的記憶體佈局如下: 只有這個成員變數,並沒有定義的函式資訊。
成員的記憶體地址標準
一個類中的成員變數是如何佈局的? 現在我們有一段程式碼,程式碼的如下。
class A{
public:
int a;
char a1;
char a2;
char a3;
};
在C++的標準中規定後出現的成員變數應該在記憶體的更高位地址(這邊注意沒有規定連續),所以A中的成員變數應該從低地址->高地址順序為:a->a1->a2->a3。下面這張圖是通過vs編譯器檢視編譯後的記憶體結構,但是隻能說明是按一定順序排列的,我們可以打印出地址檢視是否後出現的元素在地址。
通過該程式碼直接打印出類A中元素的記憶體地址
A a; cout << "a.a = "<< (int)(&a.a) << endl; cout << "a.a1 = " << (int)(&a.a1) << endl; cout << "a.a2 = " << (int)(&a.a2) << endl; cout << "a.a3 = " << (int)(&a.a3) << endl;
輸出如下
上面輸出可以看出,類中的成員變數由出現順序a->a3, 在記憶體地址由低到高中的順序也是a->a3。 這個例子,為了說明成員的地址是根據出現的順序由低到高這個標準。
什麼時候記憶體不會連續
標準只規定了後面出現的成員變數地址更大(在編譯器沒有給你做優化的情況下),沒有規定連續。 在有記憶體對齊(詳細介紹)的情況(記憶體對齊是因為某些平臺不支援隨意的讀取記憶體,只能支援特定位置開始)類成員變數就不會有連續的記憶體地址。 當類A的定義如下圖,這個時候會產生記憶體對齊。
#include <iostream> #include "stdio.h" using namespace std; class A{ public: char a1; int a;//產生記憶體對齊 char a2; char a3; }; int main() { A a; //切記輸出順序和變數先後順序是一樣的 cout << "a.a1 = " << (int)(&a.a1) << endl; cout << "a.a = " << (int)(&a.a) << endl; cout << "a.a2 = " << (int)(&a.a2) << endl; cout << "a.a3 = " << (int)(&a.a3) << endl; }
a1變數雖然是char型別,但是距離a變數也有4個對應位元組,編譯器會在a1後插入3個位元組,a2,a3後有2個位元組用於記憶體對齊。
經過記憶體對齊後,佈局帶有“alignment”填充欄位。
有虛繼承的時候也會導致記憶體不連續。
0X01帶繼承的記憶體佈局
繼承無虛擬函式無虛繼承
在只有單繼承的情況看下類的記憶體佈局,下面是一個類的程式碼
class A{
int a;
};
class B : public A{
int b;
};
B繼承自A,編譯後我們看下B的記憶體佈局
可以看出其實就是很簡單的把A記憶體佈局拷貝一份到B的起始位置,然後接下去放置B的成員變數。只有的單繼承並不會新增別的東西。
多繼承的情況也是類似,不會新增任何的東西,只是順序的把父類的記憶體佈局根據繼承的先後順序拷貝下來(沒有虛繼承的情況)。
class A{
int a;
};
class B{
int b;
};
class C : public B , public A {
int c;
};
記憶體佈局圖如下
只帶虛擬函式的繼承
在c++我們經常會宣告一個函式為虛擬函式,那麼在有虛擬函式的時候是什麼樣子的呢?我們定義一個類A看下編譯後的結果
class A{
public:
//規定了一個虛擬函式
virtual void func() {
}
int a;
};
這個類中除了成員還有一個虛擬函式,擁有虛擬函式的類中都有一個指向虛擬函式表的指標,用於在執行期確定呼叫的是哪一個函式。
現在我們知道具有虛擬函式的類記憶體佈局,那麼加上繼承是什麼樣呢?其實和沒有虛擬函式一樣,子類會把父類的記憶體空間佈局完美的複製一份(在沒有虛繼承的情況下)。
下面這個類B的記憶體佈局,B繼承自A。
class A{
//規定了一個虛擬函式
virtual void func() {
}
int a;
};
class B : public A{
virtual void func2() {
};
int b;
};
這個是經過編譯後看到B的記憶體佈局。 擁有一個虛擬函式表指標,還有子類的成員和自己的成員。自始至終都只有一個虛擬函式表指標,儲存實際函式地址在於另外一個表中,如下圖。 c++中並沒有規定虛擬函式表的實現,不同的編譯器對虛擬函式表也是有各自不同的實現方式。
帶虛繼承的函式
c++中虛繼承主要是用於重複繼承相同的父類。解決的問題是:在重複繼承父類元素後,一個類中會有重複父類相同的拷貝。
我們有如下的程式碼,C中重複繼承了A類,那麼我們C中就會有兩個C記憶體佈局的拷貝。
class A{
int a;
};4
class B : public A{
int b;
};
class C : public B, public A{
int c;
};
下面是編譯後的C中的記憶體佈局。 很容易看出來在地址為0的時候有a變數,在地址為8的時候有a變數,對使用者來說他只知道有一個a,這就導致了記憶體的浪費。如果我們用虛繼承就可以解決掉這個問題。
當我們某個類(或者這個類的子類)有可能出現重複繼承某個基類時,我們需要使用虛繼承。 如下段程式碼:
class A{
int a;
};
class B : virtual public A{
int b;
};
class C : virtual public A{
int c;
};
class D : public B, public C {
int d;
};
編譯後D的記憶體佈局如下圖,注意我們這邊用的是vs的cl編譯器編譯後的結果,不同編譯器對虛繼承的實現也不一樣 下面是虛擬函式表的內容 即使我們重複繼承了物件A,但是在虛繼承作用下還是隻有一個類A的記憶體佈局。在虛基類(使用了虛繼承關鍵字的類)中有一個指標vbptr,這個指標指向一個虛繼承表,表中記錄著表距離類開始位置的偏移和公共變數距離vbptr的偏移(切記是相對於vbptr的偏移),比如B中距離類開始偏移為0,距離公共位置的偏移是20。當我們需要訪問公共變數的時候,編譯器就需要通過vbptr來尋找具體位置。
為什麼虛繼承要這樣做呢?
為什麼需要vbptr這種東西,類的成員變數在哪編譯器應該知道的啊?其實vbptr和vfptr作用相似,子類指標型別賦值給一個父類的指標型別時才會展示出作用。 還是上面那一段程式碼中,其中類B的記憶體佈局是 看下B中虛繼承表的內容,第一個表示距離類開始的偏移,第二個值表示到公共變數的偏移 假設我們有一段程式碼,ptr1中儲存的是D類的變數指標,ptr2儲存的是B類的變數指標。
D d;
B *ptr1 = &d;
B b;
B *ptr2 = &b;
我們都用B類指標訪問a類成員,在執行期間我們也不清楚這個指標指向的記憶體到底是什麼型別,D類記憶體中和B類記憶體中需要偏移不同的值才能找到a變數。如果有虛繼承表時我們先去查下偏移多少到a,B類中儲存的是8,D類中儲存的是20,這樣就能準確的找到公共變數的位置了。