第23篇 结构体

第23篇 结构体
一、自定义类型结构体底层理论与声明规范1.1 结构体类型回顾与声明在C语言的基础数据类型如int,char,float无法满足复杂数据描述需求时我们需要自定义类型。结构体Structure允许我们将不同类型的数据聚合在一起形成一个有机的整体。1.1.1 结构体基础定义与语法结构体本质上是一系列值的集合这些值被称为成员变量。与数组不同结构体的成员可以是完全不同的数据类型。根据课件内容描述一个学生信息姓名、年龄、性别、学号的标准声明如下#include stdio.h struct Stu { char name[20]; // 名字 int age; // 年龄 char sex[5]; // 性别 char id[20]; // 学号 }; // 分号不能丢1.1.2 硬件视角的内存布局初探 从电子信息专业的硬件视角来看结构体不仅仅是代码逻辑的封装更是内存中一段连续存储空间的抽象。当我们定义上述struct Stu时编译器会根据成员的排列在内存RAM中规划出一块特定的区域。每个成员变量对应内存中特定的偏移地址这种映射关系是程序能够正确读写数据的基础。1.2 结构体变量的创建与初始化结构体变量的创建即是在内存中分配上述规划的空间。初始化则是对这片空间进行赋值。1.2.1 顺序初始化与指定初始化C语言支持按照成员定义的顺序进行初始化也支持C99标准下的指定成员初始化乱序初始化。int main(void) { // 1. 按照结构体成员的顺序初始化 struct Stu s1 { ZhangSan, 20, Male, 2023001 }; // 2. 按照指定的顺序初始化推荐可读性更强 struct Stu s2 { .age 18, .name LiSi, .id 2023002, .sex Female }; printf(Name: %s, Age: %d\n, s1.name, s1.age); printf(Name: %s, Age: %d\n, s2.name, s2.age); return 0; }1.2.2 匿名结构体与类型唯一性在声明结构体时可以省略标签tag这被称为匿名结构体。struct { int a; char b; } x; struct { int a; char b; } *p;注意虽然上述两个匿名结构体的成员看起来一模一样但在编译器眼中它们是完全不同的两个类型。因此执行p x;是非法的。匿名结构体类型如果没有通过typedef重命名基本上只能使用一次。1.3 结构体的自引用在构建链表、树等数据结构时结构体需要包含指向自身类型的指针。1.3.1 错误的自引用方式struct Node { int data; struct Node next; // 错误会导致无限递归定义大小无法确定 };如果结构体包含自身类型的变量sizeof(struct Node)将包含另一个struct Node而后者又包含另一个……这将导致结构体大小无穷大这是不合理的。1.3.2 正确的指针自引用正确的做法是使用结构体指针因为指针的大小是固定的32位系统为4字节64位系统为8字节。struct Node { int data; struct Node* next; // 正确指针大小固定 };1.3.3 Typedef与自引用的陷阱在使用typedef简化类型名时需注意作用域问题// 错误示范 typedef struct { int data; Node* next; // 错误Node 是重命名后的名字在结构体内部尚未生效 } Node; // 正确示范 typedef struct Node { int data; struct Node* next; // 正确使用 struct Node 标签 } Node;二、结构体内存对齐底层规则与性能优化2.1 内存对齐规则详解计算结构体大小时不能简单地将成员大小相加必须遵循内存对齐规则。这是为了平衡硬件访问效率与存储空间。2.1.1 对齐规则推导首地址对齐第一个成员位于结构体变量起始位置偏移量为0的地址处。成员对齐其他成员变量要对齐到“对齐数”的整数倍地址处。对齐数min(编译器默认对齐数, 该成员大小)。VS环境下默认对齐数为8Linux GCC下默认对齐数为成员自身大小。总大小对齐结构体总大小必须是“最大对齐数”所有成员对齐数中的最大值的整数倍。嵌套对齐若嵌套结构体嵌套的结构体对齐到自己的最大对齐数的整数倍处整体大小包含嵌套结构体的最大对齐数。2.1.2 内存布局实战演练让我们分析以下结构体的大小假设默认对齐数为8struct S1 { char c1; // 占1字节对齐数1位于偏移0 // 浪费3字节 (偏移1-3) int i; // 占4字节对齐数4位于偏移4-7 char c2; // 占1字节对齐数1位于偏移8 // 总大小为9最大对齐数为4需补齐到4的倍数 - 12 };结论sizeof(struct S1)为 12 字节。2.1.3 优化空间布局通过调整成员顺序可以节省空间struct S2 { char c1; // 占1字节位于偏移0 char c2; // 占1字节位于偏移1 // 浪费2字节 (偏移2-3) int i; // 占4字节位于偏移4-7 // 总大小为8最大对齐数为48是4的倍数 };结论sizeof(struct S2)为 8 字节。虽然成员相同但顺序不同导致空间节省了4字节。2.2 硬件视角的对齐原因为什么存在内存对齐这主要源于硬件层面的限制与优化平台兼容性移植原因并非所有硬件平台都能访问任意地址上的任意数据。某些硬件只能在特定地址边界读取特定类型的数据否则会抛出硬件异常。性能优化空间换时间未对齐访问如果数据跨越了两个内存块例如一个int分布在地址3-6处理器可能需要进行两次内存访问才能读取完整数据。对齐访问对齐的数据可以在一次内存周期内读取完毕。硬件通识内存对齐本质上是拿空间换取时间的做法。2.3 修改默认对齐数使用#pragma pack指令可以修改编译器的默认对齐数常用于网络协议包处理或节省空间。#include stdio.h #pragma pack(1) // 设置默认对齐数为1即取消对齐紧凑排列 struct S { char c1; int i; char c2; }; #pragma pack() // 取消设置还原默认 int main(void) { // 1 4 1 6 printf(Size of S: %zu\n, sizeof(struct S)); return 0; }三、结构体传参传值与传址的底层差异3.1 传值调用与栈帧开销在讲解函数栈帧时我们提到函数调用会在栈区开辟空间。结构体传参同样遵循这一规则。3.1.1 传值调用的性能损耗struct S { int data[1000]; int num; }; void print1(struct S s) // 传值 { printf(%d\n, s.num); }底层分析结合函数栈帧生命周期当调用print1()时从栈帧创建到销毁的完整过程如下参数压栈与栈帧创建系统会在栈区为被调函数print1()开辟新的栈帧并在这个栈帧中为形参s分配一块与原结构体大小完全相同的内存空间约4004字节。随后系统会将实参s的每一个字节完整地拷贝memcpy到这块新空间中。函数执行与数据隔离程序控制权转移至updateNum函数内部。此时函数内部对形参s的任何操作如修改num的值都是针对栈上这份独立的拷贝进行的完全不会影响main函数栈帧中的原始结构体变量。栈帧销毁当updateNum函数执行完毕return时系统会直接销毁整个栈帧。这意味着那块为了传参而临时拷贝的、占用数千字节的大块内存会被瞬间释放。结论传值调用不仅涉及昂贵的数据拷贝还会大量占用栈空间。如果结构体很大这种拷贝会消耗大量的时间和空间资源导致性能显著下降。3.2 传址调用与指针效率3.2.1 传址调用的优势void print2(struct S* ps) // 传址 { printf(%d\n, ps-num); } int main(void) { struct S s {{0}, 100}; print2(s); // 仅传递地址 return 0; }底层分析 调用print2(s)时压栈的仅仅是结构体的地址。在32位系统中仅占4字节64位系统中占8字节。相比于拷贝整个结构体传递地址的开销微乎其微。结论结构体传参时首选传址调用。四、结构体位段比特级的存储控制4.1 位段的概念与声明位段Bit-field允许我们指定成员变量占用的比特位数常用于节省空间或对应硬件寄存器/网络协议。4.1.1 位段语法规范成员必须是int,unsigned int,signed int或charC99起支持。成员名后跟冒号和数字位数。struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; };4.2 位段成员的底层存储类型Char 刚才C99起支持提到位段成员可以是char类型。从底层原理来看ASCII码存储char类型在内存中本质上是以ASCII码值整数存储的。整数特性位段操作的是二进制位而char在计算机底层被视为一个8位的整数。因此编译器允许对char类型进行位段切割将其视为一个长度为8的位域容器。4.3 位段的内存分配与跨平台问题位段的空间分配涉及很多不确定因素因此位段是不跨平台的。4.3.1 内存分配规则依赖编译器位段按照int4字节或char1字节的方式开辟空间。跨平台风险int位段被当成有符号还是无符号是不确定的。最大位数限制不确定16位机器最大1632位机器最大32。成员是从左向右分配还是从右向左分配标准未定义。当剩余位不足以容纳下一个成员时是舍弃剩余位还是利用标准未定义。4.3.2 位段的应用场景尽管存在跨平台问题位段在特定场景非常有用如网络协议IP数据报和硬件寄存器控制因为它们能精确控制比特位极大节省传输带宽或存储空间。协议字段位数说明版本号4仅需4位即可表示首部长度4仅需4位服务类型8...4.4 位段使用的注意事项4.4.1 地址与取址操作符位段的成员共享同一个字节其起始位置可能不是字节的起始位置例如从第3个bit开始。内存中每个字节分配一个地址但字节内部的bit位是没有独立地址的。因此不能对位段成员使用操作符。struct A sa {0}; // scanf(%d, sa._b); // 错误无法获取位段成员的地址 // 正确做法 int temp 0; scanf(%d, temp); sa._b temp;五、全章节逻辑闭环总结本章从结构体的声明出发深入探讨了其底层的内存布局与性能优化策略。声明与初始化掌握结构体的基本语法、匿名结构体的限制以及正确的自引用方式指针。内存对齐理解了结构体大小并非成员简单相加而是受对齐规则约束。这是硬件为了空间换时间的优化策略。通过调整成员顺序如将小类型集中可以有效节省内存。函数传参结合函数栈帧知识明确了结构体传参应首选传址调用以避免大规模数据拷贝带来的性能损耗。位段学习了如何利用位段进行比特级控制。虽然位段能极大节省空间如网络协议但因其跨平台兼容性差且无法取地址使用时需格外谨慎。