首頁 > 軟體

C++記憶體對齊的實現

2023-02-06 06:00:19

記憶體對齊的基本原則:

  • 結構(struct/class)的內建型別資料成員,第一個資料成員放在offset為0的地方,以後每個資料成員的起始位置要從自身大小的整數倍開始儲存(特別注意64位元機器的指標大小為8個位元組)。
  • 如果一個結構A裡有結構體成員B,則結構體B要從其內部"最寬基本型別成員”的整數倍地址開始儲存(如struct a裡存有struct b,b裡有char, int, double等元素,那b應該從8的整數倍位置開始儲存)。
  • 結構體的總大小為結構體的有效對齊值的整數倍,結構體的有效對齊值的確定:
    • 當未明確指定時,以結構體或結構體所包含結構體成員中最長的成員長度為其有效值。
    • 當用#pragma pack(n)指定時,以n和結構體中最長的成員的長度中較小者為其值。
    • 當用__attribute__ ((packed))指定長度時,強制按照此值為結構體的有效對齊值。
    • 不管# pragma pack和__attribute__如何指定,結構體內部成員的自對齊仍然按照其自身的對齊值。
  • union以結構裡面size最大元素為union的size,因為在某一時刻,union只有一個成員真正儲存於該地址。

空類/靜態成員

程式 1

class A{
};

int main() {
    cout << sizeof(A) << endl;    // 1
}

對於一個什麼都沒有的空類,實際並不是空的,因為有預設的函數,具體可以參考 (待填入網址),大小是 1,這是因為需要有一個地址,C++ 不允許兩個不同的物件有相同的地址,所以 C++ 中空的類和結構體大小都是 1。

程式 2

class A{
    A(){}
    ~A(){}
    void print() { printf("print()n"); }
    void foo() { printf("print()n"); }

    static void sprint() { printf("sprint()n"); }
};

int main() {
    cout << sizeof(A) << endl;    // 1
}

這個類的大小仍然是1,成員函數、靜態成員函數、靜態成員變數都是不佔用類的記憶體的,這是因為這些東西都是類的,而不是每個物件分別儲存。static變數就是儲存在全域性靜態區。

需要注意的是,子類繼承空類後,子類如果有自己的資料成員,而空基礎類別的1個位元組並不會加到子類中去。

程式 3

class Empty {};
struct D : public Empty {
    int a;
};

sizeof(D)為4。

再來看另一種情況,一個類包含一個空類物件資料成員,則空類物件的大小仍為1。

程式 4

class Empty {};
class HaveAnInt {
    int x;
    Empty e;
}

在大多數編譯器中,你會發現 sizeof(HaveAnInt) 輸出為8。這是由於,Empty類的大小雖然為1,然而為了記憶體對齊,編譯器會為HaveAnInt額外加上一些位元組,使得HaveAnInt被放大到足夠又可以存放一個int。

內建型別資料成員

程式 1

class Data
{
    char c;
    int a;
};
 
cout << sizeof(Data) << endl;

程式 2

class Data
{
    char c;
    double a;
};
 
cout << sizeof(Data) << endl;

顯然程式 1 輸出的結果為 8,程式 2 輸出的結果為 16 .

程式 1 最大的資料成員是4bytes,1+4=5,補齊為4的倍數,也就是8。而程式 2 為8bytes,1+8=9,補齊為8的倍數,也就是16。

程式 3

class Data
{
    char c;
    int a;
    char d;
};
 
cout << sizeof(Data) << endl;

程式 4

class Data
{
    char c;
    char d;
    int a;
};
 
cout << sizeof(Data) << endl;

程式 3 執行結果為 12,程式 4 執行結果為 8

class中的資料成員放入記憶體的時候,記憶體拿出一個記憶體塊來,資料成員們排隊一個一個往裡放,遇到太大的成員時,不是將其劈成兩半能放多少就放多少,而是等下一個記憶體塊過來。這樣的話,就可以理解為什麼程式 3 和程式 4 兩段程式碼輸出結果不一樣了,因為程式 3 是
1 + (3) + 4 + 1 + (3) = 12,而程式 4 是1 + 1 + (2) + 4 = 8。括號中為補齊的bytes。

結構體資料成員

在預設條件下,記憶體對齊是以class中最大的那個基本型別為基準的,如果class中的資料成員包含其他class,則遞迴的取其中最大的基本型別來參與比較。

程式 1

class BigData
{
    char array[33];
};
 
class Data
{
    BigData bd;
    int integer;
    double d;
};
 
cout << sizeof(BigData) << "   " << sizeof(Data) << endl;

程式 2

class BigData
{
    char array[33];
};
 
class Data
{
    BigData bd;
    double d;
};
 
cout << sizeof(BigData) << "   " << sizeof(Data) << endl;

程式 1 和程式 2 執行結果均為:33 48

程式 1 和程式 2 中記憶體對其的基準均為8位元組,BigData的大小均為33。在程式 1 中,BigData接下來是個int(4bytes),能夠放下,這時候記憶體塊還剩3bytes,而接下來是個double(8bytes),放不下,所以要等下一個記憶體快到來。因此,程式 1 的Data的size = 33 + 4 + (3) + 8 = 48,同理程式 2 應該是
33 + (7) + 8 = 48。

程式 3

 class A {                                                                                         
 public:                                                                                           
     double len;                                                                                   
     char str[33];                                                                                 
 };                                                                                                
                                                                                                   
 class B {                                                                                         
 public:                                                                                                                                          
     A a;                                                                                          
     int b;                                                                                        
 };

cout << sizeof(A) << "  " << sizeof(B) << endl;

以上程式碼輸出的結果為: 48 56
不同於程式 1 和程式 2 ,程式 3 中的class A實際會佔用41位元組,但會發生8位元組對齊,所以大小為48位元組。對於class B,成員b的起始位置已發生8位元組對齊,而class B整體還會發生8位元組對齊,所以最終大小為56。

虛擬函式

C++ 的類中如果有虛擬函式,類內就會有一個虛擬函式表的指標 _vptr,指向自己的虛擬函式表,vptr 一般都是在類的最前邊(取決於編譯器的實現)。

class A {
public:
    A(){}
    virtual ~A(){}
    virtual void foo(){}
    virtual void print() {}
};

由於只是存一個指向虛擬函式表的指標,所以不管有多少個虛擬函式,都是 4 位元組大小(32位元下,任何指標大小都是 4,64位元下,任何指標大小都是 8),比如上面這個類 A,size 就是 4。

需要注意的是就是,對於沒有 override 的虛擬函式,基礎類別和子類中 _vptr 指向的虛擬函式表中,這個虛擬函式的地址是一樣的,也就是上邊的 foo() 函數,而對於重寫了的或者預設重寫的解構函式來說,_vptr 指向的虛擬函式表中,函數地址是不一樣的(當然兩個類的 _vptr 地址也是不一樣的,這是肯定的),這就能窺探到多型的實現了。

繼承

不同的編譯器對繼承後類的大小的計算方式不同,有的是先繼承後對齊,有的是先對齊後繼承。

class A
{
    int i;
    char c1;
}

class B:public A
{
    char c2;
}

class C:public B
{
    char c3;
}

sizeof(C)結果是多少呢,gcc和vs給出了不同的結果,分別是8、16。

  • gcc中:C相當於把所有成員i、c1、c2、c3當作是在一個class內部,(先繼承後對齊)
  • vs中:對於A,對齊後其大小是8;對於B,c2加上對齊後的A的大小是9,對齊後就是12;對於C,c3加上對齊後的B大小是13,再對齊就是16 (先對齊後繼承)

記憶體對齊的意義

  • 效率原因:經過記憶體對齊之後,CPU的記憶體存取速度大大提升。
  • 平臺原因(移植原因):不是所有的硬體平臺都能存取任意地址上的任意資料,某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。

比較兩個結構體可以使用memcmp(void*, void*)嗎?

不可以,memcmp函數是逐個位元組進行比較的,而struct存在記憶體對齊,記憶體對齊時補的位元組內容是垃圾值,所以無法比較。

到此這篇關於C++記憶體對齊的實現的文章就介紹到這了,更多相關C++記憶體對齊內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com