ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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 个字符源串超长时不补结束符后面的strcmp和strlen就可能读过头。二是find_user的prev参数用的是指针的指针我第一次写的时候只用返回值删除头节点时总是要单独分支写成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_user加insert_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里的head和keyword[]本质上都是指针所以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 一行消费记录闭店时按日期过滤求和营业报表就不再是“当前快照”而是当天营收明细了。本文还有配套的精品资源点击获取
返回列表