C语言学习笔记(八):结构体进阶 目录1. 结构体类型声明进阶1.1 嵌套结构体类型1.2 结构体的自引用1.3 typedef简化结构体类型1.4 匿名结构体类型2. 结构体数组3. 结构体内存对齐3.1 对齐规则3.2 为什么存在内存对齐3.3 结构体设计的技巧3.4 修改默认对齐数1. 结构体类型声明进阶1.1 嵌套结构体类型在之前章节中我们学习了结构体的基本声明。今天我们来探讨嵌套结构体类型——也就是在结构体类型中包含其他结构体类型。场景描述一个学生学生信息包含名字、生日年、月、日、性别、学号。我们可以把生日也设计成一个独立的struct Date结构体类型然后在struct Student中嵌套它。#includestdio.h// 描述日期structDate{intyear;intmonth;intday;};// 描述学生信息嵌套了Date结构体structStudent{charname[20];structDatebirthday;// 嵌套结构体charid[20];};intmain(){// 嵌套结构体的初始化用 { } 套 { }structStudents{aoyun,{2008,8,8},2026010102};// 成员的访问通过 . 一层一层访问printf(生日:%d-%d-%d\n,s.birthday.year,s.birthday.month,s.birthday.day);return0;}访问方式结构体变量.嵌套结构体变量.成员名如s.birthday.year1.2 结构体的自引用结构体的自引用是指在结构体内部包含一个指向同类型结构体的指针。这是实现链表等数据结构的基础。错误写法❌ 会导致无穷递归无法计算大小structNode{intdata;structNodenext;// ❌ 错误包含了同类型的完整结构体会无限递归};正确写法✅ 用指针structNode{intdata;structNode*next;// ✅ 正确指针大小固定4或8字节};为什么这样可行每个节点需要存储数据还要能找到下一个节点。只需存储下一个节点的地址指针就能通过指针找到下一个节点。指针的大小是固定的32位下4字节64位下8字节不会造成无限递归。示意图节点1 节点2 节点3 ------ ------ ------ | data | | data | | data | | next |----| next |----| next |---- NULL ------ ------ ------1.3 typedef简化结构体类型struct Student写起来有点长可以用typedef给它起一个简短的别名。没有typedef时structStudent{charname[20];intage;};structStudents1;// 每次都要写 structstructStudents2;使用typedef后typedefstructStudent{charname[20];intage;}Student;// Student 就是 struct Student 的别名Student s1;// 简洁多了Student s2;包含自引用的结构体也可以使用typedeftypedefstructNode{intdata;structNode*next;// 注意这里必须写 struct Node不能写 Node}Node;Node n1{0};注意在结构体内部自引用时typedef的别名还未生效所以必须写完整的struct Node。1.4 匿名结构体类型匿名结构体类型是指在声明时省略了结构体标签tag。// 匿名结构体类型同时创建了变量xstruct{inta;charb;floatc;}x;特点匿名结构体类型只能使用一次就是定义变量这一次之后不能再用来创建新变量。即使两个匿名结构体的成员完全相同编译器也会把它们当成完全不同的类型。struct{inta;charb;floatc;}x;struct{inta;charb;floatc;}a[20],*px;// ❌ 错误x 不能赋值给 p因为类型不同使用typedef给匿名结构体起别名typedefstruct{inta;charb;floatc;}X;// X就是新类型名X x1;// 创建全局变量intmain(){X x2;// 创建局部变量return0;}2. 结构体数组就像有整型数组、字符数组一样我们也可以有结构体数组用来存放多个结构体变量。#includestdio.hstructDate{intyear;intmonth;intday;};typedefstructStudent{charname[20];structDatebirthday;charid[20];}Student;intmain(){// 结构体数组的初始化Student stu_arr[3]{{zhangsan,{2008,8,8},2026010101},{lisi,{2009,1,10},2026010102},{wangwu,{2009,3,20},2026010103}};// 遍历结构体数组for(inti0;i3;i){printf(name:%s\tbirth:%d-%d-%d\tid:%s\n,stu_arr[i].name,stu_arr[i].birthday.year,stu_arr[i].birthday.month,stu_arr[i].birthday.day,stu_arr[i].id);}return0;}3. 结构体内存对齐这是一个非常重要的考点先看一段代码猜猜输出结果是多少#includestdio.hstructS1{charc1;charc2;inti;};structS2{charc1;inti;charc2;};intmain(){printf(%zu\n,sizeof(structS1));printf(%zu\n,sizeof(structS2));return0;}输出结果8 12两个结构体的成员完全一样只是顺序不同但大小却不同这就是结构体内存对齐导致的。3.1 对齐规则第一个成员对齐到结构体起始位置偏移量为0。从第二个成员开始每个成员都要对齐到对齐数的整数倍偏移处。对齐数 编译器默认对齐数 与 成员自身大小的较小值VS中默认对齐数为8Linuxgcc没有默认对齐数对齐数就是成员自身大小结构体总大小必须是所有成员中最大对齐数的整数倍。嵌套结构体嵌套的结构体对齐到自己的最大对齐数的整数倍偏移处整体大小为所有最大对齐数的整数倍。以 struct S2 为例分析VS默认对齐数8成员自身大小对齐数起始偏移占用的字节c1 (char)1100i (int)4444~7c2 (char)1188总大小9对齐到最大对齐数4的整数倍 →12S2的内存布局 | c1 | 填充 | 填充 | 填充 | i4字节 | c2 | 填充 | 填充 | 填充 | 0 1 2 3 4 5 6 7 8 9 10 11 ↑ ↑ ↑ 偏移0 偏移4 偏移83.2 为什么存在内存对齐原因1平台原因移植性某些硬件平台只能在特定地址取特定类型的数据否则会抛出硬件异常。原因2性能原因现代CPU不是逐字节读写内存的而是按块通常4或8字节读取。如果数据是对齐的CPU只需一次内存访问如果数据不对齐CPU需要两次访问并拼接性能明显下降。总结结构体内存对齐是用空间换取时间的做法通过填充少量字节换取更快的CPU访问速度。3.3 结构体设计的技巧既然对齐会带来空间浪费我们可以通过合理安排成员顺序来节省空间。技巧将占用空间小的成员尽量集中在一起。// ❌ 浪费空间大的放在中间structS1{charc1;inti;charc2;};// 大小12字节// ✅ 节省空间小的集中在一起structS2{charc1;charc2;inti;};// 大小8字节两个结构体的成员一模一样只是顺序不同struct S2就比struct S1少了4个字节3.4 修改默认对齐数在VS中可以使用#pragma pack()来修改默认对齐数。#includestdio.h#pragmapack(1)// 设置默认对齐数为1structS{charc1;inti;charc2;};#pragmapack()// 取消设置恢复默认intmain(){printf(%zu\n,sizeof(structS));// 输出6无对齐填充return0;}对齐数为1时相当于没有对齐结构体大小为所有成员大小之和。总结嵌套结构体结构体里面可以套结构体用.一层层访问。自引用结构体内部用指针指向同类型结构体是链表的基础。typedef给结构体起别名让代码更简洁。匿名结构体没有名字的结构体只能使用一次不推荐使用。内存对齐结构体成员在内存中不是紧密排列的会为了CPU访问效率而填充字节。对齐规则每个成员对齐到对齐数的整数倍总大小为最大对齐数的整数倍。节省空间的技巧将占用空间小的成员集中在一起。修改对齐数使用#pragma pack()可以修改默认对齐数。