本文將介紹以下內容:
一、什麼是內存對齊?
二、結構體大小如何計算?
三、爲什麼存在內存對齊?
四、如何修改默認對齊數?
五、筆試題練習
正文
一、什麼是內存對齊?
首先,內存對齊是屬於編譯器的“管轄”範圍,編譯器爲程序中的每個數據單元安排在適當的位置上。但是在 c 語言中我們是被允許干預內存對齊的,也就是說我們可以一定程度上的控制如何內存對齊。
內存對齊是爲了硬件計算起來效率更高而設計的
我們通過一些例子來理解下什麼是內存對齊:(以下程序皆在vs2013下運行)
例1:
typedef struct s1
{
char a;
int b;
char c;
} s1;
printf("%d\n", sizeof(s1));
運行結果是12。
typedef struct s1
{
char a;
char b;
char c;
} s1;
printf("%d\n", sizeof(s1));
運行結果是3。
typedef struct s1
{
char a;
short b;
char c;
} s1;
printf("%d\n", sizeof(s1));
運行結果是6。
typedef struct s1
{
char a;
double b;
char c;
} s1;
printf("%d\n", sizeof(s1));
運行結果是24。
從例1直觀的來看,感覺好像是低字節的元素在向高字節對齊。我們繼續往下看!!!
例2:
typedef struct s2
{
char a;
char b;
int c;
} s2;
printf("%d\n", sizeof(s2));
運行結果是8。
例3:
typedef struct s3
{
double d;
char c;
int i;
} s3;
printf("%d\n", sizeof(s3));
運行結果是16。
例 2 應該是將兩個 char 湊成一個4字節的 int,所以計算出來是8。
例 3 應該是將 char 和 int 湊成了一個 8字節的double,所以計算出來是16。
這看起來也是往低字節往高字節的元素對齊。那麼看下一個例子!!
例4:
typedef struct s3
{
double d;
char c;
int i;
} s3;
typedef struct s4
{
char c1;
struct s3 s3;
double d;
} s4;
printf("%d\n", sizeof(s4));
運行結果是32。
注意:這裏結果發生了變化,如果按照我們上面的推論,那麼這個計算出來應該是往 s3 這個結構體上對齊,s3 是16個字節,那麼按理應該是 48 纔對,看起來這並不是無休止的往最大的字節元素對齊。那麼我們就來看看結構體的大小到底應該如何計算?
二、結構體大小如何計算?
我們先來看看結構體的對齊規則:
- 第一個成員在與結構體變量偏移爲 0 的地址處。
- 其他成員變量要對齊到對齊數的整數倍
對齊數 = 編譯器默認的一個對齊數 與 該成員中最大的變量 兩者之間較小的那個
提一下:vs中默認的對齊數是 8
Linux下默認的對齊數是 4
- 結構體總大小爲最大對齊數(每一個成員變量都有一個對齊數)的整數倍。
- 如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊的整數倍處,結構體的整體大小就是所有最大對齊數(含嵌套結構體的對齊數)的整數倍。
看了這些規則,我們來一條條分析:
第一條很好理解,簡單來說,結構體第一個成員的地址和結構體的地址是一樣的。但是意義不一樣,第一個成員的地址是給結構體的地址加上一個 0 得到的就是第一個成員的地址。
主要是第二條看起來比較繞口,但是仔細分析也是很好理解,這時候可以看我們的例子,在例2中,結構體中最大的成員是 int ,而vs默認的對齊數是8,我們選兩者中較小的一個作爲對齊數,那麼就是取 4 作爲對齊數。
再看例 3 中,結構體成員最大的是 double 有 8 個字節,而vs的默認對齊數也是 8 ,所以取 8 作爲對齊數。
再看例 4 中,結構體成員中最大的是結構體 s3 ,是 16 個字節,而vs默認對齊數是8,取兩者較小的作爲對齊數,所以就取的是 8 作爲對齊數,s3 的大小還是 16 個字節,其他成員變量向 8 對齊。
三、爲什麼存在內存對齊?
大部分的參考資料都是這樣解釋的:
1、平臺原因(移值原因):
不是所有的硬件平臺都能訪問任意地址上的任意數據的,某些硬件平臺只能在某些地址處取特定類型的數據,否則拋出硬件異常。
2、性能原因:
數據結構(尤其是棧)應該儘可能地在自然邊界上對齊。
原因在於,爲了訪問未對齊的內存,處理器需要作出兩次內存訪問,而對齊的內存僅需要一次訪問,也就是下面的解釋。
cpu在取數據的時候一般都是 4 個字節或 8 個字節的取,如果沒有內存對齊這回事那麼取數據就會出現下面的情況
沒有內存對齊的話,取出來的數據就是一個 char 和 3/4 個 int,不是一個完整的 int,爲了取出一個完整的 int 還得再取一次,這樣就很僵硬了。
但是當我們把 char 補成四個字節,那麼每次就可以取出來完整的數據了,cpu 再去解析也就方便了很多。所以說內存對齊是爲了硬件計算起來效率更高設計的。
總的來說,結構體的內存對齊是用 空間 換 時間 的做法
四、如何修改默認對齊數?
藉助預處理指令 #pragma pack()
#pragma pack(2) :將默認對齊數修改爲2。默認對齊數一旦修改,那麼結構體計算的大小也就發生了相應的變化。
#pragma pack() :取消設置的默認對齊數,還原爲默認對齊數。
五、筆試題練習
提問:寫一個宏,計算結構體中某變量相對於首地址的偏移,並給出說明?
答:
typedef struct s1
{
char a;
int b;
char c;
} s1;
#define OFFSET(a, b) ((char*)(&b) - (char*)(&a))
int main()
{
s1 s;
printf("成員 b 相對於首地址的偏移量爲:%d\n", OFFSET(s, s.b));
system("pause");
return 0;
}
如果直接用兩個指針進行相減,減出來肯定有問題,因爲這裏的指針是帶類型的,所以我們需要將指針強轉爲char*進行相減,那麼計算出來的就是字節數。