学生信息管理系统 —— 课程设计报告(C语言有头双向链表)

声明 本文是C语言,不是C++!!!

一、项目背景与需求分析

学生信息管理是常见的课程设计题目,主要考察:

  • C语言结构体 的使用

  • 链表数据结构 的构建与操作

  • 文件读写 的实现

  • 模块化编程与头文件管理

本项目要求实现一个 基于双向链表的学生信息管理系统,提供如下功能:

  1. 学生信息录入(增)

  2. 学生信息查询(按学号、姓名、模糊查询、显示全部)

  3. 学生信息修改(按学号)

  4. 学生信息删除(按学号)

  5. 学生成绩统计(平均分、最高/最低分、及格率)

  6. 学生信息排序(按成绩/学号 升序/降序)

  7. 文件存取(保存学生数据、加载历史数据)

1. 系统结构图

学生信息管理系统
│
├── main.c (主函数,系统入口)
├── student.c / student.h (学生数据结构与操作)
├── node.c / node.h (链表节点定义与操作)
├── linkhead.c / linkhead.h (链表头与链表操作函数)
└── student_data.txt (学生信息持久化文件)

2. 数据结构定义

学生结构体(STU)

// student.h
typedef struct Student {
    char *name;   // 动态分配的姓名缓冲区(当前在 StudentInit 里分配 50 字节)
    int  age;     // 年龄(建议:非负,合理上限 150)
    char gender;  // 性别:'M' 或 'F'
    int  id;      // 学号(全局唯一)
    float score;  // 成绩(0.0 ~ 100.0)
} STU;

设计意图与要点:

  • typedef 起别名 STU,后续代码更简洁。

  • name 设计成指针 + 动态内存,方便以后扩展更长名字(当前实现固定分配 50 字节)。

  • id 作为主键,后续增、删、改、查都围绕它进行。

  • scorefloat,能表示小数分;与统计功能(平均分、最高/最低)一致。

建议(可作为课程设计的“改进点”写进总结):

  • 为了避免 scanf("%s", ...) 溢出,建议读入时使用长度限定scanf("%49s", data->name);

  • 如果不想动态分配,也可以把 name 改成定长数组

char name[50];

这样更安全、简单,但修改姓名长度需要改结构体并重新编译。

链表节点(Node)

// node.h
typedef struct Node {
    STU *data;        // 指向一条学生记录
    struct Node *next; // 后驱指针
    struct Node *prev; // 前驱指针
} Node;

设计意图与要点:

  • 采用双向链表:增删节点更方便(删除时可 O(1) 接回前后),后续做排序/遍历也灵活。

  • data 与链表结构解耦:排序时只交换 data 指针即可,不动节点本身,链表结构更稳定。

不变式(Invariants,面试/答辩常问):

  • 对任一有效节点 p

    • p->next != NULL,则 p->next->prev == p

    • p->prev != NULL,则 p->prev->next == p

链表头(LH)

// linkhead.h
typedef struct LinkHead {
    Node *first;  // 头节点指针(可能为 NULL)
    Node *last;   // 尾节点指针(可能为 NULL)
    int   size;   // 当前链表元素个数
} LH;

设计意图与要点:

  • 通过链表头同时保存 firstlastsize

    • 头插/尾插都能做到 O(1)

    • 统计数量无需遍历,size 直接可用。

  • 空表不变式:当 size == 0 时,first == NULL && last == NULL

头文件依赖与组织

student.h     定义学生数据结构 STU 及其相关函数声明
    ▲
node.h        依赖 student.h,定义 Node(持有 STU*)
    ▲
linkhead.h    依赖 node.h,定义链表头 LH 与链表操作

这种自下而上的依赖关系,能避免循环包含(include cycle),也方便模块化。

头文件保护(Include Guard):

  • 使用了 #ifndef / #define / #endif,防止重复包含。

  • 宏名与文件名一致(如 _STUDENT_H_NODE_H_LINKHEAD_H),清晰规范。

与结构体相关的“生命周期”梳理(非常适合课设答辩)

  • 创建阶段

    • StudentInit():分配 STUname 缓冲区。

    • InitNode(STU *data):分配 Node,并挂上 data

    • LHInit(NULL):分配并初始化链表头(first/last=NULL, size=0)。

  • 使用阶段

    • 通过 CreatLink(linkH, node) 插入链表(首个节点需要同时设置 firstlast)。

    • 遍历时从 linkH->firstlinkH->last 出发,按 next/prev 行走。

    • 排序时只交换 Node->data,不动 Node 本身,避免破坏指针结构。

  • 释放阶段

    • ReleaseNode(Node *n):先 ReleaseStudent(n->data),再 free(n)

    • ReleaseLINK(LH *linkH):遍历逐个释放节点与学生,最后 free(linkH)

为什么交换 data 而不是交换节点?

  • 交换节点需要重连四个指针(两侧前后各两个),容易出错;

  • 交换 data 只改两个指针(STU* 互换),简单安全。

二、内存管理与对象生命周期

在我们这个系统里,有三类对象需要动态分配内存:

  1. 学生信息 STU

  2. 链表节点 Node

  3. 链表头 LH

因此,对应有 初始化函数(负责分配内存+初始化字段),和 释放函数(负责释放内存+避免内存泄漏)。

1)学生初始化 StudentInit

STU *StudentInit() {
    STU *data = (STU *)malloc(sizeof(STU));
    data->name = (char *)malloc(50 * sizeof(char));
    data->age = 0;
    data->gender = '\0';
    data->id = 0;
    data->score = 0.0;
    return data;
}

逐行讲解:

  • malloc(sizeof(STU))

    • 动态分配一个学生结构体。返回类型是 void*,在 C 中可以直接赋值给 STU*,加 (STU*) 只是为了清晰。

  • data->name = malloc(50)

    • 单独给姓名分配了 50 个字节。相当于每个学生都拥有独立的名字缓冲区。

    • 注意:这里只是分配内存,里面内容是随机的,不是空字符串。

  • data->age = 0;

    • 给所有字段赋初值。避免未初始化的脏数据。

2)学生释放 ReleaseStudent

void ReleaseStudent(STU *data) {
    free(data->name);
    free(data);
}

   

逐行讲解:

  • free(data->name)

    • 释放姓名缓冲区。

    • 必须先释放它,否则如果直接 free(data),就失去了 name 的地址,造成内存泄漏

  • free(data)

    • 释放学生本身。

常见问题:

  • 释放后继续使用

    • 如果调用了 ReleaseStudent(data); 后还访问 data->namedata->score,就是野指针错误

    • 改进:在释放后将指针设为 NULL,如:

      free(data->name);
      free(data);
      data = NULL;
      

        

3)节点初始化 InitNode

Node *InitNode(STU *data) {
    Node *node = (Node *)malloc(sizeof(Node));
    node->data = data;
    node->next = NULL;
    node->prev = NULL;
    return node;
}

要点:

  • 为链表节点分配内存,挂上一个学生数据指针。

  • 初始时 nextprev 都置空。

  • 后续由链表操作函数(如 CreatLink)把它接到链表里。


4)节点释放 ReleaseNode

void ReleaseNode(Node *n) {
    ReleaseStudent(n->data);
    free(n);
}

void ReleaseNode(Node *n) { ReleaseStudent(n->data); free(n); }

要点:

  • 必须先释放 Node->data(学生),再释放节点本身。

  • 否则学生数据会泄漏。

   

5)链表头初始化 LHInit

LH *LHInit() {
    LH *linkH = (LH *)malloc(sizeof(LH));
    linkH->first = NULL;
    linkH->last = NULL;
    linkH->size = 0;
    return linkH;
}

要点:

  • 初始化时链表为空,所以 first=last=NULLsize=0

  • size 的存在让统计和判断更快。


6)链表释放 ReleaseLINK

void ReleaseLINK(LH *linkH) {
    Node *p = linkH->first;
    while (p != NULL) {
        Node *temp = p->next;
        ReleaseNode(p);
        p = temp;
    }
    free(linkH);
}

逐行讲解:

  • 从头节点开始遍历整个链表。

  • 用一个 temp 保存 p->next,否则释放后链条断掉找不到后续。

  • 调用 ReleaseNode 释放每个节点及其学生。

  • 最后释放链表头本身。


7)生命周期总结图(很适合放进课设报告)

flowchart LR
    A[StudentInit] --> B[STU* data in heap]
    B --> C[InitNode(data)]
    C --> D[Node in heap]
    D --> E[CreatLink -> LinkHead管理]
    E -->|使用| F[增删改查]
    F -->|不再需要| G[ReleaseNode / ReleaseLINK]
    G --> H[free释放内存]

三、链表操作函数解析

在我们的实现中,链表采用 双向链表 + 带头指针的管理结构,支持插入、删除、遍历、查找等基本功能。下面我们逐个讲。


1)插入函数 CreatLink

void CreatLink(LH *linkH, Node *node) {
    if (linkH->first == NULL) {          // 如果链表为空
        linkH->first = node;
        linkH->last = node;
    } else {                             // 插入到尾部
        linkH->last->next = node;
        node->prev = linkH->last;
        linkH->last = node;
    }
    linkH->size++;
}

工作流程:

  1. 链表为空时

    • firstlast 都指向新节点。

    • 形成一个单节点链表。

  2. 链表非空时

    • 将新节点接到尾部:

      • linkH->last->next = node;

      • node->prev = linkH->last;

    • 更新 last 指针为新节点。

  3. 维护 size

    • 每次插入,链表元素数量 +1

图解(链表从空到插入两个节点):

flowchart LR
    subgraph Step1[链表为空]
        H1[head: first=NULL,last=NULL,size=0]
    end
    subgraph Step2[插入第1个节点]
        H2[head: first->N1,last->N1,size=1]
        N1[Node1(STU)]
    end
    subgraph Step3[插入第2个节点]
        H3[head: first->N1,last->N2,size=2]
        N1 --> N2
        N2 --> N1
    end

2)删除函数 DeleteNode

void DeleteNode(LH *linkH, int id) {
    Node *p = linkH->first;
    while (p != NULL) {
        if (p->data->id == id) {
            if (p->prev != NULL) {
                p->prev->next = p->next;
            } else {
                linkH->first = p->next;  // 删除的是第一个节点
            }
            if (p->next != NULL) {
                p->next->prev = p->prev;
            } else {
                linkH->last = p->prev;   // 删除的是最后一个节点
            }
            ReleaseNode(p);
            linkH->size--;
            return;
        }
        p = p->next;
    }
    printf("未找到学号为 %d 的学生信息。\n", id);
}

要点:

  • 遍历链表,找到 id 匹配的节点。

  • 删除时要更新前驱/后继指针

    • 如果不是头节点,要让前驱 ->next 指向后继。

    • 如果是头节点,要更新 linkH->first

    • 如果是尾节点,要更新 linkH->last

  • 最后释放节点。

图解:删除中间节点

flowchart LR
    N1[Node1] <--> N2[Node2] <--> N3[Node3]
    style N2 fill:#f99
    subgraph 删除N2后
    N1 --> N3
    N3 --> N1
    end

注意点:

  • 删除后要 linkH->size--

  • 如果链表只有一个节点,删除后 firstlast 都会变成 NULL


3)遍历函数(查询所有)

void Display(LH *linkH) {
    Node *p = linkH->first;
    while (p != NULL) {
        printf("ID: %d, 姓名: %s, 年龄: %d, 性别: %c, 成绩: %.2f\n",
               p->data->id, p->data->name, p->data->age,
               p->data->gender, p->data->score);
        p = p->next;
    }
}

要点:

  • first 开始,依次向后访问 next

  • 格式化打印信息。

  • 因为是双向链表,也可以写一个 ReverseDisplaylast 向前遍历。


4)查找函数 FindById

STU *FindById(LH *linkH, int id) {
    Node *p = linkH->first;
    while (p != NULL) {
        if (p->data->id == id) {
            return p->data;
        }
        p = p->next;
    }
    return NULL;
}

要点:

  • 顺序遍历,找到后返回指向 STU 的指针。

  • 没找到则返回 NULL

  • 这是 O(n) 的操作,适合链表结构。


5)更新函数 UpdateStudent

void UpdateStudent(LH *linkH, int id, const char *newName, int newAge, char newGender, float newScore) {
    STU *stu = FindById(linkH, id);
    if (stu != NULL) {
        strcpy(stu->name, newName);
        stu->age = newAge;
        stu->gender = newGender;
        stu->score = newScore;
    } else {
        printf("未找到学号为 %d 的学生信息。\n", id);
    }
}

要点:

  • 先调用 FindById 找到学生。

  • 找到则修改字段,否则提示“未找到”。

  • 注意安全strcpy 可能导致缓冲区溢出,改进方法是 strncpy(stu->name, newName, 49)


6)链表操作小结

  • 插入(CreatLink)

    • 尾插法,O(1)。

    • 空链表要特殊处理。

  • 删除(DeleteNode)

    • 更新前后指针,分情况(头/尾/中间)。

    • 释放节点,维护 size。

  • 查找(FindById)

    • 顺序遍历,O(n)。

  • 更新(UpdateStudent)

    • 基于查找,修改数据。

  • 遍历(Display)

    • 正向/反向都可实现。

四、排序与统计功能

在学生信息管理系统中,排序和统计功能主要集中在 成绩 这一字段上。
典型功能包括:

  • 按成绩升序/降序排序

  • 计算平均分、最高分、最低分


1)按成绩排序函数 SortByScore

先给出一个简单的实现(冒泡排序思想,直接在链表上交换学生数据)。

void SortByScore(LH *linkH, int ascending) {
    if (linkH->size < 2) return;

    Node *i, *j;
    for (i = linkH->first; i != NULL; i = i->next) {
        for (j = i->next; j != NULL; j = j->next) {
            int condition = ascending ? (i->data->score > j->data->score) 
                                      : (i->data->score < j->data->score);
            if (condition) {
                // 交换学生数据指针
                STU *temp = i->data;
                i->data = j->data;
                j->data = temp;
            }
        }
    }
}

思路讲解:

  1. 遍历链表中的每一对节点 (i, j)

  2. 根据 ascending 参数决定升序或降序。

    • ascending = 1 → 升序

    • ascending = 0 → 降序

  3. 如果顺序不对,就交换两个节点的 data(即学生指针)。

    • 注意:这里没有改变链表结构,只交换了数据指针,效率更高。

图解:升序排序过程(示意)

flowchart LR
    A[80] --> B[95] --> C[60]
    subgraph 排序后
    C2[60] --> A2[80] --> B2[95]
    end

2)平均分函数 AverageScore

float AverageScore(LH *linkH) {
    if (linkH->size == 0) return 0.0;
    float sum = 0;
    Node *p = linkH->first;
    while (p != NULL) {
        sum += p->data->score;
        p = p->next;
    }
    return sum / linkH->size;
}

要点:

  • 遍历链表,把所有成绩加起来。

  • 除以 size 得到平均分。

  • 如果链表为空,返回 0.0,避免除零错误。


3)最高分和最低分

float MaxScore(LH *linkH) {
    if (linkH->size == 0) return 0.0;
    float max = linkH->first->data->score;
    Node *p = linkH->first->next;
    while (p != NULL) {
        if (p->data->score > max) {
            max = p->data->score;
        }
        p = p->next;
    }
    return max;
}

float MinScore(LH *linkH) {
    if (linkH->size == 0) return 0.0;
    float min = linkH->first->data->score;
    Node *p = linkH->first->next;
    while (p != NULL) {
        if (p->data->score < min) {
            min = p->data->score;
        }
        p = p->next;
    }
    return min;
}

思路:

  • 遍历一遍链表,用变量 maxmin 记录。

  • 遇到更大/更小的值就更新。

  • 时间复杂度 O(n)。


4)功能测试(示例输出)

printf("平均分: %.2f\n", AverageScore(linkH));
printf("最高分: %.2f\n", MaxScore(linkH));
printf("最低分: %.2f\n", MinScore(linkH));

SortByScore(linkH, 1);
printf("按成绩升序排序:\n");
Display(linkH);

SortByScore(linkH, 0);
printf("按成绩降序排序:\n");
Display(linkH);

示例运行结果(假设有 3 个学生:张三 80,李四 95,王五 60):

平均分: 78.33
最高分: 95.00
最低分: 60.00

按成绩升序排序:
ID: 3, 姓名: 王五, 年龄: 21, 性别: M, 成绩: 60.00
ID: 1, 姓名: 张三, 年龄: 20, 性别: M, 成绩: 80.00
ID: 2, 姓名: 李四, 年龄: 22, 性别: F, 成绩: 95.00

按成绩降序排序:
ID: 2, 姓名: 李四, 年龄: 22, 性别: F, 成绩: 95.00
ID: 1, 姓名: 张三, 年龄: 20, 性别: M, 成绩: 80.00
ID: 3, 姓名: 王五, 年龄: 21, 性别: M, 成绩: 60.00

5)本节小结

  • 排序:用冒泡思想在链表上交换数据即可,避免修改指针结构。

  • 平均分/最高分/最低分:单次遍历即可得到,复杂度 O(n)。

  • 适合课设写法:逻辑简单清晰,代码短小,老师容易看懂。

五、主函数与菜单驱动

一个学生管理系统需要有清晰的 用户交互界面,通常用 菜单循环 来实现:

  • while(1)do...while 形成无限循环

  • 打印菜单,提示用户输入操作编号

  • 根据编号调用相应功能函数

  • 输入 0q 时退出

1)主函数基本框架

int main() {
    LH *linkH = CreateLinkH();  // 初始化链表
    int choice;
    int id;
    char name[50];
    int age;
    char gender;
    float score;

    while (1) {
        printf("\n===== 学生信息管理系统 =====\n");
        printf("1. 添加学生\n");
        printf("2. 删除学生\n");
        printf("3. 查找学生\n");
        printf("4. 修改学生信息\n");
        printf("5. 显示所有学生\n");
        printf("6. 成绩排序\n");
        printf("7. 成绩统计\n");
        printf("0. 退出系统\n");
        printf("请输入操作编号: ");
        scanf("%d", &choice);

        switch (choice) {
            case 1: // 添加
                printf("请输入 学号 姓名 年龄 性别(M/F) 成绩:\n");
                scanf("%d %s %d %c %f", &id, name, &age, &gender, &score);
                CreatLink(linkH, CreateNode(CreateStu(id, name, age, gender, score)));
                break;

            case 2: // 删除
                printf("请输入要删除的学号: ");
                scanf("%d", &id);
                DeleteNode(linkH, id);
                break;

            case 3: // 查找
                printf("请输入要查找的学号: ");
                scanf("%d", &id);
                {
                    STU *stu = FindById(linkH, id);
                    if (stu) {
                        printf("找到: ID=%d, 姓名=%s, 年龄=%d, 性别=%c, 成绩=%.2f\n",
                               stu->id, stu->name, stu->age, stu->gender, stu->score);
                    } else {
                        printf("未找到该学生。\n");
                    }
                }
                break;

            case 4: // 修改
                printf("请输入要修改的学号: ");
                scanf("%d", &id);
                printf("请输入新的 姓名 年龄 性别 成绩:\n");
                scanf("%s %d %c %f", name, &age, &gender, &score);
                UpdateStudent(linkH, id, name, age, gender, score);
                break;

            case 5: // 显示
                Display(linkH);
                break;

            case 6: // 排序
                printf("1. 升序  2. 降序\n");
                int order;
                scanf("%d", &order);
                SortByScore(linkH, order == 1 ? 1 : 0);
                Display(linkH);
                break;

            case 7: // 统计
                printf("平均分: %.2f\n", AverageScore(linkH));
                printf("最高分: %.2f\n", MaxScore(linkH));
                printf("最低分: %.2f\n", MinScore(linkH));
                break;

            case 0: // 退出
                printf("系统已退出。\n");
                return 0;

            default:
                printf("无效选项,请重新输入。\n");
        }
    }
}

2)流程图

flowchart TD
    Start[开始] --> Menu[显示菜单]
    Menu --> Input[输入选择]
    Input -->|1| Add[添加学生]
    Input -->|2| Del[删除学生]
    Input -->|3| Find[查找学生]
    Input -->|4| Update[修改学生]
    Input -->|5| Show[显示所有学生]
    Input -->|6| Sort[成绩排序]
    Input -->|7| Stat[成绩统计]
    Input -->|0| End[退出系统]
    Add --> Menu
    Del --> Menu
    Find --> Menu
    Update --> Menu
    Show --> Menu
    Sort --> Menu
    Stat --> Menu

3)运行效果演示

示例操作过程:

===== 学生信息管理系统 =====
1. 添加学生
2. 删除学生
3. 查找学生
4. 修改学生信息
5. 显示所有学生
6. 成绩排序
7. 成绩统计
0. 退出系统
请输入操作编号: 1
请输入 学号 姓名 年龄 性别(M/F) 成绩:
1001 ZhangSan 20 M 88.5

请输入操作编号: 1
请输入 学号 姓名 年龄 性别(M/F) 成绩:
1002 LiSi 21 F 95.0

请输入操作编号: 5
ID: 1001, 姓名: ZhangSan, 年龄: 20, 性别: M, 成绩: 88.50
ID: 1002, 姓名: LiSi, 年龄: 21, 性别: F, 成绩: 95.00

4)本节小结

  • 主函数采用 菜单驱动,用户体验清晰直观。

  • 每个 case 对应一个功能模块,体现了模块化编程思想。

  • while(1) 确保程序持续运行,直到用户选择退出。

六、总结与心得体会

经过本次 学生信息管理系统 的课程设计,我对 C 语言的 结构体、链表、函数模块化编程 有了更深刻的理解,同时也锻炼了 程序设计与调试能力。以下是我的一些收获与感悟:


1)功能实现总结

  • 链表管理:通过自定义 NodeLH(链表头结构),成功实现了学生信息的动态存储与管理。

  • 基本功能:实现了添加、删除、查找、修改、显示等核心操作,保证了系统的实用性。

  • 扩展功能:增加了按成绩排序、统计平均分/最高分/最低分的功能,使系统更贴近实际需求。

  • 模块化设计:每个功能都封装为独立函数,主函数只负责菜单驱动,体现了良好的代码结构。


2)遇到的难点与解决方法

  1. 链表操作易出错

    • 初次实现删除节点功能时,常常出现“野指针”或内存泄漏问题。

    • 解决方法:仔细分析指针前后关系,删除前先保存 next 节点,删除后及时释放内存。

  2. 输入输出问题

    • 使用 scanf 时,字符串和字符输入容易出现换行符残留,导致性别输入错误。

    • 解决方法:学习并尝试 getchar() 清空缓冲区,或者使用 scanf(" %c", &gender) 这种写法避免换行符干扰。

  3. 排序效率

    • 链表的冒泡排序效率不高,但考虑到学生数量有限,复杂度 O(n²) 可以接受。

    • 思考:如果以后要处理大数据量,可以尝试 快排 或者 链表转数组排序 的方式。


3)学习心得

  • 编程思想:通过该项目,我更加熟悉了 数据结构与算法结合应用 的思路。

  • 调试能力:遇到 bug 时,学会用 printf 打印链表状态逐步排查,大大提高了调试效率。

  • 工程习惯:学会了 模块化设计结构体封装,代码更加易读、易维护。


4)未来改进方向

  • 文件存储功能:目前数据仅存在内存中,程序退出后就会丢失。可以拓展 文件读写功能(保存为 txt 或二进制文件),让数据持久化。

  • 用户权限管理:可以增加“管理员/普通用户”模式,适合在学校环境中应用。

  • 界面优化:使用 ncurses 或 C++ 的图形界面库,做一个更加直观的学生管理界面。

  • 数据分析:增加更多统计功能,例如分数段分布(优秀、良好、及格、不及格),并绘制柱状图/饼图。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值