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

说实话,链表学了那么多理论,到底能干啥?

我当年刚学完链表时也有这个疑问。直到有一次,我需要给一个培训班写个简单的成绩管理工具——增删改查、排序、还要能存文件。用数组?学生人数不确定,太死板。用数据库?杀鸡用牛刀。链表,刚刚好。

今天我们就手撸一个完整的学生成绩管理系统。麻雀虽小,五脏俱全。

系统功能概览

先看看我们要实现什么:

  • 增加:录入新学生信息(学号、姓名、成绩)
  • 删除:按学号删除学生记录
  • 修改:按学号修改成绩
  • 查询:按学号或姓名查找
  • 排序:按成绩从高到低排序
  • 文件存储:程序退出时保存,启动时加载

嗯,这就是一个迷你版的学生管理系统。你想想看,很多公司的后台管理,核心逻辑不也就这些吗?

数据结构设计

先定义学生节点。我习惯把数据域和指针域分开想清楚再写代码。

typedef struct Student {
    int id;               // 学号
    char name[20];        // 姓名
    float score;          // 成绩
} Student;

typedef struct Node {
    Student data;         // 数据域
    struct Node *next;    // 指针域
} Node;

小提示:把学生信息单独定义成一个结构体,以后扩展起来方便。比如要加个「班级」字段,改一处就行。我在项目中吃过这个亏,一开始把字段全塞在节点里,后来改需求改到想哭。

核心功能实现

1. 创建节点与插入

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

void insertNode(Node **head, Student stu) {
    Node *newNode = createNode(stu);
    newNode->next = *head;
    *head = newNode;
}

这里我用了头插法。为什么?简单、高效。如果你需要按学号顺序插入,那就改成尾插或者有序插入。我个人习惯在原型阶段先用头插,后面再优化。

2. 删除节点

int deleteNode(Node **head, int id) {
    Node *cur = *head, *prev = NULL;
    while (cur) {
        if (cur->data.id == id) {
            if (prev) prev->next = cur->next;
            else *head = cur->next;
            free(cur);
            return 1;  // 删除成功
        }
        prev = cur;
        cur = cur->next;
    }
    return 0;  // 未找到
}

注意:删除节点后一定要 free 掉内存。我曾经在嵌入式项目里忘了 free,结果跑了三天后系统内存耗尽,直接死机。排查了一整天才发现是链表删除没释放内存。

3. 修改与查询

int updateScore(Node *head, int id, float newScore) {
    Node *cur = head;
    while (cur) {
        if (cur->data.id == id) {
            cur->data.score = newScore;
            return 1;
        }
        cur = cur->next;
    }
    return 0;
}

Node* searchById(Node *head, int id) {
    Node *cur = head;
    while (cur) {
        if (cur->data.id == id) return cur;
        cur = cur->next;
    }
    return NULL;
}

查询和修改的逻辑很像,都是遍历链表找节点。说白了,链表的增删改查核心就是「遍历 + 条件判断」。你把这个套路记住,90%的链表操作都能搞定。

4. 排序(冒泡排序)

void sortByScore(Node *head) {
    if (!head || !head->next) return;
    int swapped;
    Node *cur;
    Node *last = NULL;
    do {
        swapped = 0;
        cur = head;
        while (cur->next != last) {
            if (cur->data.score < cur->next->data.score) {
                Student tmp = cur->data;
                cur->data = cur->next->data;
                cur->next->data = tmp;
                swapped = 1;
            }
            cur = cur->next;
        }
        last = cur;
    } while (swapped);
}

这里我交换的是数据域,不是节点本身。为什么?简单。如果你追求性能,可以交换指针,但代码会复杂很多。对于学生成绩这种小规模数据,交换数据完全够用。

文件存储

程序退出时保存数据,启动时加载数据。这是让系统「活」起来的关键一步。

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

Node* loadFromFile(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) return NULL;
    Node *head = NULL;
    Student stu;
    while (fread(&stu, sizeof(Student), 1, fp) == 1) {
        insertNode(&head, stu);
    }
    fclose(fp);
    return head;
}

避坑指南:用二进制方式读写结构体,速度快、代码少。但要注意结构体字节对齐问题。如果你要在不同平台间交换文件,建议用文本格式(如CSV)。我曾经在ARM和x86之间传文件,因为字节对齐不一样,读出来全是乱码。

系统主流程

把上面这些功能串起来,就是一个完整的主程序了。我画了个流程图,帮你理清逻辑:

学生成绩管理系统流程图 程序启动 从文件加载学生数据 显示主菜单(1增 2删 3改 4查 5排 6存 0退) 用户选择 1-6: 执行对应操作 0: 保存并退出 其他: 提示重新输入 继续操作 保存数据,程序退出

完整代码示例

下面给出主函数的骨架,你可以直接拿去用:

int main() {
    Node *head = loadFromFile("students.dat");
    int choice, id;
    float score;
    Student stu;
    
    while (1) {
        printf("\n===== 学生成绩管理系统 =====\n");
        printf("1. 添加学生  2. 删除学生\n");
        printf("3. 修改成绩  4. 查询学生\n");
        printf("5. 成绩排序  6. 保存数据\n");
        printf("0. 退出系统\n");
        printf("请选择: ");
        scanf("%d", &choice);
        
        switch (choice) {
            case 1:
                printf("输入学号 姓名 成绩: ");
                scanf("%d %s %f", &stu.id, stu.name, &stu.score);
                insertNode(&head, stu);
                break;
            case 2:
                printf("输入要删除的学号: ");
                scanf("%d", &id);
                if (deleteNode(&head, id))
                    printf("删除成功\n");
                else
                    printf("未找到该学生\n");
                break;
            // ... 其他 case 类似
            case 0:
                saveToFile(head, "students.dat");
                // 释放所有节点内存
                while (head) {
                    Node *tmp = head;
                    head = head->next;
                    free(tmp);
                }
                return 0;
        }
    }
}

核心要点总结

  • 链表的核心操作就是「遍历 + 条件判断」,增删改查都是这个套路
  • 文件存储用二进制读写结构体,简单高效
  • 排序时交换数据域比交换节点更易实现
  • 程序退出前一定要释放所有动态内存,这是好习惯

这个系统虽然简单,但已经涵盖了链表在实际项目中的典型用法。你想想看,从学生管理到员工管理,从商品库存到订单系统,底层不都是这套增删改查的逻辑吗?

把今天这个例子吃透,以后遇到类似需求,你就能直接复用这套代码框架了。


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