天天看點

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

0X00不帶繼承類記憶體布局

類變量記憶體中有哪些内容

靜态變量:靜态變量被放在全局區的靜态區中,并不在變量中。

函數(非類成員函數,成員函數):代碼區

每一個類變量的記憶體布局中沒有這個類的函數資訊,隻包含成員,虛函數表指針(vfptr),虛繼承表指針(vtptr)(不同編譯器對虛繼承實作不一緻,本篇用微軟的cl編譯器做執行個體)。

class A{

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

類A的記憶體布局如下:

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

隻有這個成員變量,并沒有定義的函數資訊。

成員的記憶體位址标準

一個類中的成員變量是如何布局的?

現在我們有一段代碼,代碼的如下。

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

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

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

通過該代碼直接列印出類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;
           

輸出如下

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

上面輸出可以看出,類中的成員變量由出現順序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個位元組用于記憶體對齊。

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

經過記憶體對齊後,布局帶有“alignment”填充字段。

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

有虛繼承的時候也會導緻記憶體不連續。

0X01帶繼承的記憶體布局

沒有虛繼承和虛函數的繼承情況

在隻有單繼承的情況下類的記憶體布局,根據下面的代碼做分析

class A{
    int a;
};

class B : public A{
    int b;
};

           

B繼承自A,編譯後我們看下B的記憶體布局

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

可以看出其實就是很簡單的把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;
};

           

這個類中除了成員還有一個虛函數,擁有虛函數的類中都有一個指向虛函數表的指針,用于在運作期确定調用的是哪一個函數。

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

現在我們知道具有虛函數的類記憶體布局,那麼加上繼承是什麼樣呢?其實和沒有虛函數一樣,子類會把父類的記憶體空間布局完美的複制一份(在沒有虛繼承的情況下)。

下面這個類B的記憶體布局,B繼承自A。

class A{
    //規定了一個虛函數
    virtual void func() {

    }
    int a;
};

class B : public A{
    virtual void func2() {

    };
    int b;
};
           

這個是經過編譯後看到B的記憶體布局。

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

擁有一個虛函數表指針,還有子類的成員和自己的成員。自始至終都隻有一個虛函數表指針,儲存實際函數位址在于另外一個表中,如下圖。

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

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中的記憶體布局。

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編譯器編譯後的結果,不同編譯器對虛繼承的實作也不一樣

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

下面是虛函數表的内容

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

即使我們重複繼承了對象A,但是在虛繼承作用下還是隻有一個類A的記憶體布局。在虛基類(使用了虛繼承關鍵字的類)中有一個指針vbptr,這個指針指向一個虛繼承表,表中記錄着表距離類開始位置的偏移和公共變量距離vbptr的偏移(切記是相對于vbptr的偏移),比如B中距離類開始偏移為0,距離公共位置的偏移是20。當我們需要通路公共變量的時候,編譯器就需要通過vbptr來尋找具體位置。

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

為什麼需要vbptr這種東西,類的成員變量在哪編譯器應該知道的啊?其實vbptr和vfptr作用相似,子類指針類型指派給一個父類的指針類型時才會展示出作用。

還是上面那一段代碼中,其中類B的記憶體布局是

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

看下B中虛繼承表的内容,第一個表示距離類開始的偏移,第二個值表示到公共變量的偏移

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

假設我們有一段代碼,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

繼續閱讀