第30章:结构体与联合体综合大作业——学生信息管理系统

终于到了最后一章。说实话,我每次带新人时,都会让他们写一个类似的小系统。不是为了炫技,而是因为——只有把结构体、联合体、链表、文件操作串起来用一遍,你才算真正掌握了C语言的数据管理能力

这一章,我们直接动手。做一个学生信息管理系统,支持增、删、改、查。我会带着你一步步拆解,把前面29章的知识点全部揉进去。

30.1 系统需求与设计思路

先明确我们要做什么:

  • 数据存储:每个学生有学号、姓名、性别、年龄、成绩(可存多科)。
  • 操作功能:增加学生、删除学生、修改信息、按学号或姓名查找。
  • 持久化:程序退出后数据不丢,下次启动能加载回来。
  • 数据结构:用链表管理动态数据,用联合体处理成绩类型的灵活存储。

我个人习惯先把数据模型画出来。你想想看,如果连数据长什么样都没想清楚,代码写一半肯定要重构。

核心设计原则:链表负责动态增删,联合体负责节省内存,文件操作负责数据持久化。三者各司其职。

学生信息管理系统架构 主控模块 增加学生 删除学生 修改信息 查找学生 链表(动态数据) 文件(持久化) 数据同步(保存/加载)

30.2 数据结构定义

先定义学生信息。这里我用了联合体来处理成绩——有的科目是整数分,有的可能是等级制(A/B/C)。

// 成绩联合体:支持整数分数或等级制
typedef union {
    int score;      // 百分制分数
    char grade;     // 等级制:'A','B','C','D','F'
} Score;

// 学生结构体
typedef struct Student {
    int id;                 // 学号
    char name[32];          // 姓名
    char gender;            // 'M' 或 'F'
    int age;                // 年龄
    Score scores[3];        // 三门课的成绩
    int useGrade;           // 0=用分数,1=用等级
    struct Student *next;   // 链表指针
} Student;

我的经验:联合体在这里最大的好处是——如果某个学生只记录了等级制成绩,那分数字段就不占额外内存。虽然省的不多,但养成这种习惯,以后做大项目时内存管理会从容很多。

30.3 核心功能实现

30.3.1 增加学生

增加学生就是往链表尾部插入一个新节点。注意要检查学号是否重复——我曾经在一个项目中忘了做这个检查,结果系统里出现了两个学号相同的学生,查成绩时数据全乱了。

Student* addStudent(Student *head, int id, const char *name, 
                    char gender, int age, int *scores, char *grades, int useGrade) {
    // 检查学号是否已存在
    Student *cur = head;
    while (cur) {
        if (cur->id == id) {
            printf("学号 %d 已存在!\n", id);
            return head;
        }
        cur = cur->next;
    }

    // 创建新节点
    Student *newNode = (Student*)malloc(sizeof(Student));
    if (!newNode) {
        printf("内存分配失败!\n");
        return head;
    }

    newNode->id = id;
    strcpy(newNode->name, name);
    newNode->gender = gender;
    newNode->age = age;
    newNode->useGrade = useGrade;

    for (int i = 0; i < 3; i++) {
        if (useGrade)
            newNode->scores[i].grade = grades[i];
        else
            newNode->scores[i].score = scores[i];
    }

    newNode->next = NULL;

    // 插入链表尾部
    if (head == NULL) {
        return newNode;
    }

    cur = head;
    while (cur->next) cur = cur->next;
    cur->next = newNode;
    return head;
}

30.3.2 删除学生

删除操作要处理三种情况:删头节点、删中间节点、删尾节点。说白了就是指针重连,但千万别忘了 free 掉内存。

Student* deleteStudent(Student *head, int id) {
    if (head == NULL) return NULL;

    Student *temp;

    // 如果要删除的是头节点
    if (head->id == id) {
        temp = head->next;
        free(head);
        printf("已删除学号 %d\n", id);
        return temp;
    }

    // 查找要删除的节点
    Student *cur = head;
    while (cur->next && cur->next->id != id) {
        cur = cur->next;
    }

    if (cur->next == NULL) {
        printf("未找到学号 %d\n", id);
        return head;
    }

    temp = cur->next->next;
    free(cur->next);
    cur->next = temp;
    printf("已删除学号 %d\n", id);
    return head;
}

注意:删除节点后,一定要把被删节点指向的 next 指针先保存下来,否则链表就断了。我见过有人直接 free 掉节点,然后才想起来 next 还没取——嗯,那叫一个酸爽。

30.3.3 修改与查找

查找很简单,遍历链表比对学号或姓名。修改就是找到节点后,直接改字段值。

Student* findById(Student *head, int id) {
    Student *cur = head;
    while (cur) {
        if (cur->id == id) return cur;
        cur = cur->next;
    }
    return NULL;
}

void updateStudent(Student *stu, const char *newName, int newAge) {
    if (stu == NULL) return;
    if (newName) strcpy(stu->name, newName);
    if (newAge > 0) stu->age = newAge;
    printf("信息已更新\n");
}

30.4 文件操作:让数据活下来

没有文件操作的系统,程序一关数据就没了。那还叫系统吗?

保存时,我建议用二进制方式写文件。速度快,而且结构体可以直接整体写入,省去逐字段格式化的麻烦。

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("数据已保存到 %s\n", filename);
}

Student* loadFromFile(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) return NULL;

    Student *head = NULL;
    Student *tail = NULL;
    Student temp;

    while (fread(&temp, sizeof(Student), 1, fp) == 1) {
        Student *newNode = (Student*)malloc(sizeof(Student));
        if (!newNode) break;

        *newNode = temp;
        newNode->next = NULL;

        if (head == NULL) {
            head = newNode;
            tail = newNode;
        } else {
            tail->next = newNode;
            tail = newNode;
        }
    }

    fclose(fp);
    return head;
}

避坑指南:用 fwrite 直接写结构体虽然方便,但要注意——如果结构体里有指针(比如 char *name 而不是 char name[32]),那保存的只是指针地址,下次加载时那个地址早就失效了。所以这里我用了定长数组,就是为了能直接序列化。

30.5 主控逻辑与菜单

最后,写一个简单的菜单循环,把上面所有功能串起来。

int main() {
    Student *head = loadFromFile("students.dat");
    int choice, id, age, scores[3];
    char name[32], gender, grades[3], useGrade;

    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:
                // 输入学生信息,调用 addStudent
                break;
            case 2:
                printf("输入要删除的学号: ");
                scanf("%d", &id);
                head = deleteStudent(head, id);
                break;
            case 3:
                // 查找后修改
                break;
            case 4:
                // 按学号或姓名查找
                break;
            case 5:
                // 遍历打印链表
                break;
            case 6:
                saveToFile(head, "students.dat");
                // 释放链表内存
                return 0;
            default:
                printf("无效选项\n");
        }
    }
    return 0;
}

30.6 总结与思考

这个系统虽然简单,但五脏俱全。你仔细看:

  • 结构体:定义了学生的数据模型
  • 联合体:灵活处理成绩的两种表示方式
  • 链表:实现了动态增删,不浪费内存
  • 文件操作:让数据在程序重启后依然存在

说实话,很多嵌入式项目里的数据管理模块,本质上就是这套东西的变体。只不过把学生换成了传感器节点,把成绩换成了采集数据。

如果你能把这个系统完整写出来,并且跑通所有功能——恭喜你,C语言的结构体与联合体,你已经真正掌握了。

最后送你一句话:学编程,动手永远比看书重要。代码写出来,跑起来,出错了再改——这个过程本身,就是最好的老师。


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