3. 结构体数组:结构体数组的定义、初始化、遍历与典型应用(学生成绩管理系统)
好,咱们接着往下聊。上一节我们把结构体这个「自定义数据类型」给讲透了。但说实话,在真实项目中,你很少会只定义一个结构体变量。你想想看,一个学生管理系统,难道只存一个学生的信息?一个仓库管理系统,只记录一件货物?
所以,结构体数组就派上用场了。它本质上就是一个数组,只不过数组里的每个元素都是一个结构体。说白了,就是把我们之前学的「数组」和「结构体」这两个知识点缝合在一起。
3.1 结构体数组的定义
定义结构体数组,其实就两步:先定义结构体类型,再声明这个类型的数组。我习惯把结构体类型定义放在全局区,这样整个文件都能用。
// 第一步:定义学生结构体类型
struct Student {
char name[20];
int id;
float score;
};
// 第二步:声明结构体数组
struct Student class[30]; // 一个班最多30人
嗯,这里要注意一点:struct Student class[30] 这行代码,实际上是在栈上分配了连续的内存空间。每个 Student 占多少字节?我记得在32位系统下,name[20] 占20字节,id 占4字节,score 占4字节,但因为有内存对齐,实际可能不止28字节。这个细节我们后面再聊。
#define MAX_STUDENTS 30。这样后期改容量时,只需要改一个地方,不用满世界找数字。
3.2 结构体数组的初始化
初始化结构体数组,有两种常见方式。一种是定义时直接给值,另一种是定义后再逐个赋值。
方式一:定义时初始化
struct Student class[3] = {
{"张三", 1001, 88.5},
{"李四", 1002, 92.0},
{"王五", 1003, 76.5}
};
这种写法最直观,每个花括号对 {} 对应一个学生的信息。顺序必须和结构体成员声明顺序一致。我个人比较推荐这种写法,尤其是数据量小的时候,一眼就能看出谁是谁。
方式二:定义后逐个赋值
struct Student class[3];
class[0] = (struct Student){"张三", 1001, 88.5};
class[1] = (struct Student){"李四", 1002, 92.0};
class[2] = (struct Student){"王五", 1003, 76.5};
这里用了C99引入的复合字面量语法 (struct Student){...}。如果你用的是老编译器,可能不支持。那怎么办?老老实实逐个成员赋值:
strcpy(class[0].name, "张三");
class[0].id = 1001;
class[0].score = 88.5;
class[0].name = "张三" 这种写法,在C语言里是行不通的!因为 name 是字符数组,不能直接赋值字符串。必须用 strcpy 或者初始化时赋值。这个错误我犯过不止一次,每次都是编译报错才想起来。
3.3 结构体数组的遍历
遍历结构体数组,和遍历普通数组没什么两样。无非就是用 for 循环,然后通过 . 运算符访问每个成员的字段。
int i;
for (i = 0; i < 3; i++) {
printf("姓名: %s, 学号: %d, 成绩: %.1f\n",
class[i].name, class[i].id, class[i].score);
}
输出结果:
姓名: 张三, 学号: 1001, 成绩: 88.5
姓名: 李四, 学号: 1002, 成绩: 92.0
姓名: 王五, 学号: 1003, 成绩: 76.5
这里有个小细节:class[i].name 本身就是一个 char * 类型的指针,所以用 %s 打印没问题。但如果你用 class[i].name[0],那就是取第一个字符了,别搞混。
3.4 典型应用:学生成绩管理系统
好了,理论讲完了,咱们来点实战。我当年刚学C语言时,老师布置的第一个大作业就是学生成绩管理系统。说实话,那时候写得一塌糊涂,但回过头来看,这个例子确实能很好地展示结构体数组的用法。
下面是一个简化版的学生成绩管理系统,包含录入、显示、统计平均分、查找最高分等功能。
#include <stdio.h>
#include <string.h>
#define MAX_STUDENTS 50
struct Student {
char name[20];
int id;
float score;
};
// 录入学生信息
void inputStudents(struct Student stu[], int n) {
int i;
for (i = 0; i < n; i++) {
printf("请输入第%d个学生的姓名、学号、成绩:\n", i + 1);
scanf("%s %d %f", stu[i].name, &stu[i].id, &stu[i].score);
}
}
// 显示所有学生信息
void displayStudents(struct Student stu[], int n) {
int i;
printf("\n========== 学生成绩列表 ==========\n");
printf("姓名\t学号\t成绩\n");
for (i = 0; i < n; i++) {
printf("%s\t%d\t%.1f\n", stu[i].name, stu[i].id, stu[i].score);
}
}
// 计算平均分
float calcAverage(struct Student stu[], int n) {
float sum = 0;
int i;
for (i = 0; i < n; i++) {
sum += stu[i].score;
}
return sum / n;
}
// 查找最高分学生
int findMaxScore(struct Student stu[], int n) {
int maxIdx = 0;
int i;
for (i = 1; i < n; i++) {
if (stu[i].score > stu[maxIdx].score) {
maxIdx = i;
}
}
return maxIdx;
}
int main() {
struct Student class[MAX_STUDENTS];
int num;
float avg;
int maxIdx;
printf("请输入学生人数:");
scanf("%d", &num);
inputStudents(class, num);
displayStudents(class, num);
avg = calcAverage(class, num);
printf("\n班级平均分:%.2f\n", avg);
maxIdx = findMaxScore(class, num);
printf("最高分学生:%s,成绩:%.1f\n",
class[maxIdx].name, class[maxIdx].score);
return 0;
}
运行效果:
请输入学生人数:3
请输入第1个学生的姓名、学号、成绩:
张三 1001 88.5
请输入第2个学生的姓名、学号、成绩:
李四 1002 92.0
请输入第3个学生的姓名、学号、成绩:
王五 1003 76.5
========== 学生成绩列表 ==========
姓名 学号 成绩
张三 1001 88.5
李四 1002 92.0
王五 1003 76.5
班级平均分:85.67
最高分学生:李四,成绩:92.0
3.5 结构体数组的内存布局
为了让你更直观地理解结构体数组在内存中是怎么存的,我画了一张图。
从这张图可以看得很清楚:结构体数组在内存中是连续存放的。每个元素内部,各个成员也是按定义顺序依次排列。但要注意,因为内存对齐的存在,成员之间可能会有「空洞」。比如 name[20] 后面可能空几个字节,才轮到 id。这个我们后面讲内存对齐时会详细说。
3.6 避坑指南
最后,我总结几个实际开发中容易踩的坑:
- 数组越界: 结构体数组也是数组,下标从0开始。访问
class[30]时,如果数组只定义了30个元素,那就是越界了。C语言不会帮你检查,后果自负。 - 字符串赋值: 再次强调,不能用
=给字符数组赋值字符串。必须用strcpy或者在初始化时赋值。 - 函数传参: 结构体数组作为参数传递时,实际上传的是指针。如果你在函数内部修改了数组元素,外部也会跟着变。如果你不想被修改,可以加
const修饰。 - 内存对齐: 不同编译器、不同平台下,结构体的大小可能不一样。不要硬编码结构体的大小,用
sizeof运算符来获取。
printf("%p", &class[i]) 来打印每个元素的地址,看看它们是不是连续排列的。如果发现地址间隔不一致,那多半是内存对齐在作怪。
好了,结构体数组的内容就讲到这里。说白了,它就是「数组 + 结构体」的组合拳,用好了能解决很多实际问题。下一节我们聊聊结构体指针,那又是一个新世界。
公众号:蓝海资料掘金营,微信deep3321