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

发布时间:2026/10/17 8:37:03

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——优化内存从学会排队开始
延伸阅读

更多相关文章

2026/10/15 21:04:19

AI行业日报|2026-08-22:5个热点事件

AI行业日报|2026-08-22:5个热点事件 本期日报聚焦大模型基础设施演进、AI数据产业链动态及企业级服务架构优化。内容涵盖推理调度、训练数据需求、模型路由方案、隐私安全机制及智能体工程化实践,供AI从业者与开发者参考。 1. Nvidia研究指出…

2026/10/17 15:40:39

基于卷积神经网络的火灾检测系统:从选题到部署的毕设完整指南

从去年开始,陆续有几位同学找我参谋深度学习方向的毕业设计选题,聊得最多的一个方向就是“基于卷积网络结构的火灾检测系统”。说实话,这个题目在毕设选题里属于那种“看起来不惊艳、但实际做起来非常顺手”的类型——它不追新不炫技&#xf…

2026/10/17 15:40:39

用代号管理个人项目:从混乱到有序的实践指南

从乱七八糟的文件夹里翻出自己三年前写的项目时,我其实挺崩溃的。命名要么是 test_final_v2 ,要么干脆叫 1111 ,打开一看根本不知道当初想干什么。直到某天我给自己定了规矩——所有个人项目统一挂在一个代号下面:realhuizhu。…

2026/10/17 15:40:39

分页存储管理:从内存碎片到页表、TLB与缺页中断的实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 15:35:38

终端AI编程助手Super Code实战:安装配置、使用技巧与避坑指南

1. 为什么我最终选择了"终端里的AI编程助手"而不是再装一个IDE插件先说说我遇到的真实场景。上个月我接手了一个线上服务的排查工作,服务器是Linux环境,所有操作都只能通过SSH终端完成。那台机器上没有装任何IDE,所以我遇到的每个报…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/17 14:20:58

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑