简介:一套面向C语言课程设计期末大作业的校医院管理系统源码与配套报告,适合高校学生课程设计、期末大作业参考。系统分为用户端与医生端,实现医生信息、就诊人信息的注册/注销/查找/更新,以及预约单的生成、查找、删除、修改;医生可管理预约时间段与就诊人数,并能查看排序就诊单。数据采用文本文件存储,读取时借助链表完成增删查改,查找时按时间排序,兼顾可扩展性与使用友好性。资源共79个文件,以42个头文件为主,配合c源文件、txt数据文件、png截图、docx设计报告与exe可执行程序,压缩包仅2.15MB,结构清晰便于部署和二次开发。配套报告和操作说明可辅助理解模块划分、函数封装及排错思路,目前已有1090人学习,适合课程设计参考、期末答辩准备或C语言综合能力训练。
1. 从控制台大作业到数据流分离:为什么校医院管理系统值得拆开看
C语言课程设计里,“校医院管理系统”看起来是普通的管理信息系统,但你真去写会发现它比学生通讯录多了一层麻烦:要分用户端和医生端,预约单要按时间排序,医生还要维护自己的出诊时段,而这些数据全部落在文本文件里。网上很多代码为了跑通,把所有功能塞进一个 main.c,能演示但没法答辩。这份源码的不同在于它把界面、登录、文件读写、业务功能拆成了十几个 .h 和 .c,用户注册、医生排班、预约生成、时间排序、取消预约这些步骤能被清晰地串成一条数据流。对赶课程设计的人来说,这是可以直接运行的完整参考;对想弄懂 C 语言的人,它把链表、指针、文件流、排序和内存回收都实际调动起来了。适合期末突击,也适合把文件操作和链表重新过一遍。
2. 模块化设计:源文件这么多,实际是四条主线
2.1 先分清哪些文件是编译器提供的,哪些是项目自己写的
压缩包里出现的 conio.h、windows.h、stdlib.h、string.h、stdio.h 并不是你的代码。conio.h 是 Windows 控制台环境提供的,stdlib.h 和 string.h 是 C 标准库,真正需要自己维护的逻辑,集中在 main.c、PatientFunction.h、DoctorFunction.h、Add.h、cancel.h、appointmentCheck.h、findD.h、findP.h、look.h、ioRead.h、ioWrite.h、menu.h 这一批文件里。很多同学答辩时被问“你的系统有哪些模块”,开口就把 conio.h、stdio.h 算进去,然后被追问“这个头文件内容是什么”就说不清了。模块化设计的印象,是从这里开始建立的。
2.2 从文件名逆推项目职责划分
看这个项目的文件名,基本能还原出作者的分层思路:
| 功能域 | 主要文件 | 职责 |
|---|---|---|
| 应用入口 | main.c、menu.h、illustrate.h | 菜单循环、程序说明 |
| 登录 / 权限 | userLogin.h、userAdd.h、changPassword.h、findPassword.h | 用户注册、登录、找回密码 |
| 用户端功能 | PatientFunction.h、Add.h、appointmentCheck.h、cancel.h | 生成预约、查看预约、取消预约 |
| 医生端功能 | DoctorFunction.h、DoctorLook.h、findD.h、findMe.h | 管理出诊安排、查看对应预约 |
| 数据持久化 | ioRead.h、ioWrite.h、ioRead_input.h、ioWrite_input.h | 从文件读入链表,把链表写回文件 |
| 工具函数 | inputnumW.h、inputnumR.h、timeJudge.h、timecpy.h、freeList.h | 输入校验、时间判断、内存回收 |
这种命名方式的好处是,每个文件只有一个明确主题。改预约逻辑不需要动登录部分的代码,报告里“分模块分函数”的设计原则也能直接用文件名来印证。比硬写一段“系统分为六大模块”的干巴巴说明有说服力得多。
2.3 main.c 的骨架:一张菜单把两条业务线接起来
入口文件没必要写几屏,我一般会把它做成一个 while 循环,配合菜单函数和带范围校验的输入工具函数,例如:
#include "include.h" // 统一包含各业务头文件 int main() { int choice = 0; while (1) { menu(); // 显示主菜单 choice = inputnumW(); // 读取用户选择,带范围限制 switch (choice) { case 1: userLogin(); break; // 用户端登录 case 2: doctorLogin(); break; // 医生端登录 case 3: userAdd(); break; // 新用户注册 case 0: exit(0); default: break; } } }这里的 inputnumW 专门处理“菜单选择数字”的输入,它内部会检查 scanf 的返回值,过滤掉非数字字符。main 只负责分发,不做业务。启动时先在 userLogin 前调用一次 ioRead,把用户数据.txt、医生数据.txt、预约数据.txt 加载进链表,这个动作放在哪儿、是否每次登录前都重新载入,决定了后面数据一致性的坑有多深。
3. 数据选型:为什么文件里存文本,运行时却要用链表
3.1 直接读文件和“文件 + 链表缓存”的差别
最简单的管理系统是每次查预约都 fopen、fscanf、遍历文件、fclose,代码确实短,但增删改等于重写整个文件,查找还要反复访问磁盘。这个项目的做法,是在程序启动时把 txt 数据全部读入内存,构建成链表,运行时的所有操作都在链表上完成,只有需要保存时才通过 ioWrite 写回文件。对课程设计来说,“文件负责持久化,链表负责增删查改”这句话,值得写进设计报告的原理部分。
| 对比项 | 数组 | 链表 |
|---|---|---|
| 插入删除 | 要移动后续所有元素 | 只需要修改相邻节点的 next |
| 内存空间 | 连续,长度固定 | 分散,按需 malloc |
| 文件加载 | 需要先统计总行数 | 边读边创建节点 |
| 按时间排序 | 可以直接用下标交换 | 交换节点数据即可 |
预约总量通常不会大到影响性能,链表最大的优势其实是“内存动态申请”这件事本身,它让数据和文件的条数解耦。老师如果追问“为什么不用数组”,你答“数组插入删除需要移动元素,链表只需要改指针,而且行数不定时链表不用预先知道长度”,这一分就拿到了。
3.2 结构体定义:三张核心表对应三个链表
从资源里的 Patient.h、Doctor.h、appointmentCheck.h、timeJudge.h 能看出来,至少有三类数据:就诊人、医生、预约单。课程设计里的结构体字段不会太复杂,参考常见写法:
typedef struct Patient { char id[20]; // 病历号 / 学号 char name[20]; // 姓名 char password[20]; // 登录密码 char phone[15]; // 联系电话 int age; // 年龄 struct Patient *next; // 指向下一个节点 } Patient;预约单结构体里建议带上时间字段和状态位。时间排序的关键就在这里:
typedef struct Appointment { char patientId[20]; // 就诊人编号 char doctorId[20]; // 医生编号 char date[11]; // 2025-06-20 char timeSlot[10]; // "08:00-09:00" int status; // 0-未就诊 1-已取消 struct Appointment *next; } Appointment;每个结构体里都有一个指向同类型结构体的指针,这就是链表节点。日期用固定长度的 char 数组存,比用 int 存更方便格式化输出,也方便后面按字典序排序。答辩时手写这几行定义,比空口说“我用了链表”更有说服力。
3.3 文件到链表的加载:ioRead 里的读法有讲究
ioRead.h 和 ioRead_input.h 这一组文件,承担的是“把磁盘文本搬进内存链表”。一种稳妥的写法是用 fgets 按行读取,再 sscanf 拆分字段:
Patient *loadPatients(const char *filename) { FILE *fp = fopen(filename, "r"); if (!fp) { printf("无法打开 %s\n", filename); return NULL; } Patient *head = NULL, *tail = NULL; char line[256]; // txt 每行格式:id name password phone age while (fgets(line, sizeof(line), fp)) { Patient *p = (Patient *)malloc(sizeof(Patient)); sscanf(line, "%s %s %s %s %d", p->id, p->name, p->password, p->phone, &p->age); p->next = NULL; if (tail == NULL) { head = tail = p; // 第一个节点成为头 } else { tail->next = p; // 挂到链表尾部 tail = p; } } fclose(fp); return head; }这段代码里两个地方值得注意。第一,fgets 返回 NULL 时说明文件读完或出错,不能继续循环。第二,sscanf 的格式要与 txt 里的存储格式完全一致,如果原文件用逗号分隔,就要写成sscanf(line, "%[^,],%[^,],%[^,],%[^,],%d", ...)。常见错误是文件里多一个空格或换行符导致 sscanf 匹配错位,然后链表里某个节点字段变成空串。调试这种问题时,把 line 打印出来检查最快。
3.4 别忘了 freeList:链表用得爽,内存回收要负责
malloc 了多少次,最终就应该 free 多少次。freeList.h、freeUser.h、freeFormlistp.h 这几个文件,就是把“释放整个链表”和“释放某个类型的链表”做成公共函数。典型的实现是:
void freeList(Patient *head) { Patient *cur = head; while (cur != NULL) { Patient *tmp = cur; // 先保存当前节点 cur = cur->next; // 再移动,之后才能 free free(tmp); } }这里最容易犯的错是free(cur); cur = cur->next;,当 free 之后再去访问 cur->next 属于未定义行为,可能崩溃也可能值已经被篡改。先保存临时指针,再把游标往后移,最后 free,这才是正确顺序。写报告时补一句“退出时按 user、doctor、appointment、schedule 顺序依次释放”,内存管理这部分就完整了。
4. 核心功能实现:预约单的生成、查找、删除与排序
4.1 预约生成:从 Add 到 appointmentCheck 的调用链
用户登录后能看到的“预约挂号”功能,内部通常由 Add.h 负责收集输入,appointmentCheck.h 负责检查时段冲突,最后插入链表。校医院的约束很具体:同一个患者,同一个日期,同一个时间段只能有一条有效预约。
int addAppointment(Appointment **head, Appointment *newOne) { Appointment *cur = *head; while (cur != NULL) { if (strcmp(cur->patientId, newOne->patientId) == 0 && strcmp(cur->date, newOne->date) == 0 && strcmp(cur->timeSlot, newOne->timeSlot) == 0 && cur->status == 0) { printf("该时段已有预约,请选择其他时间\n"); return 0; } cur = cur->next; } newOne->next = *head; // 头插法,O(1) 完成插入 *head = newOne; return 1; }参数Appointment **head是二级指针,因为要在函数内部修改头指针的指向;如果只传一级指针,调用结束后新插入的节点就丢了。头插法的特点是插入快,但链表的记录顺序和预约时间顺序是反的,所以后面一定要有排序步骤兜底。这里的 strcmp 比较日期和时段,依赖“2025-06-20”这种零填充格式,如果日期写成“2025-6-20”,字典序就乱了。
4.2 医生端出诊安排:预约系统的反例
医生端不直接增加预约单,而是维护自己的可预约时间段,以及每个时间段的就诊人数。出诊安排数据.txt 和出诊安排数量.txt 记录的就是这类信息。对应的结构体大致是:
typedef struct Schedule { char doctorId[20]; char date[11]; char timeSlot[10]; int capacity; // 该时段可容纳人数 int booked; // 当前已预约人数 struct Schedule *next; } Schedule;预约成功的标志,除了插入 Appointment 链表,还把对应 Schedule 的 booked 字段 +1。医生查看排班时直接读这个冗余字段,不用临时遍历预约单去统计。这种“用空间换时间”的做法在课程设计里很容易被忽略,但属于能让老师眼前一亮的细节。取消预约时,记得同时把 booked 减回去,否则排班表会出现“已预约人数比实际预约单多”的数据不一致。
| 状态位 | 含义 | 展示时处理 |
|---|---|---|
| 0 | 预约有效 | 正常显示 |
| 1 | 用户取消 | 打印时标灰或过滤 |
| 2 | 已经就诊 | 医生端归档 |
4.3 按时间排序:为什么链表上冒泡也不丢人
这个项目强调“对表单数据按照时间进行排序”。在链表结构里做排序,选择排序或冒泡排序都是合理的,因为校医院一天的出诊量撑死几百条,O(n^2) 完全能接受。常见做法是只交换节点内部的数据字段,不改节点的 next 指向,这样不会把链表拆断。
void sortByTime(Appointment *head) { if (head == NULL) return; for (Appointment *i = head; i->next != NULL; i = i->next) { for (Appointment *j = i->next; j != NULL; j = j->next) { // 按 date 升序,date 相同则按 timeSlot 升序 if (strcmp(i->date, j->date) > 0 || (strcmp(i->date, j->date) == 0 && strcmp(i->timeSlot, j->timeSlot) > 0)) { swapAppointmentData(i, j); } } } }swapAppointmentData 需要把 id、日期、时段、status 全部交换,不能只换一个字段。因为只是逗号分隔,写出来比较冗长,但逻辑上没有任何难点。日期格式固定为 yyyy-mm-dd 时,strcmp 的字典序就是时间序;timeSlot 用“HH:MM-HH:MM”也是同理。这里不要用 atoi 把日期转数字再比,转完还得处理月和日,反而容易出错。
4.4 用户注册、找回密码、注销:编码问题最容易翻车
userAdd.h 处理注册,findPassword.h 处理找回密码,changPassword.h 处理修改密码。它们大量用到 strcmp、strcpy 字符串操作。最容易翻车的不是字符串函数本身,而是终端编码。假如源文件按 GBK 保存,控制台也用 GBK 显示,中文“内科”和文件里的“内科”才能比较相等。一旦编辑器把一个文件存成 UTF-8,另一个存成 ANSI,外界看起来都是“内科”,strcmp 却认为不相等。排查方法是先检查源文件右下角编码格式,再把所有 .txt 文件统一成同一种编码。这个问题在课程设计报告里作为“环境问题及解决方案”写,很能展示你真正调试过。
5. 多文件编译与 Windows 排错:从 Makefile.win 到“一运行就闪退”
5.1 Dev-C++ 里的多文件组织
压缩包里的校医院管理系统.dev 和 Makefile.win 说明原项目是在 Dev-C++ 这类 MinGW 工具链下搭建的。多文件项目里,.dev 保存了工程文件列表,Makefile.win 则描述了编译规则。如果重新建立工程,必须把 main.c、PatientFunction.c、DoctorFunction.c 以及所有实现 .h 声明的 .c 文件加进项目,只加 main.c 会得到一堆 undefined reference。
OBJS = main.o PatientFunction.o DoctorFunction.o Add.o cancel.o ALL: 校医院管理系统.exe 校医院管理系统.exe: $(OBJS) gcc -o 校医院管理系统.exe $(OBJS) main.o: main.c PatientFunction.h DoctorFunction.h gcc -c main.c -o main.o这个片段展示了 Makefile 最基本的依赖关系。实际用 Dev-C++ 时不需要手写,它在保存 .dev 时会自动重新生成 Makefile.win。但碰到“修改了一个 .c 文件,重新编译还是旧行为”时,可以手动执行一次 Build Clean,把 .o 文件全部删掉再重新编译,问题往往就解决了。
5.2 conio.h 的作用和跨平台差异
conio.h 不是标准 C 头文件,它是 DOS/Windows 时期遗留下来的控制台函数库,提供 getch、clrscr、gotoxy 之类专门操作字符终端的函数。这节课设项目大量用它来刷菜单和定位光标,效果比 printf 换行粗糙一点,但胜在代码简单。在 Windows 上使用 Dev-C++ 或 Visual Studio,conio.h 自带,不用额外配置。Linux 上默认没有这个头文件,所以源码在本地跑不起来十有八九是它引起的。解决办法要么去 Windows 环境演示,要么写一个头文件把 getch 用 termios 实现,但课程设计没有必要折腾跨平台。
5.3 闪退、乱码和路径错误的排查顺序
“双击 exe 后秒退”最常见的原因是没有暂停。main 函数结束相当于进程退出,控制台窗口会被系统回收。原作者一般会在 main 结尾留一句system("pause")或getch(),如果自行裁剪代码时删掉了,就会闪退。其次是文件路径问题。程序里如果写的是相对路径“用户数据.txt”,那它依赖“当前工作目录”。从 IDE 里运行时,工作目录是项目目录;双击 exe 时,工作目录是 exe 所在目录。两者不一致,fopen 就返回 NULL,链表加载为空,登录界面都没看到就退出。解决办法是把 txt 文件复制到 exe 同一目录,或者在代码里使用基于程序位置的绝对路径。
| 现象 | 根因 | 排查/修改方向 |
|---|---|---|
undefined reference touserLogin | .c 文件没有加入项目 | 把实现 userLogin 的 .c 添加进工程 |
| 无法打开用户数据.txt | 当前工作目录不对 | 复制 txt 到 exe 目录,或检查 fopen 路径 |
| 中文显示成乱码 | 源文件、txt 编码不一致 | 全部统一为 ANSI/GBK |
| 菜单输入数字后无限循环 | scanf 读到残留换行符 | 用 inputnumW 这类工具函数清空 stdin |
| 排序后预约记录丢失 | 写回时机不对 | 每次增删完成后立即调用 ioWrite |
调试顺序我习惯是:先确认 exe 目录里有对应 txt,再确认 fopen 的返回值,最后确认 fscanf 每行读的字段数和 txt 行数一致。百分之八十的“运行不了”都死在这几步。
6. 答辩之前,用几个小手段给代码加固
6.1 让所有回调函数都能容忍 NULL 链表
老师演示时很可能先清空预约数据,或者直接选“取消预约”而链表为空。你的程序不能在这个节点崩溃。每个函数入口做一次空判断:
void cancelAppointment(Appointment **head, const char *patientId) { if (head == NULL || *head == NULL) { printf("当前没有可取消的预约\n"); return; } // 继续删除逻辑…… }6.2 在关键路径上打印排序前后的链表
把预约单按时间排序这个功能,答辩时最好直接可视化。写一个简短的 print 函数,排序前后各调一次,讲清楚“排序前是乱的,排序后按日期时段排好”,比 PPT 里的示意图更真实。
void printAppointments(Appointment *head) { if (!head) { printf("无预约数据\n"); return; } for (Appointment *p = head; p != NULL; p = p->next) { printf("%s %s %s\n", p->date, p->timeSlot, p->patientId); } }在演示程序里加一个隐藏调试选项,输入管理员密码后进入调试模式,执行 print 和 sortByTime 两步操作。这个过程能直观证明数据流是通的。
6.3 用重复预约 + 重复取消验证内存是否泄漏
内存泄漏在课程设计里很难用肉眼发现,但答辩时被问“你记忆体处理合理吗”不能没准备。Windows 环境下最简单的验证方式:在菜单里循环执行 50 次“创建预约 + 取消预约”,同时打开任务管理器观察内存占用。如果“校医院管理系统.exe”的内存持续稳定在同一个值附近,说明 freeList、cancel 里的释放函数基本有效。如果内存稳步上涨,就需要回到 free 相关代码检查是否有节点被遗漏。还有一个更弱的验证:在 main 返回前调用一次所有 freeList 函数,然后重复 free 同一个头指针,如果程序立刻崩溃,说明该节点状态有问题;但反过来,不能证明没有泄漏,只能证明释放路径被执行过。找泄漏最稳妥的还是 Valgrind,Windows 下可以用 Dr. Memory 或 Visual Studio 的 CRT 调试堆,跑一遍能直接看到 leaked blocks 的数量。最后一步是把这些检测结果截图放进报告的“测试与分析”章节,这一条很能加分。
本文还有配套的精品资源,点击获取