http://blog.chinaunix.net/uid-26548237-id-3969720.html
一、快速理解
1、什么是字节对齐?
在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构中的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除。
2、字节对齐有什么作用?
字节对齐的作用不仅是便于CPU的快速访问,同时合理的利用字节对齐可以有效地节省存储空间。
对于32位机来说,4字节对齐能够使CPU访问速度提高,比如说一个long类型的变量,如果跨越了4字节边界存储,那么CPU要读取两次,这样效率就低了。但是在32位机中使用1字节或者2字节对齐,反而会使变量访问速度降低。所以这要考虑处理器类型,另外还得考虑编译器的类型。在VC中默认是4字节对齐的,GNU gcc也是默认4字节对齐。
3、更改C编译器的缺省字节对齐方式
在缺省的情况下,C编译器为每一个变量或是数据单元按其自然对齐条件分配空间。一般地,可以通过下面的方法改变缺省的对齐条件:
(1)使用伪指令#pragma pack(n),C编译器将按照n个字节对齐;
(2)使用伪指令#pragma pack(),取消自定义字节对齐方式;
二、字节对齐对程序的影响
先让我们看看几个例子吧(32位,x86环境,gcc编译器):
设结构体如下定义:
- struct A
- {
- int a;
- char b;
- short c;
- };
- struct B
- {
- char b;
- int a;
- short c;
- };
现在已知32位机器上各种数据类型的长度如下:
char: 1(有符号无符号同)
short:2(有符号无符号同)
int:4(有符号无符号同)
long:4(有符号无符号同)
float:4 double:8
那么上面的两个结构体大小如何呢?
结果是:
sizeof(struct A) = 8
sizeof(struct B) = 12
结构体A中包含了4个字节长度的int一个,1字节长度的char一个核2字节长度的short型数据一个,B也一样;按理说,A、B大小应该是相同的,都是7个字节。
之所以出现上面的结果是因为编译器要对数据成员在空间上进行对齐。上面是按照编译器的默认设置进行对齐的结果,那么我们是不是可以改变编译器的这种默认设置呢?当然可以,例如:
- #pragma pack(2) //指定按2字节对齐
- struct C
- {
- char b;
- int a;
- short c;
- };
- #pragma pack() //取消指定对齐,恢复缺省对齐
- sizeof(struct C) = 8
- 修改对齐方式为1:
- #pragma pack(1) //指定按1字节对齐
- struct D
- {
- char b;
- int a;
- short c;
- };
- #pragma pack() //取消指定对齐,恢复缺省对齐
- sizeof(struct D) = 7
后面在介绍#pragma pack()的作用。
三、编译器是按照什么样的原则进行对齐的
先看四个重要的基本概念:
(1)数据类型自身对齐值
对于char型数据,其自身对齐值为1,对于short型为2,对于int、float、double类型,其自身对齐值为4,单位字节。
(2)结构体或者类的自身对齐值
其成员中自身对齐值最大的那个值。
(3)指定对齐值
#pragma pack(value)时的指定对齐值value。
(4)数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
有了这些值,我们可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。
有效对齐N,就是表示”对齐在N上“。也就是说该数据的”存放起始地址%N = 0“。而数据结构中的数据变量都是按定义的先后顺序来排列的。第一个数据变量的起始地址就是数据结构的其实地址。结构体的成员变量要对齐排列,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效地址值的整数倍,结合下面例子理解)。这样就不难理解上面几个例子的值了。
例子分析,以B为例。
- 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,这些已有类型的自身对齐值也是基于数组考虑的,只是因为这些类型的长度已知了,所以他们的自身对齐值也就已知了。
同理,分析上面例子C。
- #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。
四、如何修改编译器的默认对齐值
(1)在VC IDE中,可以修改:【Project】->【Setting】,C/C++选项卡Category的Code Generation选项的Struct Member Alignment中修改,默认是8字节;
(2)在编码时,可以这样动态修改:#pragma pack。注意:是pragma而不是progma。
五、针对字节对齐,我们在编程中如何考虑
如果在编程的时候要考虑节约空间的话,那么我们只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照类型大小从小到大声明,尽量减少中间的填补空间。还有一种就是为了以空间换取时间的效率,我们显示的进行填补空间进行对齐。比如:有一种使用空间换时间做法是显式的插入reserved成员:
- struct A
- {
- char a;
- char reserved[3];//使用空间换时间
- int b;
- };
reversed成员对我们程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员,通常编译器也会给我们自动填补对齐,我们自己加上它只是起到显式的提醒作用。
六、字节对齐可能带来的隐患
代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。例如:
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,因为它们要求必须字节对齐。
七、如何查找与字节对齐方面的问题
如果出现对齐或者赋值问题,首先查看:
(1)编译器的big/little端设置;
(2)看这种体系本身是否支持对齐访问;
(3)如果支持,看设置了对齐与否;如果没有,则看访问时需要加某些特殊的修饰来标志其特殊访问操作;
八、引用
九、补充:
字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺序,通常有小端、大端两种字节顺序。小端字节指低字节数据存放在内存低地址处,高字节数据存放在内存的高地址处;大端字节序是高字节数据存放在低地址处,低字节数据存放在高地址处。基于X86平台的PC机是小端字节序的,而有的嵌入式平台则是大端字节序的。因而对int、uint16、uint32等多于1字节类型的数据,在这些嵌入式平台上应该变换其存储顺序。通常我们认为,在网络中传输的字节的顺序即网络字节序为标准顺序,考虑到与协议的一致以及与同类其他平台产品的互通,在程序中发数据包时,将主机字节序转换为网络字节序,收数据包处将网络字节序转换为主机字节序。
对于下面的结构体:
- struct test
- {
- char x1;
- short x2;
- float x3;
- char x4;
- };
结构各成员空间分配情况是怎么样的?
文章中解释:
结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了空字节。结构的第三个成员x3和第四个成员x4恰好落在了其自然对界地址上,在它们前面不需要额外的填充字节。在test结构体中,成员x3要求4字节对齐,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了 3个空字节。整个结构所占据空间为12字节。
同时,说明了更改C编译器的缺省字节对齐方式:在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:
· 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
· 使用伪指令#pragma pack (),取消自定义字节对齐方式。
另外,还有如下的一种方式:
· __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
· __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
以上的n = 1, 2, 4, 8, 16... 第一种方式较为常见。
下面是CSDN上提出的问题(原文<>:http://community.csdn.net/Expert/TopicView3.asp?id=3804035)
- #pragma pack(8)
- struct s1
- {
- short a;
- long b;
- };
- struct s2
- {
- char c;
- s1 d;
- long long e;
- };
- #pragma pack()
问:
(1)sizeof(s2) = ?
(2)s2的c后面空了几个字节接着是d?
下面是redleaves给出解释:
很详尽,很透彻,从论坛原文后头的对话可以看出redleaves对C/C++的标准理解很深刻,值得学习:
- #pragma pack(8)
- struct S1
- {
- char a;
- long b;
- };
- struct S2 {
- char c;
- struct S1 d;
- long long e;
- };
- #pragma pack()
sizeof(s2)结果为24。
成员对齐有一个重要的条件,即每个成员分别对齐,即每个成员按自己的方式对齐。
也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐。其对齐的规则是:每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对
齐参数(这里是8字节)中较小的一个对齐。并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。
s1中,成员a是1字节,默认按1字节对齐,指定对齐参数为8,这两个值中取1,a按1字节对齐;成员b是4个字节,默认是按4个字节对齐,这时就按4个字节对齐。所以sizeof(s1) = 8。
s2中,c和s1中的a一样,按1字节对齐,而d是个结构,它是8个字节,它按什么对齐呢?对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个,s1的就是4。所以,成员d就是按4字节对齐,成员e是8个字节对齐,它是默认按8个字节对齐,和指定的一样,所以它对到8字节的边界。这时,已经使用了12个字节了,所以又添了4个字节的空,从第16个字节开始放置成员e。这是,长度为24,已经可以按8(成员e按8字节对齐)整除。这样,一共使用了24个字节。
a b
s1的内存布局:11**,1111,
c s1.a s1.b d
s2的内存布局:1***,11**,1111,****11111111
这里有三点很重要:
(1)每个成员分别按自己的方式对齐,并能最小化长度;
(2)复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度;
(3)对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐;
对于数组,比如:
char a[3];这种,它的对齐方式和分别写3个char是一样的.也就是说它还是按1个字节对齐。
如果写:typedef char Array3[3];
Array3这种类型的对齐方式还是按1个字节对齐,而不是按它的长度。
不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个。
下面是从另外一篇文章(http://community.csdn.net/Expert/TopicView3.asp?id=3564500)的一些对各个类型以及结构体对齐方式的补充:
为了能使CPU对变量进行高效快速的访问,变量的起始地址应该具有某些特性,即所谓的”对齐“。例如对于4字节的int类型变量,其起始地址应该位于4字节边界上,即起始地址能够被4整除。变量的对齐规则如下(32位系统):
Type
Alignment
char
在字节边界上对齐
short (16-bit)
在双字节边界上对齐
int and long (32-bit)
在4字节边界上对齐
float
在4字节边界上对齐
double
在8字节边界上对齐
structures
单独考虑结构体的每个成员,它们在不同的字节边界上对齐。
其中最大的字节边界数就是该结构的字节边界数。
如果结构体中有结构体,那么这是一个递归的过程。
设编译器设定的最大对齐字节边界数为n,对于结构体中的某一成员item,它相对于结构首地址的实际字节对齐数目x应该满足以下规则:
x = min(n, sizeof(item)
例如,对于结构体:
- struct
- {
- char a;
- long b;
- }T;
当位于32位系统,n = 8时:
a的偏移量为0,b的偏移量为4,中间填充了3个字节,b的x为4。
当位于32位系统,n = 2时:
a的偏移量为0,b的偏移为2,中间填充了1个字节,b的x为2。
结构体的sizeof:
设结构体的最后一个成员为LastItem,其相对于结构体首地址的偏移为offset(LastItem),其大小为sizeof(LastItem),结构体的字节对齐数为N,则:结构体的sizeof 为: 若offset(LastItem)+ sizeof(LastItem)能够被N整除,那么就是offset(LastItem)+ sizeof(LastItem),否则,在后面填充,直到能够被N整除。
另外:
1) 对于空结构体,sizeof == 1;因为必须保证结构体的每一个实例在内存中都有独一无二的地址.
2)
结构体的静态成员不对结构体的大小产生影响,因为静态变量的存储位置与结构体的实例地址无关。例如:
struct {static int I;} T;
struct {char a; static int I;} T1;
sizeof(T) == 1; sizeof(T1) == 1;