您的位置:首页 > 其它

结构体struct字对齐问题

2016-11-04 22:00 309 查看
在用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题。从理论上讲,对于任何 变量的访问都可以从任何地址开始访问,但是事实上不是如此,实际上访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按一定的规则排列, 而不是简单地顺序排列,这就是内存对齐。

     计算结构变量的大小必须讨论数据对齐的问题。为了使CPU存取的速度最快(这同CPU取数操作有关),c++在处理数据时经常把结构变量中的成员的大小按照4或8的倍数计算,这就叫数据对齐(data alignment)。这样做可能会浪费一些内存,但在理论上CPU速度快了。

       内存对齐的原因:

      1)某些平台只能在特定的地址处访问特定类型的数据

      2)提高存取数据的速度。比如有的平台每次都是从偶地址处读取数据,对于一个int型的变量,若从偶地址单元处存放,则只需一个读取周期即可读取该变量;但是若从奇地址单元处存放,则需要2个读取周期读取该变量。

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

     对于结构体的字节对齐主要有下面两点:

      1)结构体每个成员相对结构体首地址的偏移量(offset)是对齐参数(这句话中的对齐参数是 取每个变量自身对齐参数和系统默认对齐参数#pragma pack(n)中较小的一个)的整数倍,如有需要会在成员之间填充字节。编译器在为结构体成员开辟空间时,首先 检查预开辟空间的地址相对于结构体首地址的偏移量是否为对齐参数的整数倍,若是,则存放该成员;若不是,则填充若干字节,以达到整数倍的要求。

      2)结构体变量所占空间的大小是对齐参数(它是取结构体中所有变量的对齐参数的最大值和系统 默认对齐参数#pragma pack(n)比较,较小者作为对齐参数)大小的整数倍。如有需要会在最后一个成员末尾填充若干字节使得所占空间大小是对齐参数大小的整数倍。

   注意:在看这两条原则之前,先了解一下对齐参数这个概念。对于每个变量,它自身有对齐参数,这个自身对齐参数在不同编译环境下不同。下面列举的是两种最常见的编译环境下各种类型变量的自身对齐参数



   从上面可以发现,在windows(32)/VC6.0下各种类型的变量的自身对齐参数就是该类型变量所占字节数的大小,而在 Linux(32)/GCC下double类型的变量自身对齐参数是4,是因为linux(32)/GCC下如果该类型变量的长度没有超过CPU的字长,
则以该类型变量的长度作为自身对齐参数,如果该类型变量的长度超过CPU字长,则自身对齐参数为CPU字长,而32位系统其CPU字长是4,所以 linux(32)/GCC下double类型的变量自身对齐参数是4,如果是在Linux(64)下,则double类型的自身对齐参数是8。

   除了变量的自身对齐参数外,还有一个对齐参数,就是每个编译器默认的对齐参数#pragma pack(n),这个值可以通过代码去设定,如果没有设定,则取系统的默认值。在windows(32)/VC6.0下,n的取值可以为1、2、4、8, 默认情况下为8。在linux(32)/GCC下,n的取值只能为1、2、4,默认情况下为4。注意像DEV-CPP、MinGW等在windows下n 的取值和VC的相同。

  了解了这2个概念之后,可以理解上面2条原则了。对于第一条原则,每个变量相对于结构体的首地址的偏移量必须是对齐参数的整数倍,这句话中的对齐参数是取每个变量自身对齐参数和系统默认对齐参数#pragma pack(n)中较小的一个。举个简单的例子,比如在结构体A中有变量int a,a的自身对齐参数为4(环境为windows/vc),而VC默认的对齐参数为8,取较小者,则对于a,它相对于结构体A的起始地址的偏移量必须是4
的倍数。

  对于第二条原则,结构体变量所占空间的大小是对齐参数的整数倍。这句话中的对齐参数有点复杂,它是取结构体中所有变量的对齐参数的最大值和系统 默认对齐参数#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整除。

下面是测试例子:

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

 

#include <iostream>

using namespace std;

//#pragma pack(4)    //设置4字节对齐

//#pragma pack()     //取消4字节对齐

 

 

typedef struct node1

{

    int a;

    char b;

    short c;

}S1;

 

typedef struct node2

{

    char a;

    int b;

    short c;

}S2;

typedef struct node3

{

    int a;

    short b;

    static int c;

}S3;

typedef struct node4

{

    bool a;

    S1 s1;

    short b;

}S4;

typedef struct node5

{

    bool a;

    S1 s1;

    double b;

    int c;

}S5;

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

{

    cout<<sizeof(char)<<" "<<sizeof(short)<<" "<<sizeof(int)<<" "<<sizeof(float)<<" "<<sizeof(double)<<endl;

    S1 s1;

    S2 s2;

    S3 s3;

    S4 s4;

    S5 s5;

    cout<<sizeof(s1)<<" "<<sizeof(s2)<<" "<<sizeof(s3)<<" "<<sizeof(s4)<<" "<<sizeof(s5)<<endl;

    return 0;

}

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

比如对于node2

typedef struct node2
{
char a;
int b;
short c;
}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,#pragma pack(n)的默认值为8,则结构体变量最后的大小必须能被4整除。而10不能够整除4,所以需要在后面填充2字节达到12字节。其存储如下:

  |char|----|----|----|  4字节

    |--------int--------|  4字节

    |--short--|----|----|  4字节

  总共占12个字节

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

typedef struct node3
{
int a;
short b;
static int c;
}S3;


 

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

  |--------int--------|   4字节

  |--short-|----|----|    4字节

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

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



typedef struct node5
{
bool a;
S1 s1;
double b;
int c;
}S5;




 

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字节。

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

   s5的内存分配应该如下:

|--------bool--------| 4字节

|---------s1---------| 4字节

|---------s1---------| 4字节

|---------------------| 空出

|--------double-----| 8字节

|----int----|---------| 8字节

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

   结构体的长度一定是最长的数据元素的整数倍。

   CPU的优化规则大致原则是这样的:对于n字节的元素(n=2,4,8,...),它的首地址能被n整除,才能获得最好的性能。

   出这类题并不在于考查理解语言本身和编译器,而在于应聘者对计算机底层机制的理解和设计程序的原则。也就是说,如果让你设计编译器,你将怎样解决内存对齐的为问题。

Intel、微软等公司曾经出过一道类似的面试题:

[cpp] view
plain copy

 





#include <iostream.h>  

#pragma pack(8)  

struct example1  

{  

short a;  

long b;  

};  

struct example2  

{  

char c;  

example1 struct1;  

short e;  

};  

#pragma pack()  

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

{  

example2 struct2;  

cout << sizeof(example1) << endl;  

cout << sizeof(example2) << endl;  

cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl;  

return 0;  

}  

问程序的输入结果是什么?
答案是:
8
16
4

  面试题的解答
至此,我们可以对Intel、微软的面试题进行全面的解答。
程序中第2 行#pragma pack (8)虽然指定了对界为8,但是由于struct example1 中的成员最大
size 为4(long 变量size 为4),故struct example1 仍然按4 字节对界,struct example1 的size
为8,即第18 行的输出结果;
struct example2 中包含了struct example1,其本身包含的简单数据成员的最大size 为2(short
变量e),但是因为其包含了struct example1,而struct example1 中的最大成员size 为4,struct
example2 也应以4 对界,#pragma pack (8)中指定的对界对struct example2 也不起作用,故19 行的
输出结果为16;
由于struct example2 中的成员以4 为单位对界,故其char 变量c 后应补充3 个空,其后才是
成员struct1 的内存空间,20 行的输出结果为4。

[cpp] view
plain copy

 





union A  

{  

int a[5];  

char b;  

double c;  

};  

  

struct B  

{  

int n;  

A a;  

char c[10];  

}  

sizeof(B) = ?

答案:

[cpp] view
plain copy

 





union A:   

{   

int a[5]; //20   

char b; //1   

double c; //8   

}  

我想的是union中变量共用内存,应以最长的为准,那就是20。可实际不然,sizeof(A)=24,

A中各变量的默认内存对齐方式,必须以最长的double 8字节对齐,故应该是sizeof(A)=24。

[cpp] view
plain copy

 





struct B  

{  

int n; // 4字节  

A a; // 24字节  

char c[10]; // 10字节  

};  

实际占用38字节,但由于A是8字节对齐的,所以int n和char c[10]也需要8字节对齐,总共8+24+16=48
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: