当前位置: 首页 > news >正文

21. 【C语言】打包不同类型:结构体

前面二十篇文章,我们和数据类型打了很多交道——intdoublechar、数组、指针……但它们都有一个共同点:一个变量只能存一种类型的数据。

可真实世界里,一个“东西”往往由很多属性拼成。比如一个学生,有姓名(字符串)、学号(整数)、成绩(浮点数);一个坐标点,有 x 和 y 两个坐标值。如果各存各的,数据散落一地,很难管理。

C 语言给了我们一个“打包”工具:结构体(struct)。它能把多个不同类型的数据捆在一起,变成一个自定义的复合类型。今天我们就来学会怎么定义它、初始化它、访问它的成员,以及如何用指针优雅地操作它。


一、结构体的定义

定义结构体的基本语法:

struct结构体名{类型 成员1;类型 成员2;// ... 更多成员};// 分号!分号!分号!

例如,定义一个“学生”结构体:

structStudent{charname[50];intid;floatscore;};

这就创造了一个新的类型叫struct Student。注意:struct关键字和Student是一体的,不能只用Student(除非配合typedef,后面会讲)。


二、声明结构体变量

有了struct Student这个类型,就可以像用intdouble一样声明变量:

structStudents1;// 未初始化structStudents2={"Alice",1001,92.5};// 声明并初始化

也可以在定义结构体时顺便声明变量:

structPoint{intx;inty;}p1,p2;// 同时声明两个全局变量

甚至可以不写结构体名(匿名结构体),但这种只能在那一次声明变量,后续无法再使用该类型,不常用:

struct{intday;intmonth;intyear;}birthday;

推荐做法:老老实实写好结构体定义,然后在需要的地方声明变量。


三、初始化结构体

1. 完全初始化

按成员顺序列出所有初始值:

structStudents={"Bob",1002,88.0};

2. 部分初始化

只写前几个值,剩余的成员会被自动初始化为 0(或空字符):

structStudents={"Charlie"};// id=0, score=0.0

这和数组的部分初始化规则一致。

3. C99 指定初始化器(Designated Initializer)

可以点名初始化某些成员,不按顺序也可以:

structStudents={.score=95.5,.name="Diana",.id=1003};

这种写法清晰、不怕记错顺序,推荐使用。


四、访问结构体成员:点运算符.

变量名.成员名读写:

#include<stdio.h>structStudent{charname[50];intid;floatscore;};intmain(void){structStudents={"Eve",1004,89.5};printf("姓名: %s\n",s.name);printf("学号: %d\n",s.id);printf("成绩: %.1f\n",s.score);s.score=92.0;// 修改成员printf("新成绩: %.1f\n",s.score);return0;}

输出:

姓名: Eve 学号: 1004 成绩: 89.5 新成绩: 92.0

五、结构体数组

结构体本身是一种类型,自然可以拿来组成数组。比如一个班的学生:

#include<stdio.h>structStudent{charname[50];intid;floatscore;};intmain(void){structStudentclass[3]={{"Alice",1001,92.5},{"Bob",1002,85.0},{"Carol",1003,78.5}};for(inti=0;i<3;i++){printf("%s (%d): %.1f\n",class[i].name,class[i].id,class[i].score);}return0;}

class[i]是数组的第 i 个元素,它是一个struct Student,再用.访问其成员。

结构体数组结合循环和排序算法,就能做出学生成绩管理系统的基础功能。后面的实战篇会不断用到它。


六、结构体指针与箭头运算符->

每个结构体变量在内存中都有地址,可以声明指向它的指针:

structStudents={"Frank",1005,76.0};structStudent*p=&s;// p 指向 s

要通过指针访问成员,有两种等价写法:

方式一:(*p).成员—— 先解引用,再用点

printf("%s\n",(*p).name);

括号必须加,因为.优先级高于**p.name会被解释成*(p.name),那是错的。

方式二:p->成员—— 箭头运算符(推荐)

printf("%s\n",p->name);// 简洁明了

p->name就是(*p).name的语法糖。几乎所有人都用箭头,所以请记住它。

示例:

#include<stdio.h>structStudent{charname[50];intid;floatscore;};intmain(void){structStudents={"Grace",1006,88.0};structStudent*p=&s;printf("姓名: %s\n",p->name);printf("学号: %d\n",p->id);printf("成绩: %.1f\n",p->score);p->score=91.5;// 用指针修改成员printf("新成绩: %.1f\n",s.score);// s.score 也变了return0;}

七、结构体作为函数参数

按值传递:整个结构体被复制

voidprint_student(structStudents){printf("%s %d %.1f\n",s.name,s.id,s.score);}

调用时,print_student(s1)会把整个s1复制一份给形参s。如果结构体很小(比如几个int),这没什么;但若结构体很大(比如包含一个大数组),复制开销就大了。而且,函数内修改s不会影响原始结构体。

按指针传递:只复制一个地址(推荐)

voidprint_student(conststructStudent*s){printf("%s %d %.1f\n",s->name,s->id,s->score);}

调用时传&s1,只复制 4 或 8 字节的指针。const修饰符表示函数不会修改结构体内容,让调用者放心。

同理,如果要让函数修改结构体的成员,传指针也很自然:

voidraise_score(structStudent*s,floatdelta){s->score+=delta;}

结论:传递结构体时,优先传指针,并用const标明只读意图。


八、常见错误与陷阱

1. 结构体定义忘记末尾分号

structPoint{intx;inty;}// 错误!缺少分号

这个错误会导致后面连续的代码报一堆莫名其妙的错。看到奇怪错误时,先检查前一个结构体定义是否少了;

2. 用==比较两个结构体

structStudenta={"Tom",1,80};structStudentb={"Tom",1,80};if(a==b){...}// 错误!不能直接用 == 比较结构体

C 语言不允许对结构体直接使用==。需要自己写函数逐个成员比较,或使用memcmp(但要小心内存对齐产生的填充字节,后面会讲)。

3. 结构体指针未初始化就使用

structStudent*p;p->id=100;// 危险!p 没有指向有效内存

指针必须指向已存在的结构体变量,或通过malloc分配空间。

4. 返回局部结构体的指针

structPoint*get_point(void){structPointp={0,0};return&p;// 危险!p 在函数返回后消失}

和普通变量一样,局部结构体在栈上,函数返回后失效。如果要返回结构体,可以直接返回值(结构体类型,会复制),或返回动态分配的结构体指针。

5. 混淆.->

  • 用变量本身访问成员:.s.name
  • 用指针访问成员:->p->name

初学时常会写出p.name(p 是指针,应该用->)或s->name(s 不是指针,应该用.)。编译器通常会给出清晰的错误提示,仔细读。


九、小结

今天你学会了把各种数据打包成一个“复合类型”——结构体:

  • struct关键字定义,最后别忘了分号。
  • 声明变量、初始化(包括指定初始化器)。
  • .访问成员,用->通过指针访问成员。
  • 结构体可以组成数组,解决了批量管理复杂数据的问题。
  • 传参时优先传指针,既高效又安全。

结构体是 C 语言面向对象思想的雏形。后面你写的链表节点、树节点、哈希表条目,全都靠它来定义。下一篇,我们会深入结构体在内存中的真实布局——内存对齐到底是什么?为什么结构体的大小往往比成员加起来更大?以及那个诡异又实用的柔性数组是怎么工作的。这些底层细节,会让你对内存的理解再上一个台阶。


课后小练习

  1. 定义一个struct Point(包含int xint y),写一个函数double distance(const struct Point *a, const struct Point *b),计算两点之间的欧几里得距离并返回。在main中测试。
  2. 用前面定义的struct Student创建一个包含 5 个学生的数组,从键盘输入他们的信息,然后找出成绩最高和最低的学生并打印其信息。
  3. 用结构体指针和动态内存分配,写一个程序:先让用户输入学生数量,然后用malloc分配一个struct Student的动态数组,输入数据、打印、最后free
  4. (陷阱修复)下面的代码有错误,请找出并修正:
    structBook{chartitle[100];intpages;}intmain(void){structBookb;b.title="C Programming";b.pages=500;printf("%s\n",b.title);return0;}

我们下期见!

💡获取本系列示例代码请访问 GitCode 仓库。

http://www.jsqmd.com/news/1128880/

相关文章:

  • 英伟达市值突破 5.4 万亿美元,大模型厂商沦为算力佃农了吗?
  • 如何快速配置开源Android电视播放器:VLC电视版完整操作指南
  • AtCoder Weekday Consest 赛情分析及题解 | 汇总(更新至 AWC 0101 Beta)
  • 【关注可白嫖源码】--课程设计+毕业设计+springbootDream car车辆租赁系统[编号:project37878](案例分析)
  • 【关注可白嫖源码】--课程设计--毕业设计--30887基于微信小程序的社区志愿者服务平台设计与实现(案例分析)
  • 羞羞答答地搞了个数学宝典
  • 原子力显微镜(AFM)常见问题(二)
  • 【项目编号 project00919】Express社区生活服务系统:Node.js+MySQL打造社区服务预约与后台运营平台
  • [MAF Workflow编排模式-05]Group Chat:构建多人智囊团式的自由协作大群
  • 22. 【C语言】更深入的 struct:内存对齐与柔性数组
  • ArcGIS 10.x 空间参考实战:3步修复“数据源缺少空间参考”错误
  • 25. 【C语言】二进制文件与随机读写
  • Windows系统优化终极指南:三分钟让电脑焕然一新
  • 技术避坑(一):MetaPhlan 4和StrainPhlan 4联用分析菌株水平的传递
  • ZLMediaKit 9.0版本下载编译
  • groupby + agg:数据分析 80% 的活就这两招
  • 5个理由告诉你为什么VIA是机械键盘配置的终极选择
  • YOLO目标检测全栈实战:从v1到v13算法精讲与项目部署指南
  • AWS、微软、谷歌和 Anthropic 悄悄做了同一件事:Session 正在取代请求,成为 Agent 的新计算单元
  • HTTP(HyperText Transfer Protocol,超文本传输协议)是位于OSI七层模型和TCP/IP四层模型中**应用层**的协议
  • 终极Wand-Enhancer完全指南:5分钟解锁游戏修改器完整高级功能
  • 不同进程的线程切换**不一定引起进程切换**,但**必然涉及进程上下文切换(即进程切换)**——这里需要明确概念辨析
  • 55-LangChain核心概念-Chain-Agent-Tool-Memory关系
  • 从0到1用C#开发ABB机器人上位机:PC SDK通信+运动控制+状态监控
  • PyTorch 2.0+ 实战:Fashion MNIST 图像分类从 91% 到 95% 的 3 个调优技巧
  • XPS深度剖析概述
  • 2026全球汽车资本风向:为什么Tier 1供应商正在比主机厂赚得更多?
  • 测试框架体系 TDD DDT BDD ATDD 介绍
  • 2026年7月亲测,汽修引流这样干超有效!
  • 2026 AI 开发者生存指南(9):AI 产品的数据分析与增长方法——从流量到留存