尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C语言外卖管理系统课设全解析:结构体、链表与文件操作实战

C语言外卖管理系统课设全解析:结构体、链表与文件操作实战 简介面向C语言课程设计与毕业设计场景这份外卖管理系统完整源码覆盖用户登录、菜品管理、订单生成、余额查询等业务闭环。项目以C/C实现核心程序为单个源文件通过结构体组织数据、链表维护动态节点结合文件操作将管理员、用户、订单、申请等信息持久化到多个txt数据文件中。压缩包共13个文件包含cpp源程序、doc实验报告、md说明文档、exe可执行程序及多个txt数据文件结构清晰整体体积约998KB。目前已有205人学习适合准备课设或毕设的学生参考通过阅读源码与实验报告可掌握结构体与指针、链表增删改查、字符串处理、菜单交互、文件读写等C语言核心技能也能学习业务需求拆分与模块化实现思路。附带可执行程序便于直接运行体验是完成度较高的实战范例。1. 为什么一个外卖管理系统要用C语言写拿到takeout_management这份压缩包时很多人第一反应是外卖系统不都应该用 Java Spring Boot 或者 Python Django 吗但打开文件列表你会发现外卖管理系统——课设.cpp、food.txt、dingdan.txt、users.txt、yue.txt这些文件暴露了它的真实身份——一个用 C 语言实现、以纯文本文件做存储的课程设计项目。它没有数据库、没有网络层所有业务逻辑都靠结构体、链表和文件读写硬扛下来。这恰恰是 C 语言课设里最典型也最有教学价值的形态把订单、用户、余额、申请入驻这些业务压缩进一个.cpp文件里用命令行交互完成全流程。适合两类人一是正在做 C 语言课设、需要参考完整源码结构的学生二是想快速复习 C 语言文件操作、链表和字符串处理的一线开发。2. 结构体、链表与 txt 文件存储系统数据层怎么搭2.1 文件清单先读懂存储约定压缩包里的文本文件不是随便命名的它们构成了这个系统的“数据库表”。先看一遍能少踩很多坑文件名职责典型字段推断users.txt用户账号用户名、密码、角色、余额、状态admin.txt管理员账号与 users 分离避免越权shop.txt商家信息店铺名、商家账号、营业状态food.txt菜品表菜品ID、名称、价格、所属商家dingdan.txt订单表订单号、用户、商家、菜品、金额、状态shenqing.txt入驻申请申请者、店铺名、审核状态yue.txt余额信息用户与余额的映射yanzheng.txt验证信息大概率是注册或登录时的校验码这些文件统一采用“一行一条记录、字段间用分隔符隔开”的格式。用文本文件而不是二进制好处是便于课设答辩时直接打开给老师看数据坏处是你必须自己处理字符串分割、追加写入和覆盖写的边界问题。我在调试时通常先用cat users.txt确认格式再决定fscanf还是fgets sscanf解析。2.2 用户、菜品、订单的结构体定义C 语言课设的数据组织核心是结构体。下面这段定义覆盖了系统里最常见的三类实体#define MAX_NAME 32 #define MAX_PASS 32 #define MAX_FOOD 64 #define MAX_ADDR 128 typedef struct { char username[MAX_NAME]; char password[MAX_PASS]; int role; // 0:顾客 1:商家 2:管理员 int status; // 0:正常 1:禁用 double balance; // 余额与yue.txt同步 } User; typedef struct { int food_id; char name[MAX_FOOD]; double price; char shop_name[MAX_NAME]; int stock; // 库存 } Food; typedef struct { int order_id; char customer[MAX_NAME]; char shop_name[MAX_NAME]; char food_name[MAX_FOOD]; int quantity; double total_price; int state; // 0:待接单 1:已接单 2:已完成 3:已取消 char create_time[24]; } Order;role字段直接决定了菜单权限后面登录逻辑会拿它做分支判断。balance虽然有独立yue.txt但结构体里保留一份缓存避免每次都要读文件。Order.state是订单状态机的基础用整数比字符串比较速度更快也更容易在switch-case里处理。写结构体时要注意对齐和内存占用char数组长度不要拍脑袋要和文本文件里的最长字段匹配否则fread或fscanf读进缓冲区时会截断。2.3 链表动态管理与文件读写的配合订单数量在运行时会增长用固定数组容易越界所以订单管理一般用单向链表。每次程序启动时从dingdan.txt加载数据构建链表退出时再把链表写回文件。核心逻辑如下typedef struct OrderNode { Order data; struct OrderNode *next; } OrderNode; OrderNode* load_orders(const char *filename) { FILE *fp fopen(filename, r); if (!fp) { perror(打开订单文件失败); return NULL; } OrderNode *head NULL, *tail NULL; Order tmp; while (fscanf(fp, %d|%31[^|]|%31[^|]|%63[^|]|%d|%lf|%d|%23[^\n], tmp.order_id, tmp.customer, tmp.shop_name, tmp.food_name, tmp.quantity, tmp.total_price, tmp.state, tmp.create_time) 8) { OrderNode *node (OrderNode*)malloc(sizeof(OrderNode)); node-data tmp; node-next NULL; if (tail) { tail-next node; } else { head node; } tail node; } fclose(fp); return head; }load_orders用fscanf按|分隔符解析每一行%31[^|]表示读取最多 31 个非|字符防止缓冲区溢出。返回值是链表头指针tail指针保证插入是 O(1) 操作而不是每次遍历到末尾。这里没有做内存释放课设答辩时老师经常问“内存泄漏怎么办”你可以补一个free_order_list(head)递归释放节点。写回文件时用fprintf(fp, %d|%s|%s|%s|%d|%.2f|%d|%s\n, ...)重新生成文本。注意%.2f保留两位小数否则余额计算会出现浮点误差累积。3. 从 users.txt 到 session登录校验与密码存储的实现3.1 注册与登录的流程管理系统的入口是登录。users.txt里每行是一条用户记录格式类似zhangsan|123456|0|0|100.00 lisi|abc123|1|0|0.00字段顺序和User结构体对应。登录流程就是读取文件、逐行解析、strcmp比对。为了让代码好看一般会把“按用户名查找用户”抽成一个函数User* find_user(const char *username) { FILE *fp fopen(users.txt, r); if (!fp) return NULL; User *u (User*)malloc(sizeof(User)); char line[256]; while (fgets(line, sizeof(line), fp)) { sscanf(line, %31[^|]|%31[^|]|%d|%d|%lf, u-username, u-password, u-role, u-status, u-balance); if (strcmp(u-username, username) 0) { fclose(fp); return u; } } fclose(fp); free(u); return NULL; }fgets按行读取sscanf按格式解析比fscanf更安全因为fgets能避免行读取一半的情况。查找到用户后立即fclose并返回堆上的User*调用方用完必须free否则反复登录会泄漏内存。这里默认密码是明文存储。课设场景可以接受但如果要加分可以对密码做简单异或加密比如char ch c ^ 0x23;。3.2 密码校验代码登录函数除了比对用户名还需要处理三次重试、用户禁用这类边界情况。一个健壮的校验片段如下int login(User *current_user) { char name[MAX_NAME], pass[MAX_PASS]; for (int i 0; i 3; i) { printf(用户名: ); scanf(%31s, name); printf(密码: ); scanf(%31s, pass); User *u find_user(name); if (u NULL) { printf(用户不存在剩余尝试 %d 次\n, 2 - i); continue; } if (u-status 1) { printf(账号已禁用请联系管理员\n); free(u); return -1; } if (strcmp(u-password, pass) 0) { *current_user *u; free(u); printf(登录成功当前角色: %d\n, current_user-role); return 0; } printf(密码错误剩余尝试 %d 次\n, 2 - i); free(u); } return -1; }scanf(%31s, name)限制了读取长度防止name数组溢出这是 C 语言必背的防御写法。登录成功后把u指向的内容拷贝到current_user然后free(u)这样外层函数可以一直持有登录态。角色判断放在登录成功后统一处理不要在login里分开写顾客和商家的入口职责更单一。3.3 登录态的全局维护与权限切换current_user相当于一个简易的会话对象。它的role字段驱动主菜单分支void main_menu(User *u) { while (1) { switch (u-role) { case 0: customer_menu(u); break; case 1: shop_menu(u); break; case 2: admin_menu(u); break; default: printf(未知角色\n); return; } } }customer_menu、shop_menu、admin_menu各自独立互不干扰。切换角色通常通过管理员修改users.txt中对应行的role字段来实现或者在退出登录后重新走一遍login。这里容易踩的坑是函数传参只传了User *u但多个菜单函数都要修改u-balance如果不注意指针传参容易在函数内部复制了一份结构体导致数据不回写。4. 核心业务下单、接单与订单状态流转4.1 订单文件 dingdan.txt 的字段设计订单是外卖系统的核心。dingdan.txt的一行记录可以设计为1|zhangsan|张记快餐|鱼香肉丝|2|42.00|0|2025-01-10 12:30:00字段顺序与Order结构体一致订单号、顾客、商家、菜品、数量、总价、状态、创建时间。其中状态字段0/1/2/3分别对应待接单、已接单、已完成、已取消。在演示时我会刻意保留几条不同状态的订单这样能直观展示状态流转。4.2 下单操作的完整代码片段用户从菜品列表中选择菜品后系统需要生成订单号、计算总价并追加写入dingdan.txt。同时要更新food.txt中的库存。这是一个典型的“先检查、再扣减、最后落盘”流程void place_order(User *u) { show_food_menu(); // 打印food.txt里的所有菜品 int food_id, qty; printf(请输入商品ID: ); scanf(%d, food_id); printf(请输入数量: ); scanf(%d, qty); if (qty 0) { printf(数量必须大于0\n); return; } Food *f find_food_by_id(food_id); if (!f) { printf(商品不存在\n); return; } if (f-stock qty) { printf(库存不足当前库存: %d\n, f-stock); free(f); return; } double total f-price * qty; if (u-balance total) { printf(余额不足需要 %.2f当前余额 %.2f\n, total, u-balance); free(f); return; } // 扣减余额 u-balance - total; update_user_balance(u-username, u-balance); // 扣库存 f-stock - qty; update_food_stock(f-food_id, f-stock); // 追加订单 OrderNode *head load_orders(dingdan.txt); int new_id get_max_order_id(head) 1; Order new_order { .order_id new_id, .customer , .total_price total, .quantity qty, .state 0 }; snprintf(new_order.customer, MAX_NAME, %s, u-username); snprintf(new_order.shop_name, MAX_NAME, %s, f-shop_name); snprintf(new_order.food_name, MAX_FOOD, %s, f-name); snprintf(new_order.create_time, 24, %s, get_current_time()); append_order_to_file(dingdan.txt, new_order); printf(下单成功订单号: %d\n, new_id); free(f); free_order_list(head); // 这里要释放链表中除了用于读取ID外的节点 }find_food_by_id内部会遍历food.txt返回堆上分配的Food*用完记得free。get_max_order_id(head)用于生成自增订单号虽然简单但并发时不安全。课设单机演示不存在这个问题。先检查库存和余额再执行扣减避免出现“余额扣了但库存不够”的脏数据。snprintf比strcpy更安全能限定目标缓冲区大小是 C 语言内存管理上的标准做法。4.3 商家接单与状态更新商家登录后能看到所有state0的订单。接单操作本质是修改dingdan.txt中对应行的state字段为1。实现方式有两种重写整个文件或者用fseek定位到指定行改写。课设更稳妥的是全量重写void accept_order(int order_id) { OrderNode *head load_orders(dingdan.txt); OrderNode *cur head; while (cur) { if (cur-data.order_id order_id cur-data.state 0) { cur-data.state 1; printf(订单 %d 已接单\n, order_id); break; } cur cur-next; } if (!cur) { printf(未找到待接单的订单 %d\n, order_id); } // 将链表写回文件 FILE *fp fopen(dingdan.txt, w); if (fp) { cur head; while (cur) { fprintf(fp, %d|%s|%s|%s|%d|%.2f|%d|%s\n, cur-data.order_id, cur-data.customer, cur-data.shop_name, cur-data.food_name, cur-data.quantity, cur-data.total_price, cur-data.state, cur-data.create_time); cur cur-next; } fclose(fp); } free_order_list(head); }先读文件构建链表在内存里修改再全量写回。这样处理比按行定位简单且不容易出错。写回时使用fopen(dingdan.txt, w)会清空原文件如果中途写失败数据会丢失。更稳妥的做法是先写临时文件dingdan.tmp写完再rename覆盖原文件。状态流转的边界只有state0的订单才能被接单已接单或已完成的订单不能重复接这就是状态机最基本的约束。5. 余额、申请与多角色权限几个容易忽略的模块5.1 yue.txt 与余额更新yue.txt可以看作users.txt中余额字段的冗余表设计它的原因通常是users.txt里存的用户信息太多每次充值、消费都要重写整个用户文件开销大于是单独用一张表维护余额。更新时要注意两个文件的一致性void update_user_balance(const char *username, double new_balance) { // 更新yue.txt中的余额 FILE *fp fopen(yue.txt, r); FILE *tmp fopen(yue.tmp, w); char line[256]; while (fgets(line, sizeof(line), fp)) { char name[MAX_NAME]; double bal; sscanf(line, %31[^|]|%lf, name, bal); if (strcmp(name, username) 0) { fprintf(tmp, %s|%.2f\n, username, new_balance); } else { fputs(line, tmp); } } fclose(fp); fclose(tmp); remove(yue.txt); rename(yue.tmp, yue.txt); // 同步更新users.txt中的余额字段 fp fopen(users.txt, r); tmp fopen(users.tmp, w); while (fgets(line, sizeof(line), fp)) { char name[MAX_NAME], pass[MAX_PASS]; int role, status; double bal; sscanf(line, %31[^|]|%31[^|]|%d|%d|%lf, name, pass, role, status, bal); if (strcmp(name, username) 0) { fprintf(tmp, %s|%s|%d|%d|%.2f\n, name, pass, role, status, new_balance); } else { fputs(line, tmp); } } fclose(fp); fclose(tmp); remove(users.txt); rename(users.tmp, users.txt); }用removerename代替直接覆盖写保证文件操作原子性避免程序中途崩溃导致文件损坏。两个文件同步更新很重要否则会出现users.txt显示余额 100yue.txt却是 50 的不一致问题。这种冗余存储是课设常见的省事做法但如果被问“为什么不直接更新 users.txt”你可以说为了减少大文件重写频率——虽然在这个项目里数据量很小但这体现你对文件 IO 成本的思考。5.2 shenqing.txt 申请入驻的逻辑外卖系统一般允许顾客申请成为商家。shenqing.txt记录申请信息管理员审核后把申请者加入shop.txt并把users.txt中该用户的role改为1。申请流程的代码要点如下int apply_shop(User *u) { if (u-role ! 0) { printf(只有顾客可以申请入驻\n); return -1; } FILE *fp fopen(shenqing.txt, a); if (!fp) { printf(无法打开申请文件\n); return -1; } fprintf(fp, %s|%s|0\n, u-username, 未填写店名); // 0:待审核 fclose(fp); printf(申请已提交等待管理员审核\n); return 0; }管理员审核函数会遍历shenqing.txt中所有state0的记录决定通过或拒绝。通过时要在users.txt中把该用户的role改为1并在shop.txt末尾追加店铺记录。注意用户提交申请后不能立刻修改自己的角色否则系统里会多出大量未审核的“商家”。5.3 权限边界与越权检查这个系统有三类角色最容易出现越权的地方在菜单入口和订单操作。比如顾客直接输入商家菜单的序号或者用调试器修改内存跳过检查。课设代码里至少要保证三层检查菜单层根据role显示不同菜单不越级分发。函数层每个业务函数入口处再判断一次if (u-role ! 1)防止通过按钮或输入漏洞调用。文件层商家只能操作自己店铺的订单判断strcmp(cur_order.shop_name, u-username) 0。在accept_order里如果商家 A 尝试接商家 B 的订单应该在循环体里加一个归属校验if (strcmp(cur-data.shop_name, u-username) ! 0) { printf(无权操作其他店铺的订单\n); return; }这种检查看起来冗余但课设答辩时老师通常会故意问“如果我这个用户去操作别人的订单会怎样”有这一行就能直接演示防御效果。6. 答辩前必做的验证GDB 调试与文件一致性自查课设最后一关不是写代码而是让程序在老师面前稳定跑完整个流程。我一般会按下面这个清单自查比反复点菜单更有效率。用gcc编译时开-Wall -ggcc -Wall -g 外卖管理系统——课设.cpp -o takeout_management-Wall开启所有警告-g保留调试信息。如果源文件里有中文注释乱码可以在编译时加-finput-charsetUTF-8 -fexec-charsetGBK在 Windows 下用 GBK 终端跑能避免乱码。运行前备份原始 txt 文件cp users.txt users_bak.txt cp food.txt food_bak.txt然后按“注册顾客 → 充值 → 下单 → 商家接单 → 管理员审核申请 → 余额扣减”的顺序走一遍。每一步操作后用tail -n 5 dingdan.txt和cat yue.txt验证数据是否更新。用 GDB 检查内存泄漏和非法地址gdb ./takeout_management在 GDB 里设置断点b place_order单步跟踪scanf输入后的变量值重点看total_price是否符合预期。如果程序崩溃输入bt打印调用栈定位到具体的fscanf或memcpy行。文件一致性是课设最容易被扣分的点。做一张对照表把操作前后的文件变化列出来操作users.txtyue.txtdingdan.txtshop.txt下单不变余额减少新增一条 state0不变接单不变不变state 0→1不变完成订单不变不变state 1→2不变管理员审核通过role 0→1不变不变新增店铺如果在自查中发现yue.txt没同步优先检查update_user_balance里的remove和rename是否执行成功perror会提示是不是文件被占用。如果订单重复生成检查get_max_order_id的链表释放逻辑确认free_order_list没有把head指针置空后继续访问。最后运行一遍make脚本或手工编译命令把README.md里的环境要求、编译命令、默认账号密码截图保存。答辩时直接展示程序在命令行刷数据的过程比讲空话有效得多。user.txt里那个yanzheng.txt如果你没在代码里用到提前说明它用于注册时的验证码扩展避免被追问时卡壳。本文还有配套的精品资源点击获取
返回列表