结构体内存分配

来源:互联网 发布:工口动作数据 编辑:程序博客网 时间:2024/05/01 15:08


关于内存对齐

一:

1.什么是内存对齐

假设我们同时声明两个变量:

char a;

short b;

用&(取地址符号)观察变量a,

b的地址的话,我们会发现(以16位CPU为例):

如果a的地址是0x0000,那么b的地址将会是0x0002或者是0x0004。

那么就出现这样一个问题:0x0001这个地址没有被使用,那它干什么去了?答案就是它确实没被使用。因为CPU每次都是从以2字节(16位CPU)或是4字节(32位CPU)的整数倍的内存地址中读进数据的。如果变量b的地址是0x0001的话,那么CPU就需要先从0x0000中读取一个short,取它的高8位放入b的低8位,然后再从0x0002中读取下一个short,取它的低8位放入b的高8位中,这样的话,为了获得b的值,CPU需要进行了两次读操作

但是如果b的地址为0x0002,那么CPU只需一次读操作就可以获得b的值了。所以编译器为了优化代码,往往会根据变量的大小,将其指定到合适的位置,即称为内存对齐(对变量b做内存对齐,a、b之间的内存被浪费,a并未多占内存)。

2.结构体内存对齐规则

结构体所占用的内存与其成员在结构体中的声明顺序有关,其成员的内存对齐规则如下:

(1)每个成员分别按自己的对齐字节数和PPB(指定的对齐字节数,32位机默认为4两个字节数最小的那个对齐,这样可以最小化长度。

2)复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度。

(3)结构体对齐后的长度必须是成员中最大的对齐参数(PPB)的整数倍,这样在处理数组时可以保证每一项都边界对齐。

4)计算结构体的内存大小时,应该列出每个成员的偏移地址,则其长度=最后一个成员的偏移地址+最后一个成员数的长度+最后一个成员的调整参数(考虑PPB)。


例子如下:
假设机器 sizeof(char) = 1   sizeof(int) = 4    sizeof(double) = 8
枚举类型只为最宽的数据分配内存,在不同时候,用的是同一块内存;

在默认情况下,VC规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。下面列出常用类型的对齐方式(vc6.0,32位系统)。
类型 对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)
Char      偏移量必须为sizeof(char)即1的倍数
Short       偏移量必须为sizeof(short)即2的倍数
int      偏移量必须为sizeof(int)即4的倍数
float     偏移量必须为sizeof(float)即4的倍数
double     偏移量必须为sizeof(double)即8的倍数
各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。同时VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。

结构体的内存分配依赖于结构成员的类型;
一:struct s {
    int i;
    char c;
    double d;
    char* e;
    char a[];
};

首先来看对结构体 struct s {
    int i;
    char c;
}; 的内存分配
    第1个成员偏移量为0,是4(int型大小)的整数倍。所以为其分配四个字节的空间,
    第2个成员 c 为char型,大小为1,首先假设在成员i和c之间没有填充字节,由于i是整型,占4个字节那么在没有填充之前,第2个成员c 相对于结构体的偏移量为 4,是1 ( char型的大小)的4倍,符合此条件,所以系统在给结构体第2个成员分配内存时,不会在i和c之间填充字节以到达对齐的目的。 所以在紧接着i 后为c 分配一个字节,现在结构体的大小应该是5 (4+1); 结构体要求:结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。所以编译器会在后面填充3个字节, 因此该结构体的实际大小: sizeof(struct s) = 8;

再看结构体 struct s {
    int i;
    char c;
    double d;
};

    上面说到: 如果该结构体只包括i、c两个成员时,编译器会自动在最末一个成员之后加上3 填充字节,但是现在有了第3 个成员d, 该怎么分配呢? 根据上面的规则,由于成员c 相对于结构体的偏移量为 4,1 ( char型的大小)的4倍, 所以在紧接着i 后为c 分配一个字节,现在结构体的大小应该是5 (4+1),那么成员d 的偏移量就应该是5,但是5 并不是8 (double 型的大小)的倍数,会在成员c 之后(或者说d 之前)填充3个字节,以使d 的偏移量到达8而成为4的整数倍。然后为d 分配8 个字节,因此现在struct s 的大小就是16 (4 + 1 + 3 + 8);
    
    同理,由于指针占4 个字节,所以
struct s {
    int i;
    char c;
    double d;
    char* e;
};  的大小为20

   由于没有为数组a 指定大小,不为其分配空间, 因此struct s {
    int i;
    char c;
    double d;
    char* e;
    char a[];
};的大小为20; 

二: struct s1{
            char a; //为a 分配1 个字节    
            struct s b; //20个字节
            char c; //1个字节
    }; 的大小为28 = 1+3+20+1+3

字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:

1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

对于上面的准则,有几点需要说明:
1) 前面不是说结构体成员的地址是其大小的整数倍,怎么又说到偏移量了呢?因为有了第1点存在,所以我们就可以只考虑成员的偏移量,这样思考起来简单。结构体某个成员相对于结构体首地址的偏移量可以通过宏offsetof()来获得,这个宏也在stddef.h中定义,如下:
#define offsetof(s,m) (size_t)&(((s *)0)->m)
例如,想要获得S2中c的偏移量,方法为size_t pos = offsetof(S2, c);// pos等于4

2) 基本类型是指前面提到的像char、short、int、float、double这样的内置数据类型,这里所说的“数据宽度”就是指其sizeof的大小。由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。

0 0