首页 / 资讯中心 / 文章详情

C语言四层十字链表教务信息管理系统课程设计

C语言四层十字链表教务信息管理系统课程设计 ★ FEATURED ARTICLE
简介这份教务信息管理系统 C 语言课程设计资料面向计算机相关专业正在完成 C 语言课程设计、需要教务管理类项目参考的学生。内容模拟辅导员处理年级成绩表格采用图形界面中文菜单和模块化编译借助十字交叉链表组织班级、学生、课程与成绩四层信息并覆盖新建、保存、打开文件以及插入、删除、排序、查找、统计均分、筛选不及格信息、模拟打印报表等典型操作。资源包共 1 个 doc 文件约 361KB是一份较完整的课程设计实验报告内含源代码、程序流程图和详细说明便于理解链表创建规则、功能模块划分与文件保存策略。报告从程序简介、数据结构图解与详解展开可帮助读者对照梳理实现思路、完善功能设计。目前已有 1120 人学习适合需要参考报告结构或查漏补缺的学生使用。1. 一份辅导员手里的成绩表被写成了四层十字链表第一次翻到这份 C 语言课程设计的时候我以为它只是个把学生信息存进文件的练习看到结构体定义才发现它的做法比想象中更认真整个教务信息管理系统用班级、学生、课程、成绩四层节点串成十字交叉链表增删改查、排序、统计均分、模拟打印、断点式模糊查找全都实现了而且全部跑在 80×25 的字符界面里。它适合两类人正在做 C 语言课程设计、需要一个能演示能交差的完整骨架的同学以及想回头看看“没有 STL、没有数据库、没有框架”的年代一个教务管理系统是怎么靠裸指针和定长字符数组把数据管起来的开发者。下面按工程结构、数据结构、交互主循环、持久化与统计、踩坑排查、二次开发六段把它拆开。2. 工程结构与编译方式九个 .c 文件怎么在 TC 里拼出一个 .prj2.1 先说清楚它所谓的“图形界面”是什么项目正文里反复强调“全部采用图形界面用中文字输出菜单”。如果按今天的理解去找graphics.h、initgraph、outtextxy会走进死胡同——Turbo C 的 BGI 字符输出函数只认单字节 ASCII直接outtextxy画中文得到的是一堆乱码或者空白。那个年代中文显示的通行做法是让程序跑在 DOS 中文环境如 UCDOS提供的文本模式下操作系统层面把双字节汉字写进显存程序只需要用gotoxy把光标挪到指定坐标再printf汉字就出来了。所以这份课设的“图形”本质是文本模式下的伪图形界面textcolor、textbackground、clrscr、gotoxy加上 ASCII 字符画出来的边框通过颜色反差和光标定位做出菜单的层次感。动态登入界面同理一个循环里逐字printf再delay就能做出“标题一个字一个字蹦出来”的效果不需要任何绘图 API。/* 文本模式下画一个菜单盒坐标单位是字符而不是像素 */ void draw_box(int x1, int y1, int x2, int y2, int fg, int bg) { int i; textcolor(fg); textbackground(bg); gotoxy(x1, y1); putch(); /* 老机器上制表符会因代码页变乱码 */ for (i x1 1; i x2; i) /* 用 - | 画框反而最保险 */ putch(-); putch(); for (i y1 1; i y2; i) { gotoxy(x1, i); putch(|); gotoxy(x2, i); putch(|); } gotoxy(x1, y2); putch(); for (i x1 1; i x2; i) putch(-); putch(); }这段代码里x1/y1/x2/y2是字符行列号不是像素坐标textbackground(bg)只影响后续输出字符的底色不会填充整块矩形所以真正的“填充菜单背景”还得靠循环输出空格。这也是这类程序看起来总有点“格子感”的原因。理解这一点很关键后面所有关于按键、屏幕滚动、分页预览的实现都建立在字符坐标之上而不是像素坐标。2.2 九个源文件的职责边界正文给的编译清单是main2.c、tongji.c、da_yin.c、in_out.c、files.c、edit.c、help.c、search.c、qita.c九个文件从命名就能反推出作者的分层思路文件职责关键入口main2.c主函数、主菜单、按键分发、程序总开关main()、main_menu()files.c新建 / 打开 / 保存 / 退出四件事file_new()、file_save()in_out.c菜单绘制、链表信息回显、格式化输出show_class()、print_student()edit.c插入 / 删除 / 修改 / 排序edit_insert()、edit_sort()search.c按班级号、学号、姓名片段、课程号查找find_student_frag()tongji.c班级均分、学生加权排名、不及格名单stat_class_avg()da_yin.c模拟打印落盘到student.txtfake_print()help.c帮助界面show_help()qita.c字符串裁剪、长度校验、数字与字符串互转trim()、is_digit_str()这种拆法在今天是常识但在两千行就编不动的年代是被逼出来的单个.c文件越写越大TC 的编译时间会指数级上升更致命的是 Small 模式下数据段只有 64KB全局数组、静态缓冲区、字符串常量全挤在里面稍微多几个char buf[1024]就溢出。作者在体会里写“写到两千行 TC2.0 编不动换 Win-TC 到四千行也不行换 TC3.0 到五千行还不行”本质上就是这个 64KB 数据段在卡脖子只是当时没人告诉他。拆成九个文件之后每个模块只暴露少量全局符号编译单元变小链接阶段再合并代码段同时在 IDE 里把内存模式从 Small 改成 Large指针变成 far 指针数据段可以跨 64KB问题基本就压住了。代价是 far 指针访问比 near 指针慢一个链表遍历的效率会掉一截但对几百条学生记录来说完全无感。2.3 .prj 工程文件的建立与编译链路在 TC 里多文件项目的建立顺序是先把九个.c全部写好放在同一目录在 IDE 里按Project → Open project输入一个文件名后缀必须写.prj比如jiaowu.prj。这个文件本质是一个纯文本清单一行一个源文件名main2.c files.c in_out.c edit.c search.c tongji.c da_yin.c help.c qita.c然后在Options → Compiler → Code generation里确认内存模式Large在Options → Directories里把 Include 和 Lib 目录指向 TC 安装路径。按CtrlF9执行时TC 的 make 逻辑会逐个检查.c和.obj的时间戳只重新编译改过的文件再统一链接成jiaowu.exe。这个增量编译机制在改一个模块、试一次效果的循环里非常省时间也是模块化最直接的好处。提示.prj里绝对不要写绝对路径换一台机器路径一变就找不到文件。文件名大小写在 DOS 下不敏感但.c后缀不能省。真正容易翻车的是头文件共享。九个文件要共用那四个struct定义和一堆#define通常的做法是抽一个jiaoowu.h里面只放结构体、常量和函数声明然后每个.c都#include jiaowu.h。原清单里没有列出.h文件但只要是这个规模的工程共享头文件几乎是必然选择如果所有文件都重复写一遍struct定义只要有一处漏改字段链接期就会出现“结构体大小不一致”这种极难排查的问题——编译能过运行时数据错位。3. 四层十字交叉链表从 struct 定义到建链顺序3.1 四个结构体的字段设计先看班级链表它是最外层也是整棵数据结构的入口typedef struct classes { char class_no[5]; /* 班号如 0510 */ struct classes *next1; /* 下一个班级 */ struct students *sthead; /* 该班级的学生链头指针 */ } CLASS;class_no定了 5 字节意味着班号最多 4 个字符加一个结束符。这个长度不是随便定的班号在城市、年级、序号三段式编码下刚好 4 位定死之后大量地方可以直接strncpy而不用strdup在 64KB 数据段里能省一点是一点读文件时也能根据这个长度处理。学生节点是整条链的核心字段最多struct students { char fall1[2]; /* 从属关系fall1[0] 所属班级编号 */ char student_no[13]; /* 学号 */ char name[17]; /* 姓名 */ char sex[2]; /* 性别0 男1 女 */ char age[3]; /* 年龄 */ char phone[12]; /* 寝室电话 */ char com_phone[12]; /* 手机号 */ char jia_quan[7]; /* 加权分 */ char all_credit[4]; /* 已得总学分 */ struct students *next2; struct subjects *suhead;/* 该学生的课程链头指针 */ };课程节点和成绩节点类似课程里有course_no[11]、course_name[17]、credit[4]和一个指向成绩链的chead成绩节点里有考试次数、考试成绩、平时成绩、综合成绩、考试时间以及fall3[4]的从属编码。这里有一个贯穿全项目的设计取舍所有字段都是定长 char 数组连浮点数也存成字符串。好处很直接——写文件时每个字段宽度固定fgets读一行、按位置切分即可不需要处理变长字符串坏处也很明显jia_quan[7]最多存 6 个字符加权分算出来 85.678 就会被截断成 “85.678” 刚好六位再多一位就溢出。作者在正文里专门提醒“在保存的文件中修改数据要注意数据的长度不要被改变”说的就是这件事故。3.2 fall1 / fall2 / fall3用字符数组记录从属关系这是整份课设里最有年代感、也最容易出问题的设计。四个链表是嵌套关系班级 → 学生 → 课程 → 成绩父层通过sthead、suhead、chead三个头指针指向子层方向是自上而下的。但反过来从一条成绩记录要找回它属于哪个学生、哪门课光靠指针是做不到的所以每个节点额外背了一份“从属关系”编码。成绩节点的fall3[4]fall3[0]是班级编号fall3[1]是学生编号fall3[2]是课程编号课程节点的fall2[3]fall2[0]班级、fall2[1]学生学生节点的fall1[2]只存班级。插入、查找、删除时代码就是靠这串编码在某一层里定位父节点然后再往下走。/* 在指定班级、指定学生下找一门课程返回课程节点指针 */ SUBJECT *find_subject(CLASS *cls, STUDENT *stu, const char *course_no) { SUBJECT *s; if (cls NULL || stu NULL || course_no NULL) return NULL; for (s stu-suhead; s ! NULL; s s-next3) { /* 先校验从属编码是否对得上避免串班数据引发误匹配 */ if (s-fall2[0] ! cls-class_no[2]) /* 只取班号后一位做编码 */ continue; if (s-fall2[1] ! stu-fall1[1]) continue; if (strcmp(s-course_no, course_no) 0) return s; } return NULL; }这段代码暴露了这套编码的致命弱点fall数组每个元素只存一个字符本质上是用0 序号或者取班号某一位来做编号。一旦班级数超过 10编号就撞车fall2[0]无法区分第 1 班和第 11 班查找函数会把别的班的数据当成自己的返回。这个坑在后面第 5 章还会展开它是这套数据结构里最应该被改造的地方。3.3 建链、插入与“00”退出约定建链的流程在正文里写得很细先建班级链每个班级挂学生链每个学生挂课程链每门课程挂成绩链。层级之间的退出条件是统一的——在任意一个输入层输入“00”就结束当前层返回到上一层。这个约定贯穿新建流程也是操作上最容易记住的一条规则。实际实现上通常写成一个递归或者层层嵌套的建链函数每读一条记录就malloc一个新节点挂到链尾。用尾插而不是头插是为了让链表顺序和用户输入顺序一致后面按学号排序时心理预期不会乱。/* 新建一个班级并挂到链表尾部返回新的尾节点 */ CLASS *append_class(CLASS **phead, const char *class_no) { CLASS *node, *tail; node (CLASS *)malloc(sizeof(CLASS)); if (node NULL) return NULL; /* 内存不足必须判TC 下很容易 */ memset(node, 0, sizeof(CLASS)); /* 定长数组中未用字节清零写文件才干净 */ strncpy(node-class_no, class_no, sizeof(node-class_no) - 1); node-next1 NULL; node-sthead NULL; if (*phead NULL) { *phead node; return node; } for (tail *phead; tail-next1 ! NULL; tail tail-next1) ; tail-next1 node; return node; }memset这一行别省。定长字符数组如果在malloc之后不初始化尾部会残留上一块内存的垃圾字节strncpy只填有效字符不清尾巴写进文件后就是一堆乱码更麻烦的是如果垃圾字节里恰好没有\0后续所有strlen、strcmp都会越界读。这类问题在老代码里极常见表现是“有时候正常、有时候数据全乱”也就是俗称的玄学 bug——原因就是未初始化内存跟着上一次的分配内容在变。插入操作是同一个逻辑的复刻版只是多了“先定位父节点”这一步插班级直接加尾插学生要先输入班号找到班级插课程要先班号后学号插成绩要先班号、学号、课程号。每一层找不到就打印“没有找到”并回退这条错误路径必须写全否则一次输错就掉进空指针。4. 菜单主循环与功能实现从一次按键到一次完整的增删改查4.1 主菜单状态机与 Esc 回退整个程序是一个大状态机主菜单有文件、编辑、打印、统计、查找、帮助、退出七个入口每个入口下面还有子菜单子菜单下面还可能再有一层比如编辑下的插入还要再分班级、学生、课程、成绩。正文描述的操作方式是上下键选主菜单、左右键切到别的菜单的子项、回车进入、Esc 返回上层。这套交互如果用状态变量硬编码会写成一团乱麻。常见的写法是维护一个level变量加一组cur[]数组每一层记住自己当前高亮的是第几项#define LV_MAIN 0 #define LV_SUB 1 int level LV_MAIN; int cur[2] {0, 0}; /* cur[LV_MAIN]、cur[LV_SUB] 各记各的 */ void menu_loop(void) { int key; for (;;) { if (level LV_MAIN) draw_main_menu(cur[LV_MAIN]); /* 重画并高亮当前项 */ else draw_sub_menu(cur[LV_MAIN], cur[LV_SUB]); key read_key(); switch (key) { case KEY_UP: cur[level] (cur[level] N - 1) % N; break; case KEY_DOWN: cur[level] (cur[level] 1) % N; break; case KEY_LEFT: if (level LV_SUB) cur[LV_MAIN] (cur[LV_MAIN] N - 1) % N; break; case KEY_RIGHT: if (level LV_SUB) cur[LV_MAIN] (cur[LV_MAIN] 1) % N; break; case KEY_ENTER: if (level LV_MAIN) { level LV_SUB; } else { run_sub_item(cur[0], cur[1]); } break; case KEY_ESC: if (level LV_SUB) level LV_MAIN; /* 回上层 */ else if (confirm_exit()) return; /* 主菜单按 Esc 才真退 */ break; } } }(cur N - 1) % N这个写法是为了让在一项时按上键自动绕到菜单最后一项用户体验上顺滑很多。cur用数组按层索引好处是回退到主菜单时上次选的那一项还在不用重新从头选。左右键只在子菜单层有效逻辑上等价于“跳到相邻主菜单的子菜单”这就是正文说的“按左右键就可以进入其它主菜单选项的子选项”。4.2 上下左右键怎么读扫描码与 BackspaceTurbo C 下读按键的坑几乎每个初学者都会踩一次。getch()遇到方向键、功能键这类扩展键时第一次返回0第二次返回的是扫描码而不是字符本身。#define KEY_ENTER 13 #define KEY_ESC 27 #define KEY_BACK 8 /* 读一个逻辑键值扩展键统一加 128避免和普通 ASCII 冲突 */ int read_key(void) { int c getch(); if (c 0) { /* 扩展键再读一次拿扫描码 */ c getch() 128; /* 上72 下80 左75 右77加偏移后为 200/208/203/205 */ } return c; }加 128 的意义在于把“普通字符键”和“扩展键”放进同一个整数空间里判断字符键永远小于 128扩展键永远大于等于 128switch里不会出现case 72到底是字符H还是上方向键的歧义。注意回车是 13、Esc 是 27、退格是 8这三个不是扩展键走的是第一条分支。退格的处理也要单独提一句。正文写的是“输入有误可以按 Back space 删除错误信息”但要真的删对得配合一条输入缓冲区收到退格时把缓冲区末位清掉然后gotoxy回退一格、输出一个空格、再回退一格屏幕上的字符才会消失。只gotoxy不输出空格光标位置变了但字符还留在屏幕上这就是很多人都遇到过的“退格之后残影不消失”。4.3 排序、统计与模拟打印的实现排序功能限定在班级内部按学号或加权分排。链表排序有两条路换节点指针或者换节点里的数据。换指针要处理父节点的next指回稍不注意就把链断掉换数据则简单粗暴得多尤其这套结构体里每个学生节点还挂着课程链头指针必须一起搬走。/* 交换两个学生节点的数据域但要保住各自的链表指针不丢 */ static void swap_student(STUDENT *a, STUDENT *b) { STUDENT t; STUDENT *na a-next2, *nb b-next2; SUBJECT *sa a-suhead, *sb b-suhead; t *a; *a *b; *b t; /* 整体搬数据 */ a-next2 na; a-suhead sa; /* 链接关系写回原位 */ b-next2 nb; b-suhead sb; } /* 班级内按加权分降序冒泡 */ void sort_by_weight(CLASS *cls) { STUDENT *p; int swapped; if (cls NULL || cls-sthead NULL) return; do { swapped 0; for (p cls-sthead; p-next2 ! NULL; p p-next2) { if (atof(p-jia_quan) atof(p-next2-jia_quan)) { swap_student(p, p-next2); swapped 1; } } } while (swapped); }atof把存成字符串的加权分转成浮点比较这一点呼应了前面“全部存字符串”的设计。冒泡在几百条数据下完全够用swapped标志让最好情况退化成一趟遍历。统计模块有三块班级均分、学生均分排名、不及格学生名单。班级均分要对每门课求平均做法是按课程号分组累加综合成绩学生均分本质是加权分排序直接复用sort_by_weight再顺序打印不及格名单则遍历成绩链把综合成绩小于 60 的挂到结果里。/* 统计某班各门课程的平均综合成绩 */ void stat_class_avg(CLASS *cls) { SUBJECT *s; SCORE *sc; int sum, cnt; if (cls NULL) { printf(请先新建或打开文件\n); return; } printf(班级 %s 各科均分\n, cls-class_no); for (s cls-sthead ? cls-sthead-suhead : NULL; s ! NULL; s s-next3) { sum 0; cnt 0; for (sc s-chead; sc ! NULL; sc sc-next4) { sum atoi(sc-coll_score); /* 综合成绩字符串转整数 */ cnt; } if (cnt 0) printf( %-16s 人数 %2d 均分 %.2f\n, s-course_name, cnt, (double)sum / cnt); } }最后是模拟打印。正文交代得很清楚打印时信息先输出到屏幕预览同时写一份到默认文件student.txt用来模拟真实打印机的排版。实现上就是两个FILE *一个指向stdout一个fopen(student.txt, w)每一行同时往两处写屏幕预览部分还要支持上下左右移动让用户看到超过一屏的内容。屏幕移动的做法通常是记住一个“视口起始行”变量上下键改这个变量左右键改一个横向偏移然后整体重画一遍——在字符模式下没有真正的“滚动视口”只能重绘。5. 常见问题排查这套课设最容易翻车的五个地方5.1 编译与运行期的三个坑坑一代码写到两三千行编译直接卡死或者报内存不足。现象是 TC 编译到一半进度条不动或者链接时报Segment exceeds 64K。原因不是代码有问题而是 Small 模式下的数据段上限 64KB 被全局数组和字符串占满了。解决办法是把程序拆成多个.c用.prj管理同时在Options → Compiler → Code generation里把内存模式切到 Large拆完之后每个编译单元的静态数据大幅下降问题基本消失。拆的时候要顺手把头文件抽出来否则结构体定义写九遍后面改一个字段要改九处必错。坑二菜单中文变成方框或者只剩半个字。现象是程序在自己的机器上好好的拷到别人机器上菜单全是乱码。原因是这套程序的中文输出依赖 DOS 中文环境代码本身并不带字库对端机器没有加载中文平台或者代码页不是 GBK汉字就退化成扩展 ASCII。解决办法是把程序放进统一的中文环境运行并且在源文件里避免使用±、§这类特殊符号画框改成纯 ASCII 的 - |能显著降低跨机器乱码的概率。坑三方向键完全没反应只有回车能用。现象是按上下键菜单不动按回车却正常进入。原因是只调了一次getch()拿到扩展键返回的 0 就当成普通字符处理了扫描码留在缓冲区里没人读。解决办法是封装成read_key()判断第一次返回 0 就再getch()一次拿扫描码并加上 128 的偏移。加这个偏移是为了让扩展键和普通字符键在同一个switch里不冲突这是一条血泪经验不加早晚会撞上某个字符。5.2 数据与文件层的两个坑坑四班级一多查出来的数据就串班。现象是建到第十来个班之后按班号查学生结果里混进了别的班的人。原因是fall1、fall2、fall3这几个从属编码每个元素只存一个字符本质是用0 序号或者班号某一位来表示编号超过 10 就绕回去了两个班共用同一套编码。解决办法有两条路短期把编号改成两位定长字符串fall2[0]扩成fall2[2]存 “01”~“99”长期干脆去掉这套冗余编码改成节点里直接存一个全局唯一的 id父层查询时按 id 比较。短期改造改动面小但要把所有读写fall的地方一起改漏一处就继续串。坑五手动编辑保存出来的 txt程序打开就乱。现象是在记事本里改了学生的姓名长度从 4 个字变成 6 个字程序打开后所有字段错位。原因是这四个分表文件的所有字段都是定长的读取逻辑默认按固定宽度切长度一变整个偏移全乱。解决办法是把读文件改成按行读、按分隔符切把fgets 定长偏移换成分隔符解析成本是要重写一遍读写函数。如果只是临时改数据那就严格遵守“不改变长度”的约束多出来的位置用空格补齐这条约束也是作者在正文里专门强调过的。6. 二次开发把这套课设改成能真用的版本这份代码的骨架是好的问题集中在三个历史包袱从属关系用单字符编码、所有字段定长、文件按固定偏移读写。真要二次开发按下面的顺序改投入产出比最高。改造项现状建议方案改动范围从属关系fall1/fall2/fall3单字符每层加int parent_id父节点持唯一 id四个 struct 全部查找函数数据存储四个定长 txt 分表改成一个 CSV 或 SQLite 表按主键关联files.c 整体重写字段长度name[17]、jia_quan[7]改成指针或宽数组按需分配结构体 所有strncpy界面文本模式字符框保留字符界面抽象出draw_menu()一层in_out.c 收敛接口第一步先稳住从属关系因为它牵连面最广、出错最难查。做法很简单在每个 struct 里加一个自增的int id全局维护一个next_id插入时分配父节点里存的不是字符编码而是子节点的 id 列表起点/* 给所有节点打上唯一 id替代单字符从属编码 */ static int g_next_id 1; int assign_id(void) { return g_next_id; } /* 按 id 在学生链里找节点比字符串匹配稳得多 */ STUDENT *find_student_by_id(CLASS *cls, int id) { STUDENT *p; if (cls NULL) return NULL; for (p cls-sthead; p ! NULL; p p-next2) if (p-id id) return p; return NULL; }第二步把文件层换掉。四个分表改成一张宽表一行一个成绩记录字段写成班级,学号,姓名,课程号,课程名,学分,平时,考试,综合,学期读取时一次fgets切一次分隔符字段长度不再受限改数据也不用担心错位。这样一改原来files.c里那套“一个文件名派生出四个文件”的逻辑全部作废代码反而短了一半。第三步才轮到界面。字符界面不用急着换掉它在这个场景下其实够用且没有依赖。可以做的是把draw_box、draw_menu、highlight收进一个界面层让业务函数只负责拿数据、调界面层渲染这样将来想换图形库只需要替换界面层那几个函数。我自己踩过最深的坑是有一年改一个类似的链表项目族编码没改就先动了文件层结果旧数据读进来一半是新格式一半是旧格式查错查了整整一个下午才发现是编码没对齐。从那以后我每次动这种“结构 持久化”双改的项目都强制先把从属关系改成 id、跑一遍全量读写校验确认数据能原样进出再去动文件格式。希望这套拆法能帮到你少走一遍我当年绕的那条弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站