1.6:结构体的内存对齐(超重点!!★★★):
1.6.1:结构体的数据在内存中如何存储的?
经过上面的学习,我们就已经基本掌握了结构体的使用了。接下来我们将要深入研究结构体大小的计算过程,即结构体内存对齐,而这也是近年来许多公司面试与笔试中的热门考点。
我们先来看看下面这段计算结构体变量大小的代码:
struct S1 { char c1; int i; char c2 }; struct S2 { char c1; char c2; int i; }; int main() { printf("%d\n", sizeof(struct S1)); printf("%d\n", sizeof(struct S2)); return 0; } //结果: 12 8
这里计算结果显示struct S1的大小是12个字节,而struct S2的结果是8个字节,为什么是这样的结果呢?
要想弄清楚究竟是如何进行结构体变量大小计算的,我们首先得掌握结构体的对齐规则:
第一个成员在结构体变量偏移量为0的位置(偏移量:该成员的存放地址与结构体空间起始地址之间的距离)
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员自身大小的较小值
vs中的默认对其数是8,Linux和gcc都没有默认对齐数,对齐数就是成员自身大小
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
如果嵌套了结构体的情况,嵌套的结构体对齐到自己自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体成员中的对齐数)的整数倍
结构体成员中如果有数组,可以这样来理解:比如结构体中有一个int c[5];,其实这就相当于这个结构体中放了5个int型的变量。
知晓了结构体的对齐规则,我们再回过头来分析上面的结构体变量大小计算过程。下面,我们以结构体变量 test1 中为例展示结构体的对齐存储。
第一个成员为占据一个字节的 char 类型变量 a,我们按照规则将其放置在偏移量为0,即结构体空间的起始位置.
第二个成员为占据4个字节的 int 类型变量 b,按照规则我们首先要计算它的对齐数,我们将变量 b 的大小4与对齐数默认值8进行比较,得出较小值为4,即对齐数为4,于是我们将它放在对齐数的整数倍处,即最近位置第四字节处。
第三个结构体成员占据1个字节的 char 类型变量 c,同样按照规则我们计算出它的对齐数为1,并将它放在对齐数的整数倍处,即最近位置第九字节处:
最后,根据规则,结构体的总大小为最大对齐数的整数倍,而这三个变量中,对齐数最大的是 int 类型变量的对齐数4,则总大小应当为4的倍数。而既为4的倍数,又要能够容纳所有的结构体成员,最小的结构体大小应当为12个字节,即为结构体变量 test1 的大小。
同理各位小伙伴们下去以后可以自己尝试推算结构体变量 test2 的大小并进行验证。
1.6.2:为什么存在内存对齐?
但是我们发现,这样的方式造成了很大程度上的空间浪费,以 test1 为例,12个字节的大小中有六个字节的空间申请了但却没有被使用。那么为什么还要采用这样的办法呢?主要有以下两个原因:
平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总的来说,结构体的内存对齐是拿空间来换取时间的做法
那么在设计结构体的时候如何实现节省空间呢?
让占用空间小的成员尽量集中在一起。
1.7:修改默认对齐数:
在我们的代码编写过程中,默认的对齐数可能会不够合适。而当这个时候,我们就可以通过使用下面这个预处理指令来修改我们的默认对齐数:
#pragma pack(8) //修改默认对齐数为8
我们也可以通过该指令在修改过默认对齐数之后,取消设置的默认对齐数,将其还原:
#pragma pack() //取消设置的默认对齐数,还原为默认
1.8:结构体传参:
结构体传参与函数传参类似,没有什么疑难点,我们直接来看下面的示例:
//结构体传参 struct S { int data[4]; int num; }; //直接传结构体变量 void print1(struct S s)//实参传的是一个结构体变量,所以形参用一个对应的结构体变量来接收 { int i = 0; for (i = 0; i < 4; i++) { printf("%d ", s.data[i]); } printf("%d\n", s.num); } //传结构体的地址 void print2(struct S* ps)//实参传的是结构体变量的地址,所以形参用一个对应类型的结构体指针来接收 { int i = 0; for (i = 0; i < 4; i++) { printf("%d ", ps->data[i]); } printf("%d\n", ps->num); } int main() { struct S s = { {1,2,3,4}, 1000 }; print1(s); //传结构体 print2(&s); //传地址 return 0; }
而在上面这段代码中,我们一般认为 Print2 函数更为优秀。原因是当函数传参的时候,参数是需要压栈的,在这个过程中就会产生时间和空间上的系统开销。如果传递一个结构体对象时结构体过大,那么将会导致参数压栈的的系统开销较大,最终将会导致程序性能的下降。