畫成這樣你想打我嗎?

st1
+---+---+---+---+
| a | b |
+---+---+---+---+
| b | c |
+---+---+---+

st2
+---+---+---+---+
| c | a | b |
+---+---+---+---+
| b |
+---+---+---+

這樣畫不香嗎?

st1
+---+
| a |
+---+---+---+---+
| b |
+---+---+---+---+
| c |
+---+---+

st2
+---+---+---+
| c | a |
+---+---+---+---+
| b |
+---+---+---+---+

然後(各種意義上)把空白處補齊:

st1
+---+---+---+---+
| a | XXXXXXXXX |
+---+---+---+---+
| b |
+---+---+---+---+
| c | XXXXX |
+---+---+-------+

st2
+---+---+---+---+
| c | a | X |
+---+---+---+---+
| b |
+---+---+---+---+

把一個 b 拆到兩行,無論人類還是機器讀起來都沒有不拆來得開心。至於圖中為什麼是 4 個格子一換行,這是你的機器決定的。

了解更多:維基百科 - 數據結構對齊


C 設計上存在遺憾。

當前只保證順序不保證 padding 策略(標準甚至沒有禁止對齊必要之外的 padding,意味著你拿 alignof 和 max_align 算出來的也是依賴實現定義的)不上不下怪難受的。當你對順序有要求的時候,難免也得依賴實現定義的對齊和 padding 策略了,說實話成員順序先後這種保證是現在這樣單獨放標準里還是都丟給實現定義就現狀來說區別不大(除了第一個成員頂頭放),反而是導致了我不依賴布局的情況下如果期望緊湊還得人肉精打細算,沒有編譯器會幫我優化順序。

當然更優解是 attribute 開關控制。C 標準也有 attribute 了。


為了簡化 CPU 和內存 RAM 之間的介面和硬體設計。比如一個32位的計算機系統,CPU 讀取內存時,硬體設計上可能只支持4位元組或4位元組倍數對齊的地址訪問,CPU 每次往內存 RAM 讀寫數據時,一個周期可以讀寫4個位元組。如果我們把一個數據放在4位元組對齊的地址上,那麼CPU一次就可以把數據讀寫完畢;如果我們把一個 int 型數據放在一個非4位元組對齊的地址上,那 CPU 就要分2次才能把這個4位元組大小的數據讀寫完畢。

為了配合計算機的硬體設計,編譯器在編譯程序時,對於一些基本數據類型,比如 int、char、short、float 等,會按照其數據類型的大小進行地址對齊,按照這種地址對齊方式分配的存儲地址,CPU 一次就可以讀寫完畢。雖然邊界對齊會造成一些內存空洞,浪費一些內存單元,但是在硬體上的設計卻大大簡化了。這也是編譯器給我們定義的變數分配地址時,不同類型變數按不同位元組數地址對齊的原因。

除了 int、char、short、float 這些基本類型數據,對於一些複合類型數據,也要滿足地址對齊要求。

結構體作為一種複合數據類型,編譯器在給一個結構體變數分配存儲空間時,不僅要考慮結構體內各個基本成員的地址對齊,還要考慮結構體整體的對齊。為了結構體內的成員地址對齊,編譯器可能會在結構體內填充一些空間;為了結構體整體對齊,編譯器可能會在結構體的末尾填充一些空間。

接下來,我們定義一個結構體,結構體內定義 int、char 和 short 三種成員,並列印結構體的大小和各個成員的地址。

struct data{
char a;
int b ;
short c ;
}
int main(void)
{
struct data s;
printf("size:%d
",sizeof(s));
printf("a:%p
",s.a);
printf("b:%p
",s.b);
printf("c:%p
",s.c);
}

程序運行結果如下。

size: 12
s.a: 0028FF30
s.b: 0028FF34
s.c: 0028FF38

我們可以看到,因為結構體的成員 b 需要4位元組對齊,編譯器在給成員 a 分配完空間後,接著會空出3個位元組,在滿足4位元組對齊的 0x0028FF34 地址處才給成員 b 分配存儲空間。接著是 short 類型的成員 c 佔據2位元組的存儲空間。三個結構體成員一共佔據4+4+2=10位元組的存儲空間,根據結構體的對齊規則,結構體的整體對齊要向結構體所有成員中最大對齊位元組數或其整數倍對齊,或者說結構體的整體長度要為其最大成員位元組數的整數倍,如果不是整數倍要補齊。因為結構體最大成員 int 為4個位元組,或者說按4位元組的整數倍對齊,所以結構體的長度要為4的整數倍,要在結構體的末尾補充2個位元組,所以最後結構體的 size 為12個位元組。

結構體成員中,不同的排放順序,可能也會導致結構體的整體長度不一樣,我們修改一下上面的程序。

struct data{
char a;
short b ;
int c ;
};
int main(void)
{
struct data s;
printf("size: %d
",sizeof(s));
printf("s.a: %p
",s.a);
printf("s.b: %p
",s.b);
printf("s.c: %p
",s.c);
}

程序運行結果如下。

size: 8
s.a: 0028FF30
s.b: 0028FF32
s.c: 0028FF34

我們調整了一些成員順序,你會發現,char 型變數 a 和 short 型變數 b,分配在了結構體的前4個位元組存儲空間中,而且都滿足各自的地址對齊,整個結構體大小是8位元組,只造成一個位元組的內存空洞。我們繼續修改程序,讓 short 型的變數 b 按4位元組對齊:

struct data{
char a;
short b __attribute__((aligned(4)));
int c ;
};

程序運行結果如下。

size: 12
s.a: 0028FF30
s.b: 0028FF34
s.c: 0028FF38

你會發現,結構體的大小又重新變為12個位元組。這是因為,我們顯式指定 short 變數以4位元組地址對齊,導致變數 a 的後面填充了3個位元組空間。int 型變數 c 也要4位元組對齊,所以變數 b 的後面也填充了2個位元組,導致整個結構體的大小為12位元組。

我們不僅可以顯式指定結構體內某個成員的地址對齊,也可以指定整個結構體的對齊方式。

struct data{
char a;
short b;
int c ;
}__attribute__((aligned(16)));

程序運行結果如下。

size: 16
s.a: 0028FF30
s.b: 0028FF32
s.c: 0028FF34

在這個結構體中,各個成員一共佔8個位元組。通過前面學習我們知道,整個結構體的對齊只要是最大成員對齊位元組數的整數倍即可。所以這個結構體整體就以8位元組對齊,結構體的整體長度為8位元組。但是我們在這裡,顯式指定結構體整體以16位元組對齊,所以編譯器就會在這個結構體的末尾填充8個位元組以滿足16位元組對齊的要求,導致結構體的總長度變為16位元組。

原文地址:

嵌入式C語言自我修養 07:地址對齊那些事兒?

w.url.cn圖標

這裡引發一個數據結構定義中的位對齊問題,而位對齊又牽涉到內存分頁需要,內存是按頁管理,每一個頁一般是4K,如果發生基本數據的位元組在一個頁面不完整,也就是跨頁了,會出現系統操作數據異常,雖然不會引起程序崩潰,但是會額外消耗時間讓系統存取這樣的數據。


因為要位元組對齊


推薦閱讀:
相关文章