- 结构体类型的声明
- 结构体的自引用
- 结构体内存对齐
- 结构体传参
自学b站“鹏哥C语言”笔记。
一、结构体类型的声明
详见文章【初识结构体】第一部分。补充说明:
匿名结构体类型:省略结构体标签(tag)
注意:上述代码会报错。因为编译器认为这两个匿名结构体是不同的,则p = &x;
会出错。
二、结构体的自引用
详见文章【初识结构体】第二部分。补充说明:
错误示例:
注意:上述结构体Node的存储空间不可知。
正确示例:运用指针
注意:自引用不能使用匿名结构体类型,否则会出错。
三、结构体内存对齐
1.结构体内存对齐的规则
(1)结构体第一个成员放在与结构体变量偏移量为0的地址处。
(2)结构体其他成员要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = min { 编译器默认的一个对齐数,该成员大小 }
VS中默认的对齐数是8
(3)结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
(4)特例:嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体总大小是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
例1:一般情况
输出结果:12 8
解析:
char是1字节,int是4字节,VS中默认的对齐数是8。
那么,c1对齐数=min{1,8}=1
a对齐数=min{4,8}=4
c2对齐数=min{1,8}=1
S1的内存:第一行的数字指的是靠左这条线上的指针
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
c1 |
空 |
a |
c2 |
最大对齐数=max{1,4,1}=4
目前,所有成员所占大小为9字节,不是最大对齐数的倍数。
因此,应补齐到12字节。
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
c1 |
空 |
a |
c2 |
空 |
同理,S2的内存:
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
c1 |
c2 |
空 |
a |
最大对齐数=max{1,4,1}=4
目前,所有成员所占大小为8字节,是最大对齐数的倍数。
因此,S2所占内存就是8字节。
例2:结构体嵌套情况
输出结果:32
解析:
先计算S3:
double是8字节,char是1字节,int是4字节,VS中默认对齐数是8。
那么,double对齐数=min{8,8}=8
char对齐数=min{1,8}=1
int对齐数=min{4,8}=4
最大对齐数=max{8,1,4}=8
S3内存:和例1同理,得16字节
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
d |
c |
空 |
i |
再计算S4:
char是1字节,S3是16字节,double是8字节,VS中默认对齐数是8。
那么,char对齐数=min{1,8}=1
S3对齐数=S3自己的最大对齐数=8
double对齐数=min{8,8}=8
最大对齐数=max{1,8,8}=8
S4内存:32字节
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
c1 |
空 |
s3 |
d |
2.内存对齐的意义
为什么存在内存对齐?
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的,即某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。比如,可能存在平台只能在地址为4的倍数处读取int类型的值。
- 性能原因:数据结构,尤其是栈,应该尽可能在自然边界上对齐。因为访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存只需要一次访问。
总结:结构体的内存对齐是用空间换取时间的做法。
3.设计结构体
一个好的结构体设计,要能做到既节省空间又节省时间。
节省时间已经通过内存对齐实现了。
我们需要在满足内存对齐的前提下,尽量节省空间:让占用空间小的成员尽量集中在一起。
4.修改默认对齐数
#pragma
可以更改默认对齐数。
5.宏
offsetof(struct tag, 成员变量名)
能够输出成员相对首地址的偏移量,引用前要写#include <stddef.h>
。
例1:
输出结果:0 4 8
四、结构体传参
结论:结构体传参的时候,优先选择传地址。
详见文章【初识结构体】。补充例题:
例1:
运行后s仍然都是0。原因是传参传的是s值,改的是tmp,并没有改变s。
改进:
运行后s改变了。原因是传参传的是s的地址,改变tmp的同时,也改变了s。
改进:打印出改变后的s值
注意:
Print1函数的传参传的是s的值,可以实现。原因是该函数的目标不需要改变s的值。
当然,用传递s的值来实现也是可以的,如Print2函数。