1. 程式人生 > >C++ Data Member記憶體佈局

C++ Data Member記憶體佈局

如果一個類只定義了類名,沒定義任何方法和欄位,如class A{};那麼class A的每個例項佔用1個位元組的記憶體,編譯器會會在這個其例項中安插一個char,以保證每個A例項在記憶體中有唯一的地址,如A a,b;&a!=&b。如果一個直接或是間接的繼承(不是虛繼承)了多個類,如果這個類及其父類像A一樣沒有方法沒有欄位,那麼這個類的每個例項的大小都是1位元組,如果有虛繼承,那就不是1位元組了,每虛繼承一個類,這個類的例項就會多一個指向被虛繼承父類的指標。還有一點值得說明的就是像A這樣的類,編譯器不一定會產生傳說中的那6個方法,這些方法只會在需要的時候產生,如class  A沒有被任何地方使用那這些方法編譯器就沒有必要產生,如果這個類例項化了,那麼會產生default constructor,而destructor則不一定產生。

如果一個類中有static data member,nonstatic data member,還有const data member,enum,那麼它的記憶體佈局會是什麼樣的呢,看下面簡單的類Point:

class Point
{
public:
    Point():maxCount(10){}
private:
    int X;
    static int count;
    int Y;
    const int maxCount ;
    enum{
        minCount=2
    };
};

Sizeof(Point)=12,為什麼佔12位元組呢,我相信很多人都知道是哪幾個成員變數佔用的,就是X,Y,maxCount,maxCount作為常量欄位,但在Point的每個例項中可能有不同的值,當然屬於Point例項的一部分,如果把maxCount定義成static,那它就不不是Point例項的一部分了,如果定義成static  const int maxCount=1;則maxCount分配在.data段中,如果沒有初始化則分配在.bss段中,反正跟Point的例項無關,count分配在.bss段中,minCount分配在.rdata段中,總之count,maxCount,minCount在編譯連線完成之後,記憶體(虛擬地址)就分配好了,在程式載入的時候,會把他們的虛擬地址對應上實際的實體地址。

Data member的記憶體佈局:nonstatic data member在class object中的順序和其申明的順序一樣,static data  member和const member不在class object中因為他們只有一份,被class object共享,所以static data member和const data member,列舉並不會響應class object的大小。關於段的資訊,我覺得是每個C/C++程式設計師必須知道的。而Point每次例項化的時候則只需要分配X,Y,maxCount需要的記憶體。

每個類的data member在記憶體中應該是連續的,如果出現數據對齊的情況,可能中間會有空白地帶。請看下面幾個類:

class AA
{ 
protected:
    int X;
    char a; 
};

class BB:public AA
{ 
protected: 
    char b;
};

class CC:public BB
{ 
protected: 
    char c;
};

Sizeof(AA)=8//對齊3位元組

Sizeof(BB)=12//兩個3位元組對齊

Sizeof(CC)=16//編譯器“無恥”的用了3個3位元組對齊

關於記憶體對齊問題我相信大家經常遇到,我就不廢話,我之前寫過一篇關於記憶體對齊的文章《資料對齊

編譯器為什麼要無恥的在class CC中加3個3位元組對齊呢,這樣每個CC的例項就大了9位元組。如果編譯器不加這9位元組的空白,那麼CC的每個例項就是8位元組,前面的X佔4位元組,後面的a,b,c佔3位元組,加1位元組的空白對齊,剛好8位元組,沒有誰很傻很天真的以為最好是佔7位元組吧。

如果CC佔用8位元組記憶體,同樣的AA,BB都是8位元組的記憶體,這樣的話,如果把一個指向AA例項的指標賦給一個指向CC例項的指標,那麼就會把AA中的8位元組直接蓋到CC的8位元組上,結果CC例項中的b,c都被賦上了不是我們想要的值,這很可能會導致你的程式出問題。

父類的data member會在子類的例項中有完整的一份,這樣在有繼承關係的類之間進行型別轉換,就只用簡單的修改指標的指向。

Data Member的存取。對一個data member的存取,編譯器把物件例項的起始地址加上data member的偏移量。如CC c;

c.X=1;相當於&c+(&CC::X-1),減一其實是為了區分是指向object的指標還是指向data member的指標,指向data member的要減一。每一個data member的偏移量在編譯的時候是知道的,根據成員變數的型別和記憶體對齊,存在virtual繼承或是虛方法的情況編譯器會自動加上一些輔助的指標,如指向虛方法的指標,指向虛繼承父類的指標等。

在data member的存取效率上,struct member 、class member、單一繼承或是多重繼承的情況下效率都是一樣的,因為他們的儲存其實都是&obj+(&class.datamember-1)。在虛繼承的情況下,可能會影響儲存效能,如通過一個指標來存取一個指向虛繼承而來的data member,那麼效能會有影響,因為在虛繼承的時候,在編譯的時候還不能確定這個data member是來自子類還是父類,只有在執行的時候才能推斷出來,其實就是多了一步指標的操作,在虛繼承中,如果是通過物件例項來操作虛繼承而來的data member,則不會有任何效能問題,因為不存在什麼多型性,所有東西在編譯的時候記憶體地址都確定了。

虛繼承還是虛方法為了實現多型一樣,多了一步,如果不需要多型,而是通過物件例項呼叫相關的方法就不會有效能問題。