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 结构体数组的内存布局

为了让你更直观地理解结构体数组在内存中是怎么存的,我画了一张图。

结构体数组内存布局示意图 class[0] name[20] → "张三" id → 1001 score → 88.5 偏移量: 0x00 ~ 0x1B class[1] name[20] → "李四" id → 1002 score → 92.0 偏移量: 0x1C ~ 0x37 class[2] name[20] → "王五" id → 1003 score → 76.5 偏移量: 0x38 ~ 0x53 每个 struct Student 占 28 字节(含内存对齐),数组元素在内存中连续存放 class 数组首地址 = &class[0],class[1] 地址 = class 首地址 + 28

从这张图可以看得很清楚:结构体数组在内存中是连续存放的。每个元素内部,各个成员也是按定义顺序依次排列。但要注意,因为内存对齐的存在,成员之间可能会有「空洞」。比如 name[20] 后面可能空几个字节,才轮到 id。这个我们后面讲内存对齐时会详细说。

3.6 避坑指南

最后,我总结几个实际开发中容易踩的坑:

  • 数组越界: 结构体数组也是数组,下标从0开始。访问 class[30] 时,如果数组只定义了30个元素,那就是越界了。C语言不会帮你检查,后果自负。
  • 字符串赋值: 再次强调,不能用 = 给字符数组赋值字符串。必须用 strcpy 或者在初始化时赋值。
  • 函数传参: 结构体数组作为参数传递时,实际上传的是指针。如果你在函数内部修改了数组元素,外部也会跟着变。如果你不想被修改,可以加 const 修饰。
  • 内存对齐: 不同编译器、不同平台下,结构体的大小可能不一样。不要硬编码结构体的大小,用 sizeof 运算符来获取。
我的一个小技巧: 在调试结构体数组时,我经常用 printf("%p", &class[i]) 来打印每个元素的地址,看看它们是不是连续排列的。如果发现地址间隔不一致,那多半是内存对齐在作怪。

好了,结构体数组的内容就讲到这里。说白了,它就是「数组 + 结构体」的组合拳,用好了能解决很多实际问题。下一节我们聊聊结构体指针,那又是一个新世界。


公众号:蓝海资料掘金营,微信deep3321