C语言有一些针对内存的优化,结构体内存对齐就是一种内存优化
1. 结构体定义
结构体是C语言中的一种自定义类型,有关结构体定义百度解释如下:
结构体是由一批数据组合而成的结构型数据。组成结构型数据的每个数据称为结构型数据的“成员 ,其描述了一块内存区间的大小及解释意义。
结构体可以用属性刻画一个具体的事物,事物的属性有多种类型,为了表示这个事物,可以将事物的属性集合在一起,就成了结构体。
2. 结构体内存
1. 结构体内存大小
结构体是一些值的集合,这些值称为成员变量,每个成员变量都有各自的类型,所占的内存空间不同,那么计算一个结构体的大小,你们是怎么计算的呢?
struct test
{
char a;
int b;
char c;
};
上面这个结构体你认为占据内存多少byte,相信有不少第一次了解结构体内存的朋友看到这个结构体会下意识的将结构体内部所有的类型所占内存相加(至少我第一次是这样认为的😑),也就是认为这个结构体占内存6byte。然而当我写出代码运行结果是这样的
代码:
#include<stdio.h>
struct test1
{
char a;
int b;
char c;
}p1;
int main()
{
printf("%d", sizeof(p1));
return 0;
}
运行结果:
运行结果是12!!!
当时的我是百思不得解,然而令人更难解的还有:
#include<stdio.h>
struct test2
{
char a;
char c;
int b;
}p2;
int main()
{
printf("%d", sizeof(p2));
return 0;
}
运行结果:
可以看出下面这个代码与上面不同的就只有结构体内的成员变量顺序不同,然而内存却是相差4byte。
这又是怎么回事呢?经过查阅,我了解到了专属于结构体的操作:结构体内存对齐(简称:内存对齐)。
2. 结构体内存对齐
结构体内存对齐是编译器对结构体内存排布做的一种优化,查找到的内存对齐遵从四条规则
第一个成员在与结构体变量偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
对第一个结构体分析如下:
①第一个成员在与结构体变量偏移量为0的地址处。
②其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
VS中默认对齐数为8,取数据类型和默认对齐数较小值,此处对齐数为4,从第一个结构体成员内存往后看偏移量,偏移量为1,2,3处地址不满足第二个条件,从偏移量为4处向后放入第二个成员变量,如图。
第三个成员变量放在这里:
到这里内存分配完了?
我们可以数一下这里内存有多少byte;9个
那上面的运行结果为什么是12byte呢?这里就要说到第三个规则了:
③ 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
对于这句话解读为:
上面这个结构体的对齐数为1,4,1
最大对齐数为4,那么这个结构体的整体大小就应该是4的倍数,从第9个偏移量处向后空出空间直到4的倍数处,离9最近的4的倍数是12。这就是这个结构体内存的计算规则。
对第二个结构体分析:
两个char类型的内存排布如图,遵守第①,②条规则,第三个成员变量内存排布为:
第三个成员变量对齐数为4,从4的倍数处开始往后存放,存放完刚好如图,在最大对齐数4的倍数处,满足第③个规则。则这个结构体的内存大小为8byte。
3. 关于嵌套结构体的内存大小计算
结构体:
struct test2
{
char a;
char c;
int b;
}p2;
struct test3
{
char a;
struct test2 b;
}p3;
结构体test3是一个和test2嵌套起来的嵌套结构体,关于这个结构体的内存编译器运行结果为:
对这个结构体分析:
第一个成员变量位置如图所示,那么第二个成员变量的对齐数是否是8呢?(上面算过test2的内存了)
我们先按8算一下:
找到偏移量为8的倍数处往后分配,分配到偏移量为15处刚好满足②③条件,按照这种计算方法,test3的内存应该是16,与前面编译器运行结果不符,那么这种方法显然是错误的。
当我知道第四条规则的时候才明白嵌套结构体的内存算法。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
这段规则说明了当结构体内有嵌套结构体的时候,嵌套的结构体的对齐数不是内存大小,而是对齐到嵌套的结构体内部的最大对齐数的整数倍,也就是说test2应该对齐到4的倍数处:
test3这个结构体内部最大对齐数经计算是内嵌结构体test2的对齐数4,所以test3内存是12。
4. 修改默认对齐数
集成开发环境一般都有默认对齐数,比如博主用的vs2019的默认对齐数是8,值得一提的是Linux系统上编程是没有默认对齐数的,修改vs2019的默认对齐数可以用预处理指令 #pragma,使用语法如下:
#pragma pack(4)//设置默认对齐数为4
放出一个修改实例:
如果修改默认对齐数为1,那么结构体的所有成员变量都将会在内存中紧密排放,内存计算就将是加起来所有类型内存数
前面说过,内存对齐是编译器对内存排布做的一种优化,那么既然是优化就肯定有这样做的好处,下来我们就来列举一下内存对齐的好处。
3. 结构体内存对齐的好处
既然内存对齐需要浪费这么多空间,那为什么还要设计这个呢?这不是变复杂了吗?
这还真不是多此一举的设计。
首先,我们的机器大多有32位和64位(就是一次性可以读取多少个bit位),32位机器可以一次读取四个byte,若我们紧密排放:
就会如图,若读取四个byte
那么将会出现第二个成员变量读取不完的情况,若想将第二个成员变量读取完,就应该读取两次,这将大大减小内存读取效率,所以内存对齐可以说是一种牺牲空间换取效率的方法。
还有一个更重要的原因,那就是C语言是一门强大的语言,可以在多种设备上运行,这其中就有些设备如单片机一般运算能力弱,只能从特定偏移量处读取数据,比如: 0, 4, 8, 12…位考虑到C语言的可移植性而设计内存偏移。
总之,C语言迭代这么多年,其中出现的一些变化一般都是有用的,我们要相信C语言。
总结
内存对齐是面试中有时会问到的问题,就是本文这样的解释,若你不留意,那么它还会存在,若你想探究,打开编译器运行一下就会有结果,本篇博文是为没有注意到这一块的人们帮助理解的希望能给看本篇博文的读者带来帮助
o(〃^▽^〃)o