C语言课设:链表与文件读写实现网吧管理系统

发布时间:2026/9/19 14:58:13
C语言课设:链表与文件读写实现网吧管理系统 简介这份C语言课程设计文档围绕“网吧管理系统”展开面向高校计算机专业学生及C语言初学者适合用作课程设计报告撰写和程序开发参考。内容完整覆盖系统设计目标会员信息录入、删除、浏览、积分计算及密码登陆并在设计思路上规划了main主程序与quit、loading、menu、input、save、load、list、window、add、del、search、point等12个子函数模块。文档还整理了设计流程图以及开发过程中遇到的9类典型难点与解决办法例如结构体数组在float类型下的报错处理、文件保存与导入、形参实参对应、死循环调试等能有效帮助读者排查同类问题。资源包含1个doc文档大小约116KB正文含课程设计题目与知识点、设计思路、难点分析、小结及参考书目逻辑层次清晰。目前已有137人学习浏览对需要完成类似C语言管理系统课题的学生有直接的借鉴价值。1. 这门C语言课设不是在做一个“网吧”是在压榨你的指针和文件读写课设清单里出现《C语言课程设计-网吧管理系统.doc》这种名字时很多人的第一反应是去找一个现成源码把标题改一改就交上去。这个方向不太对C语言课程设计真正要练的是把现实问题抽象成数据结构再用指针和文件读写把数据完整走一遍。网吧管理系统算是最典型的练手题因为它既有电脑编号、状态、用户姓名这些可以用C语言结构体表达的字段又需要链表做插入删除、按时间算费用、退出时把数据写回文件。把这几个点做扎实答辩时就能讲清楚“为什么这么设计”只堆菜单和 if else反而容易被一句“你这个数据是怎么存下来的”问住。这篇按“建模 → 模块划分 → 核心实现 → 文件读写 → 验收打磨”的顺序把一套能跑、能查错、能应付答辩的控制台版网吧管理系统讲透。2. 先用C语言结构体和链表把“网吧”建模后面所有函数都围着它转2.1 用结构体把电脑信息聚合成一条记录网吧里一台电脑在业务上至少需要知道编号、当前是否被占用、谁在用、从什么时候开始用、已经欠了多少钱。如果写成五个平行数组函数传参时要带五组参数读写文件时还要挨个对齐下标代码越写越容易串位。C语言结构体的作用就是把这一组内聚字段合成一个整体。typedef struct Computer { int id; /* 电脑编号作为唯一标识 */ int status; /* 0 空闲1 占用 */ char user_name[24]; /* 上机用户空闲时填 - */ time_t start_time; /* 上机开始时间空闲时填 0 */ double fee; /* 本次上机已产生费用单位元 */ } Computer; typedef struct Node { Computer data; struct Node *next; } Node;代码里有两个关键点。第一time_t不是标准整数类型写文件时要转换成long long读取时再转回time_t否则在 64 位 Linux 和 Windows 上打印、截断规则都可能不一致。第二链表结点里的next必须写成struct Node *因为typedef的右边还没有定义完不能直接写成Node *next。这两个细节经常出现在指针相关的答辩追问里。2.2 链表比数组更适合课设里的频繁增删用Node[100]固定数组也能做但课程设计答辩时老师更想看到的是你会用C语言链表。链表删除一个结点只需要改前一个结点的next数组删除中间元素却要memmove或循环前移链表新增结点时按需malloc数组则要预先知道最大电脑台数。网吧管理里“登记新电脑”“淘汰旧电脑”属于明显的频繁增删场景链表在语义上和它更贴合。Node *add_machine(Node *head, Computer c) { Node *node (Node *)malloc(sizeof(Node)); if (node NULL) { perror(malloc); exit(EXIT_FAILURE); } node-data c; node-next NULL; if (head NULL) { return node; } Node *p head; while (p-next ! NULL) { p p-next; } p-next node; return head; }这段代码最需要注意的是malloc的返回值检查。课设里机器数量少内存申请失败概率不高但一旦发生perror能直接告诉你原因比后面毫无征兆地段错误好查得多。尾插法保持了电脑编号的录入顺序代价是 O(n) 的遍历数据量只有几十台时这个开销可以忽略。2.3 文件读写格式先定下来fscanf 和 fprintf 才不会互相打架写系统前最该做的是约定数据文件格式。常见做法是每行一条电脑记录字段之间用|分隔而不是用空格。原因很简单中文姓名、英文姓名里都可能出现空格用fscanf(%s, ...)会被空格切断|在姓名和编号中几乎不会出现适合做列分隔符。业务字段C语言类型写入格式示例电脑编号int%d1使用状态int%d0 或 1用户姓名char[24]%[^]上机开始时间long long%lld1720000000本次费用double%lf/%.2f12.50对应文本行如下1|0|-|0|0.00 2|1|zhangsan|1720000000|12.50空闲电脑的用户姓名字段写-是为了让sscanf的%[^|]能稳定读取。如果把用户姓名字段留空像1|0||0|0.00这种行用%[^|]会读到一个长度为零的字段很多 C语言字符串函数处理起来会多余填坑。提前把这一行格式定死后面的load_data和save_data就只是同一套格式的逆过程。3. 从菜单到计费把C语言链表的增删和time.h都用起来3.1 主循环菜单scanf 读数字之后要处理残留换行控制台菜单最常见的毛病是选择一次之后菜单连跳两次根因是scanf(%d, opt)只把数字读走留在缓冲区里的\n被下一次scanf或getchar当成了输入。网课上管系统的主循环里这个问题几乎必出。int main(void) { Node *list load_data(machine.dat); int opt; while (1) { printf( 网吧管理系统 \n); printf(1. 登记上机\n2. 结账下机\n3. 浏览全部\n0. 退出并保存\n); printf(请选择); if (scanf(%d, opt) ! 1) { while (getchar() ! \n); /* 清掉非数字输入 */ printf(输入不是数字请重试\n); continue; } switch (opt) { case 0: save_data(machine.dat, list); destroy_list(list); return 0; case 1: /* 登记上机 */ break; case 2: /* 结账下机 */ break; case 3: show_all(list); break; default: printf(选项无效\n); break; } } }这里用了while(1)而不是do-while关键区别是退出前要做save_data和destroy_list两件收尾事。如果菜单至少弹一次就行do-while更简洁但课设里退出工序多用while(1)显式写case 0的分支读代码的人能一眼看到保存动作在哪。这也是C语言 while 和 do-while 区别在这个项目里最实际的体现。3.2 上机登记和电脑删除链表指针操作的重点登记上机时先根据用户输入的编号找到对应电脑再把status改成 1把user_name和start_time填上。这里不涉及链表结构变化只是结构体字段赋值。真正扣链表操作的是“添加新电脑”和“删除旧电脑”。Node *delete_machine(Node *head, int id) { Node dummy; dummy.next head; /* 栈上临时头节点 */ Node *p dummy; while (p-next ! NULL p-next-data.id ! id) { p p-next; } if (p-next ! NULL) { Node *tmp p-next; p-next tmp-next; free(tmp); } return dummy.next; }给链表加一个不真正存数据的dummy头节点是处理“删除第一个节点”最干净的办法。如果不加删除头节点时要单独把head往后移一格返回新头加了栈上假头后无论删哪个位置逻辑都统一成“让前一个节点的 next 越过当前节点”。注意free(tmp)之后不能再访问tmp-data否则就是典型的“野指针”。在使用结构体指针时尤其容易犯。3.3 计费不能依赖Sleep等待程序计时有人会在登记上机后写while(1)空转或Sleep模拟计费这既占 CPU 又得不到真实时间。正确做法是把上机开始时刻存成time_t结账下机时用当前时间减开始时间。C语言标准库的time.h已经提供了现成函数。#include time.h double calc_fee(time_t start_time, double price_per_hour) { time_t now time(NULL); double hours difftime(now, start_time) / 3600.0; if (hours 0) { hours 0.0; /* 系统时间被改回去的兜底 */ } return hours * price_per_hour; }difftime返回秒差除以 3600 变成小时数。实际业务里通常是“不足一小时按一小时算”那就不应该用这个返回值直接乘而是先转成分钟数再向上取整int minutes (int)(difftime(now, start_time) / 60.0); int bill_minutes (minutes % 60 0) ? minutes : minutes - minutes % 60 60;把计费逻辑单独抽成函数而不是写在下机分支里好处是测试时能传一个固定的start_time手动验证结果不用真的等一小时。4. C语言文件读写操作启动加载与退出保存这样写才不会丢数据4.1 用 fgets sscanf 按行加载数据坏行不致命很多课设样例用fscanf直接读格式化文件遇到一行数据损坏或文件末尾多余空行时会很脆弱。更可靠的做法是每次用fgets读一行再用sscanf解析这一行这样能精确定位坏行。Node *load_data(const char *path) { FILE *fp fopen(path, r); if (fp NULL) { return NULL; /* 文件不存在交给主程序处理 */ } Node *head NULL, *tail NULL; char line[256]; while (fgets(line, sizeof(line), fp) ! NULL) { if (line[0] \n || line[0] \0) { continue; /* 跳过空行 */ } Node *node (Node *)malloc(sizeof(Node)); long long start 0; int n sscanf(line, %d|%d|%23[^|]|%lld|%lf, node-data.id, node-data.status, node-data.user_name, start, node-data.fee); if (n ! 5) { free(node); /* 解析不完整丢弃这条记录 */ continue; } node-data.start_time (time_t)start; node-next NULL; if (tail NULL) { head tail node; } else { tail-next node; tail node; } } fclose(fp); return head; }这个函数有三个值得在参数讲解里强调的点。第一%23[^|]限制最多读 23 个字符user_name分配了 24 字节最后一位留给\0这是 C语言字符串函数最需要重视的越界防护。第二凡是malloc后sscanf返回值不对必须先把node释放否则文件里的坏行会让程序泄漏一块内存。第三tail指针让链尾插入从 O(n) 降到 O(1)文件行数多时不会越读越慢。4.2 退出保存先写临时文件再 rename避免文件写一半就崩溃最直接的保存方式是fopen(machine.dat, w)直接覆盖原文件。这个写法在正常退出的课设演示里没问题但如果在写文件过程中程序被强制结束原文件已经打开了 “w” 模式内容被清空可能只留下半行记录。更稳的落地路径是写入临时文件确认成功后再替换原文件。int save_data(const char *path, Node *head) { char tmp_path[512]; snprintf(tmp_path, sizeof(tmp_path), %s.tmp, path); FILE *fp fopen(tmp_path, w); if (fp NULL) { perror(tmp_path); return -1; } for (Node *p head; p ! NULL; p p-next) { fprintf(fp, %d|%d|%s|%lld|%.2f\n, p-data.id, p-data.status, p-data.status ? p-data.user_name : -, (long long)p-data.start_time, p-data.fee); } fclose(fp); remove(path); /* 先删旧文件 */ rename(tmp_path, path); /* 再换上新文件 */ return 0; }这里为什么不用rename(tmp_path, path)直接覆盖Windows 上的 rename 遇到目标文件已存在时行为不符合直觉先remove再rename更不容易出问题。代价是两步之间存在极短的窗口如果中间断电会留下machine.dat.tmp下次启动时手动把它改回来即可。比起原文件写一半这个结果要好得多。4.3 文件读写的几个边界空行、编码和 fscanf 返回值实战里最容易翻车的不是读写本身而是边界输入。空行处理已经在加载函数里跳过中文姓名的编码问题则需要注意源码文件的保存编码。Windows 控制台默认用 GBK如果 .c 文件存成 UTF-8printf和fgets读到的中文字节序不一致会显示成乱码。常见做法是把源码保存成 GBKANSI或者统一用不带 BOM 的 UTF-8并在程序开头调用setlocale(LC_ALL, )。另一个高频错误是用while (!feof(fp))控制读文件循环。feof只有“试图往后读但已经到末尾”之后才置位fgets读到最后一个完整行时feof可能还是 0循环会多执行一次。用fgets的返回值判断读到没有是最稳妥的这也是fscanf后必须检查返回值的同一套思路先确定读成功再使用读到的数据。5. 答辩前把这三个点做掉内存泄漏、排序展示、边界输入5.1 用 valgrind 查内存泄漏malloc 和 free 要有配对的意识链表程序最容易在 free 和 return 之间把节点丢掉。答辩前跑一次 valgrind 能省很多尴尬valgrind --leak-checkfull --show-leak-kindsall ./netbar命令跑完看 “definitely lost” 这一项不为 0 就说明有节点只 malloc 没 free。常见的泄漏点是delete_machine里只改指针不free(tmp)以及load_data里解析失败后忘了释放已分配节点。手动排查时可以在destroy_list里用递归或循环 free并在每个返回点之前确认没有分支提前 return。5.2 查看全部电脑前先按编号排序演示效果更直观链表本身是插入顺序不代表编号递增。演示“浏览全部”时最省事的方法是把结点指针放进一个临时数组对数组做冒泡排序再遍历数组打印。这样不破坏原链表代码量也很小。void show_sorted(Node *head, int count) { Node **arr (Node **)malloc(sizeof(Node *) * count); int n 0; for (Node *p head; p ! NULL; p p-next) { arr[n] p; } for (int i 0; i n; i) { for (int j 0; j 1 n - i; j) { if (arr[j]-data.id arr[j 1]-data.id) { Node *t arr[j]; arr[j] arr[j 1]; arr[j 1] t; } } } printf(%-4s %-6s %-12s %-8s\n, 编号, 状态, 用户, 费用); for (int i 0; i n; i) { Computer *c arr[i]-data; printf(%-4d %-6s %-12s %8.2f\n, c-id, c-status ? 占用 : 空闲, c-status ? c-user_name : -, c-fee); } free(arr); }这里的排序交换的是指针不是整个Computer结构体。结构体里有字符串数组和浮点数整体交换要拷贝几十字节指针交换只拷 8 字节速度更快也不会碰链表本身的连接。打印user_name时用三目运算符区分占用和空闲避免对空闲电脑打印出未初始化的内存。5.3 至少跑一遍这五个边界场景再交程序交课设前可以按下面这组场景过一遍程序启动时 data 文件不存在连续登记同一台电脑对空闲电脑执行结账下机手动改坏一行数据后启动程序输入字母代替菜单数字。每个场景都要有明确预期。登记同一台电脑应提示“该机正在使用”空闲下机应提示“无法结账”坏文件行应被跳过而不是让整个程序退出。非法输入最容易在登记编号处暴露问题给scanf加一个整数校验就能堵住大部分崩溃if (scanf(%d, id) ! 1 || id 0) { while (getchar() ! \n); printf(编号必须是正整数\n); continue; }这样重复编号、越界编号和乱输入都会被同一个条件拦在函数外面指针不沾脏数据后续链表操作的安全性也就有了保证。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询