struct求大小补充

来源:互联网 发布:路由器升级软件 编辑:程序博客网 时间:2024/05/16 07:36

一、什么是对齐,以及为什么要对齐:

1. 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

2. 对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况, 但是最常见的是如果不按照适合其平台的要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32位)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低 字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。

二、对齐的实现

通常,我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。
但是,正因为我们一般不需要关心这个问题,所以因为编辑器对数据存放做了对齐,而我们不了解的话,常常会对一些问题感到迷惑。最常见的就是struct数据结构的sizeof结果,出乎意料。为此,我们需要对对齐算法所了解。
对齐的算法:
由于各个平台和编译器的不同,现以本人使用的gcc version 3.2.2编译器(32位x86平台)为例子,来讨论编译器对struct数据结构中的各成员如何进行对齐的。
设结构体如下定义:
struct A {
    int a;
    char b;
    short c;
};
结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个。所以A用到的空间应该是7字节。但是因为编译器要对数据成员在空间上进行对齐。
所以使用sizeof(strcut A)值为8。
现在把该结构体调整成员变量的顺序。
struct B {
    char b;
    int a;
    short c;
};
这时候同样是总共7个字节的变量,但是sizeof(struct B)的值却是12。
下面我们使用预编译指令#pragma pack (value)来告诉编译器,使用我们指定的对齐值来取代缺省的。
#progma pack (2) /*指定按2字节对齐*/
struct C {
    char b;
    int a;
    short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct C)值是8。

修改对齐值为1:
#progma pack (1) /*指定按1字节对齐*/
struct D {
    char b;
    int a;
    short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct D)值为7。

于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
这里面有四个概念值:
1)数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。

2)指定对齐值:#pragma pack (value)时的指定对齐值value。

3)结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。

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;

同理,分析上面例子C:
#pragma pack (2) /*指定按2字节对齐*/
struct C {
    char b;
    int a;
    short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
第一个变量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.

有 了以上的解释,相信你对C语言的字节对齐概念应该有了清楚的认识了吧。在网络程序中,掌握这个概念可是很重要的喔,在不同平台之间(比如在Windows 和Linux之间)传递2进制流(比如结构体),那么在这两个平台间必须要定义相同的对齐方式,不然莫名其妙的出了一些错,可是很难排查的哦^_^。

 

根据上面的解释,个人对参考文章1作如下总结:

struct S {   int i;   // size 4   short j;   // size 2   double k;   // size 8};#pragma pack(2)struct T {   int i;   short j;   double k;};int main() {   printf("%d ", offsetof(S, i));   printf("%d ", offsetof(S, j));   printf("%d\n", offsetof(S, k));   T tt;   printf("%d ", offsetof(T, i));   printf("%d ", offsetof(T, j));   printf("%d\n", offsetof(T, k));}

struct S中,i的偏移量为0毋庸置疑,j的对齐值为2,但是由于i的对齐值占用了4个字节,所以由于4%2=0,所以对j而言,起始对齐位置是上文所说的自然位置,所以j的起始对齐位置为4,而对于k,由于前两个成员的总和偏移仅仅为4+2=6,而6%8!=0,所以必须在j后,k前填两个0(当然,视编译器不同而异),达到8位,使8%8=0,所以K的起始对齐位置为8,所以输出结果为:0,4,8;

第二个例子我就不多讲解了,因为原理相似,只是将默认对齐位置强制变为2而已。

感谢两位前者,或者前辈的分享!


列子:

  1. #pragma pack(1)  
  2. struct test  
  3. {  
  4. static int a; //static var  
  5. double m4;  
  6. char m1;  
  7. int m3;  
  8. }  
  9. #pragma pack()  
  10. //sizeof(test)=13;  
  11.   
  12. Class test1{ };  
  13. //sizeof(test1)=1;  
  14.   
  15. /* 注明:  
  16. 1、结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置与结构或者类的实例地址无关;  
  17. 2、没有成员变量的结构或类的大小为1,因为必须保证结构或类的每一个实例在内存中都有唯一的地址。*/  

  1. //分析下面的例子C:  
  2. //  
  3. #pragma pack (2)     /*指定按2字节对齐*/  
  4. struct C  
  5. {  
  6.   char b;  
  7.   int a;  
  8.   short c;  
  9. };  
  10. #pragma pack () //恢复对齐状态  
  11. /*  
  12. 第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1 = 0;  
  13. 第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续的字节空间中,符合0x0002%2=0。  
  14. 第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序在0x0006、0x0007中,符合0x0006%2=0。  
  15. 所以从0x0000到0x0007共八字节存放的是struct C的变量。又struct C的自身对齐值为4,所以struct C的有效对齐值为2。  
  16. 又8%2=0,struct C只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8。  
  17. 如果把上面的#pragma pack(2)改为#pragma pack(4),那么我们可以得到结构的大小为12。  
  18. */  


  1. //再看下面这个例子  
  2. //  
  3. #pragma pack(8)  
  4. struct S1  
  5. {  
  6.   char a;  
  7.   long b;  
  8. };  
  9. struct S2 {  
  10.   char c;  
  11.   struct S1 d;  
  12.   long long e;  
  13. };  
  14. #pragma pack()  
  15.   
  16. sizeof(S2)结果为24.  
  17. /*  
  18. S1中:  
  19. 成员a是1字节默认按1字节对齐,指定对齐参数为8,这两个值中取1,a按1字节对齐;  
  20. 成员b是4个字节,默认是按4字节对齐,这时就按4字节对齐,所以sizeof(S1)应该为8;  
  21.   
  22. S2 中:  
  23. c和S1中的a一样,按1字节对齐,  
  24. d 是个结构,它是8个字节,它按什么对齐呢?对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个,S1的就是4.所以,成员d就是按4字节对齐.  
  25. 成员e是8个字节,它是默认按8字节对齐,和指定的一样,所以它对到8字节的边界上,这时,已经使用了12个字节了,所以又添加了4个字节的空,从第16个字节开始放置成员e;  
  26. 长度为24,已经可以被8(成员e按8字节对齐)整除.一共使用了24个字节.  
  27. a b  
  28. S1的内存布局:11**,1111,  
  29. c S1.a S1.b d  
  30. S2的内存布局:1***,11**,1111,****11111111  
  31. */  

这里就是因为IDE 规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。

对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量) 

Char 偏移量必须为sizeof(char)即1的倍数 

Short 偏移量必须为sizeof(short)即2的倍数

int 偏移量必须为sizeof(int)即4的倍数 

float 偏移量必须为sizeof(float)即4的倍数 

double 偏移量必须为sizeof(double)即8的倍数 

各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节IDE 会自动填充。同时IDE 为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。



首先obj1 这个结构体。先为第一个成员char a 分配空间,他的起始偏移量为0,是1 的整数倍,所以直接分配1 个空间。接着为第二个成员double b 分配空间,他的起始偏移量现在为1(char a 占据了1 个字节空间),他不是8 的倍数,所以要在当前偏移量的后面填充空字节,使得偏移量是8 的倍数,这里填上7 个空字节。所以偏移量为8,然后再加上double b 的8个字节。接着为 int c 分配空间,他的起始偏移量为 1(char a 的长度) + 7(空字节)+ 8(double b 的长度) = 16,所以当前偏移量是4 的整数倍,故而直接将int c 需要的4 个字节添加在后面就OK。然后得到的总长度是 1 + 7 + 8 + 4 = 20,这里我们又要考虑一个问题,struct 的总长度必须是结构体成员中数据类型中最大长度的整数倍,这里最大为double 8 个字节,所以得到整个struct 的最后长度为 24(大于20 的8 的最小倍数)。

接着我们分析obj2 这个结构体,方法完全相同,char a 分配1 个字节。接着为 int c 分配,先添加3 个空字节,然后构成偏移量是4 的整数倍,接着为double b 分配空间,偏移量是8 是8 的整数倍,所以直接分配空间。所以得到的大小为:1 + 3 + 4 + 8 = 16。 因为16 是最大类型double 的整数倍,故而obj2 的struct 长度大小为16.

所以,别看struct 内部成员变量数量相同,类型相同,但是长度结果却不一样,因这还跟他们的排列顺序有关。

首先int num 起始偏移量为0,分配4 个字节空间。接着为char name[20]分配空,考虑到char 类型长度为1, 他的偏移量为4,是他的整数倍,故而直接分配20 个字节空间。再为char sex 分配空间,同理分配1 个字节空间。那么当前偏移量为 4 + 20 + 1 = 25.接着为int age 分配空间,因为偏移量是25 不是4 的整数倍,故而填3 个空字节,使得偏移量为28, 成为4 的整数倍,然后为int age 分配空间。偏移量为28 + 4  = 32 是float 类型4 的整数倍,故而直接分配空间。偏移量变为 32 + 4 = 36.在为char addr[30] 分配空间。得到struct 的长度为 66. 最后考虑struct 的长度必须是整个成员变量中,最长类型的整数倍,这里最长为int 和float 的4 个字节。所以struct 的长度必须是4 的整数倍,故而最终struct 的长度为 68.

typedef struct

char a;               //1
double b;             //8
int c;                //4
}obj1; 

typedef struct
{
char a;               //1
int c;                //4
double b;             //8
}obj2;

int main()
{
int len1, len2;
len1 = sizeof(obj1);           // len1 = 24
len2 = sizeof(obj2);           // len2 = 16
printf("%d\n%d\n", len1, len2);
return 0;
}
struct Student              

int num;                 //4
char name[20];           //20
char sex;                //1
int age;                 //4
float score;             //4
char addr[30];           //30
}stu1;

0 0
原创粉丝点击