结构体字节对齐详解


为什么要对齐?


效率问题,不同架构,不同处理方法。

在实际的程序开发中,为了提高数据的读取效率,在内存资源足够的情况下,一般定义数据结构时候,因该考虑四字节对齐,其原因很简单,现在的计算机大部分是32位机,也就是四个字节。在cpu每次执行读取数据时候,则相关处理数据的寄存器/ 累加器均只能处理32位数据,则只能读取(一次)32位数据。有些情况下字节对齐的数据结构,要比非对齐的数据结构上占用更少空间。

对齐跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。比如在32位cpu下,假设一个整型变量的地址为0x00000004,那它就是自然对齐的。

需要字节对齐的根本原因在于CPU访问数据的效率问题。假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。而如果变量在自然对齐位置上,则只要一次就可以取出数据。

在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的”对齐”. 比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除.

内存对齐的三条重要原则


1:数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储。
2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int
,double等元素,那b应该从8的整数倍开始存储.)
3:收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐. 

example1:


typedef struct bb
{
    int id; //[0]....[3]
    double weight; //[7].....[15]      原则1
    float height; //[16]..[19],总长要为8的整数倍,补齐[20]...[23]     原则3
}BB;
typedef struct aa
{
    char name[2]; //[0],[1]
    int id; //[4]...[7]          原则1
    double score; //[8]....[15]    
    short grade; //[16],[17]        
    BB b; //[24]......[47]          原则2
}AA;

cout<<sizeof(a)<<" "<<sizeof(b)<<endl;

输出:48 24

在代码前加一句#pragma pack(1),上面的代码输出为
32 16
bb是4+8+4=16,aa是2+4+8+2+16=32;
这是理想中的没有内存对齐,#pragma pack(1),告诉编译器,所有的对齐都按照1的整数倍对齐,换句话说就是没有对齐规则.

example2:

对于标准数据类型,它的地址只要是它的长度的整数倍就行了,而非标准数据类型按下面的原则对齐:
数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
联合 :按其包含的长度最大的数据类型对齐。
结构体: 结构体中每个数据类型都要对齐。
比如有如下一个结构体:

struct stu
{
    char sex;
    int length;
    char name[10];
};

struct stu my_stu;

cout << sizeof(my_stu) << endl;

输出 :20

由于在x86下,GCC默认按4字节对齐,它会在sex后面跟name后面分别填充三个和两个字节使length和整个结构体对齐。于是我们sizeof(my_stu)会得到长度为20,而不是15.

char 占1个字节,填充3个字节,int占4个字节,char数组占10个字节,一共18个字节,18不是4的整数倍,结尾填充2个字节,一共20个字节。


example3:

struct MyStruct  
{  
    double doub;  
    char ch;  
    int i;  
};
对结构MyStruct采用sizeof会出现什么结果呢?sizeof(MyStruct)为多少呢?

sizeof(ms)为16。这是编译器对变量存储的一个特殊处理。为了提高CPU的存储速度,编译器对一些变量的起始地址做了“对齐”处理。在默认情况下,编译器规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。

为上面的结构分配空间的时候,编译器根据成员变量出现的顺序和对齐方式,先为第一个成员doub分配空间,其起始地址跟结构的起始地址相同(刚好偏移量0刚好为sizeof(double)的倍数),该成员变量占用sizeof(double)=8个字节;接下来为第二个成员ch分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把ch存放在偏移量为8的地方满足对齐方式,该成员变量占用sizeof(char)=1个字节;接下来为第三个成员i分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,不是sizeof(int)=4的倍数,为了满足对齐方式对偏移量的约束问题,编译器自动填充3个字节(这三个字节没有放什么东西),这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是sizeof(int)=4的倍数,所以把i存放在偏移量为12的地方,该成员变量占用sizeof(int)=4个字节;这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:8+1+3+4=16,刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以没有空缺的字节需要填充。所以整个结构的大小为:sizeof(MyStruct)=8+1+3+4=16,其中有3个字节是编译器自动填充的,没有放任何有意义的东西。

各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节编译器会自动填充。同时编译器为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。 

下面再举个例子,交换一下上面的MyStruct的成员变量的位置,使它变成下面的情况: 

struct MyStruct 
{ 
    char ch; 
    double doub; 
    int i; 
}; 

在编译器环境下,可以得到sizeof(MyStruct)为24。结合上面提到的分配空间的一些原则,分析下编译器怎么样为上面 的结构分配空间:
struct MyStruct
{
    char ch;        // 偏移量为0,满足对齐方式,ch占用1个字节;
    double doub;    //下一个可用的地址的偏移量为1,不是sizeof(double)=8  
                    //的倍数,需要补足7个字节才能使偏移量变为8(满足对齐  
                    //方式),因此编译器自动填充7个字节,doub存放在偏移量为8  
                    //的地址上,它占用8个字节。  
    int i;            //下一个可用的地址的偏移量为16,是sizeof(int)=4的倍
    //数,满足int的对齐方式,所以不需要编译器自动填充,i存  
                    //放在偏移量为16的地址上,它占用4个字节。  
};                    //所有成员变量都分配了空间,空间总的大小为1+7+8+4=20,不是结构  
 //的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof  
                    //(double)=8)的倍数,所以需要填充4个字节,以满足结构的大小为  
                    //sizeof(double)=8的倍数。
所以该结构总的大小为:sizeof(MyStruct)为1+7+8+4+4=24。其中总的有7+4=11个字节是编译器自动填充的,没有放任何有意义的东西。


example4:

struct{
    short a1;
    short a2;
    short a3;
}A;
struct{
    long a1;
    short a2;
}B;

sizeof(A)=6, sizeof(B)=8

成员对齐有一个重要的条件,即每个成员按自己的方式对齐.其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里默认是8字节)中较小的一个对齐.并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节.结构体A中有3个short类型变量,各自以2字节对齐,结构体对齐参数按默认的8字节对齐,则a1,a2,a3都取2字节对齐,则sizeof(A)为6,其也是2的整数倍;B中a1为4字节对齐,a2为2字节对齐,结构体默认对齐参数为8,则a1取4字节对齐,a2取2字节对齐,结构体大小6字节,6不为4的整数倍,补空字节,增到8时,符合所有条件,则sizeof(B)为8


example5:

typedef struct ms1
{
    char a;
    short b;
    double c;
} MS1;
typedef struct ms2
{
    char a;
    MS1 b;
} MS2;

cout<<sizeof(MS1)<<" "<<sizeof(MS2)<<endl;

输出:16  24

ANSI C保证结构体中各字段在内存中出现的位置是随它们的声明顺序依次递增的,并且第一个字段的首地址等于整个结构体实例的首地址。

许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。某些处理器在数据不满足对齐要求的情况下可能会出错,但是Intel的IA32架构的处理器则不管数据是否对齐都能正确工作。不过Intel奉劝大家,如果想提升性能,那么所有的程序数据都应该尽可能地对齐。Win32平台下的微软C 编译器(cl.exe for 80x86)在默认情况下采用如下的对齐规则: 任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。比如对于double类型(8字节),就要求该类型数据的地址总是8的倍数,而 char类型数据(1字节)则可以从任何一个地址开始。ANSI C规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小之和。嗯?填充区?对,这就是为了使结构体字段满足内存对齐要求而额外分配给结构体的空间。

MS1:Bytes: 1 1 2 4 8

sizeof(short)等于2,b字段应从偶数地址开始,所以a的后面填充一个字节,而sizeof(double)等于8,c字段要从8倍数地址开始,前面的a、b字段加上填充字节已经有4
bytes,所以b后面再填充4个字节就可以保证c字段的对齐要求了。sizeof(MS1)等于16,b的偏移是2,c的偏移是8。

MS3中内存要求最严格的字段是c,那么MS3类型数据的对齐模数就与double的一致(为8),a字段后面应填充7个字节,因此MS4的布局应该是:

Bytes: 1 7 16

显然,sizeof(MS4)等于24,b的偏移等于8。







你可能感兴趣的:(C++,sizeof,cc++,笔试面试)