sizeof计算结构体大小:自然对齐

要正确计算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 的域都一样,仅次序不同,为什么它们需要的存储空间不一样?

分析:此题为中国科学院研究生入学考试题。考查了结构体自然对齐的规则。下面画出结构的存储图11

 


11结构体a自然对齐存储结构

如图11所示为结构体a在自然对齐后的存储结构,从图中可以看出:c1char类型,占一个字节。之后为了让i自然对齐,因此必须填充3个字节。之后iint类型,占4个字节。c2char类型,占用1个字节。之后的fdouble类型,为了让f对齐到8的整数倍地址必须填充7个字节。之后是f的存储大小占8个字节。所以,a的存储大小为:

sizeof(a)=1+3+4+1+7+8=24

如图12所示为结构体b在自然对齐后的存储结构,从图中可以看出:c1占用1个字节,c2占用1个字节,为了让i自然对齐而填充2个字节,i占用了4个字节,f占用了8个字节。

                                               12 结构体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)的倍数。

 

也就是说:

第一、如果 n 大于等于该变量所占用的字 节数,那么偏移量必须满足默认的对齐方式;如果 n 小于该变量的类型所占用 的字节数,那么偏移量为 n 的倍数,不用满足默认的对齐方式。

 

第二:结构的总大小也有个 约束条件,分下面两种情况:如果 n 大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数; 否则必须为 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.s2c后面空了几个字节接着是d?

 

分析:此题为微软公司的一道笔试题,同样考查了关于结构对齐的规则。只不过这里不是自然对齐,而是用了#pragma pack()来规定了对齐的方式。


首先看s1。由于#pragma pack(8)要求8字节对齐,但s1中所有成员都比8小,所以各个成员只需要按照自然对齐即可,所以a占2个字节,b占4个字节,a后面需要补齐2字节,才能使long类型的b自然对齐。然后b占4个字节,因此整个s1结构共占用:2+2(pad)+4=8字节。

再看s2c1个字节,d为s1结构体成员,s1结构体成员根据对齐规则,按照其中最大成员long的4字节对齐,所以c之后需要补3字节才能让d达到4字节对齐。对于x86long long数据类型为8字节,所以存完d后,需要在后面补4个字节,让e 8字节对齐。因此s2结构共占用:1+3(pad)+8+4(pad)+8=24

最后看s3。c占1个字节,由于a为short类型,所以c之后需要填充1个字节,才能让a对齐。此时,b已经对齐,占4个字节。而e大小为8个字节,已经对齐。所以s3结构共占用:1+1(pad)+2+4+8=16。

 

答案:sizeof(s2) = 24sizeof(s3) = 16

 

那么在编程中如何处理字节对齐情况呢?如果在编程的时候要考虑节约空间的话,那么只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照类型大小从小到大声明,尽量减少中间的填补空间。还有一种就是为了以空间换取时间的效率,显示的进行填补空间进行对齐,比如有一种使用空间换时间做法是显式的插入pad成员:

 

struct Demo

{

     char      a;

     char      pad[3];  //使用空间换时间

     int b;       //对齐到4的整数倍

}

pad成员对程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员通常编译器也会自动填补对齐,自己加上它只是起到显式的提醒作用。

 

栈空间对齐:

 

typedef struct _s

{

       char c1;

       char c2;

       short s1;

       int x;

 

}S;

 

int main(int argc, char* argv[])

{

       printf("Hello World!\n");

 

       char c1 =0;

       char c2 = 0;

       short s1 = 0;

       int x = 0;

 

       float f = 0.0f;

       double d = 0.0;

       S s={0};

 

       printf("c1:%p,c2:%p,s1:%p,x:%p, f:%p,d:%p\n",&c1,&c2,&s1,&x,&f,&d);

       printf("sizeof s =%d\n", sizeof(s));

 

       return 0;

}

周哥教IT,分享编程知识,提高编程技能,程序员的充电站。跟着周哥一起学习,每天都有进步。

通俗易懂,深入浅出,一篇文章只讲一个知识点。

当你决定关注「周哥教IT」,你已然超越了90%的程序员!

IT黄埔-周哥教IT技术交流QQ群:213774841,期待您的加入!

二维码
微信扫描二维码关注