第四十章:综合项目实战——学生成绩管理系统

终于到了最后一章。说实话,每次带新人或者做培训,我都会把这一章放在最后。为什么?因为这一章几乎用到了我们之前学过的所有核心知识:结构体、链表、动态内存分配、文件操作、排序算法……说白了,这就是一次“大阅兵”。

我个人习惯把这种综合项目叫做“缝合怪”——把各个零散的知识点缝在一起,变成一个能真正跑起来的东西。你想想看,学了一堆语法,如果最后连一个像样的项目都写不出来,那跟没学有什么区别?

项目需求分析

我们先明确一下,这个学生成绩管理系统要做什么:

  • 能录入学生信息(学号、姓名、各科成绩)
  • 能计算总分和平均分
  • 能按总分排序
  • 能保存数据到文件,也能从文件读取
  • 能增删改查

嗯,听起来功能不少,但别慌。我们一步步拆解。

第一步:定义数据结构

核心数据结构当然是结构体。我在项目中遇到过很多新手一上来就写死数组,结果学生数量一多就崩了。所以这里我们用链表——动态、灵活、不怕扩容。

typedef struct Student {
    int id;                // 学号
    char name[20];         // 姓名
    float chinese;         // 语文
    float math;            // 数学
    float english;         // 英语
    float total;           // 总分
    float average;         // 平均分
    struct Student *next;  // 指向下一个节点
} Student;

注意,我把总分和平均分也存到了结构体里。为什么?因为每次排序都要算一遍的话,效率太低了。这叫“空间换时间”,我在做嵌入式项目时经常用这招。

第二步:核心功能模块

我把整个系统拆成了几个模块,每个模块对应一个函数。这样代码清晰,也方便调试。

1. 创建节点

Student* createNode() {
    Student *p = (Student*)malloc(sizeof(Student));
    if (p == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    p->next = NULL;
    return p;
}

这里有个坑:一定要检查 malloc 的返回值。我曾经在一个嵌入式项目里忘了检查,结果在内存不足时程序直接崩溃,排查了半天才发现是这里的问题。

2. 插入学生信息

void insertStudent(Student **head) {
    Student *newNode = createNode();
    printf("请输入学号:");
    scanf("%d", &newNode->id);
    printf("请输入姓名:");
    scanf("%s", newNode->name);
    printf("请输入语文成绩:");
    scanf("%f", &newNode->chinese);
    printf("请输入数学成绩:");
    scanf("%f", &newNode->math);
    printf("请输入英语成绩:");
    scanf("%f", &newNode->english);
    
    newNode->total = newNode->chinese + newNode->math + newNode->english;
    newNode->average = newNode->total / 3.0;
    
    // 头插法
    newNode->next = *head;
    *head = newNode;
    
    printf("添加成功!\n");
}

我建议用头插法,简单高效。尾插法也行,但要多维护一个尾指针,麻烦。

3. 按总分排序(冒泡排序)

排序是重头戏。这里我用冒泡排序,因为链表不适合快速排序(你想想看,链表怎么随机访问?)。

void sortByTotal(Student *head) {
    if (head == NULL || head->next == NULL) return;
    
    int swapped;
    Student *ptr;
    Student *last = NULL;
    
    do {
        swapped = 0;
        ptr = head;
        
        while (ptr->next != last) {
            if (ptr->total < ptr->next->total) {
                // 交换数据,不交换节点
                Student temp = *ptr;
                *ptr = *(ptr->next);
                *(ptr->next) = temp;
                // 修复指针
                temp.next = ptr->next;
                ptr->next = temp.next;
                swapped = 1;
            }
            ptr = ptr->next;
        }
        last = ptr;
    } while (swapped);
}

注意,我交换的是节点里的数据,而不是节点本身。为什么?因为交换节点要改一堆指针,容易出错。我在项目中吃过这个亏,后来就老老实实交换数据了。

4. 文件操作

保存到文件:

void saveToFile(Student *head, const char *filename) {
    FILE *fp = fopen(filename, "wb");
    if (fp == NULL) {
        printf("文件打开失败!\n");
        return;
    }
    
    Student *p = head;
    while (p != NULL) {
        fwrite(p, sizeof(Student), 1, fp);
        p = p->next;
    }
    
    fclose(fp);
    printf("保存成功!\n");
}

从文件读取:

Student* loadFromFile(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) {
        printf("文件不存在,将创建新文件。\n");
        return NULL;
    }
    
    Student *head = NULL;
    Student temp;
    
    while (fread(&temp, sizeof(Student), 1, fp) == 1) {
        Student *newNode = createNode();
        *newNode = temp;
        newNode->next = head;
        head = newNode;
    }
    
    fclose(fp);
    printf("读取成功!共读取 %d 条记录。\n", countNodes(head));
    return head;
}

这里用二进制读写,比文本快得多。但要注意:不同平台的结构体对齐方式可能不同,跨平台时会有问题。不过我们这个项目只在本地跑,无所谓。

知识体系结构图

下面这张图展示了整个系统的模块划分和数据流向:

学生成绩管理系统架构图 主菜单 录入学生信息 删除学生信息 查询学生信息 按总分排序 链表(动态内存管理) 文件操作(二进制读写)

完整代码框架

下面给出 main 函数的框架,你可以把它和上面的函数拼起来:

int main() {
    Student *head = NULL;
    int choice;
    
    // 启动时从文件加载数据
    head = loadFromFile("students.dat");
    
    while (1) {
        printf("\n===== 学生成绩管理系统 =====\n");
        printf("1. 录入学生信息\n");
        printf("2. 删除学生信息\n");
        printf("3. 查询学生信息\n");
        printf("4. 按总分排序\n");
        printf("5. 显示所有学生\n");
        printf("6. 保存并退出\n");
        printf("请选择:");
        scanf("%d", &choice);
        
        switch (choice) {
            case 1: insertStudent(&head); break;
            case 2: deleteStudent(&head); break;
            case 3: searchStudent(head); break;
            case 4: sortByTotal(head); break;
            case 5: displayAll(head); break;
            case 6: 
                saveToFile(head, "students.dat");
                freeList(head);
                printf("再见!\n");
                return 0;
            default: printf("无效选择!\n");
        }
    }
}
⚠️ 重要提醒:
  • 每次修改链表后,记得检查内存泄漏。我建议在退出前调用 freeList() 释放所有节点。
  • 文件读写时,一定要检查 fopen 的返回值。文件打不开就继续跑,数据全丢了。
  • 排序时如果链表为空或只有一个节点,直接返回,别做无用功。
💡 扩展思路:

如果你觉得这个系统太简单,可以试试加几个功能:

  • 按科目统计最高分、最低分、平均分
  • 支持多条件排序(先按总分,再按学号)
  • 用哈希表优化查询速度
  • 加一个图形界面(用 GTK 或者 Qt)

避坑指南

最后,我把自己踩过的坑列出来,你写代码时注意避开:

  • scanf 缓冲区问题:输入字符串后,记得用 getchar() 吃掉换行符,否则下一个 scanf 会直接跳过。
  • 链表断链:删除节点时,一定要先把前一个节点的 next 指向后一个节点,再 free 当前节点。顺序错了链表就断了。
  • 文件路径:默认路径是当前工作目录。如果你找不到文件,试试用绝对路径。
  • 结构体对齐:用 fwrite 写结构体时,不同编译器可能有不同的对齐方式。如果你要跨平台,建议用文本格式。

好了,这一章的内容就到这里。这个项目虽然不大,但五脏俱全。你把它跑通了,C语言的基础就算真正打牢了。以后遇到再复杂的项目,也不过是这些基础知识的排列组合而已。

记住:写代码不是目的,解决问题才是。动手试试吧!


专注资料整理