在[C语言入门]的板块,我们浅显地讨论过结构体的内容,这一期将深刻介绍结构体,后面的一期内容会做联合、枚举相关的内容。
目录
1.结构体类型的声明
结构体是一些值的集合,这些值称之为成员变量,结构的每个成员变量都可以是不同类型的变量;
关于结构体类型的定义,举一个简单的例子:写一个关于学生的结构体,成员列表含姓名、年龄。
有读者会问了,结构体中的成员变量不需要初始化吗?我们可以把结构体当做一个普通的类型,例如int、char类型,类型是修饰变量的,所以不需要初始化;
上图中的s1,s2是根据这个类型创造出的结构体变量,可以不加,此时这个两个结构体变量是全局变量,当然可以创建局部的结构体变量,如下图所示:
创建结构体变量的时候需要书写struct XXX 结构体变量名,过于繁琐,我们可以使用以下这种方式:
匿名结构体,不能手动创建结构体变量只能使用一次。
匿名结构体是有一个缺点的,那就是没有类型,如果两个匿名结构体的内部成员变量,我们依然认为这是两个不同类型的结构体。
能不能对匿名结构体进行使用typedef呢?是不行的,匿名结构体本身没有类型,所以不能进行简化。
我们还可以这么写,相当于创建了一个结构体指针。
2.结构体的自引用
我们知道在数据结构中有顺序表和链表。顺序表例如数组,可以理解成在内存中连续存储的数据;
链表在内存中是分散存储的,节点和节点之间是靠存储下一个节点的信息进行关联的,存储的节点类型和本类型完全一致,这就叫自引用。
这么写是有问题的,试想一下:next是你下一个节点的内容,next也存放着关于它下一个结构体的内容,如此以来只要链表一长,在最前面的结构体存的内容是海量的,所以为了解决这个问题,我们只需要存储下一个节点的地址即可,每一个节点只能获取下一个节点的相关信息,我们可以用结构体指针来指向下一个结构体;这就是我们常说的数据域+指针域。
3.结构体变量的定义和初始化
struct Point
{
int x;
int y;
}p1; // 结构体变量
struct Point p2;// 定义结构体变量p2
// 初始化,定义变量的同时赋初值
struct Point p3 = {x,y};
struct Stu
{
char name[15];
int age;
};
struct Stu s = {"zhangsan",20};// 初始化
我们可以把结构体(类型)当做图纸,结构体变量当做具体的房子。
结构体中含结构体,进行初始化的时候,只需要加一个大括号即可~
我们可以使用结构体变量+.的形式访问结构体变量的成员变量。
4.结构体内存对齐(重点)
这个问题的本质就是计算结构体的大小。分析一下下图结构体所占空间的大小,似乎6个字节足矣;
结果是12个字节,思考一下为什么会这样?
我们再进行一次尝试,这一次S2的大小是8个字节 ,我们惊奇的发现,即使成员相同,成员之间的顺序不同,那么结构体所占的内存空间仍然发生了变化。
4.1内存对齐的规则
①假设结构体变量s1在内存中从下图的位置开始,那么第一个字节相对于内存的其起始位置偏移量是0,以此类推......所以c1占的就是第一个字节;
②其他的成员对齐到对齐数的整数倍的地址处;默认的对齐数是由编译器和其他成员字节数的较小值,VS的对齐数是8;s1中的i大小是4个字节,大于默认对齐数8个字节,所以对齐数是4;所以i要存到偏移量为4的地址处(4是4的倍数,如下图所示);中间空的3个字节是没有使用的。
③其他成员都要对齐到对齐数的整数倍数,第三个成员是一个字节,小于8个字节,所以对齐数是1,由于8是1的倍数,所以可以直接往下存。
④此时所有的成员变量存储完毕之后只占用了9个字节,还是和之前预期的12个字节有所偏差。
⑤最后一个规则解释了原因,结构体的大小是最大对齐数的整数倍,那么最大对齐数是4,现在占用的字节数是9,所以想要是4的整数倍,只能是12个字节;所以一共浪费了6个字节。
⑥验证一下分析的过程,使用offsetof这样一个宏定义,可以返回结构体的偏移量。
输出结果与我们分析的一致。
同理来分析一下S2的内存分布:
①首先c1占用一个字节,内存偏移是0;
②c2的对齐数是1,所以可以继续往下存内存偏移是1,占用一个字节;
③最后i的对齐数是4,所以偏移必须是4的整数,偏移量就是4,中间空了2个字节,i一共四个字节占用到偏移量是8的位置。
④目前占用的字节数是8,最大的对齐数是4,所以整个结构体内存占用必须是4的倍数,所以刚好是8;
练习: 计算s3的大小
①double占8个字节,起始位置偏移量是0,一直占用到偏移量为7的地址;
②char的对齐数是1,可以直接放在偏移量为8的位置;
③int对齐数是4,必须放在4的倍数的偏移量地址,只能是12,跳过了3个字节,占用了四个字节,最后一共占用了16个字节。
④最大对齐数是8,16刚好是8的倍数,那么S3占用16个字节。
练习: 计算s4的大小
①char占用0偏移量的地址;
②s3占用的空间是16个字节,这是一个结构体,关于结构体嵌套,使用第四条规则,使用s3的的最大对齐数的整数倍8,直接从8向后存放16个字节,到偏移量23,空了7个字节;
③double d的对齐数是8,24刚好是8的倍数,所以存在24往后顺延8个字节那就是32,一共占用了32个字节;
④32是最大对齐数8的倍数,所以合法,最后占用32个字节。
4.2 内存对齐的原因
①平台原因:不是所有的硬件平台都可以访问任意地址,某些硬件平台只能在某些地址取出数据,否则抛出硬件异常。
②性能原因:数据结构(尤其是栈)经可能地在自然边界对齐;原因是是为了访问没有对齐的内存,处理器需要两次访问,对齐的内存只需要一次访问。
举一个例子:
下图左边是没有对齐的内存存储方式,如果32位机器一次访问4个字节,那么访问c之后需要两次才能读取到i完整的数据;
下图右边是对齐内存的存储方式,读完c之后可以直接读取完整的i。
4.3 内存对齐的缺点
经过之前的学习,我们可以知道内存对齐的缺点就是浪费空间,但是换来了效率,这既是所谓的空间换时间。
我们通过观察s1和s2,两个结构体只是交换了成员变量,中间相差了很多字节,诀窍就是先把占用小的成员放在一起,我们可以一定程度上减少空间的浪费。
4.4 修改VS默认对齐数
我们可以轻易的知道s5占用16个字节,修改默认对齐数为4之后占用字节为12;
其实就是更改了8的对齐数为4;
①i:占用0~3;
②d:对齐数为4,可以从4~11;
③一共12个字节。
如果不想对齐的话,可以使用以下代码:设置默认对齐数为1;
5.结构体传参
想要打印一个结构体变量的所有内部成员,需要怎么做。
①直接传入结构体。
②传入结构体指针
这两种形式哪一种更好呢?传值调用还是传址调用 ?函数在传入参数的时候,参数会压入栈空间,这就意味着参数越大占用的栈空间也就越多,所以最好是传入一个指针;
6.结构体实现位段(位段的填充&可移植性)
位段是由结构体实现的,位段的声明和结构体是类似的,有两个不同:
①位段的成员必须是int、unsigned int或者signed int。
②位段的成员名后面后面有冒号和一个数字。
例如:
位段的位是比特位的意思,上图的意思是_a需要两个比特位,_b只需要5个比特位以此类推......
在实际开发过程中可能存在一个判断真假的成员变量取值只有0或1,此时使用位段就会非常节省空间。
上面的位段占用多少个字节?仔细分析一下有47个bit,那么只需要6个字节就能存下,但是事实上需要8个字节,这是为什么呢??
6.1 位段的内存分配
位段的内存分配是一次开辟一个或者4个字节,根据成员的类型进行分配,如果成员是int一次就开辟4个字节,如果成员是char,一次开辟1个字节;不够在进行开辟。至此上面的问题得到解决了。
①先开辟4个字节也就是32位,abc用完之后还剩了17bit位,不够用。
②所以又开辟4个字节,一共8个字节。
③d是存在之前没用用完的bit位呢还是存在新开辟的内存中呢?
举一个例子说明:
①给a一个字节8bit,用了3个还剩五个。
②b正好用四个,还剩1个bit。
③c需要用5个,而剩下1个,所以需要重新开辟一个字节,之前的一个bit浪费了,这个字节用了5个还剩3个。
④d需要用4个,还剩三个,于是有开辟一个字节,之前3个bit浪费了。
总结:如果成员不够存,需要重新开辟空间,那么本成员直接存入新开辟的空间。
这个代码还存在一个问题,a只有3个bit位,10的二进制是1010,这里需要进行截断,存入010:
每次申请一个字节,需要从低位到高位使用。
验证:
6.2 位段的跨平台问题
①int位段被当成有符号或无符号是不确定的。
②位段中最大位的数目不能确定。(如果16位机器最大16,如果写成27在16位机器上会出现问题)。
③位段成员在内存中从左向右还是从右向左的分配标准没有定义。
④当一个结构包含两个位段,第二个位段的成员比较大,无法容纳第一个位段的剩余位的时候,是舍弃剩余的位还是使用者是不确定的。
6.3 位段的应用
ip数据报的底层源码就是使用位段实现的。
字段名称 | 长度(位) | 含义说明 |
---|---|---|
版本(Version) | 4 | 指示 IP 协议版本,IPv4 为 4。 |
首部长度(IHL) | 4 | 单位为 32 位字(4 字节),表示 IP 首部总长度(包括固定部分和可选部分),最小值 5(20 字节),最大值 15(60 字节)。 |
服务类型(TOS) | 8 | 早期用于指定服务质量,现通常称为区分服务(DS):前 6 位为 DSCP(区分服务代码点),后 2 位为 ECN(显式拥塞通知)。 |
总长度 | 16 | 整个 IP 数据报的总长度(字节),最大值为 65535 字节(受限于 16 位)。 |
标识(Identifier) | 16 | 用于分片重组,同一数据报的所有分片具有相同标识。 |
标志(Flags) | 3 | 分片控制标志: - 最高位(保留位):必须为 0; - DF(Don't Fragment):1 表示不分片; - MF(More Fragments):1 表示还有后续分片。 |
片偏移 | 13 | 表示分片在原数据报中的相对位置,单位为 8 字节(因此分片大小必须是 8 的倍数)。 |
生存时间(TTL) | 8 | 限制数据报在网络中的存活时间,每经过一个路由器减 1,值为 0 时丢弃,防止循环。 |
协议 | 8 | 指示上层协议类型,如 TCP(6)、UDP(17)、ICMP(1)等。 |
首部校验和 | 16 | 仅校验 IP 首部,用于检测首部在传输中的错误(数据部分由上层协议校验)。 |
源 IP 地址 | 32 | 发送方的 32 位 IP 地址。 |
目的 IP 地址 | 32 | 接收方的 32 位 IP 地址。 |
可选字段 | 可变(0-40 字节) | 用于特殊功能(如源路由、记录路由等),长度可变,最长 40 字节(需为 32 位的整数倍)。 |
填充 | 可变 | 确保首部长度为 32 位的整数倍,用 0 填充。 |