C語言記憶體對齊和結構補齊
技術標籤:C++基礎
一、為什麼要位元組對齊?
32位CPU是以雙字(DWORD)為單位進行資料傳輸的,因此我們的資料無論是8位或16位都是以雙字進行資料傳輸。
比如,一個int型別4位元組的資料如果放在上圖記憶體地址1開始的位置,那麼這個資料佔用的記憶體地址為1~4,那麼這個資料就被分為了2個部分,一個部分在地址0~3中,另外一部分在地址4~7中,又由於32位CPU以雙字進行傳輸,所以,CPU會分2次進行讀取,一次先讀取地址0~3中內容,再一次讀取地址4~7中資料,最後CPU提取並組合出正確的int型別資料,捨棄掉無關資料。那麼反過來,如果我們把這個int型別4位元組的資料放在上圖從地址0開始的位置會怎樣呢?讀到這裡,也許你明白了,CPU只要進行一次讀取就可以得到這個int型別資料了。沒錯,就是這樣,這次CPU只用了一個週期就得到了資料,由此可見,對記憶體資料的擺放是多麼重要啊,擺放正確位置可以減少CPU的使用資源。
二、記憶體對齊原則
那麼,記憶體對齊有哪些原則呢?我總結了一下大致分為三條:
第一條:第一個成員的首地址為0
第二條:每個成員的首地址是自身大小的整數倍
第二條補充:以4位元組對齊為例,如果自身大小大於4位元組,都以4位元組整數倍為基準對齊。
第三條:最後以結構總體對齊。
第三條補充:以4位元組對齊為例,取結構體中最大成員型別倍數,如果超過4位元組,都以4位元組整數倍為基準對齊。(其中這一條還有個名字叫:“補齊”,補齊的目的就是多個結構變數挨著擺放的時候也滿足對齊的要求。)
上述的三原則聽起來還是比較抽象,那麼接下來我們通過一個例子來加深對記憶體對齊概念的理解,下面是一個結構體,我們動手算出下面結構體一共佔用多少記憶體?假設我們以32位平臺並且以4位元組對齊方式:
#pragma pack(4)
typedef struct MemAlign
{
char a[18];
double b;
char c;
int d;
short e;
}MemAlign;
下圖為對齊後結構如下:
我們就以這個圖來講解是如何對齊的:
第一個成員(char a[18]):首先,假設我們把它放到記憶體開始地址為0的位置,由於第一個成員佔18個位元組,所以第一個成員佔用記憶體地址範圍為0~18。
第二個成員(double b):由於double型別佔8位元組,又因為8位元組大於4位元組,所以就以4位元組對齊為基準。由於第一個成員結束地址為18,那麼地址18並不是4的整數倍,我們需要再加2個位元組,也就是從地址20開始擺放第二個成員。
第四個成員(int d):由於int型別佔4位元組,但是地址29並不是4的整數倍,所以我們需要再加3個位元組,也就是從地址32開始擺放這個成員。
第五個成員(short e):由於short型別佔2位元組,地址36正好是2的整數倍,這樣我們就可以直接擺放,無需填充位元組,緊跟其後即可。
這樣我們記憶體對齊就完成了。但是離成功還差那麼一步,那是什麼呢?對,是對整個結構體補齊,接下來我們就補齊整個結構體。那麼,先讓我們回顧一下補齊的原則:“以4位元組對齊為例,取結構體中最大成員型別倍數,如果超過4位元組,都以4位元組整數倍為基準對齊。”在這個結構體中最大型別為double型別(佔8位元組),又由於8位元組大於4字 節,所以我們還是以4位元組補齊為基準,整個結構體結束地址為38,而地址38並不是4的整數倍,所以我們還需要加額外2個位元組來填充結構體,如下圖紅色的就是補齊出來的空間:、
我們可以通過一個程式來觀察記憶體變化:
#include <stdio.h>
#include <memory.h>
// 通過預編譯來通知編譯器我們以4位元組對齊
#pragma pack(4)
// 用於測試的結構體
typedef struct MemAlign
{
char a[18]; // 18 bytes
double b; // 08 bytes
char c; // 01 bytes
int d; // 04 bytes
short e; // 02 bytes
}MemAlign;
int main()
{
// 定義一個結構體變數
MemAlign m;
// 定義個以指向結構體指標
MemAlign *p = &m;
// 依次對各個成員進行填充,這樣我們可以
// 動態觀察記憶體變化情況
memset( &m.a, 0x11, sizeof(m.a) );
memset( &m.b, 0x22, sizeof(m.b) );
memset( &m.c, 0x33, sizeof(m.c) );
memset( &m.d, 0x44, sizeof(m.d) );
memset( &m.e, 0x55, sizeof(m.e) );
// 由於有補齊原因,所以我們需要對整個
// 結構體進行填充,補齊對齊剩下的位元組
// 以便我們可以觀察到變化
memset( &m, 0x66, sizeof(m) );
// 輸出結構體大小
printf( "sizeof(MemAlign) = %d", sizeof(m) );
}