1. 程式人生 > 實用技巧 >[C/C++知識點] 記憶體對齊機制

[C/C++知識點] 記憶體對齊機制

一、引言
首先看一個例子,現在有一個結構體如下:

typedef struct {
    char a;     // a佔1個位元組 
    int b;      // b佔4個位元組 
    float c;   // c佔4個位元組 
} STR;

問這個結構體STR在記憶體中總共佔多少個位元組?是否等於其內部所有成員所佔位元組數相加,即等於9個位元組?
可以很方便地寫段程式碼測試一下:

#include "stdio.h"

typedef struct {
    char a;   // a佔1個位元組 
    int b;    // b佔4個位元組 
    float c;  // c佔4個位元組 
} STR; int main() { printf("%d\n", sizeof(STR)); return 0; }

打印出12,顯然不等於9,也即一個結構體所佔位元組數並非等於其內部所有成員所佔位元組數之和。這是為何?

二、位元組對齊
1、對齊原因
(1)平臺原因(移植原因):
並非所有的硬體平臺都能訪問任意地址上的任意資料的,某些平臺只能在特定地址處存取某些特定型別的資料,否則會丟擲硬體異常。
(2) 效能原因:
如果不按照適合其平臺要求對資料存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設為32位系統)存放在偶地址開始的地方,那麼一個讀週期就可以讀出這32bit,而如果存放在奇地址開始的地方,就需要2個讀週期,並對兩次讀出的結果的高低位元組進行拼湊才能得到該32bit資料,這樣資料讀取效率就會很差。


往往程式設計師在寫程式的時候,不需要考慮對齊問題,編譯器會自行選擇適合目標平臺的對齊策略。
總的來講:結構體的記憶體對齊是拿空間來換取時間的做法。

2、結構體對齊規則
(1) 第一個成員位置在偏移量0,之後每個成員的偏移量必須是其本身所佔位元組數的整數倍。
(2) 巢狀結構體中,巢狀的結構體起始偏位置為其自己內部成員最大位元組數的整數倍。
(3) 巢狀結構體中,巢狀的結構體的總偏移量是其自己內部成員最大位元組數的整數倍。
(4) 結構體的總偏移量必須是它最大成員(含巢狀結構體內的成員)位元組數的整數倍,不足需補齊。

3、聯合體對齊規則
(1) 聯合體的記憶體不會為了所有成員安排,而是隻取最大成員的所需記憶體大小,每次只能使用其中一個成員。


(2) 聯合體大小還需滿足是所有成員大小的最小公倍數,若不滿足時,需要繼續補齊使滿足。

三、舉例分析
1、結構體
(1) 簡單結構體1

typedef struct {
    char a;   // a佔1個位元組 
    int b;    // b佔4個位元組 
    float c;  // c佔4個位元組 
} STR;

a佔1位元組,從0位置開始偏移1位,現在到1位置;
b佔4位元組,1不是4的整數倍,因此繼續往後偏移,直到4位置開始偏移4位,現在到8位置;
c佔4位元組,8是4的整數倍,從8位置開始偏移4位,現在到12位置;
該結構體成員中佔位元組數最大的是4位元組,而12是4的整數倍,因此偏移結束;
因此該結構體總共佔領記憶體大小為12個位元組。

(2) 簡單結構體2

typedef struct {
    char a;   // a佔1個位元組
    int b;    // b佔4個位元組
    char c;   // c佔4個位元組
} STR;

a佔1位元組,從0位置開始偏移1位,現在到1位置;
b佔4位元組,1不是4的整數倍,因此繼續往後偏移,直到4位置開始偏移4位,現在到8位置;
c佔1位元組,8是1的整數倍,從8位置開始偏移1位,現在到9位置;
該結構體成員中佔位元組數最大的是4位元組,而9不是4的整數倍,需補齊到12位置處;
因此該結構體總共佔領記憶體大小為12個位元組。

(3) 含陣列

typedef struct {
    char a;   // a佔1個位元組
    int b;    // b佔4個位元組
    char c[6];   // c佔1個位元組
    float d[3];  // d佔4個位元組 
} STR;

a佔1位元組,從0位置開始偏移1位,現在到1位置;
b佔4位元組,1不是4的整數倍,因此繼續往後偏移,直到4位置開始偏移4位,現在到8位置;
c佔1位元組,8是1的整數倍,因此從8位置開始偏移6位,現在到14位置;
d佔4位元組,14不是4的倍數,因此繼續向後偏移,直到16位置開始向後偏移12位,現在到28位置;
該結構體成員中佔位元組數最大的是4位元組,而28是4的整數倍,因此無需補齊;
因此該結構體總共佔領記憶體大小為28個位元組。

(4) 巢狀結構體1

typedef struct {
    char a;   // a佔1個位元組 
    int b;    // b佔4個位元組 
    struct str1 {
        int c;  // c佔4個位元組
        char d;  // d佔1個位元組
    };
    char e;  // e佔1個位元組
} STR;

a佔1位元組,從0位置開始偏移1位,現在到1位置;
b佔4位元組,1不是4的整數倍,因此繼續往後偏移,直到4位置開始偏移4位,現在到8位置;
struct str1中,其內部成員最大位元組為4,而8位置是4的整數倍,因此從8位置開始偏移;
c佔4位元組,8是4的整數倍,因此c從8位置偏移4位,現在到12位置;
d佔1位元組,12是1的整數倍,因此d從12位置偏移1位,現在到13位置;
由於13不是4的整數倍,因此需補齊到16位置,現在到16位置;
e佔1位元組,16是1的整數倍,因此e從16位置偏移1位,現在到17位置;
整個結構體中所有成員最大為4位元組,17不是4的整數倍,因此需補齊到20。
因此該結構體總共佔領記憶體大小為20個位元組。

(5) 巢狀結構體2

typedef struct {
    char a;   // a佔1個位元組 
    struct str1 {
        char b;  // b佔1個位元組
        long long c;  // c佔8個位元組
    };
    int d;    // d佔4個位元組
    char e;  // e佔1個位元組
} STR;

a佔1位元組,從0位置開始偏移1位,現在到1位置處;
struct str1中,其內部成員最大位元組為8,1不是8的整數倍,繼續向後直到8位置開始偏移;
b佔1位元組,8是1的整數倍,c從8位置偏移1位,現在到9位置處;
c佔8位元組,9不是8的整數倍,繼續向後直到16,因此d從16位置偏移8位,現在到24位置;
由於24是8的整數倍,因此不需補齊,現在到24位置;
d佔4位元組,24是4的整數倍,因此d從24位置偏移4位,現在到28位置;
e佔1位元組,28是1的整數倍,因此e從28位置偏移1位,現在到29位置;
整個結構體中所有成員最大為8位元組,29不是8的整數倍,因此需補齊到32。
因此該結構體總共佔領記憶體大小為32個位元組。

2、聯合體
(1) 簡單聯合體

typedef union {
    char a;   // a佔1個位元組 
    int b;    // b佔4個位元組 
    float c;  // c佔4個位元組 
} STR;

該聯合體中所有成員,佔位元組數最大的為b或c是4位元組,因此該聯合體總共記憶體大小為4位元組。

(3) 含陣列1

typedef union {
    char a;   // a佔1個位元組
    int b;    // b佔4個位元組
    char c[5];   // c佔1個位元組
    float d[3];  // d佔4個位元組 
} STR;

該聯合體中所有成員,佔位元組數最大的為d是12位元組,而12是所有成員的整數倍,因此該聯合體總共記憶體大小為12位元組。

(3) 含陣列2

typedef union {
    char a;   // a佔1個位元組
    int b;    // b佔4個位元組
    double c;   // c佔8個位元組
    float d[3];  // d佔4個位元組 
} STR;

該聯合體中所有成員,佔位元組數最大的為d是12位元組,聯合體總共記憶體大小為12位元組,但由於c佔8個位元組,12並非8的整數倍,因此需補齊到16,因此該聯合體最終記憶體大小為16。