标题:自然对齐


在每年各个公司的笔试题中,有一种典型的问题就是计算sizeof()结构体大小的问题。要掌握好这类问题,需要了解结构体的自然对齐规则。

一,基本类型的长度计算

要做好sizeof相关的计算,首先得掌握C和C++里的一些基本类型的长度(注意,sizeof计算的值都是以字节(Byte)为单位):
sizeof(char)的长度为:1
sizeof(short)的长度为:2
sizeof(int)的长度为:4
sizeof(long)的长度为:4(Win X86和X64都为4,Linux X86为4,X64为8)
sizeof(float)的长度为:4
sizeof(double)的长度为:8
sizeof(bool)的长度为:1(C++里)
sizeof(BOOL)的长度为:4(windows平台)
sizeof(p)的长度:x86为4,x64为8,其中p为指针类型变量,如char *p,也就是说,在X86平台,指针的长度是4,在X64平台,指针的长度是8。

二,结构体长度的计算

要正确计算sizeof(结构体)的大小, 需要理解和掌握好数据对齐的概念。 数据对齐分为自然对齐和强制对齐两种方式。 1.自然对齐:各个类型自然对齐,即其内存地址必须是其类型本身的整数倍。结构体对齐到其中成员最大长度类型的整数倍。计算机中内存空间按照字节划分,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该数据。

无论如何,为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。

一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。

某些操作双四字的指令需要内存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常(#GP)。双四字的自然边界是能够被16整除的地址。其他的操作双四字的指令允许未对齐的访问(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。

自然对齐应该遵守如下两条规则:
1)数据成员对齐规则:
在默认情况下,各成员变量存放的起始地址相对于结构的起始地址的偏移量:sizeof(类型)或其倍数
2)整体对齐规则:
结构的总大小也有个约束条件:最大sizeof(类型)的整数倍
问题:一个C语言程序如下:
typedef struct _a
{
   char c1;
   long i;
   char c2;
   double f;
}a;
typedef struct _b
{
   char c1;
   char c2;
   long i;
   double f;
}b;
void main(void)
{
    printf("Size of double, long, char = %d, %d, %d\n",
        sizeof (double), sizeof (long), sizeof (char));
    printf("Sizeof a, b = %d, %d\n", sizeof (a), sizeof (b));
}
该程序在SPARC/Solaris工作站上的运行结果如下:
sizeof of double, long, char = 8, 4, 1
sizeof of a, b = 24, 16
结构体类型a 和 b 的域都一样,仅次序不同,为什么它们需要的存储空间不一样?
分析:此题为中国科学院研究生入学考试题。考查了结构体自然对齐的规则。下面画出结构的存储图1。

图1结构体a自然对齐存储结构

如图1所示为结构体a在自然对齐后的存储结构,从图中可以看出:c1为char类型,占一个字节。之后为了让i自然对齐,因此必须填充3个字节。之后i为int类型,占4个字节。c2是char类型,占用1个字节。之后的f为double类型,为了让f对齐到8的整数倍地址必须填充7个字节。之后是f的存储大小占8个字节。所以,a的存储大小为:
sizeof(a)=1+3+4+1+7+8=24。
如图2所示为结构体b在自然对齐后的存储结构,从图中可以看出:c1占用1个字节,c2占用1个字节,为了让i自然对齐而填充2个字节,i占用了4个字节,f占用了8个字节。

图2结构体b自然对齐存储结构


所以,b的存储大小为:
sizeof(b)=1+1+2+4+8=16。
2.强制对齐
除了自然对齐外,还有另外一种对齐方式:
#pragma pack(push) //保存对齐状态
#pragma pack(n) //定义结构对齐到n
定义结构
#pagman pack(pop)//恢复对齐状态
上面的预编译语句将定义的结构体强制对齐到n。#pragma pack(n)来设定变量以n字节对齐方式。强制对齐应该遵守如下两条对齐规则:
1)数据成员对齐规则:
n字节对齐就是说变量存放的起始地址的偏移量:min(sizeof(类型),n)或其倍数。
2)整体对齐规则:
结构的总大小也有个约束条件:min(最大的sizeof(类型),n)的倍数。
#pragma pack(8)
struct s1
{
   short a;
   long b;
};
struct s2
{
   char c;
    s1 d;
long long e;
};
struct s3
{
    char c;
    short a;
    long b;
    long long e;
};
#pragma pack()
1.sizeof(s1)=? sizeof(s2) = ? sizeof(s3) = ?
2.s2的c后面空了几个字节接着是d?
分析:此题为微软公司的一道笔试题,同样考查了关于结构对齐的规则。只不过这里不是自然对齐,而是用了#pragma pack()来规定了对齐的方式。
首先看s1。由于#pragma pack(8)要求8字节对齐,所以a后面需要补齐2字节,才能使long结构的b自然对齐。然后b占4个字节,因此整个s1结构共占用:2+2+4=8字节。
再看s2。c占1个字节,d已经是8字节对齐,所以c之后需要补7字节才能让d达到8字节对齐。对于x86,long long数据类型为8字节,所以e已经8字节对齐。因此s2结构共占用:1+7+8+8=24。
最后看s3。c占1个字节,由于a为short类型,所以c之后需要填充1个字节,才能让a对齐。此时,b已经对齐,占4个字节。而e大小为8个字节,已经对齐。所以s3结构共占用:1+1+2+4+8=16。
答案:sizeof(s2) = 24,sizeof(s3) = 16。
那么在编程中如何处理字节对齐情况呢?如果在编程的时候要考虑节约空间的话,那么只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照类型大小从小到大声明,尽量减少中间的填补空间。还有一种就是为了以空间换取时间的效率,显示的进行填补空间进行对齐,比如有一种使用空间换时间做法是显式的插入pad成员:
struct Demo
{
   char a;
   char pad[3]; //使用空间换时间
   int b; //对齐到4的整数倍
}
pad成员对程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员通常编译器也会自动填补对齐,自己加上它只是起到显式的提醒作用。

3.栈上对齐方式:
与结构体的自然对齐不同,在X86平台,栈上对齐方式是整数相关类型按照4字节对齐,浮点数按照8字节对齐;在X64平台,栈上是按照 16字节对齐。比如代码:
int main(void)
{
    char c = 'a';
    int a = 4;
    char ca[10]= "hello";
    float f = 1.1f;
    printf("%c,%d,%s,%f\n", c,a,ca,f);
    return 0;
}
我们已经知道,局部变量c,a,ca,f都存放在栈上。由于在X86平台,栈上的数据是按照4字节对齐,所以 字符类型c占4个字节,整型a占4个字节,char类型数组ca虽然只有10个字节的char元素,但是由于要4字节对齐 所以ca占12个字节(2个字节为对齐填补的),f占4个字节(4个字节为对齐填补)。 所以,栈上的空间为(X86,VC6环境下):


看文字不过瘾?点击我,进入腾讯课堂视频教学
麦洛科菲长期致力于IT安全技术的推广与普及,我们更专业!我们的学员已经广泛就职于BAT360等各大IT互联网公司。详情请参考我们的 业界反馈 《周哥教IT.C语言深学活用》视频

我们的微信公众号,敬请关注