0X00不帶繼承類記憶體布局
類變量記憶體中有哪些内容
靜态變量:靜态變量被放在全局區的靜态區中,并不在變量中。
函數(非類成員函數,成員函數):代碼區
每一個類變量的記憶體布局中沒有這個類的函數資訊,隻包含成員,虛函數表指針(vfptr),虛繼承表指針(vtptr)(不同編譯器對虛繼承實作不一緻,本篇用微軟的cl編譯器做執行個體)。
class A{
public:
void print() {
cout << d << endl;
}
int a;
};
類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個位元組,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,這樣就能準确的找到公共變量的位置了。
0X02附錄
參考書籍:《深度探索C++對象模型2012版》
轉載請聲明來自:https://blog.csdn.net/lqq_419/article/details/83314932