1. 程式人生 > >C++成員變數記憶體模型

C++成員變數記憶體模型

0X00.不帶繼承類記憶體佈局

類變數記憶體中有哪些內容

靜態變數:靜態變數被放在全域性區的靜態區中,並不在變數中。 函式(非類成員函式,成員函式):程式碼區

每一個類變數的記憶體佈局中沒有這個類的函式資訊,只包含成員,虛擬函式表指標(vfptr),虛繼承表指標(vtptr)(不同編譯器對虛繼承實現不一致,本篇用微軟的cl編譯器做例項)。

class A{

public:
    void print() {
        cout << d << endl;
    }
    int d;
};

類A的記憶體佈局如下: 類A的記憶體描述 只有這個成員變數,並沒有定義的函式資訊。

成員的記憶體地址標準

一個類中的成員變數是如何佈局的? 現在我們有一段程式碼,程式碼的如下。

class A{
public:
    int  a;
    char a1;
    char a2;
    char a3;
};

在C++的標準中規定後出現的成員變數應該在記憶體的更高位地址(這邊注意沒有規定連續),所以A中的成員變數應該從低地址->高地址順序為:a->a1->a2->a3。下面這張圖是通過vs編譯器檢視編譯後的記憶體結構,但是隻能說明是按一定順序排列的,我們可以打印出地址檢視是否後出現的元素在地址。 cl編譯器編譯後的記憶體結構

通過該程式碼直接打印出類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的記憶體佈局

B的記憶體佈局 可以看出其實就是很簡單的把A記憶體佈局拷貝一份到B的起始位置,然後接下去放置B的成員變數。只有的單繼承並不會新增別的東西。

多繼承的情況也是類似,不會新增任何的東西,只是順序的把父類的記憶體佈局根據繼承的先後順序拷貝下來(沒有虛繼承的情況)。

class A{
    int a;
};


class B{
    int b;
};

class C : public B , public A {
    int c;
};

記憶體佈局圖如下

C類的記憶體佈局

只帶虛擬函式的繼承

在c++我們經常會宣告一個函式為虛擬函式,那麼在有虛擬函式的時候是什麼樣子的呢?我們定義一個類A看下編譯後的結果

class A{
public:
    //規定了一個虛擬函式
    virtual void func() {
    }
    int a;
};

這個類中除了成員還有一個虛擬函式,擁有虛擬函式的類中都有一個指向虛擬函式表的指標,用於在執行期確定呼叫的是哪一個函式。

類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編譯器編譯後的結果,不同編譯器對虛繼承的實現也不一樣 類D的記憶體佈局 下面是虛擬函式表的內容 虛擬函式表 即使我們重複繼承了物件A,但是在虛繼承作用下還是隻有一個類A的記憶體佈局。在虛基類(使用了虛繼承關鍵字的類)中有一個指標vbptr,這個指標指向一個虛繼承表,表中記錄著表距離類開始位置的偏移和公共變數距離vbptr的偏移(切記是相對於vbptr的偏移),比如B中距離類開始偏移為0,距離公共位置的偏移是20。當我們需要訪問公共變數的時候,編譯器就需要通過vbptr來尋找具體位置。

為什麼虛繼承要這樣做呢?

為什麼需要vbptr這種東西,類的成員變數在哪編譯器應該知道的啊?其實vbptr和vfptr作用相似,子類指標型別賦值給一個父類的指標型別時才會展示出作用。 還是上面那一段程式碼中,其中類B的記憶體佈局是 類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,這樣就能準確的找到公共變數的位置了。