综合实战:学生成绩管理系统

说实话,每次带新人做项目,我都会让他们先写一个学生成绩管理系统。为什么?因为这东西麻雀虽小五脏俱全——增删改查、排序、文件操作,几乎涵盖了C语言数据结构的全部核心操作。我自己当年入行时,第一个拿得出手的项目就是这个。

今天咱们就一步步把它拆开,揉碎了讲清楚。

需求分析:别急着写代码

我见过太多人上来就敲键盘,结果写到一半发现结构设计不合理,又推倒重来。嗯,这是典型的「手比脑子快」。

先想清楚我们要做什么:

  • 存储什么:学生信息,包括学号、姓名、三门课成绩(语文、数学、英语)
  • 能干什么:添加学生、删除学生、修改成绩、按学号查找、按总分排序
  • 数据怎么存:运行时用内存结构,退出时保存到文件

说白了,这就是一个「带持久化功能的简易数据库」。你想想看,如果连这个都搞不定,后面那些复杂的嵌入式系统怎么玩?

核心功能清单:

  • 添加学生记录
  • 删除指定学号的学生
  • 修改学生成绩
  • 按学号查找并显示
  • 按总分从高到低排序
  • 显示所有学生信息
  • 数据保存与加载

数据结构设计:顺序表还是链表?

这个问题我每次都会被问到。我的回答是:看场景

顺序表(数组)的优点是随机访问快,缺点是插入删除要移动大量元素。链表正好相反。对于学生管理系统,学生数量一般不超过几百人,而且增删操作不频繁,我个人习惯用顺序表——代码简单,调试方便。

来看一下我设计的数据结构:

#define MAX_STUDENTS 100
#define NAME_LEN 20

typedef struct {
    int id;              // 学号
    char name[NAME_LEN]; // 姓名
    float chinese;       // 语文
    float math;          // 数学
    float english;       // 英语
    float total;         // 总分
} Student;

typedef struct {
    Student data[MAX_STUDENTS];
    int count;           // 当前学生数量
} StudentList;

这里有个小细节:total 字段我选择实时计算还是存起来?我的建议是存起来。虽然多占了一点内存,但排序时不用每次都重新算,效率高很多。我在项目中遇到过类似的选择题,后来发现「空间换时间」在嵌入式领域往往是正确的。

我的经验:结构体里尽量把常用计算结果缓存起来。比如总分,每次修改成绩时同步更新,排序时直接拿来用,省心。

功能模块实现:一个一个来

1. 添加学生

添加前先检查有没有满。满了就报错,没满就追加到末尾。注意学号不能重复——这个检查不能省。

int addStudent(StudentList *list, int id, char *name, 
                float ch, float ma, float en) {
    if (list->count >= MAX_STUDENTS) {
        printf("学生已满,无法添加\n");
        return -1;
    }
    // 检查学号是否重复
    for (int i = 0; i < list->count; i++) {
        if (list->data[i].id == id) {
            printf("学号 %d 已存在\n", id);
            return -1;
        }
    }
    Student *s = &list->data[list->count];
    s->id = id;
    strcpy(s->name, name);
    s->chinese = ch;
    s->math = ma;
    s->english = en;
    s->total = ch + ma + en;
    list->count++;
    return 0;
}

2. 删除学生

删除的核心是「找到它,然后把它后面的元素往前挪」。我曾经犯过一个低级错误——删除后忘了把 count 减1,结果后面添加学生时直接覆盖了最后一个有效数据。嗯,这种bug查起来真要命。

int deleteStudent(StudentList *list, int id) {
    int pos = -1;
    for (int i = 0; i < list->count; i++) {
        if (list->data[i].id == id) {
            pos = i;
            break;
        }
    }
    if (pos == -1) return -1;  // 没找到
    
    for (int i = pos; i < list->count - 1; i++) {
        list->data[i] = list->data[i + 1];
    }
    list->count--;
    return 0;
}

3. 修改成绩

这个简单,找到学号,直接改。别忘了同步更新总分。

int updateScore(StudentList *list, int id, 
                float ch, float ma, float en) {
    for (int i = 0; i < list->count; i++) {
        if (list->data[i].id == id) {
            list->data[i].chinese = ch;
            list->data[i].math = ma;
            list->data[i].english = en;
            list->data[i].total = ch + ma + en;
            return 0;
        }
    }
    return -1;
}

4. 查找与排序

查找用顺序查找就行,数据量不大。排序我选冒泡排序——虽然效率不是最高的,但代码可读性好,教学意义强。

void sortByTotal(StudentList *list) {
    for (int i = 0; i < list->count - 1; i++) {
        for (int j = 0; j < list->count - 1 - i; j++) {
            if (list->data[j].total < list->data[j + 1].total) {
                Student tmp = list->data[j];
                list->data[j] = list->data[j + 1];
                list->data[j + 1] = tmp;
            }
        }
    }
}

注意:排序会改变元素顺序。如果你需要保持原始插入顺序,建议加一个「序号」字段,或者排序前先备份。我曾经在项目中因为排序破坏了原始顺序,导致后面的「按学号查找」逻辑出错——教训深刻。

数据持久化:文件读写

程序关了数据就丢了?那不行。我们用二进制文件保存,读写速度快,而且不会出现浮点数精度丢失的问题。

int saveToFile(StudentList *list, const char *filename) {
    FILE *fp = fopen(filename, "wb");
    if (!fp) return -1;
    fwrite(&list->count, sizeof(int), 1, fp);
    fwrite(list->data, sizeof(Student), list->count, fp);
    fclose(fp);
    return 0;
}

int loadFromFile(StudentList *list, const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) return -1;
    fread(&list->count, sizeof(int), 1, fp);
    fread(list->data, sizeof(Student), list->count, fp);
    fclose(fp);
    return 0;
}

这里有个坑:如果你修改了 Student 结构体的定义(比如加了字段),旧的文件就没法读了。解决办法是加一个版本号,每次读文件时检查版本是否匹配。嗯,这个技巧我在做嵌入式OTA升级时用过,很实用。

系统整体流程

下面这张图展示了整个系统的核心逻辑,从启动到退出,每一步都清清楚楚:

学生成绩管理系统 - 核心流程 程序启动 加载数据文件 显示主菜单 添加学生 删除学生 修改成绩 查找/排序 保存并退出

代码整合与测试

把所有功能模块整合到一个 main.c 里,用菜单循环驱动。我习惯把每个功能封装成独立的函数,这样测试时可以直接调用,不用每次都走菜单流程。

测试用例我一般这么设计:

测试项 输入 预期结果
添加学生 学号1001,张三,90,85,88 添加成功,总分263
重复学号 再次添加1001 提示学号已存在
删除学生 删除学号1001 删除成功,数量减1
排序 添加多个学生后排序 按总分降序排列
文件持久化 保存后重启程序 数据完整恢复

测试小技巧:写一个自动测试函数,批量添加几十个随机学生数据,然后验证排序和查找的正确性。手动点菜单太慢了,我一般写个脚本跑一遍。

最后说一句:这个系统虽然简单,但它是你理解「数据结构+算法+工程实践」的绝佳起点。把代码写出来,跑通,再回头看看——你会发现很多之前模糊的概念一下子就清晰了。


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