第1章:数据结构综合应用——学生成绩管理系统

说实话,很多初学者学完结构体、链表、文件操作和排序后,总觉得这些知识点是孤立的。我当年也有这种感觉,直到第一次在项目中需要把学生成绩数据存下来、查出来、排好序——才发现,原来这些知识点是串在一起的。

这一章,我们就来做一个完整的学生成绩管理系统。它不复杂,但很实用。你想想看,面试官最喜欢问的就是这种综合题。

1. 系统功能概览

我们要实现的功能其实很清晰:

  • 添加学生信息(学号、姓名、成绩)
  • 删除指定学生
  • 修改成绩
  • 按成绩排序(升序/降序)
  • 保存到文件
  • 从文件加载

嗯,说白了就是一个迷你版的教务系统。我在做嵌入式项目时,就经常用这种结构来管理设备配置信息。

2. 核心数据结构设计

我们先定义学生结构体:

typedef struct Student {
    int id;                // 学号
    char name[32];         // 姓名
    float score;           // 成绩
    struct Student *next;  // 指向下一个节点
} Student;

这里我用了链表而不是数组。为什么?因为链表插入和删除更方便,不用搬动大量数据。我在项目中遇到过需要频繁增删的场景,数组真的让人头疼。

核心思路:用链表管理动态数据,用文件做持久化,用排序算法处理展示需求。

3. 链表操作核心函数

我们来看几个关键函数。先看创建节点:

Student* createNode(int id, char *name, float score) {
    Student *node = (Student*)malloc(sizeof(Student));
    if (!node) {
        printf("内存分配失败\n");
        return NULL;
    }
    node->id = id;
    strcpy(node->name, name);
    node->score = score;
    node->next = NULL;
    return node;
}

这里有个坑——一定要检查 malloc 返回值。我曾经在一个项目中忘了检查,结果在内存不足时程序直接崩溃,排查了好久才发现。

再看插入函数,我习惯按学号升序插入:

void insertSorted(Student **head, Student *newNode) {
    if (*head == NULL || (*head)->id > newNode->id) {
        newNode->next = *head;
        *head = newNode;
        return;
    }
    Student *cur = *head;
    while (cur->next != NULL && cur->next->id < newNode->id) {
        cur = cur->next;
    }
    newNode->next = cur->next;
    cur->next = newNode;
}

为什么用二级指针?因为我们要修改头指针本身。嗯,这个细节面试经常考。

4. 排序实现

排序我选择了冒泡排序,虽然效率不是最高,但胜在简单直观,适合教学:

void sortByScore(Student *head, int ascending) {
    if (head == NULL) return;
    int swapped;
    Student *ptr;
    Student *last = NULL;
    do {
        swapped = 0;
        ptr = head;
        while (ptr->next != last) {
            int needSwap = ascending ? 
                (ptr->score > ptr->next->score) : 
                (ptr->score < ptr->next->score);
            if (needSwap) {
                float tempScore = ptr->score;
                ptr->score = ptr->next->score;
                ptr->next->score = tempScore;
                // 同样交换 id 和 name
                int tempId = ptr->id;
                ptr->id = ptr->next->id;
                ptr->next->id = tempId;
                char tempName[32];
                strcpy(tempName, ptr->name);
                strcpy(ptr->name, ptr->next->name);
                strcpy(ptr->next->name, tempName);
                swapped = 1;
            }
            ptr = ptr->next;
        }
        last = ptr;
    } while (swapped);
}

小技巧:实际项目中,我更推荐用归并排序处理链表,时间复杂度 O(n log n),而且稳定。但面试时能写出冒泡排序已经及格了。

5. 文件操作

保存到文件,我习惯用二进制方式,读写速度快:

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

从文件加载时要注意:我们只保存了数据部分,不保存 next 指针。加载后要重新建立链表关系:

Student* loadFromFile(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) return NULL;
    Student *head = NULL;
    Student temp;
    while (fread(&temp, sizeof(Student), 1, fp) == 1) {
        Student *node = createNode(temp.id, temp.name, temp.score);
        insertSorted(&head, node);
    }
    fclose(fp);
    return head;
}

注意:二进制文件在不同平台间可能不兼容(大小端问题)。如果需要在不同系统间迁移数据,建议用文本格式(如 CSV)。

6. 系统整体流程

下面这张图展示了整个系统的核心逻辑:

学生成绩管理系统核心流程 主菜单 添加学生 删除学生 修改成绩 排序显示 链表操作(插入/删除/修改/遍历) 文件操作(保存/加载) 程序退出

7. 完整代码示例

下面是一个精简但完整的 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: {
                int id; char name[32]; float score;
                printf("输入学号 姓名 成绩: ");
                scanf("%d %s %f", &id, name, &score);
                Student *node = createNode(id, name, score);
                insertSorted(&head, node);
                break;
            }
            case 2: {
                int id;
                printf("输入要删除的学号: ");
                scanf("%d", &id);
                deleteStudent(&head, id);
                break;
            }
            case 3: {
                int id; float newScore;
                printf("输入学号和新成绩: ");
                scanf("%d %f", &id, &newScore);
                updateScore(head, id, newScore);
                break;
            }
            case 4: {
                int order;
                printf("1.升序 2.降序: ");
                scanf("%d", &order);
                sortByScore(head, order == 1);
                displayAll(head);
                break;
            }
            case 5:
                displayAll(head);
                break;
            case 6:
                saveToFile(head, "students.dat");
                freeList(head);
                return 0;
            default:
                printf("无效选择\n");
        }
    }
}

8. 避坑指南

我总结几个常见问题:

  • 内存泄漏:每次删除节点或程序退出时,一定要 free 所有节点。我曾经忘记释放,程序跑了几天后内存爆了。
  • 文件读写错误:打开文件后一定要检查返回值,不要假设文件一定存在。
  • 排序稳定性:如果成绩相同,你希望按学号排序吗?那就需要二次排序条件。
  • 输入缓冲区:scanf 后记得清空缓冲区,否则后面的 fgets 会读到换行符。

总结一下:这个系统虽然小,但涵盖了 C 语言的核心知识点。你如果能独立写出来,面试时数据结构这块基本就稳了。

嗯,代码就这些。建议你自己动手敲一遍,遇到问题再回头看。动手实践才是最好的学习方式。


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