C語言字節對齊12345
不同系統下的C語言類型長度
| Data Type | ILP32 | ILP64 | LP64 | LLP64 |
|---|---|---|---|---|
| char | 8 | 8 | 8 | 8 |
| short | 16 | 16 | 16 | 16 |
| int | 32 | 64 | 32 | 32 |
| long | 32 | 64 | 64 | 32 |
| long long | 64 | 64 | 64 | 64 |
| pointer | 32 | 64 | 64 | 64 |
絕大部分64位的Unix,linux都是使用的LP64模型;32位Linux系統是ILP32模型;64位的Windows使用的是LLP64(long long and point 64)模型。
基本概念
許多計算機系統對基本數據類型合法地址做出了一些限制,要求某種類型對象的地址必須是某個值K(通常是2,4或8)的倍數。這種對齊限制簡化了形成處理器和存儲器系統之間的接口的硬件設計。對齊跟數據在內存中的位置有關。如果一個變量的內存地址正好位於它長度的整數倍,他就被稱做自然對齊。比如在32位cpu下,假設一個整型變量的地址為0x00000004,那它就是自然對齊的。
為什么要字節對齊
需要字節對齊的根本原因在於CPU訪問數據的效率問題。例如,假設一個處理器總是從存儲器中取出8個字節,則地址必須為8的倍數。如果我們能保證將所有的double類型數據的地址對齊成8的倍數,那么就可以用一個存儲器操作來讀或者寫值了。否則,我們可能需要執行兩次存儲器訪問,因為對象可能被分放在兩個8字節存儲塊中。
另外,假設一個整型變量的地址不是自然對齊,比如為0x00000002,則CPU如果取它的值的話需要訪問兩次內存,第一次取從0x00000002-0x00000003的一個short,第二次取從0x00000004-0x00000005的一個short然后組合得到所要的數據;如果變量在0x00000003地址上的話則要訪問三次內存,第一次為char,第二次為short,第三次為char,然后組合得到整型數據。而如果變量在自然對齊位置上,則只要一次就可以取出數據。
各個硬件平台對存儲空間的處理上有很大的不同,一些平台對某些特定類型的數據只能從某些特定地址開始存取。比如有些架構的CPU在訪問一個沒有進行對齊的變量的時候會發生錯誤,那么在這種架構下編程必須保證字節對齊。比如sparc系統,如果取未對齊的數據會發生錯誤,舉個例:
char ch[8];
char *p = &ch[1];
int i = *(int *)p;
運行時會報segment error,而在x86上就不會出現錯誤,只是效率下降。
如何處理字節對齊
先讓我們看編譯器是按照什么樣的原則進行對齊的:
- 數據類型自身的對齊值:為指定平台上基本類型的長度。對於char型數據,其自身對齊值為1,對於short型為2,對於int,float,double類型,其自身對齊值為4,單位字節。
- 結構體或者類的自身對齊值:其成員中自身對齊值最大的那個值。
- 指定對齊值:#pragma pack (value)時的指定對齊值value。
- 數據成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中小的那個值。
對於標准數據類型,它的地址只要是它的長度的整數倍就行了,而非標准數據類型按下面的原則對齊:
數組 :按照基本數據類型對齊,第一個對齊了后面的自然也就對齊了。
聯合 :按其包含的長度最大的數據類型對齊。
結構體: 結構體中每個數據類型都要對齊。
當數據類型為結構體時,編譯器可能需要在結構體字段的分配中插入間隙,以保證每個結構元素都滿足它的對齊要求。第一個數據變量的起始地址就是數據結構的起始地址。結構體的成員變量要對齊排放(對於非對齊成員需要在其前面填充一些字節,保證其在對齊位置上),結構體本身也要根據自身的有效對齊值圓整(就是結構體總長度需要是結構體有效對齊值的整數倍),此時可能需要在結構末尾填充一些空間,以滿足結構體整體的對齊—-向結構體元素中最大的元素對齊。
通過上面的分析,對結構體進行字節對齊,我們需要知道四個值:
- 指定對齊值:代碼中指定的對齊值,記為packLen;
- 默認對齊值:結構體中每個數據成員及結構體本身都有默認對齊值,記為defaultLen;
- 成員偏移量:即相對於結構體起始位置的長度,記為offset;
- 成員長度:結構體中每個數據成員的長度(注結構體成員為補齊之后的長度),記為memberLen。
及兩個規則:
- 對齊規則:
offset % vaildLen = 0,其中vaildLen為有效對齊值vaildLen = min(packLen, defaultLen); - 填充規則: 如成員變量不遵守對齊規則,則需要對其補齊;在其前面填充一些字節保證該成員對齊。需填充的字節數記為pad
Linux和Microsoft Windows的對齊方式
一.Linux的對齊策略:
在Linux中2字節數據類型(例如short)的地址必須是2的倍數,而較大的數據類型(例如int,int *,float和double)的地址必須是4的倍數。也就是說Linux下要么2字節對齊,要么4字節對齊,沒有其他格式的對齊。
二.Microsoft Windows的對齊策略:
在Windows中對齊要求更嚴–任何K字節基本對象的地址都必須是K的倍數,K=2,4,或者8.特別地,double或者long long類型數據的地址應該是8的倍數。可以看出Windows的對齊策略和Linux還是不同的。
更改C編譯器的缺省字節對齊方式
在缺省情況下,C編譯器為每一個變量或是數據單元按其自然對界條件分配空間。一般地,可以通過下面的方法來改變缺省的對界條件:
- 使用偽指令#pragma pack (n),C編譯器將按照n個字節對齊。
- 使用偽指令#pragma pack (),取消自定義字節對齊方式。
另外,還有如下的一種方式:
__attribute((aligned (n))),讓所作用的結構成員對齊在n字節自然邊界上。如果結構中有成員的長度大於n,則按照最大成員的長度來對齊。__attribute__ ((packed)),取消結構在編譯過程中的優化對齊,按照實際占用字節數進行對齊。
字節對齊的作用不僅是便於cpu快速訪問,同時合理的利用字節對齊可以有效地節省存儲空間。
對於32位機來說,4字節對齊能夠使cpu訪問速度提高,比如說一個long類型的變量,如果跨越了4字節邊界存儲,那么cpu要讀取兩次,這樣效率就低了。但是在32位機中使用1字節或者2字節對齊,反而會使變量訪問速度降低。所以這要考慮處理器類型,另外還得考慮編譯器的類型。在vc中默認是4字節對齊的,GNU gcc 也是默認4字節對齊。
什么時候需要設置對齊
在設計不同CPU下的通信協議時,或者編寫硬件驅動程序時寄存器的結構這兩個地方都需要按一字節對齊。即使看起來本來就自然對齊的也要使其對齊,以免不同的編譯器生成的代碼不一樣.
結構體舉例
例子1
/************************ > File Name: struct_test.c > Author:Marvin > Created Time: Thu 22 Mar 2018 07:19:46 PM CST **********************/
#include<stdio.h>
int main()
{
struct test {
char a;
short b;
int c;
long d;
};
struct test t = {'a',11,11,11};
printf("size of struct t = %u\n", sizeof(t));
return 0;
}
在64位centos上編譯編譯后結構struct test的布局如下:

由於要保證結構體每個元素都要數據對齊,因此必須在a和b之間插入1字節的間隙使得后面的short元素2字節對齊int元素4字節對齊long元素8字節對齊,這樣最終test結構大小為16字節。
運行程序結果為:
size of struct t = 16
例子2
現在考慮這樣一個結構體:
struct test2 {
int a;
long b;
char c;
};
struct test2 t2 = {11,11,'c'};
在64位centos上編譯編譯后結構struct test2的布局如下:

結構體struct test2的自然對界條件為8字節,所以需要在最后的char型數據后面再填充7個字節使得結構體整體對齊。
運行程序結構為
size of struct test2 = 24
例子3
不妨將結構體struct test2里面成員的順序重新排列一下:
struct test3 {
char c;
int a;
long b;
};
struct test3 t3 = {'c',11,11};
在64位centos上編譯編譯后結構struct test2的布局如下:

運行結果為:
size of struct test3 = 16
可見適當地編排結構體成員地順序,可以在保存相同信息地情況下盡可能節約內存空間。
例子4
struct B
{
char b;
int a;
short c;
};
假設B從地址空間0x0000開始排放。該例子中沒有定義指定對齊值,在筆者環境下,該值默認為4。第一個成員變量b的自身對齊值是1,比指定或者默認指定對齊值4小,所以其有效對齊值為1,所以其存放地址0x0000符合0x0000%1=0.第二個成員變量a,其自身對齊值為4,所以有效對齊值也為4,所以只能存放在起始地址為0x0004到0x0007這四個連續的字節空間中,復核0x0004%4=0,且緊靠第一個變量。第三個變量c,自身對齊值為2,所以有效對齊值也是2,可以存放在0x0008到0x0009這兩個字節空間中,符合0x0008%2=0。所以從0x0000到0x0009存放的都是B內容。再看數據結構B的自身對齊值為其變量中最大對齊值(這里是b)所以就是4,所以結構體的有效對齊值也是4。根據結構體圓整的要求,0x0009到0x0000=10字節,(10+2)%4=0。所以0x0000A到0x000B也為結構體B所占用。故B從0x0000到0x000B共有12個字節,sizeof(struct B)=12;其實如果就這一個就來說它已將滿足字節對齊了,因為它的起始地址是0,因此肯定是對齊的,之所以在后面補充2個字節,是因為編譯器為了實現結構數組的存取效率,試想如果我們定義了一個結構B的數組,那么第一個結構起始地址是0沒有問題,但是第二個結構呢?按照數組的定義,數組中所有元素都是緊挨着的,如果我們不把結構的大小補充為4的整數倍,那么下一個結構的起始地址將是0x0000A,這顯然不能滿足結構的地址對齊了,因此我們要把結構補充成有效對齊大小的整數倍.其實諸如:對於char型數據,其自身對齊值為1,對於short型為2,對於int,float,double類型,其自身對齊值為4,這些已有類型的自身對齊值也是基於數組考慮的,只是因為這些類型的長度已知了,所以他們的自身對齊值也就已知了.
例子5
#pragma pack (2) /*指定按2字節對齊*/
struct C
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定對齊,恢復缺省對齊*/
第一個變量b的自身對齊值為1,指定對齊值為2,所以,其有效對齊值為1,假設C從0x0000開始,那么b存放在0x0000,符合0x0000%1=0;第二個變量,自身對齊值為4,指定對齊值為2,所以有效對齊值為2,所以順序存放在0x0002、0x0003、0x0004、0x0005四個連續字節中,符合0x0002%2=0。第三個變量c的自身對齊值為2,所以有效對齊值為2,順序存放在0x0006、0x0007中,符合0x0006%2=0。所以從0x0000到0x00007共八字節存放的是C的變量。又C的自身對齊值為4,所以C的有效對齊值為2。又8%2=0,C只占用0x0000到0x0007的八個字節。所以sizeof(struct C)=8.
對於結構體嵌套地情況,結構體對齊算法思想:深度優先填充。
padLen = getPadLen(offset , defaultLen);
int getPadLen(int offsetLen, int defaultLen)
{
int vaildLen = min(packLen,defaultLen);
if(0 == vaildLen || 0 == offsetLen % vaildLen)
{
return 0;
}
return vaildLen - (offsetLen % vaildLen);
}
先對齊內層結構體:對每個數據成員計算其defaultLen、memberLen和offset;
再遍歷每個數據成員時計算:對於基本數據類型成員defaultLen=memberLen;對於結構體成員defaultLen等於它的所有成員的最大的memberLen;遍歷時對成員的memberLen進行累加,得到當前成員的offsetLen;
運用對齊及填充規則:在當前結構體成員前填充padLen個字節;
下面是結構體作為成員的例子:
struct test1 {
int a;
long b;
};
struct test4 {
char a;
struct test1 b;
int c;
};
struct test4 t4 = {'a', {11,11},11}
test1的內存分布:

test4的內存分布:

字節對齊可能帶來的隱患
代碼中關於對齊的隱患,很多是隱式的。比如在強制類型轉換的時候。例如:
unsigned int i = 0x12345678;
unsigned char *p=NULL;
unsigned short *p1=NULL;
p=&i;
*p=0x00;
p1=(unsigned short *)(p+1);
*p1=0x0000;
最后兩句代碼,從奇數邊界去訪問unsignedshort型變量,顯然不符合對齊的規定。
在x86上,類似的操作只會影響效率,但是在MIPS或者sparc上,可能就是一個error,因為它們要求必須字節對齊.
如何查找與字節對齊方面的問題
如果出現對齊或者賦值問題首先查看
- 編譯器的big little端設置
- 看這種體系本身是否支持非對齊訪問
- 如果支持看設置了對齊與否,如果沒有則看訪問時需要加某些特殊的修飾來標志其特殊訪問操作
舉例:
#include<iostream>
using namespace std;
//windows 64 位默認 結構體對齊系數為8,32位 結構體對齊系數為4
//測試系統對齊系數
// #pragma pack(8) my_struct_1 為16字節
// #pragma pack(4) my_struct_1 為12字節
// 不加#pragma pack(8) my_struct_1 為16字節
//顧系統默認對齊系數為8
struct my_struct_1
{
char a; //1
double b; //之前補7 +8 8/8==1
};
#pragma pack(4)
struct my_struct_2
{
char a; //1
double b; //3+8
int c; //4 16/4=4
};
#pragma pack()
#pragma pack(2)
struct my_struct_3
{
char a; //1
double b; //1+8
int c; //4 14/2
};
#pragma pack()
#pragma pack(4)
struct my_struct_4
{
char a[5]; //5
double b; //3+8 16/4
};
#pragma pack()
#pragma pack(2)
struct my_struct_5
{
char a[5]; //5
double b; //1+8 14/2
};
#pragma pack()
#pragma pack(4)
struct my_struct_6
{
char a; //1
char b[3]; //3
char c; //1 1+3+1
};
#pragma pack()
#pragma pack(4)
struct my_struct_7
{
char a; //1
char b[3]; //3
char c; //1
int d; //補齊 3 +4
};
#pragma pack()
#pragma pack(4)
struct test
{
char x1; //1
short x2; //補齊1+ 2
float x3; //4
char x4; //1 補齊+3
};
#pragma pack()
int main()
{
cout<<"char:"<<sizeof(char)<<endl;
cout<<"short:"<<sizeof(short)<<endl;
cout<<"int:"<<sizeof(int)<<endl;
cout<<"long:"<<sizeof(long)<<endl;
cout<<"float:"<<sizeof(float)<<endl;
cout<<"double:"<<sizeof(double)<<endl;
cout<<"long double:"<<sizeof(long double)<<endl;
cout<<sizeof(my_struct_1)<<endl;//8
cout<<sizeof(my_struct_2)<<endl;//16
cout<<sizeof(my_struct_3)<<endl;//14
cout<<sizeof(my_struct_4)<<endl;//16
cout<<sizeof(my_struct_5)<<endl;//14
cout<<sizeof(my_struct_6)<<endl;//5
cout<<sizeof(my_struct_7)<<endl;//12
cout<<sizeof(test)<<endl;//12
system("pause");
return 0;
}
