前面咱们把C语言自带的基本类型和派生类型都学完了。但你现在想想,如果要描述一个“学生”,你有啥办法?
学生的信息有:学号(int)、姓名(字符串)、成绩(float)。按咱们以前学的,你得分开定义三个变量:
int id; char name[20]; float score;
一个学生还行,但你要是存一个班50个学生呢?定义150个变量?那代码直接炸了。
更要命的是,这三个变量在逻辑上是一伙的,都是描述同一个学生的。但在内存里,它们各待各的,你完全看不出来它们之间有什么关系。你传给函数的时候,得把三个变量都传一遍,麻烦得要死。
结构体就是来解决这个问题的。 它让你把不同类型的数据打包成一个整体,给这个整体起个名字。以后你就可以把“一个学生”当成一个变量来用了。
说白了,结构体就是把一堆零散的数据用绳子捆在一起,打成一个包裹。你想寄快递,直接把整个包裹递过去就行,不用把里面的东西一件一件掏出来分开寄。
结构体长啥样?
定义一个结构体类型,格式是这样的:
struct 结构体名 { 成员类型 成员名; 成员类型 成员名; ... };
比如描述一个学生:
struct Student { int id; // 学号 char name[20]; // 姓名 float score; // 成绩 };
这行代码的意思:我定义了一个叫Student的结构体类型,它里面包含三个成员——一个int(id)、一个字符数组(name)、一个float(score)。
注意:这只是定义了一个类型,还没有分配内存。 就像你画了一张图纸,还没有按照图纸盖房子。图纸画好了,你以后就可以照着它盖无数个房子。
定义结构体变量
有了类型,你就可以用它来定义变量了:
struct Student s1; // 定义了一个Student类型的变量s1
现在s1就是一个“学生”了,它里面包含了id、name、score三个成员。
你也可以在定义类型的同时直接定义变量:
struct Student { int id; char name[20]; float score; } s1, s2; // 顺便定义了s1和s2两个变量
但一般不建议这么写,因为类型定义和变量定义混在一起,看着乱。分开写更清晰。
访问结构体成员——用点号
定义了变量s1,怎么给里面的成员赋值?
用.(点号)运算符:
s1.id = 101; // 给学号赋值 strcpy(s1.name, "张三"); // 给姓名赋值,字符数组要用strcpy s1.score = 85.5; // 给成绩赋值 printf("学号:%d\n", s1.id); printf("姓名:%s\n", s1.name); printf("成绩:%.1f\n", s1.score);
注意: name是字符数组,你不能写s1.name = "张三",数组名是常量地址,不能赋值。必须用strcpy函数把字符串拷贝进去。
结构体初始化
跟数组一样,结构体也可以在定义的时候直接初始化:
struct Student s1 = {101, "张三", 85.5};
顺序就是成员定义的顺序:第一个值给id,第二个给name,第三个给score。
也可以指定成员来初始化(C99之后支持):
struct Student s1 = {.id = 101, .name = "张三", .score = 85.5};
这种方式更清晰,不容易搞错顺序。
结构体数组——一次存一堆
你想存一个班的学生,那就定义结构体数组:
struct Student class[50];
这就相当于50个Student打包排在一起。访问第3个学生的姓名,就写:
class[2].name
遍历结构体数组:
for (int i = 0; i < 50; i++) { printf("%d %s %.1f\n", class[i].id, class[i].name, class[i].score); }
完整例子:
#include <stdio.h> #include <string.h> struct Student { int id; char name[20]; float score; }; int main() { struct Student class[3] = { {101, "张三", 85.5}, {102, "李四", 92.0}, {103, "王五", 78.5} }; for (int i = 0; i < 3; i++) { printf("%d %s %.1f\n", class[i].id, class[i].name, class[i].score); } return 0; }
结构体指针——用箭头访问成员
结构体的指针跟普通指针一样,存的是结构体变量的地址:
struct Student s1 = {101, "张三", 85.5}; struct Student *p = &s1;
通过指针访问成员,不能用点号,得用箭头->:
printf("%d\n", p->id); // 等价于 (*p).id printf("%s\n", p->name); // 等价于 (*p).name printf("%.1f\n", p->score); // 等价于 (*p).score
p->id读作“p指向的结构体的id成员”。它跟(*p).id完全等价,但写起来更简洁。
为啥要有箭头? 因为*p.id会被编译器理解成*(p.id)——先取p的id成员(但p是指针,没有id成员),语法错误。所以必须加括号写成(*p).id。为了省事,C语言就发明了->运算符。
结构体作为函数参数
结构体可以像普通变量一样传给函数:
void print_student(struct Student s) { printf("%d %s %.1f\n", s.id, s.name, s.score); } int main() { struct Student s1 = {101, "张三", 85.5}; print_student(s1); // 传的是整个结构体的副本 return 0; }
注意:结构体传参是值传递,函数收到的是整个结构体的拷贝。 如果结构体很大(比如里面有个大数组),拷贝的开销会很大。
所以更常见的做法是传结构体的指针:
void print_student(struct Student *p) { printf("%d %s %.1f\n", p->id, p->name, p->score); } int main() { struct Student s1 = {101, "张三", 85.5}; print_student(&s1); // 传地址,效率高 return 0; }
传指针只传一个地址(8字节),不管结构体多大,开销都一样。而且通过指针还能修改原结构体的内容。
结构体嵌套——结构体里面套结构体
结构体的成员可以是另一个结构体。比如学生有出生日期,日期本身也可以是个结构体:
struct Date { int year; int month; int day; }; struct Student { int id; char name[20]; struct Date birthday; // 嵌套一个Date结构体 float score; };
访问嵌套成员要用两层点号:
struct Student s1; s1.birthday.year = 2005; s1.birthday.month = 3; s1.birthday.day = 15;
如果是指针,就用箭头加点号:
struct Student *p = &s1; p->birthday.year = 2005;
一个完整的例子:学生成绩管理
把今天学的全串起来,写一个简单的小程序:输入三个学生的信息,打印出来,再找出成绩最高的那个。
#include <stdio.h> #include <string.h> struct Student { int id; char name[20]; float score; }; int main() { struct Student class[3]; // 输入 for (int i = 0; i < 3; i++) { printf("请输入第%d个学生的学号、姓名、成绩:\n", i + 1); scanf("%d %s %f", &class[i].id, class[i].name, &class[i].score); } // 输出 printf("\n所有学生信息:\n"); for (int i = 0; i < 3; i++) { printf("%d %s %.1f\n", class[i].id, class[i].name, class[i].score); } // 找最高分 int maxIndex = 0; for (int i = 1; i < 3; i++) { if (class[i].score > class[maxIndex].score) { maxIndex = i; } } printf("\n最高分学生:%s,成绩:%.1f\n", class[maxIndex].name, class[maxIndex].score); return 0; }
这个例子虽然简单,但把结构体定义、结构体数组、成员访问、遍历、比较全用上了。你把它敲一遍,结构体的基本用法就掌握了。
几个常见的坑
坑1:忘了写struct关键字
Student s1; // 错误!类型名是struct Student struct Student s1; // 正确
用typedef可以省掉struct,但不用typedef的话必须写全。
坑2:字符数组不能用=赋值
s1.name = "张三"; // 错误!数组名不能赋值 strcpy(s1.name, "张三"); // 正确
坑3:结构体不能直接比较相等
if (s1 == s2) // 错误!结构体不能直接用==比较
要比较两个结构体,得逐个成员比较。
坑4:用指针访问成员忘了用->
p.id = 101; // 错误!p是指针,不能用点号 p->id = 101; // 正确
坑5:结构体传参效率问题
void func(struct Student s) { ... } // 值传递,拷贝整个结构体 void func(struct Student *p) { ... } // 指针传递,只拷贝地址
结构体大的时候,用指针传参。
总结一下
结构体就一句话:把不同类型的数据打包在一起,当成一个整体来用。
核心操作:
-
定义:
struct 名字 { 成员列表 }; -
定义变量:
struct 名字 变量名; -
访问成员:变量用
.,指针用-> -
初始化:
struct 名字 变量 = {值1, 值2, ...}; -
数组:
struct 名字 数组名[大小]; -
函数参数:传值(拷贝)或传指针(高效)
结构体是C语言里最常用的自定义类型,后面学链表、学文件操作、学各种数据结构,全都离不开它。你把这节课的内容吃透了,以后写复杂程序就有了基础。
回去自己动手,写一个“商品”结构体(编号、名称、价格、库存),定义几个商品,写函数打印商品信息、找最贵的商品、算总库存价值。练熟了,结构体就是你的了。下节课咱们讲共用体——那个跟结构体长得像但脾气完全不同的东西。