C/C++ 内存对齐

来源:互联网 发布:servlet ajax json 编辑:程序博客网 时间:2024/05/23 19:12

1、为什么要对齐

        《Windows核心编程》里这样说:当CPU访问正确对齐的数据时,它的运行效率最高,当数据大小的数据模数的内存地址是0时,数据是对齐的。例如:WORD值应该是总是从被2除尽的地址开始,而DWORD值应该总是从被4除尽的地址开始,数据对齐不是内存结构的一部分,而是CPU结构的一部分。当CPU试图读取的数值没有正确的对齐时,CPU可以执行两种操作之一:1)产生一个异常条件;    2)执行多次对齐的内存访问,以便读取完整的未对齐数据,若多次执行内存访问,应用程序的运行速度就会慢。在最好的情况下,是两倍的时间,有时更长。

2、变量的对齐参数


3、对齐标准
   

         在C99标准中,对于内存对齐的细节没有作过多的描述,具体的实现交由编译器去处理,所以在不同的编译环境下,内存对齐可能略有不同,但是对齐的最基本原则是一致的,对于结构体的字节对齐主要有下面两点:

      1)结构体每个成员相对结构体首地址的偏移量(offset)是对齐参数的整数倍,如有需要会在成员之间填充字节。

       第一个数据成员放在offset为0的地方。

       这里的对齐参数是取每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(#pragmapack(n),win32 系统可以取1  2  4 8,默认情况下为8)中较小的一个。编译器在为结构体成员开辟空间时,首先检查预开辟空间的地址相对于结构体首地址的偏移量是否为对齐参数的整数倍,若是,则存放该成员;若不是,则填充若干字节,以达到整数倍的要求。举个简单的例子,比如在结构体A中有变量int a,a的自身对齐参数为4(环境为windows/vc),而VC默认的对齐参数为8,取较小者,则对于a,它相对于结构体A的起始地址的偏移量必须是4的倍数。

      2)结构体变量所占空间的大小是对齐参数大小的整数倍。如有需要会在最后一个成员末尾填充若干字节使得所占空间大小是对齐参数大小的整数倍。

       对于第二条原则,结构体变量所占空间的大小是对齐参数的整数倍。这句话中的对齐参数有点复杂,它是取结构体中所有变量的对齐参数的最大值和系统默认对齐参数#pragma pack(n)比较,较小者作为对齐参数。举个例子假如在结构体A中先后定义了两个变量int a;double b;对于变量a,它的自身对齐参数为4,而#pragma pack(n)值默认为8,则a的对齐参数为4;b的自身对齐参数为8,而#pragma pack(n)的默认值为8,则b的对齐参数为8。即结构体内的每个遍历也要取自身的对齐参数和默认对齐参数比较,取较小者作为这个变量的对齐参数。由于a的最终对齐参数为4,b的最终对齐参数为8,那么两者较大者是8,然后再拿8和#pragma pack(n)作比较,取较小者作为对齐参数,也就是8,即意味着结构体最终的大小必须能被8整除

总体:先局部对齐,再全局对齐。局部取小确定偏移整除,全局取大确定结构体整除

3、测试例子

注意:以下例子的测试结果均在windows(32)/VC下测试的,其默认对齐参数为8 

[cpp] view plain copy
  1. #include <iostream>  
  2. using namespace std;  
  3. //#pragma pack(4)    //设置4字节对齐   
  4. //#pragma pack()     //取消4字节对齐   
  5.   
  6. typedef struct node1  
  7. {  
  8.     int a;  
  9.     char b;  
  10.     short c;  
  11. }S1; //4   1+1 2=8
  12.   
  13. typedef struct node2  
  14. {  
  15.     char a;  
  16.     int b;  
  17.     short c;  
  18. }S2;  //1+3  4  2=10  10+2 =12
  19.   
  20. typedef struct node3  
  21. {  
  22.     int a;  
  23.     short b;  
  24.     static int c;  
  25. }S3;  //4 + 2=6 6+2=8
  26.   
  27. typedef struct node4  
  28. {  
  29.     bool a;  
  30.     S1 s1;  
  31.     short b;  
  32. }S4;  
  33.   
  34. typedef struct node5  
  35. {  
  36.     bool a;  
  37.     S1 s1;  
  38.     double b;  
  39.     int c;  
  40. }S5;  
  41.   
  42.   
  43.   
  44. int main(int argc, char *argv[])  
  45. {  
  46.     cout<<sizeof(char)<<" "<<sizeof(short)<<" "<<sizeof(int)<<" "<<sizeof(float)<<" "<<sizeof(double)<<endl;  
  47.     S1 s1;  
  48.     S2 s2;  
  49.     S3 s3;  
  50.     S4 s4;  
  51.     S5 s5;  
  52.     cout<<sizeof(s1)<<" "<<sizeof(s2)<<" "<<sizeof(s3)<<" "<<sizeof(s4)<<" "<<sizeof(s5)<<endl;  
  53.     return 0;  
  54. }  

输出结果:

[cpp] view plain copy
  1. 1 2 4 4 8  
  2. 8 12 8 16 32  

下面解释一下其中的几个结构体字节分配的情况

比如对于node2

[cpp] view plain copy
  1. typedef struct node2  
  2. {  
  3.     chara;  
  4.     intb;  
  5.     shortc;  
  6. }S2;  

sizeof(S2)=12;

对于变量a,它的自身对齐参数为1,#pragma pack(n)默认值为8,则最终a的对齐参数为1,为其分配1字节的空间,它相对于结构体起始地址的偏移量为0,能被4整除;

  对于变量b,它的自身对齐参数为4,#pragma pack(n)默认值为8,则最终b的对齐参数为4,接下来的地址相对于结构体的起始地址的偏移量为1,1不能够整除4,所以需要在a后面填充3字节使得偏移量达到4,然后再为b分配4字节的空间;

  对于变量c,它的自身对齐参数为2,#pragma pack(n)默认值为8,则最终c的对齐参数为2,而接下来的地址相对于结构体的起始地址的偏移量为8,能整除2,所以直接为c分配2字节的空间。

  此时结构体所占的字节数为1+3+4+2=10字节

  最后由于a,b,c的最终对齐参数分别为1,4,2,最大为4,#pragmapack(n)的默认值为8,则结构体变量最后的大小必须能被4整除。而10不能够整除4,所以需要在后面填充2字节达到12字节。其存储如下:

[cpp] view plain copy
  1. |char|----|----|----|  4字节  
  2.  |--------int--------|  4字节  
  3.  |--short--|----|----|  4字节  

  总共占12个字节

对于node3,含有静态数据成员 

[cpp] view plain copy
  1. typedef struct node3  
  2. {  
  3.     int a;  
  4.     short b;  
  5.     static int c;  
  6. }S3;  

 

  则sizeof(S3)=8.这里结构体中包含静态数据成员,而静态数据成员的存放位置与结构体实例的存储地址无关(注意只有在C++中结构体中才能含有静态数据成员,而C中结构体中是不允许含有静态数据成员的)。其在内存中存储方式如下:

[cpp] view plain copy
  1. |--------int--------|   4字节  
  2. |--short-|----|----|    4字节  

  而变量c是单独存放在静态数据区的,因此用siezof计算其大小时没有将c所占的空间计算进来。

而对于node5,里面含有结构体变量

[cpp] view plain copy
  1. typedef struct node5  
  2. {  
  3.     bool a;  
  4.     S1 s1;  
  5.     double b;  
  6.     int c;  
  7. }S5;  
  8.    

sizeof(S5)=32。

  对于变量a,其自身对齐参数为1,#pragma pack(n)为8,则a的最终对齐参数为1,为它分配1字节的空间,它相对于结构体起始地址的偏移量为0,能被1整除;

  对于s1,它的自身对齐参数为4(对于结构体变量,它的自身对齐参数为它里面各个变量最终对齐参数的最大值),#pragma pack(n)为8,所以s1的最终对齐参数为4,接下来的地址相对于结构体起始地址的偏移量为1,不能被4整除,所以需要在a后面填充3字节达到4,为其分配8字节的空间;

  对于变量b,它的自身对齐参数为8,#pragma pack(n)的默认值为8,则b的最终对齐参数为8,接下来的地址相对于结构体起始地址的偏移量为12,不能被8整除,所以需要在s1后面填充4字节达到16,再为b分配8字节的空间;

  对于变量c,它的自身对齐参数为4,#pragma pack(n)的默认值为8,则c的最终对齐参数为4,接下来相对于结构体其实地址的偏移量为24,能够被4整除,所以直接为c分配4字节的空间。

  此时结构体所占字节数为1+3+8+4+8+4=28字节。

  对于整个结构体来说,各个变量的最终对齐参数为1484,最大值为8#pragma pack(n)默认值为8,所以最终结构体的大小必须是8的倍数,因此需要在最后面填充4字节达到32字节。其存储如下:

[cpp] view plain copy
  1. |--------bool--------|   4字节  
  2. |---------s1---------|   8字节  
  3. |--------------------|    4字节  
  4. |--------double------|   8字节  
  5. |----int----|---------|    8字节   


  另外可以显示地在程序中使用#pragma pack(n)来设置系统默认的对齐参数,在显示设置之后,则以设置的值作为标准,其它的和上面所讲的类似,就不再赘述了,读者可以自行上机试验一下。如果需要取消设置,可以用#pragma pack()来取消。

当把系统默认对齐参数设置为4时,即#pragma pack(n)

输出结果是:

[cpp] view plain copy
  1. 1 2 4 4 8  
  2. 8 12 8 16 24   
例题:
下面两个结构体
1
2
3
4
5
6
7
8
9
10
structOne{
    doubled;
    charc;
    inti;
}
structTwo{
    charc;
    doubled;
    inti;
}
在#pragma pack(4)和#pragma pack(8)的情况下,结构体的大小分别是:16 16 16 24

0 0
原创粉丝点击