21. 【C语言】打包不同类型:结构体
前面二十篇文章,我们和数据类型打了很多交道——int、double、char、数组、指针……但它们都有一个共同点:一个变量只能存一种类型的数据。
可真实世界里,一个“东西”往往由很多属性拼成。比如一个学生,有姓名(字符串)、学号(整数)、成绩(浮点数);一个坐标点,有 x 和 y 两个坐标值。如果各存各的,数据散落一地,很难管理。
C 语言给了我们一个“打包”工具:结构体(struct)。它能把多个不同类型的数据捆在一起,变成一个自定义的复合类型。今天我们就来学会怎么定义它、初始化它、访问它的成员,以及如何用指针优雅地操作它。
一、结构体的定义
定义结构体的基本语法:
struct结构体名{类型 成员1;类型 成员2;// ... 更多成员};// 分号!分号!分号!例如,定义一个“学生”结构体:
structStudent{charname[50];intid;floatscore;};这就创造了一个新的类型叫struct Student。注意:struct关键字和Student是一体的,不能只用Student(除非配合typedef,后面会讲)。
二、声明结构体变量
有了struct Student这个类型,就可以像用int、double一样声明变量:
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 语言面向对象思想的雏形。后面你写的链表节点、树节点、哈希表条目,全都靠它来定义。下一篇,我们会深入结构体在内存中的真实布局——内存对齐到底是什么?为什么结构体的大小往往比成员加起来更大?以及那个诡异又实用的柔性数组是怎么工作的。这些底层细节,会让你对内存的理解再上一个台阶。
课后小练习
- 定义一个
struct Point(包含int x和int y),写一个函数double distance(const struct Point *a, const struct Point *b),计算两点之间的欧几里得距离并返回。在main中测试。 - 用前面定义的
struct Student创建一个包含 5 个学生的数组,从键盘输入他们的信息,然后找出成绩最高和最低的学生并打印其信息。 - 用结构体指针和动态内存分配,写一个程序:先让用户输入学生数量,然后用
malloc分配一个struct Student的动态数组,输入数据、打印、最后free。 - (陷阱修复)下面的代码有错误,请找出并修正:
structBook{chartitle[100];intpages;}intmain(void){structBookb;b.title="C Programming";b.pages=500;printf("%s\n",b.title);return0;}
我们下期见!
💡获取本系列示例代码请访问 GitCode 仓库。