C结构体边界对齐2

C语言结构体对齐也是老生常谈的话题了。基本上是面试题的必考题。结构体到底怎样对齐?下面总结了对齐原则,在没有#pragma pack宏的情况下:

原则1、普通数据成员对齐规则:第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。

原则2、结构体成员对齐规则:如果一个结构里有某些结构体成员,则该结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)

原则3、结构体大小对齐规则:结构体大小也就是sizeof的结果,必须是其内部成员中最大的对齐参数的整数倍,不足的要补齐。

补充一点,如果数组作为结构体成员,比如:char a[3]。它的对齐方式和分别写3个char是一样的,也就是说它还是按1个字节对齐。如果写: typedef char Array3[3];Array3这种类型的对齐方式还是按1个字节对齐,而不是按它的长度3对齐。如果共用体作为结构体成员,则该共用体成员要从其内部 最大元素大小的整数倍地址开始存储。

还有一种对齐原则,和上面三条一致,可以参考着理解。

成员对齐有一个重要的条件,即每个成员按自己的方式对齐。其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里默认是8字节)中较小的一个对齐。并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。

这三个原则具体怎样理解呢?我们看下面几个例子,通过实例来加深理解。

例1:struct {
              short a1;
              short a2;
              short a3;
       }A;

       struct{
              long a1;
              short a2;
      }B;

sizeof(A) = 6; 这个很好理解,三个short都为2。

sizeof(B) = 8; 这个比是不是比预想的大2个字节?long为4,short为2,整个为8,因为原则3。

例2:struct A{
               int a;
               char b;
               short c;
        };

        struct B{
               char b;
               int a;
               short c;
       };

sizeof(A) = 8; int为4,char为1,short为2,这里用到了原则1和原则3。

sizeof(B) = 12; 是否超出预想范围?char为1,int为4,short为2,怎么会是12?还是原则1和原则3。

深究一下,为什么是这样,我们可以看看内存里的布局情况。

                    a        b   c
A的内存布局:1111, 1*,11

                    b       a       c
B的内存布局:1***,1111,11**

其中星号*表示填充的字节。A中,b后面为何要补充一个字节?因为c为 short,其起始位置要为2的倍数,就是原则1。c的后面没有补充,因为b和c正好占用4个字节,整个A占用空间为4的倍数,也就是最大成员int类型 的倍数,所以不用补充。B中,b是char为1,b后面补充了3个字节,因为a是int为4,根据原则1,起始位置要为4的倍数,所以b后面要补充3个字 节。c后面补充两个字节,根据原则3,整个B占用空间要为4的倍数,c后面不补充,整个B的空间为10,不符,所以要补充2个字节。

再看一个结构中含有结构成员的例子:

例3:struct A{
               int a;
               double b;
               float c;
       };

       struct B{
              char e[2];
              int f;
              double g;   
              short h;
              struct A i;
       };

sizeof(A) = 24; 这个比较好理解,int为4,double为8,float为4,总长为8的倍数,补齐,所以整个A为24。

sizeof(B) = 48; 看看B的内存布局。

                    e         f       g                h                      i  
B 的内存布局:11* *,1111,11111111, 11 * * * * * *,1111* * * *, 11111111, 1111 * * * * 。 i其实就是A的内存布局。i的起始位置要为24的倍数,所以h后面要补齐。把B的内存布局弄清楚,有关结构体的对齐方式基本就算掌握了。

以上讲的都是没有#pragma pack宏的情况,如果有#pragma pack宏,对齐方式按照宏的定义来。比如上面的结构体前加#pragma pack(1),内存的布局就会完全改变。sizeof(A) = 16; sizeof(B) = 32;有了#pragma pack(1),内存不会再遵循原则1和原则3了,按1字节对齐。没错,这不是理想中的没有内存对齐的世界吗。

                    a       b               c
A的内存布局:1111,11111111,1111

                    e    f       g               h   i  
B的内存布局:11,1111,11111111,11,1111, 11111111, 1111 

 那#pragma pack(2)的结果又是多少呢?#pragma pack(4)呢?留给大家自己思考吧,相信没有问题。

 还有一种常见的情况,结构体中含位域字段。位域成员不能单独被取sizeof值。C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。

使用位域的主要目的是压缩存储,其大致规则为:  
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;  
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;  
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式;  
4) 如果位域字段之间穿插着非位域字段,则不进行压缩;  
5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。

还是让我们来看看例子。

例4:struct A{  
               char f1 : 3;  
               char f2 : 4;  
               char f3 : 5;  
       };

                    a      b          c
A的内存布局:111,1111 *,11111 * * *

位域类型为char,第1个字节仅能容纳下f1和f2,所以f2被压缩到第1个字节中,而f3只能从下一个字节开始。因此sizeof(A)的结果为2。

例5:struct B{  
               char f1 : 3;  
               short f2 : 4;  
               char f3 : 5;  
       };

由于相邻位域类型不同,在VC6中其sizeof为6,在Dev-C++中为2。

例6:struct C{  
               char f1 : 3;  
               char f2;  
               char f3 : 5;  
       };

非位域字段穿插在其中,不会产生压缩,在VC6和Dev-C++中得到的大小均为3。

考虑一个问题,为什么要设计内存对齐的处理方式呢?如果体系结构是不对齐 的,成员将会一个挨一个存储,显然对齐更浪费了空间。那么为什么要使用对齐呢?体系结构的对齐和不对齐,是在时间和空间上的一个权衡。对齐节省了时间。假 设一个体系结构的字长为w,那么它同时就假设了在这种体系结构上对宽度为w的数据的处理最频繁也是最重要的。它的设计也是从优先提高对w位数据操作的效率 来考虑的。有兴趣的可以google一下,人家就可以跟你解释的,一大堆的道理。

最后顺便提一点,在设计结构体的时候,一般会尊照一个习惯,就是把占用空间小的类型排在前面,占用空间大的类型排在后面,这样可以相对节约一些对齐空间。


先来说说为什么要有边界对齐:

简单说,任何CPU都有一个固定的基本长度,下面以32位CPU为例(也有64位或其他的)。CPU在工作时只能按照该长度的整倍数为边界进行内存操作。即只能从地址0、32、64、96...处进行存取,而不能从27、58、83等非边界地址处进行。如果一定要取这些非边界地址处的内容,则必须用若干个操作将其凑出来,因而大大影响存取效率。

另一方面,一个结构体的设计长度却并不一定是32的倍数,例如一个包含六个字符的结构其设计长度仅为48位。如果多个这样的结构在内存中顺着摆放,则许多结构的起始地址将不在边界处。因此,编译程序总是会将每个结构的尾部都加入一些必要的空白,将其凑成32的整数倍。这就是边界对齐的基本道理。

下面开始谈边界对齐:

#pragma pack(4)
  class TestB
  {
  public:
    int aa;
    char a;
    short b;
    char c;
  };
  int nSize = sizeof(TestB);
  这里nSize结果为12,在预料之中。

  现在去掉第一个成员变量为如下代码:
  #pragma pack(4)
  class TestC
  {
  public:
    char a;
    short b;
    char c;
  };
  int nSize = sizeof(TestC);
  按照正常的填充方式nSize的结果应该是8,为什么结果显示nSize为6呢?

事实上,很多人对#pragma pack的理解是错误的。
#pragma pack规定的对齐长度,实际使用的规则是:
结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
也就是说,当#pragma pack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。
而结构整体的对齐,则按照结构体中最大的数据成员 和 #pragma pack指定值 之间,较小的那个进行。

具体解释
#pragma pack(4)
  class TestB
  {
  public:
    int aa; //第一个成员,放在[0,3]偏移的位置,
    char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
    short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。
    char c; //第四个,自身长为1,放在[8]的位置。
  };
这个类实际占据的内存空间是9字节
类之间的对齐,是按照类内部最大的成员的长度,和#pragma pack规定的值之中较小的一个对齐的。
所以这个例子中,类之间对齐的长度是min(sizeof(int),4),也就是4。
9按照4字节圆整的结果是12,所以sizeof(TestB)是12。


如果
#pragma pack(2)
class TestB
  {
  public:
    int aa; //第一个成员,放在[0,3]偏移的位置,
    char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
    short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。
    char c; //第四个,自身长为1,放在[8]的位置。
  };
//可以看出,上面的位置完全没有变化,只是类之间改为按2字节对齐,9按2圆整的结果是10。
//所以 sizeof(TestB)是10。

最后看原贴:
现在去掉第一个成员变量为如下代码:
  #pragma pack(4)
  class TestC
  {
  public:
    char a;//第一个成员,放在[0]偏移的位置,
    short b;//第二个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[2,3]的位置。
    char c;//第三个,自身长为1,放在[4]的位置。
  };
//整个类的大小是5字节,按照min(sizeof(short),4)字节对齐,也就是2字节对齐,结果是6
//所以sizeof(TestC)是6。

当数据定义中出现__declspec( align() )时,指定类型的对齐长度还要用自身长度和这里指定的数值比较,然后取其中较大的。最终类/结构的对齐长度也需要和这个数值比较,然后取其中较大的。

可以这样理解, __declspec( align() ) 和 #pragma pack是一对兄弟,前者规定了对齐的最小值,后者规定了对齐的最大值,两者同时出现时,前者拥有更高的优先级。
__declspec( align() )的一个特点是,它仅仅规定了数据对齐的位置,而没有规定数据实际占用的内存长度,当指定的数据被放置在确定的位置之后,其后的数据填充仍然是按照#pragma pack规定的方式填充的,这时候类/结构的实际大小和内存格局的规则是这样的:
在__declspec( align() )之前,数据按照#pragma pack规定的方式填充,如前所述。当遇到__declspec( align() )的时候,首先寻找距离当前偏移向后最近的对齐点(满足对齐长度为max(数据自身长度,指定值) ),然后把被指定的数据类型从这个点开始填充,其后的数据类型从它的后面开始,仍然按照#pragma pack填充,直到遇到下一个__declspec( align() )。
当所有数据填充完毕,把结构的整体对齐数值和__declspec( align() )规定的值做比较,取其中较大的作为整个结构的对齐长度。
特别的,当__declspec( align() )指定的数值比对应类型长度小的时候,这个指定不起作用。

如果代码: 
#pragma pack(8) 
struct S1{ 
char a; 
long b; 
}; 
struct S2 
char c; 
struct S1 d; 
long e; 
}; 
#pragma pack() 
sizeof(S2)结果为16. 
成员对齐有一个重要的条件,即每个成员分别对齐.即每个成员按自己的方式对齐. 
也就是说上面虽然指定了按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是4个字节,它是默认按4字节对齐,和指定的一样,所以它对到4字节的边界上,这时,已经使用了12个字节了,所以又添加了4个字节的空,从第12个字节开始放置成员e.这时,长度为16,已经可以被4(成员e按4字节对齐)整除.这样,一共使用了16个字节. 

S1的内存布局:11**,1111, 
S1.a S1.b 
S2的内存布局:1***,1***,1111,1111 

这里有三点很重要: 
1.每个成员分别按自己的方式对齐,并能最小化长度

2.复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度 
3.对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值