前言
在c语言编程中,为了实现各种需求,我们会大量运用结构体这一数据类型,但是作为聚合数据类型,在内存中所占的空间大小并不像基本数据类型那样固定,不同的成员类型,会导致结构体变量所占用的空间也有所不同,今天我就来说一说结构体在内存中是如何分配空间的。
4条规则
无论你在任何平台通过任何编译器所跑起来的代码,结构体的内存分配必须遵从下面4条规则:
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到 对齐数 的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 (VS中默认的对齐数值为8)
- 结构体总大小要为最大对齐数(每个成员变量都有一个对齐数)的整数倍。如果不是就扩充
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
初看这4条规则,只是从字面意思来理解都会有些困难,我就是这样的,更不用说去想为什么要制定这样的规则来存储结构体了。那么下面我们通过代码先来理解规则的字面意思,论证一下结构体是否是按照这些规则来分配内存存储的。
代码论证
我们先从vs的环境下来看看,首先要明确一点VS环境下内存的默认对齐数是8。代码如下:
typedef struct {
char a;
char b;
int c;
}st;
int main() {
st s = { 0 };
printf("结构体所占内存大小为 %d 字节\r\n", sizeof(s));
return 0;
}
看到这个代码,按照以前的惯性思维,我们会理所当然的认为结构体和数组类型一样,在一块连续的内存中,各成员紧挨着存储,便会认为上述代码的结果便是6个字节,但是事实并非如此。结果如下:
初看到这个结果,会觉得奇怪,但是如果按照上述所说的内存对齐的4条规则来论证,便会理解为什么会是8个字节了,这里我会用图一步步论证。
首先要存储一个结构体,就要开辟一块内存,按照第一条规则,结构体首先存放的是第一个成员变量,从结构体内存的地址的偏移量0开始存储如下图所示:
如图所示第一个char类型的变量a存储在了0偏移处,只占用一个字节。下一个变量是char类型的变量b要继续存,我们看规则二,存其他的成员变量要进行内存对齐,怎么对齐呢?需要要对齐到 对齐数 的整数倍的地址处。那么接下来我们需要知道对齐数具体是多少,在我们没有人为设置对齐数的情况下,不同编译器的默认对齐数不同,这里我们先用的是vs环境来运行代码的,vs默认对齐数为8,规则二中说明对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。因此变量b的对齐数变为1,任何偏移地址都是1的整数倍,于是在地址偏移1处便可存放变量b。接下来我们看变量c,变量c为int型,大小为4个字节,因此变量c的对齐数为4,偏移地址要是4的倍数才能存放变量c,因此变量c存放在偏移地址4处,如下图所示:
看到这里,我们不能仅仅以此就得出结论结构体s就占用8个字节 ,还得继续看看第三条规则。根据上述两条规则,我们已经算出结构体s是占用了8个字节了,但是第三条规则要求根据前两条规则推算出的大小必须是最大对齐数的整数倍,不然就扩充,扩充后的大小才是最终实际结构体所占用空间的大小,因为结构体s的最大对齐数是变量c的对齐数为4,根据前两条规则推算出的结构体大小为8,刚好是变量c的整数倍。这里就不用扩充了。最终结构体s的大小为8个字节。
根据上述内容,我们还没用到第四条规则,于是这里我们再改动一下代码如下:
typedef struct {
double d;
char c;
int i;
}St1;
typedef struct {
char c1;
St1 st1;
int i;
}st2;
int main() {
st2 s = { 0 };
printf("结构体所占内存大小为 %d 字节\r\n", sizeof(s));
return 0;
}
根据第四条规则:嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。首先我们可以根据前三条规则可以得出结构体s中嵌套的结构体st1大小是16个字节,最大对齐数为8。根据这个我们再画图分析一下,便会明白了,如下图:
内存对齐的原因
性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。 总体来说: 结构体的内存对齐是拿空间来换取时间的做法。
不同平台的测试
通过上文我们知道了内存对齐是为了提高性能,方便cpu处理数据,那由此我们是否可以推断出,为了方便cpu处理数据,编译器默认的对齐数是否就是cpu的位数呢?因此我将上面的程序分别放在了8位的平台和32位的平台进行了测试。
8位平台选择51单片机
首先上代码,代码如下
typedef struct{
char a;
char b;
char h;
int c;
}st;
int main(){
st1 s={0};
Uart1Init();//串口初始化程序,让结果通过串口打印出来,方便我们查看
while(1)
{
printf("占用字节为 %bd 字节\r\n",sizeof(s));
Delay5000ms();
}
}
首先我们要知道,在8位平台上,int类型占用2个字节,如果我们还以为在c51环境下编译,默认对齐数和vs一样是8个字节,那么算出来的大小应当是6个字节。然而结果如下:
因此我们可以推断出,c51环境下,默认对齐数为1。
32位平台选择stm32
首先我们要知道32位平台,int是占用4个字节了,然后再用上述51上面跑过的代码看看是什么结果。如下:
通过上文的介绍,我们不难算出是8个字节,结果也确实如此,但是我们并不能由此推出在stm32平台上所默认的对齐数是多少。于是我们改下代码:
typedef struct {
double a;
int b;
}st2;
int main(void)
{
st2 s = { 0 };
Debug_USART_Config();//串口初始化
while (1)
{
printf(" 结构体所占内存大小为%d 个字节\r\n", sizeof(s));
}
}
我原本以为stm32是32位单片机,程序编译过后,默认对齐数应该是4,便可以得出结果为12,但是结果如下:
这有点超乎我的意料,我感觉是否我算错了,于是我将这个代码搬到vs上编译运行,然后将默认对齐数改成4,代码如下:
#pragma pack(4)
typedef struct {
double a;
int b;
}st;
int main() {
st s = { 0 };
printf("结构体所占内存大小为 %d 字节\r\n", sizeof(s));
return 0;
}
然后结果如下:
在默认对齐数为4时结果大小确实为12,那么stm32上的默认对齐数为8。
因此我们可以得出结论:不同平台上的默认对齐数不同,因此对结构体所分配的内存大小也会不同,在实际操作中,我们一定要注意这一点