您的位置:首页 > 理论基础 > 数据结构算法

[转]深入C语言之字节对齐 - [C 数据结构 算法]

2010-12-15 13:31 369 查看
 

在C程序设计中我们经常需要用到一种数据类型的长度(占内存的字节数),例如:
  int *p = NULL;
  p = (int *)malloc(10*sizeof(int));/*用sizeof(int)来的到int类型的长度*/

     
      字节对齐是为了提高CPU的读取效率.比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据.显然在读取效率上下降很多.
      C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型
(如数组、结构、联合等)的数据单元.在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间.各个成员按
照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同.
 
     先让我们看几个例子吧(32bit,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(strcut 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。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间.一般地,可以通过下面的方法来改变缺省的对界条
件:
· 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐.
· 使用伪指令#pragma pack (),取消自定义字节对齐方式.
      另外,还有如下的一种方式:
· __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上.如果结构中有成员的长度大于n,则按照最大成员的长度来
对齐.
· __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐.
   以上的n = 1, 2, 4, 8, 16... 第一种方式较为常见.

     那编译器是按照什么样的原则进行对齐的?先看四个重要的基本概念:
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;
即:结构体B:
       a        b      c     
    1xxx,1111,11xx
  0x0000 ~0x000B
       其实如果就这一个就来说它已将满足字节对齐了,因为它的起始地址是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 () /*取消指定对齐,恢复缺省对齐*/

即:结构体C:
     b  a      c     
    1x,1111,11
  0x0000 ~0x0007

  最后看一个Intel和微软和本公司同时出现的面试题,来练习一下:
#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?
结果如下:sizeof(S2)结果为24。s2的c后面空了3个字节接着是d。

分析:
  S1中,成员a是2字节默认按2字节对齐,指定对齐参数为8,这两个值中取2,a按2字节对齐; (???)
          成员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
 

第一个变量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:
#pragma pack (1) /*指定按1字节对齐*/
struct D
{
   char b;
   int a;
   short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct D)值为7。用sizeof可得到C语言中数据类型的长度,对基本数据类型而言,结果值很容易理解,但当sizeof的操作对象是一个结构类型时意想不到
麻烦就来了,其结果值经常与我们设想的不一样.为什么呢?现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的
变量的访问可以从任何地址开始,但实际情况并非如此.一些平台对某些特定类型的数据只能从某些特定地址开始存取,这就需要各种
类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放.这就是所谓的字节对齐.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息