浅谈C语言字节对齐
首先,我們得知道為什么要進行內存對齊,它的意義何在?在這兒可以先看這樣一張圖。(手繪請見諒!!!)
 
 我們知道,在32位CPU下,一個讀取周期可以讀取四個字節。一個字符變量在內存中占一個字節,而整型為在內存中占4個字節。 
 那么CPU一個讀寫周期剛好能讀取一個整形,所以如上邊左圖,兩個整型變量分別讀寫一次即可;而上面中間的圖,第一個讀寫周期讀取的四個字節為一個char變量和一個整形變量的前三個字節,第二個讀寫周期讀取的是整型變量剩余的那一個字節及其后面的三個字節的空間。這樣就導致該整型變量 用了兩個讀寫周期才讀寫成功,而且還要對兩個讀寫結果的高地地址拼湊才能得出該數據。大大降低了讀寫效率;上邊右圖是中間這幅圖采用內存對齊后的效果,第一個讀寫周期讀寫一個字符變量和三個字節的無內容的空間,第二個讀寫周期讀寫了一個整型變量。這樣,同樣是讀寫連續定義的一個字符變量和一個整型變量,第二次整型變量需要讀寫兩次,第三次卻只用讀寫一次。 
 因此要采用字節對齊的方式,可以理解為用空間換時間。
字節對齊是一個小知識點,但并不容易掌握,先給出字節對齊的幾種基本概念: 
 1、 學好字節對齊需要掌握的一些基本概念: 
 (1)基本數據類型的自身對齊值: 
 1字節:char型 
 2字節:short型 
 4字節:int,float,long、指針類型 
 8字節:doublel類型 
 對齊規則是:第一個變量從程序所占所占空間偏移量為0的位置開始存放,第二個及以后的變量從上一個變量所占空間的末尾之后找到的第一個偏移量為該變量所占空間大小的整數倍處開始存放,同時滿足加起來的值是4的倍數(避免一個變量需要多次讀寫及拼接才能讀寫成功)。 
 (2)指定對齊:編譯器提供#pragma pack(n)來設定變量以n字節對齊方式。(#pragma pack(0)可以恢復默認對齊)注:指定值必須是 2 的 N 次方。 
 (3)自定義類型的自身對齊值:結構體或類的成員中自身對齊值最大的值。(結構體的大小必須是最大對其數的整數倍結構體的成員中把所占字節較小的成員集中存放可以節省空間,縮小結構體所占空間大小,成員存放是的對其規則滿足基本數據類型的對其規則) 
 (4)自定義類型的有效對齊值:自定義類型的自身對齊值和指定對齊值(或邊界對齊值)中較小的值。
以上幾條規則基本概括了字節對齊的情況,需要掌握,據此,我們就可以很方便的來討論具體數據結構的成員和其自身的對齊方式。
注:VS、VC編譯器的默認對齊數是8、GCC編譯器的默認對齊數是4
2、幾種基本概念的對應實例: 
 (1)基本數據類型的自身對齊值:
可以看到字符變量C開辟的空間和整形 i 開辟的空間是不連續的,而是隔了中間三個字節(i 和 c 中間隔了4個字節,但C只占一個字節,還有三個字節是空著的)。為什么呢?字節對齊很容易就解釋了。此時沒有指定對齊,所以按自然對齊來看,即邊界對齊。c 是后定義的,自身對齊值為一個字節,而要滿足 c 的自身對齊值加上和 i 之間隔的空間等于 i 的自身對齊值(4個字節)且是 4 的倍數,那么c 和 i 之間要隔三個字節才行。
(2)、自定義類型的自身對齊值(結構體或類的成員中自身對齊值最大的值): 
 對結構體來說,要滿足結構體成員中已經定義的所有成員的自身對齊值加上其后面空出來的字節數之和是下一個要定義的成員的自身對齊值的倍數,且所有成員的自身對齊值加上其后面空出來的字節數(自定義的結構體類型所占字節數)是結構體自身對齊值的倍數。
這里結果為16,怎么來的?看下圖即可:
(3)自定義類型的有效對齊值(自定義類型的自身對齊值和指定對齊值中較小的值): 
 對結構體來說,要滿足每一個成員的自身對齊值加上其后面空出來的字節數是有效對齊值的倍數,且所有成員的自身對齊值加上其后面空出來的字節數(自定義的結構體類型所占字節數)是結構體有效對齊值的倍數。
按上一題的理解,這兒應該是24,但怎么又是16呢?
關鍵在于———–#pragma pack (4)
看圖說話:
(4)關于自定義類型的自身對齊值再來看兩個例子:
#include<stdio.h>#pragma pack (4)typedef struct Test {short s;struct{int i;double d;char c;};long l; }Test;int main() {printf("%d\n",sizeof(Test));return 0; }結果如下圖:
#include<stdio.h>typedef struct Test {short s;struct A{int i;double d;char c;};long l; }Test;int main() {printf("%d\n",sizeof(Test));return 0; }結果如下圖:
以上兩段代碼看著一樣,但為什么結果卻不一樣呢?細心就會發現第二段代碼在內嵌的struct后面加了一個 A。 
 當內嵌的struct后面沒有 A 時,它是一個結構體 
 當內嵌的結果提后面有 A 是,它是一個類型,因為類型肯定是可以定義變量的,那么這個定義出來的變量肯定是有大小的,那樣才能存儲數據,這個大小就是又類型決定的,所以類型肯定是有大小的,而此時不知道為它分配多大空間合適,大了,浪費,小了,不夠用,所以編譯器就為其分配了程序一般變量的最小單位–>>一個字節。
注:空結構體的所占內存空間大小是一個字節。因為它是一個類型,而想要成為類型,那么必須要能定義變量,那變量就要占空間,而這個空間大了就太浪費了,因為不知道這個類型要定要什么類型的變量,所以編譯器就自動為它分配了一個變量存儲的最小單位(一個字節)。
(5)自定義類型中聯合體的對齊方式: 
 因為聯合體的對齊方式與聯合體有不同,所以在單獨提一下。 
 聯合體的字節對齊也分自身對齊值與有效對齊值,聯合體自身對齊值為其成員中自身對齊值最小的,有效對齊值為聯合體的自身對齊值與邊界對齊值(編譯器決定,VC6.0默認為8字節,其值可在 工程·–>設置–>C/C++–>Code Generation–>Struct member Alignment 處修改)、指定對齊值中最小的,用數學表達式可以表示為: 
 有效對齊值 = min(min(成員1,成員2…成員n),邊界對齊值,指定對齊值) 
 最后要滿足,聯合體成員中自身對齊值的最大值經過字節對齊后(N = 最大值+n,,所加的n最小且能使N成為聯合體有效對齊值的倍數),就是自定義的聯合體類型所占的字節數。
下面以幾個例子來說明: 
 ①有效對齊值為聯合體成員中自身對齊值的最大值
結果如下圖:
解析如下圖:
②有效對齊值為邊界對齊值
#include<stdio.h>#pragma pack (6)typedef union Test {char c[13];int i; }Test;int main() {printf("%d\n",sizeof(Test));return 0; }表面看與第一段代碼沒區別,細心就會發現聯合體中的字符變量變成了字符數組
結果如下圖:
解析如下圖:
③有效對齊值為指定對齊值
#include<stdio.h>#pragma pack (2)typedef union Test {char c[13];short i; }Test;int main() {printf("%d\n",sizeof(Test));return 0; }結果為:
解析如下圖:
(6)自定義類型相互嵌套的字節對齊 
 ①聯合體嵌套結構體:
解析如圖:
結構體自嵌套(上面已給出(4))、聯合體自嵌套、結構體嵌套聯合體 ……….與此例道理都差不多,這里不給出示例。 
 究其根本,不管怎么嵌套,只要掌握了基本的自定義類型的對齊方式,都能很簡單的做出來,所以,自定義類型的字節對齊方式才是本文的重難點。
(7)位域 
 所謂“位域”是把一個字節中的二進位劃分為幾 個不同的區域, 并說明每個區域的位數。每個域有一個域名,允許在程序中按域名進行操作。 這樣就可以把幾個不同的對象用一個字節的二進制位域來表示。 
 位域在內存中的存儲遵循以下原則: 
 ①一個位域必須存儲在同一個字節中,不能跨字節,同樣不能垮類型。如一個字節所剩空間不夠存放另一位域時,應從下一單元起存放該位域。也可以有意使某位域從下一單元開始。例如: 
   struct bs 
   {unsigned a:4unsigned :0 /空域/unsigned b:4 /從下一單元開始存放/unsigned c:4} 
   在這個位域定義中,a占第一字節的4位,后4位填0表示不使用,b從第二字節開始,占用4位,c占用4位。 
 ②位域的長度不能大于指定類型固有長度,比如說int的位域長度不能超過32,bool的位域長度不能超過8。 
 ③位域可以無位域名,這時它只用來作填充或調整位置。無名的位域是不能使用的。例如: 
   struct k 
   {int a:1int :2 /該2位不能使用/int b:3int c:2}; 
   從以上分析可以看出,位域在本質上就是一種結構類型, 不過其成員是按二進位分配的。 
 ④位域的內存分配仍然遵循字節對齊
用一張圖來說明位域這個概念:
⑤位域的字節對齊
#include<stdio.h>typedef struct Test {char a:2;char b:4;char c:3;int d:2; }Test;int main() {printf("%d\n",sizeof(Test));return 0; }結果為:
解析如下圖:
常見的字節對齊基本就這些,當然其中的變化情況會很多很多。字節對齊是一個小而難的知識點,初學很不易掌握,寫下這篇博客的過程中自己也學到了很多。希望讀者與博主互勉共進,不吝糾正博客中的錯誤。
總結
 
                            
                        - 上一篇: mysql查看版本号_十分钟了解MySQ
- 下一篇: 悬挂“else”
