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

终于到了最后一章。说实话,每次带新人时,我都会让他们从头到尾写一遍这个系统。为什么?因为结构体、链表、文件操作、排序、查找——这些C语言的核心知识点,全都能在这个项目里串起来。你想想看,一个项目能把指针怎么用、内存怎么管、数据怎么存都讲明白,这不比背语法强多了?

30.1 需求分析——先想清楚再动手

我见过太多人上来就写代码,写到一半发现结构设计不合理,又推倒重来。咱们先花10分钟把需求理清楚。

这个系统要做什么?说白了就五件事:

  • 录入学生信息:姓名、学号、三门课成绩(语文、数学、英语)
  • 计算总分和平均分:这个必须自动算,不能让人手工填
  • 按总分排序:从高到低排,方便看排名
  • 按学号查找:输入学号,立刻调出这个学生的所有信息
  • 数据持久化:程序关了再打开,数据不能丢——这就是文件操作的意义

核心数据模型:每个学生是一个结构体,所有学生用链表串起来。为什么用链表不用数组?因为学生人数不确定,链表可以动态增删,不浪费内存。

30.2 系统架构设计

先画一张图,把整体逻辑看清楚。我个人习惯在写代码前,先用流程图把数据流走一遍。

学生成绩管理系统架构图 主菜单界面 录入学生信息 结构体+链表插入 计算总分/平均分 遍历链表计算 按总分排序 冒泡/选择排序 按学号查找 遍历链表匹配 文件操作层(读写二进制文件) fopen / fwrite / fread / fclose students.dat(二进制文件)

这张图你看懂了吗?从上到下,用户操作→功能处理→文件读写→数据存储。每一层只做自己的事,互不干扰。这就是模块化设计的思想。

30.3 数据结构定义

先定义学生结构体和链表节点。这里有个细节:成绩用float存,但显示时保留一位小数。我在项目中吃过亏,用int存成绩,结果平均分算出来全是整数,被老师骂了一顿。

// 学生信息结构体
typedef struct {
    char name[20];      // 姓名
    char id[15];        // 学号
    float chinese;      // 语文成绩
    float math;         // 数学成绩
    float english;      // 英语成绩
    float total;        // 总分
    float average;      // 平均分
} Student;

// 链表节点
typedef struct Node {
    Student data;       // 学生数据
    struct Node* next;  // 指向下一个节点
} Node;

我的习惯:结构体成员按逻辑顺序排列。姓名和学号放前面,成绩放中间,总分和平均分放最后——这样读代码时一目了然。

30.4 核心功能实现

30.4.1 录入学生信息

录入时要注意:总分和平均分不能让人手工输入,必须程序自动计算。我曾经见过一个系统,录入员把总分填错了,排名全乱套——这就是设计缺陷。

Node* insertStudent(Node* head) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败!\n");
        return head;
    }
    
    printf("请输入姓名:");
    scanf("%s", newNode->data.name);
    printf("请输入学号:");
    scanf("%s", newNode->data.id);
    printf("请输入语文成绩:");
    scanf("%f", &newNode->data.chinese);
    printf("请输入数学成绩:");
    scanf("%f", &newNode->data.math);
    printf("请输入英语成绩:");
    scanf("%f", &newNode->data.english);
    
    // 自动计算总分和平均分
    newNode->data.total = newNode->data.chinese + 
                          newNode->data.math + 
                          newNode->data.english;
    newNode->data.average = newNode->data.total / 3.0;
    
    // 头插法插入链表
    newNode->next = head;
    return newNode;
}

30.4.2 按总分排序——冒泡排序的链表版本

排序是链表操作里比较绕的地方。你想想看,数组排序交换两个元素就行,但链表排序要交换节点——这就涉及到指针的重新指向。我建议你画个图,把每个节点的next指针变化画出来,比看十遍代码都管用。

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

注意:这里我选择交换数据而不是交换节点。为什么?因为交换数据简单,不容易出错。如果你非要交换节点,那就要处理前驱节点的next指针——我曾经在这个坑里折腾了两个小时,最后发现还是交换数据最稳。

30.4.3 按学号查找

查找的逻辑很简单,就是遍历链表,比对学号字符串。但要注意:字符串比较要用strcmp,不能用==。这个错误我见过不下十次了。

Node* searchById(Node* head, const char* targetId) {
    Node* current = head;
    while (current != NULL) {
        if (strcmp(current->data.id, targetId) == 0) {
            return current;  // 找到了,返回节点指针
        }
        current = current->next;
    }
    return NULL;  // 没找到
}

30.4.4 文件操作——保存和加载

文件操作是让数据“活”起来的关键。我建议用二进制文件,因为结构体里有float类型,用文本文件存会有精度损失。你想想看,3位小数的成绩存成文本再读回来,可能就变成2位小数了——这能忍?

// 保存链表到文件
void saveToFile(Node* head, const char* filename) {
    FILE* fp = fopen(filename, "wb");
    if (fp == NULL) {
        printf("无法打开文件 %s\n", filename);
        return;
    }
    
    Node* current = head;
    while (current != NULL) {
        fwrite(&(current->data), sizeof(Student), 1, fp);
        current = current->next;
    }
    
    fclose(fp);
    printf("数据保存成功!\n");
}

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

30.5 主程序流程

主程序就是一个循环,不断显示菜单,等待用户选择。这里有个小技巧:用switch-case处理菜单选项,比if-else链清晰得多。

int main() {
    Node* head = NULL;
    int choice;
    
    // 启动时加载数据
    head = loadFromFile("students.dat");
    
    do {
        printf("\n===== 学生成绩管理系统 =====\n");
        printf("1. 录入学生信息\n");
        printf("2. 显示所有学生\n");
        printf("3. 按总分排序\n");
        printf("4. 按学号查找\n");
        printf("5. 保存到文件\n");
        printf("0. 退出\n");
        printf("请选择:");
        scanf("%d", &choice);
        
        switch(choice) {
            case 1:
                head = insertStudent(head);
                break;
            case 2:
                displayAll(head);
                break;
            case 3:
                sortByTotal(head);
                printf("排序完成!\n");
                break;
            case 4: {
                char id[15];
                printf("请输入要查找的学号:");
                scanf("%s", id);
                Node* result = searchById(head, id);
                if (result != NULL) {
                    printf("找到学生:%s\n", result->data.name);
                    printf("总分:%.1f\n", result->data.total);
                } else {
                    printf("未找到该学号的学生。\n");
                }
                break;
            }
            case 5:
                saveToFile(head, "students.dat");
                break;
            case 0:
                printf("感谢使用!\n");
                break;
            default:
                printf("无效选择,请重新输入。\n");
        }
    } while (choice != 0);
    
    // 释放链表内存
    freeList(head);
    return 0;
}

30.6 避坑指南——我踩过的那些坑

常见错误 后果 解决方案
忘记释放malloc的内存 内存泄漏,程序越跑越慢 退出前遍历链表,逐个free
文件打开失败不检查 程序直接崩溃 每次fopen后检查返回值
字符串比较用== 永远找不到匹配项 用strcmp,别偷懒
排序时交换节点指针 链表断裂,数据丢失 交换数据,别碰指针

核心要点总结

  • 结构体封装数据,链表动态管理
  • 总分和平均分自动计算,不依赖人工输入
  • 排序用数据交换,别折腾指针
  • 文件操作用二进制模式,保证精度
  • 每个malloc都要有对应的free

这个项目做完,C语言的核心知识你基本就掌握了。结构体怎么组织数据、链表怎么增删改查、文件怎么读写、排序查找怎么实现——这些能力在真实项目中天天都要用。嗯,代码不在多,在于你真正理解每一行在干什么。去动手写一遍吧,遇到问题再回来看,效果最好。


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