news 2026/9/19 22:38:34

C语言课程设计:用链表与文件实现网吧管理系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言课程设计:用链表与文件实现网吧管理系统

简介:这是一份面向C语言初学者的课程设计文档,完整记录了“网吧管理系统”从题目分析、功能设计到编码调试的全过程。内容覆盖会员信息录入、删除、浏览、积分计算及密码登录等核心模块,并系统梳理了结构体数组、函数调用、指针、文件读写等关键知识点,适合高校计算机专业学生作为课程设计参考或毕业实训模板。资源为1个doc文档,整体大小116KB,共137人浏览学习。文档除给出主程序与12个子模块的划分思路和设计流程图外,还专门整理了九项编程难点及对应解决办法,如float型数据定义报错、型参与实参不匹配、导入数据出错、死循环等调试经验,能帮助读者避开常见坑点。附录部分附有课程设计报告目录与参考结构,便于直接套用或扩展,是一份兼具示例代码思路与排错指南的实用参考资料。

1. 网吧管理系统:C语言课程设计里最锻炼链表和文件的那道题

网吧前台要管的事,说白了就四件:会员谁是谁、哪台机器被谁占着、上机下机怎么计费、闭店时的营业报表。把这些落进 C 语言课程设计,本质是把指针、结构体、链表、文件读写串成一条完整业务链——比图书管理多一层实时占机状态,比学生成绩管理多一组时间戳计费,这正是它适合做课程设计的原因。我的建议是,动手前先在 VS Code 里把 C/C++ 扩展和调试器配好,然后别急着写菜单,先把“一条会员数据在内存里长什么样、落地到文件里长什么样”定下来,后面所有模块都是在给它补输入输出。这套思路能让你在答辩时回答“为什么用链表不用数组”这类问题时不发怵。

2. 结构体加链表:网吧管理系统的数据内核

2.1 会员链表与机位数组:先定结构体再写逻辑

先定实体。网吧管理系统里最少有两类数据,一类是会员账户,一类是机位。会员的特点是按手机号登录、人数动态增减,天然适合链表;机位数量固定(比如 80 台),要随机访问“第几号机现在什么状态”,数组更直接。常见做法是,会员用带头节点或不带头节点的单链表都行,机位用定长数组。结构体定义如下:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #define MAX_NAME 32 #define MAX_PHONE 16 #define SEAT_COUNT 80 // 会员账户节点,每个节点代表一位会员 typedef struct User { char name[MAX_NAME]; // 姓名,用于显示 char phone[MAX_PHONE]; // 手机号,作为唯一登录凭证 double balance; // 余额,单位元 struct User *next; // 指向下一个会员节点 } User; // 机位:用数组保存,下标即机号 typedef struct { int status; // 0 空闲,1 占用 char cur_phone[MAX_PHONE]; // 占用者手机号,空闲时为空串 time_t start_ts; // 上机时间戳,下机计费用 } Seat;

这里有一个容易在答辩时被追问的点:为什么机位里存cur_phone而不是cur_name。手机号是会员表的主键,姓名可能重名,按下机机号去会员表里找账户时,按手机号匹配不会串位;而且结账时打印小票需要的是“从账户扣钱”,拿 phone 反查节点,一处查找函数两种用途。以后如果上机放号要记会员等级,也只需要在会员表加字段,不需要动机位表。

int first_free_seat(const Seat *seats, int n) { for (int i = 0; i < n; i++) { if (seats[i].status == 0) return i; } return -1; // 全部占用 }

first_free_seat返回的是数组下标,主流程里给用户显示时要 +1 才是真实机号。这种“下标即机号”的约定在代码里要保持一致,我一般会在常量注释里写明“0号下标对应1号机”,否则后面报表统计容易差一。

2.2 链表增删查改:头插、查找、删除的常见写法

链表操作是这个课程设计里分最高的部分,因为要现场演示给老师看,写清楚不容易。我建议按下面这套函数来组织,每个函数只做一件事:

User *create_user(const char *name, const char *phone, double bal) { User *u = (User *)malloc(sizeof(User)); if (!u) return NULL; // malloc 失败要能看出来 strncpy(u->name, name, MAX_NAME - 1); u->name[MAX_NAME - 1] = '\0'; strncpy(u->phone, phone, MAX_PHONE - 1); u->phone[MAX_PHONE - 1] = '\0'; u->balance = bal; u->next = NULL; return u; } // 头插法:新节点放在表头,O(1) User *insert_user(User *head, User *u) { if (!u) return head; u->next = head; return u; } // 按手机号查找,prev 带回前驱节点,供删除用 User *find_user(User *head, const char *phone, User **prev) { User *p = head, *last = NULL; while (p) { if (strcmp(p->phone, phone) == 0) { if (prev) *prev = last; return p; } last = p; p = p->next; } return NULL; } // 按手机号删除会员,删除成功返回新头节点 User *remove_user(User *head, const char *phone) { User *prev = NULL; User *u = find_user(head, phone, &prev); if (!u) return head; if (!prev) head = u->next; // 删的是头节点 else prev->next = u->next; // 前驱直接跨过当前节点 free(u); return head; }

写完这段要注意两点。一是strncpy之后要手动补'\0',这是 C 语言字符串函数最常见的坑,strncpy只保证拷贝 n 个字符,源串超长时不补结束符,后面的strcmpstrlen就可能读过头。二是find_userprev参数用的是指针的指针,我第一次写的时候只用返回值,删除头节点时总是要单独分支,写成User **prev之后,删除逻辑统一成“前驱节点的 next 指到当前节点的 next”,答辩讲到指针的指针时反而是一个加分点。

2.3 文件读写持久化:fprintf / fscanf 一条记录一行

课程设计要求的“管理系统”通常要能重启后保留数据,这里用文本文件存是最直白的方案。每行一条会员记录,字段之间用空格分隔,文件格式约定为:

字段格式示例说明
name字符串张三不含空格的姓名
phone字符串13800138000登录凭证
balance%lf25.50保留两位小数
int save_users(const User *head, const char *path) { FILE *fp = fopen(path, "w"); if (!fp) return -1; for (const User *p = head; p; p = p->next) { fprintf(fp, "%s %s %.2f\n", p->name, p->phone, p->balance); } fclose(fp); return 0; } User *load_users(const char *path) { FILE *fp = fopen(path, "r"); if (!fp) return NULL; User *head = NULL; char name[MAX_NAME], phone[MAX_PHONE]; double bal; while (fscanf(fp, "%31s %15s %lf", name, phone, &bal) == 3) { User *u = create_user(name, phone, bal); head = insert_user(head, u); } fclose(fp); return head; }

fscanf%31s这类宽度限制是必须写的,不然超长字符串会把缓冲区撑爆,这是文件读写操作代码里体现实力的细节。读回时用头插法,链表顺序会和保存时相反,如果之后做报表要按注册顺序显示,可以在load_users里改成尾插,或者保存时直接按链表顺序倒着写,让读回来正好正序。计费涉及浮点,%.2f输出、%lf读回,两次转换之间注意别在中间计算里积累太多浮点误差——课程设计量级这点误差无所谓,但答辩老师常会问“浮点余额为什么不用整数分存储”,能答出“用分做整数运算更精确,界面再除以100”是加分项,实现时把balance换成long balance_fen会更稳。

注意:users.dat路径是相对的,演示前要确认终端工作目录和代码目录一致,否则程序会静默新建一个空文件,看起来像数据丢了。

3. 上机与下机:计费流程和状态切换

3.1 时间戳记录上机时刻:time() 与 ctime()

计费的前提是拿到可靠的上下机时间。C 标准库的time()返回自 1970-01-01 00:00:00 UTC 以来的秒数,ctime()可以转成可读字符串。上机时记录一次时间戳存进机位结构体,下机时再取一次,差值就是占用秒数,除以 60 得到分钟数。

time_t start = time(NULL); printf("上机时间: %s", ctime(&start));

time(NULL)取当前墙钟时间,不要用clock()——clock()返回的是 CPU 占用时间,不是真实流逝时间,程序卡一秒和现实过一秒对不上,这在计费系统里是致命的。time_t本质是长整型,做now - start_ts时类型自然对齐,你甚至可以把它当成 long long 打印出来核对。

3.2 上机校验流程:查会员、查余额、找空闲机位

上机动作是整套系统里做输入校验最多的函数:查会员 → 看余额 → 找空闲机位 → 占机。任一环节不通过都要给出明确提示,不能靠链表越界掩盖问题。

int do_check_in(User **head, Seat *seats, int seat_count, double min_balance) { char phone[MAX_PHONE]; printf("请输入会员手机号: "); scanf("%15s", phone); User *u = find_user(*head, phone, NULL); if (!u) { printf("未找到该会员,请先注册。\n"); return -1; } if (u->balance < min_balance) { printf("余额不足,上机至少需要 %.2f 元。\n", min_balance); return -1; } int idx = first_free_seat(seats, seat_count); if (idx == -1) { printf("当前无空闲机位。\n"); return -1; } seats[idx].status = 1; strncpy(seats[idx].cur_phone, phone, MAX_PHONE - 1); seats[idx].cur_phone[MAX_PHONE - 1] = '\0'; seats[idx].start_ts = time(NULL); printf("上机成功:机号 %d,开始时间 %s\n", idx + 1, ctime(&seats[idx].start_ts)); return 0; }

find_user找到的是链表节点指针,上机时没有修改head本身,所以传*head就够了;但注册和充值会改节点内容,删除会改头指针,那些函数才需要二级指针。这个“什么时候传User **、什么时候传User *”的问题几乎每次答辩必问,判断标准就一条:会不会改变函数外那个头指针变量。机位查的是数组下标,first_free_seat返回 -1 表示满了,这里不能用 0 表示“没空闲机位”,因为 0 号下标是合法机位,返回 -1 才能区分。

3.3 下机结算:分钟向上取整与 -lm 链接

下机时要把占用状态反转,然后算钱、扣钱。计费规则常见是三档:普通区、包间、通宵,实现时先把费率映射做成表:

区域费率(元/分钟)最低消费
普通区0.101.00
包间0.205.00
通宵10.00(晚23点后)固定
int do_check_out(User **head, Seat *seats, int seat_count, double rate) { int seat_no; printf("请输入机号: "); scanf("%d", &seat_no); if (seat_no < 1 || seat_no > seat_count) { printf("机号不合法。\n"); return -1; } int idx = seat_no - 1; if (seats[idx].status == 0) { printf("该机位当前无人上机。\n"); return -1; } time_t now = time(NULL); double minutes = (double)(now - seats[idx].start_ts) / 60.0; int minutes_up = (int)ceil(minutes); // 不满一分钟按一分钟算 double cost = minutes_up * rate; User *u = find_user(*head, seats[idx].cur_phone, NULL); if (!u) { printf("数据不一致:占用机位的会员不存在,请检查数据文件。\n"); return -1; } if (u->balance < cost) { printf("余额不足本次消费,请先充值。\n"); return -1; } u->balance -= cost; printf("机号 %d 结账:时长 %d 分钟,费用 %.2f 元,余额 %.2f 元\n", seat_no, minutes_up, cost, u->balance); seats[idx].status = 0; seats[idx].cur_phone[0] = '\0'; seats[idx].start_ts = 0; return 0; }

向上取整用的是<math.h>里的ceil(double),它接收 double 返回 double,转成 int 时才截断。这里有个常用错误:有些同学直接(int)(minutes + 0.5),那是四舍五入不是上取整,3.2 分钟会被算成 3 分钟,网吧少收 0.1 元。用 GCC 编译时记得加-lm链接数学库,否则回报undefined reference to 'ceil'。另外now - seats[idx].start_ts要先转成 double 再除以 60,避免整型除法把 90 秒截成 1 分钟。

4. 菜单循环、模糊搜索与营业报表

4.1 主循环 while + switch:退出前统一落盘

主菜单页面是课程设计同学们最先写、却又最容易被忽略输入边界的地方。常见做法是一个死循环包 switch,每次显示菜单、读指令、分发到具体函数,退出循环前统一保存数据。

int main(void) { User *head = load_users("users.dat"); if (!head) printf("首次运行,无历史数据。\n"); Seat seats[SEAT_COUNT] = {0}; // 全部初始化为空闲 int choice; while (1) { printf("1 注册 2 上机 3 下机 4 查询 5 报表 0 退出\n"); printf("选择: "); if (scanf("%d", &choice) != 1) { while (getchar() != '\n'); // 清空脏输入 continue; } if (choice == 0) break; switch (choice) { case 1: register_user(&head); break; case 2: do_check_in(&head, seats, SEAT_COUNT, 10.0); break; case 3: do_check_out(&head, seats, SEAT_COUNT, 0.10); break; case 4: search_user(head); break; case 5: report(head, seats, SEAT_COUNT); break; default: printf("输入无效,请重试。\n"); break; } } if (save_users(head, "users.dat") != 0) { printf("警告:数据保存失败。\n"); } return 0; }

scanf读到非数字时返回值不是 1,这时候必须把缓冲区内残留字符清掉,否则下一次循环会立刻读到同一个坏字符,出现死循环刷屏。这个细节比“加上 getchar 暂停回显”更值得记录,因为答辩时老师经常故意输入字母看程序会不会崩。这里选while(1)而不是do-while,是因为退出条件在循环体中间通过 break 控制,用 do-while 还得额外维护一个 flag。register_user内部就是create_userinsert_user,注册前先find_user判断手机号重复,头指针会变,所以传&head

菜单编号对应功能关键函数是否立即落盘
1会员注册register_user
2上机do_check_in
3下机do_check_out是(改余额)
4会员查询search_user
5营业报表report
0退出并保存save_users

4.2 strstr 做会员模糊查询

查询会员是搜索意图最重的一个操作。按手机号精确查找是前面find_user的活,但前台实际使用时更常是“记不清全名,输一个字把所有带这个字的会员列出来”。此时用strstr做子串匹配:

void search_user(const User *head) { char keyword[MAX_NAME]; printf("输入姓名或手机号关键字: "); scanf("%31s", keyword); int count = 0; for (const User *p = head; p; p = p->next) { if (strstr(p->name, keyword) || strstr(p->phone, keyword)) { printf("%-16s %-14s %10.2f\n", p->name, p->phone, p->balance); count++; } } printf("共匹配 %d 位会员。\n", count); }

strstr(母串, 子串)返回母串中子串第一次出现的地址,找不到返回 NULL。注意这里匹配的是手机号的子串,意味着搜“380”也能命中“13800138000”,这有时是好事(记尾号查询),有时会带出多余结果,课程设计演示时两种都提一句会让老师觉得你想过边界。字符串数组作为参数传给函数时退化成指针,search_user里的headkeyword[]本质上都是指针,所以head->next才能一路走链表。列表输出用%-16s左对齐固定宽度,余额右对齐,屏幕上列就不会歪。

4.3 qsort 加函数指针做余额报表

营业报表要回答“现在多少人上机、总余额多少、谁余额最低”。总余额是遍历累加,上机人数是遍历机位数组。要给会员按余额升序或降序展示,C 课程里最稳的是自己写冒泡排序,但既然已经讲了函数指针,直接用标准库qsort一行比较函数就能解决,这段对后面学算法也顺。

int cmp_balance_desc(const void *a, const void *b) { const User *ua = *(const User **)a; const User *ub = *(const User **)b; if (ua->balance > ub->balance) return -1; if (ua->balance < ub->balance) return 1; return 0; } void report(const User *head, const Seat *seats, int seat_count) { int online = 0; for (int i = 0; i < seat_count; i++) if (seats[i].status == 1) online++; double total_balance = 0.0; int user_count = 0; for (const User *p = head; p; p = p->next) { total_balance += p->balance; user_count++; } printf("会员数 %d,上机中 %d,会员总余额 %.2f 元\n", user_count, online, total_balance); // 把链表节点指针收集到数组,再排序 const User **arr = malloc(sizeof(User *) * user_count); if (!arr) return; int n = 0; for (const User *p = head; p; p = p->next) arr[n++] = p; qsort(arr, n, sizeof(User *), cmp_balance_desc); for (int i = 0; i < n && i < 10; i++) printf("#%d %-16s %.2f\n", i + 1, arr[i]->name, arr[i]->balance); free(arr); }

qsort 的比较函数签名里const void *是刻意为之的,必须把 void 指针转回User **再解引用一次才能拿到节点地址。这一步绕了两层指针,不理解的人容易直接强转成User *然后解引用出问题。链表本身不能用 qsort 直接排,所以先把节点指针放进数组,排序只交换指针,8 字节的移动比搬整个结构体快,课程设计里这么写属于 C 语言指针和内存管理的双重体现。

5. 收尾技巧:atexit 自动保存与数据自检

最后再补两个让程序“像正经软件”的能力:意外退出也能保存、启动时能发现数据损坏。

很多课程设计只在菜单里保存,老师的测试流程往往是“开了好几个上机中的会员,直接关终端再重开”,回来发现刚才的数据全没了。用atexit注册一个退出处理函数,比在每个分支写保存更不容易漏:

User *g_head = NULL; Seat *g_seats = NULL; void on_exit_save(void) { if (g_head && save_users(g_head, "users.dat") != 0) printf("退出保存失败,请检查磁盘。\n"); // 释放所有节点,避免 valgrind 报内存泄漏 User *p = g_head; while (p) { User *next = p->next; free(p); p = next; } } int main(void) { g_head = load_users("users.dat"); g_seats = calloc(SEAT_COUNT, sizeof(Seat)); if (!g_seats) return 1; atexit(on_exit_save); // ... 主循环 return 0; }

用了atexit之后,菜单里那个退出前的save_users调用可以删掉,统一由注册的on_exit_save兜底。atexit只接受无参数、无返回值的函数,所以“保存”和“释放内存”都必须访问全局变量而不是主函数里的局部变量,这是把局部变量改成全局量的合理理由。注意free链表要边记 next 边释放,先取出下一节点再释放当前节点,不然走到后面就是悬垂指针。

加载自检可以放在load_users之后:

自检项判定条件失败处理
字段个数每行必须恰好三个字段跳过该行并计数
余额合法必须是有限正数或零置为 0 并告警
会员唯一手机号不得重复保留先出现的,丢弃后者

具体做法在读取循环里加一个if (find_user(head, phone, NULL))判断,重复的记录不插入,打印一行警示。文件行数也算最低标准:如果users.dat是 0 字节,load_users返回 NULL,主流程不应崩溃,而是走“首次运行”提示,让管理员注册第一位会员。

这套收尾做完,重启程序后抽查一个上机中的会员,下机扣费数字和断点前的余额完全一致,才算真正闭环。之后的扩展方向也很明确:把operator_log()流水做成追加写文件,每次下机 append 一行消费记录,闭店时按日期过滤求和,营业报表就不再是“当前快照”,而是当天营收明细了。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 22:38:12

51单片机智能抽水灌溉系统设计:从传感器选型到驱动电路与调试

简介&#xff1a;一套基于单片机的智能抽水灌溉系统毕业设计资料&#xff0c;面向电子、自动化或物联网相关专业的学生及嵌入式开发者。内容围绕AT89C51单片机、YL-69土壤湿度传感器、ADC0832模数转换器、液晶显示模块、继电器、蜂鸣器与按键等核心器件&#xff0c;系统讲解方案…

作者头像 李华
网站建设 2026/9/19 22:37:23

GitHub Copilot 遇 401?TaoToken 这样改 Base URL 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 22:35:39

如何快速上手AlwaysOnTop:3步让任意窗口永驻屏幕最顶层

如何快速上手AlwaysOnTop&#xff1a;3步让任意窗口永驻屏幕最顶层 【免费下载链接】AlwaysOnTop Make a Windows application always run on top 项目地址: https://gitcode.com/gh_mirrors/al/AlwaysOnTop AlwaysOnTop 是一款轻量级的 Windows 窗口置顶工具&#xff0…

作者头像 李华
网站建设 2026/9/19 22:35:22

大疆无人机航拍巡检数据集之车辆桥梁数据集 无人机飞机识别 轮船数据集 建筑物数据集 鸟瞰图小目标检测数据集10128期

数据集的核心信 按类别、数量、格式、应用价值整理成表格&#xff0c;再分三段分别阐述类别和数量&#xff0c;确保语言独特不生硬&#xff0c;最后提供可直接复制到md文档的格式。 类别 Classes (65) 类别&#xff08;65&#xff09; 0 BGR-DDFS1 BGR-DDFS2 BGR-DDFS3 BGR-air…

作者头像 李华
网站建设 2026/9/19 22:32:23

物理信息神经网络在电网故障模拟与数字孪生中的应用

简介&#xff1a;基于PyTorch的物理信息神经网络&#xff0c;正在为电网故障模拟开辟数字孪生新维度。一个21页PDF以“PyTorch物理信息神经网络在电网故障模拟中的实时推演”为主题&#xff0c;面向具备一定深度学习基础、希望将PINNs应用于电力系统的研究人员与PyTorch学习者&…

作者头像 李华