十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言结构体:自定义数据类型,让变量打包出行

C语言结构体:自定义数据类型,让变量打包出行 本章目标掌握结构体声明与初始化 理解内存对齐的来龙去脉 学会结构体正确传参 理解自引用原理引入数组的局限就是结构体的舞台学到现在我们接触的数据类型都是单一类型int a→ 一个整数double b→ 一个浮点数int arr[10]→ 一组整数同一种类型但现实中的数据往往不是同质化的。比如描述一个学生姓名字符串、年龄整数、性别字符、身高浮点数、学号字符串用数组不行——数组要求所有元素类型完全一致。用 5 个独立变量勉强能跑但当你要管理一个班 50 个学生的时候……// 用独立变量的地狱charname1[20],name2[20],...;// 50个名字变量intage1,age2,age3...;// 50个年龄变量charsex1[5],sex2[5]...;// 50个性别变量这根本没法维护每个学生都要对应 5 个变量改一个学生的信息要找半天。结构体struct就是为了解决这类问题而生的把不同类型的数据打包成一个逻辑整体。第一部分结构体的声明1.1 基本语法struct结构体标签{成员1的类型 成员1的名字;成员2的类型 成员2的名字;成员3的类型 成员3的名字;...};// 注意末尾分号不能丢丢了就是语法错误声明一个学生结构体structStu{charname[20];// 姓名字符数组intage;// 年龄整数charsex[5];// 性别字符数组男或女charid[20];// 学号字符数组};// ← 这个分号是必须的1.2 声明的同时定义变量// 声明类型 定义变量一行搞定structPoint{intx;inty;}p1,p2;// 同时定义两个 Point 类型的变量1.3 匿名结构体强烈不推荐但面试必问struct// ← 没有标签名字叫匿名结构体{inta;charb;floatc;}x;// 只在声明时定义了这个唯一的变量 x⚠️提醒匿名结构体看起来省事但有两个严重问题问题一匿名结构体只能定义这一次。之后想再创建同类型的变量不行因为没有类型名。问题二更坑成员完全相同的两个匿名结构体在编译器眼里是两种完全不同的类型不能互相赋值struct{inta;}x;struct{inta;}y;yx;// ❌ 编译错误类型不兼容所以面试官问能不能用匿名结构体时标准答案是尽量别用。给结构体加个标签名tag又简单又不踩坑。第二部分结构体变量的创建和初始化2.1 三种创建变量的方式// 方式一声明类型再定义变量structStus1;// 创建了一个 Stu 类型的变量 s1// 方式二声明时直接定义structStus2{张三,20,男,20230001};// 方式三用 typedef 取别名最常用typedefstructStu{charname[20];intage;charsex[5];charid[20];}Stu;// Stu 现在就是 struct Stu 的别名Stu s3{李四,22,女,20230002};// 简洁多了2.2 初始化顺序初始化按成员声明的顺序依次赋值structStus{张三,20,男,20230818001};// { name, age, sex, id }按顺序2.3 初始化指定成员初始化C99 新特性不按顺序也行用.成员名指定structStus{.age18,// 年龄放第一个OK.name李四,.id20230818002,.sex女};实战建议用指定初始化代码可读性更好。当成员很多、或者成员类型相似比如都是 int时指定初始化能避免搞错顺序而出 bug。2.4 访问结构体成员点号.structStus{张三,20,男,20230818001};// 读成员printf(姓名%s\n,s.name);// 张三printf(年龄%d\n,s.age);// 20// 写成员s.age21;// 修改年龄strcpy(s.name,王五);// 修改名字数组不能直接赋值记忆口诀小数点. “的”s.name就是s 的 name。第三部分结构体的自引用链表基础3.1 错误示范直接嵌套自己structNode{intdata;// 数据structNodenext;// ❌ 错误自身包含自身大小无穷大};为什么不行假设struct Node包含一个int data4字节和一个struct Node next。要计算struct Node的大小 4 (struct Node 的大小)→ struct Node 的大小 4 (4 (4 (4 …))) →无穷大编译器直接报错field has incomplete type3.2 正确做法用指针自引用structNode{intdata;// 4 字节structNode*next;// 8 字节64位系统};// sizeof(struct Node) 4 8 12 字节 ✅编译器自动对齐到 16核心区别指针的大小是固定的4 字节或 8 字节不会随着指向的内容变大。用指针指向自身结构体的大小就能确定了。3.3 链表节点的完整示例#includestdio.h#includestdlib.hstructNode{intdata;structNode*next;// 指向下一个节点};intmain(){// 创建三个节点1 → 2 → 3 → NULLstructNode*n1malloc(sizeof(structNode));structNode*n2malloc(sizeof(structNode));structNode*n3malloc(sizeof(structNode));n1-data1;n1-nextn2;n2-data2;n2-nextn3;n3-data3;n3-nextNULL;// 遍历链表structNode*pn1;while(p!NULL){printf(%d ,p-data);// 输出1 2 3pp-next;}// 释放内存free(n1);free(n2);free(n3);return0;}3.4 typedef 场景下的自引用陷阱错误代码typedefstruct// 匿名结构体 typedef{intdata;Node*next;// ❌ 编译错误Node 在这里还没定义完}Node;原因分析1. typedef 开始定义 2. 碰到 Node* next; 尝试使用 Node 类型 3. 但是 Node 是 typedef 给后面匿名结构体起的别名 4. typedef 还没执行完Node 还不存在 5. → 编译错误正确写法给结构体加标签typedefstructListNode// ✅ 有标签名{intdata;structListNode*next;// ✅ 用标签名引用自身不依赖 typedef}Node;// typedef 给整个类型起了别名 Node记忆口诀自引用用指针typedef 别用匿名标签名记得写上。第四部分结构体内存对齐核心难点4.1 什么是内存对齐先看一个奇怪的现象structS1{charc1;inti;charc2;};// char int charstructS2{charc1;charc2;inti;};// char char intprintf(%d\n,sizeof(structS1));// 12printf(%d\n,sizeof(structS2));// 8同样都是 1 4 1 6 字节的成员为什么大小不一样答案就是内存对齐Memory Alignment。4.2 什么是内存对齐通俗解释内存对齐结构体成员在内存中不是紧凑排列的而是按一定规则隔开存放。每个成员的起始地址必须是某个值对齐数的整数倍。高铁座位比喻想象高铁车厢每个座位的位置是固定的1车1座、1车2座……。如果硬要把两个人塞进同一个座位检票员会要求换座位——CPU 也有类似的座位规则数据必须坐在自己的对齐位置上读取才高效。4.3 四大对齐规则VS 环境默认对齐数 8规则内容规则一第 1 个成员的偏移量相对于结构体起始地址0规则二从第 2 个成员起起始地址对齐到「自身大小」和「默认对齐数」的较小值规则三结构体总大小 所有成员对齐数中最大的值的整数倍规则四嵌套结构体内层结构体成员对齐到其内部最大对齐数的整数倍默认对齐数VS 默认 8Linux gcc 没有默认对齐数就是成员自身大小。大多数场景下差异不大。4.4 练习一char int charS1 12 字节structS1{charc1;// 大小1对齐数min(1,8)1inti;// 大小4对齐数min(4,8)4charc2;// 大小1对齐数min(1,8)1};手把手推演偏移量相对结构体起始地址 偏移 0 [ c1 ] ← 规则一第1个成员偏移0占1字节 偏移 1-3 [ pad ] ← 规则二i 对齐到 4所以 1-3 要填充 3 字节 偏移 4-7 [ i ] ← i 占 4 字节4,5,6,7 偏移 8 [ c2 ] ← c2 占 1 字节 偏移 9-11 [ pad ] ← 规则三总大小最大对齐数(4)的倍数 目前 9 字节 → 补到 124的倍数 sizeof(S1) 12 字节 ✅4.5 练习二char char intS2 8 字节structS2{charc1;// 大小1对齐数1charc2;// 大小1对齐数1inti;// 大小4对齐数4};手把手推演偏移量 偏移 0 [ c1 ] ← 第1个成员偏移0占1字节 偏移 1 [ c2 ] ← c2 对齐数min(1,8)1 → 偏移1 OK1是1的倍数 偏移 2-3 [ pad ] ← i 对齐到 4 → 偏移2 不是 4 的倍数 → 补到 4 偏移 4-7 [ i ] ← i 占 4 字节 目前 8 字节最大对齐数 4 → 8 是 4 的倍数 ✅ sizeof(S2) 8 字节 ✅性能优化核心经验把相同类型的成员写在一起可以减少填充字节。S1 和 S2 成员完全一样但 S1 占 12 字节S2 占 8 字节——优化了 33% 的空间4.6 练习三double char intS3structS3{doubled;// 大小8对齐数8charc;// 大小1对齐数1inti;// 大小4对齐数4};手把手推演偏移量 偏移 0-7 [ d ] ← 第1个成员偏移0占8字节 偏移 8 [ c ] ← c 对齐数min(1,8)1 → 偏移8 OK 偏移 9-11 [ pad ] ← i 对齐到 4 → 9 不是 4 的倍数 → 补到 12 偏移 12-15 [ i ] ← i 占 4 字节 目前 16 字节最大对齐数 8 → 16 是 8 的倍数 ✅ sizeof(S3) 16 字节 ✅4.7 练习四嵌套结构体S4structS3{doubled;// 大小8对齐数8charc;// 大小1对齐数1inti;// 大小4对齐数4};// S3 内部最大对齐数 8structS4{charc1;// 大小1对齐数1structS3s3;// S3 本身对齐到 S3.max_align 8doubled;// 大小8对齐数8};手把手推演 S4偏移量 偏移 0 [ c1 ] ← 第1个成员偏移0占1字节 偏移 1-7 [ pad ] ← s3 对齐数8 → 偏移1 不是 8 的倍数 → 补到 8 偏移 8-15 [ d ] ← S3.d占 8 字节8~15 偏移 16 [ c ] ← S3.c占 1 字节 偏移 17-19 [ pad ] ← S3.i 对齐到 4 → 偏移17 不是 4 → 补到 20但 S3 总大小 16已对齐8倍数 ← S3 结束于偏移 2316 字节已是8倍数 OK 偏移 24-31 [ d ] ← S4.d占 8 字节 目前 32 字节最大对齐数 8 → 32 是 8 的倍数 ✅ sizeof(S4) 32 字节 ✅第五部分修改默认对齐数5.1 #pragma pack 基本用法#includestdio.h#pragmapack(1)// 把对齐数改为 1完全按1字节紧凑排列structS{charc1;// 偏移 0占 1 字节inti;// 偏移 1占 4 字节charc2;// 偏移 5占 1 字节};#pragmapack()// 恢复默认对齐数VS8intmain(){printf(%d\n,sizeof(structS));// 输出 6没有填充return0;}5.2 #pragma pack(4) 的效果#pragmapack(4)// 对齐数改为 4structS{charc1;// 偏移 0占 1 字节inti;// 偏移 4min(4,4)占 4 字节4~7charc2;// 偏移 8占 1 字节// 补到 12最大对齐数4的倍数};#pragmapack()printf(%d\n,sizeof(structS));// 输出 125.3 什么时候用 #pragma pack场景说明网络协议数据包协议规定的字段顺序和大小必须精确不能有多余填充文件格式解析BMP、PNG、MP3 等文件格式有固定的二进制布局硬件寄存器映射硬件的寄存器地址是固定的不能有偏移⚠️提醒#pragma pack会影响整个文件的作用范围记得用完后pack()恢复默认否则后面的所有结构体都会被影响。第六部分结构体传参——传值 vs 传地址6.1 两种传参方式structS{intdata[1000];// 4000 字节intnum;// 4 字节};structSs{{1,2,3,4},1000};// 传值整个结构体4004字节压栈voidprint1(structSs){printf(%d\n,s.num);// 只读可以}// 传地址只压一个指针8字节64位系统voidprint2(structS*ps){printf(%d\n,ps-num);// 用 - 访问指针指向的结构体成员}6.2 传值的问题性能开销函数调用时参数是怎么传递的大多数情况下函数参数是通过**栈stack**传递的。当调用print1(s)时print1(s) 传值 print2(s) 传地址 整个 s4004字节 指针 ps8字节 全部压入栈中 只需要把地址压栈 ↓ ↓ 调用开销大 调用开销小栈的空间是有限的。如果结构体很大几千字节频繁传值会导致栈空间消耗大甚至栈溢出拷贝操作耗时复制 4004 字节 vs 复制 8 字节性能显著下降6.3 - 操作符访问指针指向的结构体成员structStu{charname[20];intage;};structStus{张三,20};structStu*ps;// 两种等价写法printf(%s\n,s.name);// 普通变量用 . 点号printf(%s\n,p-name);// 指针用 - 箭头printf(%d\n,s.age);printf(%d\n,p-age);记忆口诀. “的”直接访问- “指向的”间接通过指针访问。第七部分经典面试题精讲面试题一什么是内存对齐为什么需要内存对齐参考答案定义结构体成员在内存中按一定规则存放起始地址必须是对齐数的整数倍不是想放哪儿就放哪儿。为什么需要两大原因① 性能原因核心CPU 访问内存不是逐字节的而是以字为单位读取32位系统 4字节64位系统 8字节。对齐访问int 在地址 4 地址 0 1 2 3 4 5 6 7 数据 .. .. .. .. 12 34 56 78 ──────────────── 一次读取4字节全拿到 ✅ 不对齐访问int 在地址 5 地址 0 1 2 3 4 5 6 7 8 数据 .. .. .. .. 12 34 56 78 .. ──────────────── ──── 第一次读4~7 第二次读8~11→ 合并 ✅ 但慢② 硬件兼容性某些 RISC 架构早期 SPARC、MIPS根本不支持不对齐访问会触发总线错误Bus Error直接崩溃。面试题二计算下面结构体的大小structS5{chara;// 1字节对齐数1struct{intb;// 4字节对齐数4内层 maxcharc;// 1字节对齐数1}inner;doubled;// 8字节对齐数8整体 max};手把手推演偏移量 偏移 0 [ a ] ← 第1个成员偏移0占1字节 偏移 1-3 [ pad ] ← inner 对齐到 inner.max4 → 1 不是 4 的倍数 → 补到 4 偏移 4-7 [ b ] ← inner.b4字节 偏移 8 [ c ] ← inner.c1字节 偏移 9-11 [ pad ] ← inner 自身大小9 → 对齐到 max4 → 补到 12 ← inner 结束于偏移 11总大小 124的倍数 OK 偏移 12-19 [ d ] ← S5.d对齐数8偏移 12 是 8 的倍数占 8 字节 目前 20 字节最大对齐数 8 → 补到 248 的倍数 sizeof(S5) 24 字节 ✅面试题三结构体自引用为什么用指针而不用结构体本身参考答案结构体包含自身类型变量会导致大小无穷大因为sizeof(Node) sizeof(int) sizeof(Node) 4 (4 (4 (4 ...))) → ∞结构体包含自身指针指针大小固定4 或 8 字节所以sizeof(Node) sizeof(int) sizeof(Node*) 4 8 12 字节编译器对齐后可能是 16✅面试题四下面代码的输出是什么#includestdio.h#pragmapack(2)structX{chara;// 0 1字节intb;// 2~5 4字节对齐到2charc;// 6 1字节};#pragmapack()intmain(){printf(%d\n,sizeof(structX));// ?return0;}推演偏移 0 [ a ] ← 第1个偏移01字节 偏移 1 [ pad ] ← b 对齐到 min(4,2)2 → 偏移1 不是 2 的倍数 → 补到 2 偏移 2-5 [ b ] ← b4字节2,3,4,5 偏移 6 [ c ] ← c1字节 目前 7 字节最大对齐数 2 → 补到 82 的倍数 sizeof(X) 8 ✅答案8本章总结结构体三步走// ① 声明类型加标签不用匿名structStu{charname[20];intage;charsex[5];charid[20];};// ② 创建变量structStus{张三,20,男,20230818001};// ③ 访问成员.点号printf(%s,s.name);内存对齐规则速记第一条第1个成员偏移 0 第二条其他成员对齐到「自身大小」和「默认对齐数」的较小值 第三条总大小 最大对齐数的整数倍 第四条嵌套结构体对齐到自身最大对齐数性能优化口诀成员按大小从大到小排double int char填充最少空间最省。结构体传参传值 → 拷贝整个结构体 → 栈开销大 → ❌ 传地址 → 拷贝指针4~8字节→ 栈开销小 → ✅易错点速查陷阱正确做法匿名结构体成员相同但类型不兼容避免匿名结构体加标签自引用用了结构体变量用struct Tag *next指针memset 设 int 数组为非 0/-1只用 memset 设 0 和 -1成员顺序随意导致浪费空间相同类型排一起按大小降序结构体把不同类型的变量打包成团内存对齐让 CPU 读得更快成员排好队省空间同样3个成员顺序不同大小从12变成8——优化内存从学会排队开始
返回列表