ARTICLE DETAIL

资讯详情

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

C++外卖管理系统源码:订单状态机与文件存储实战解析

C++外卖管理系统源码:订单状态机与文件存储实战解析 简介面向计算机相关专业的期末大作业与课程设计要求这份C外卖管理系统源码包提供了从点餐、订单管理到配送区域划分的完整控制台项目实现。资源共25个文件以9个cpp与8个h源文件为核心覆盖主控流程、订单队列、邻接表目的地图等模块另含5个txt数据文件用于菜单、配送距离等配置以及演示用PPT、可直接运行的exe和README说明整体仅2.26MB便于快速下载与本地编译调试。项目经导师指导评审得分98源码均已调试通过特别适合正在完成课程设计或希望提升C项目实战能力的学习者。借助配套PPT可进行答辩展示结合源码目录能清晰理解菜单管理、订单统计、邻接表路径等要点的实现思路。目前已有85人学习可作为同类系统设计的参考模板。1. 从课程设计到可演示系统用 C 把外卖管理系统做成能跑的源码如果你搜过“C 外卖管理系统”大概率看到的是控制台里一串switch菜单配上几个类和一个文本文件存储。这类代码应付答辩没问题但离“管理系统”这四个字其实还有距离。真正让人愿意往下看的 C 外卖管理系统至少要在控制台程序之上解决三件事数据怎么组织、权限怎么区分、订单状态怎么流转。尤其是订单从“已下单”到“配送中”再到“已完成”的变更如果只用全局变量硬顶后期加一个商户端或骑手端代码会立刻失控。这篇文章把我自己会做的那套方案完整讲透用 C 实现一个贴近真实业务的外卖系统包含用户、商家、骑手、管理员四种角色数据持久化用文件存储界面用控制台菜单但内部结构按工程化方式分层。重点放在菜品的增删改查、购物车与订单生成、订单状态机、文件读写与数据一致性以及答辩时一定会被问到的“数据放在哪里”“并发怎么办”“怎么扩展成图形界面版”。所有代码片段都可以直接抄下来改改跑通参数和边界条件我会逐行解释。无论你是正在做课程设计还是想把这个题目改写成求职项目这篇都能给你一条不绕弯的路。2. 先立骨架C 外卖管理系统的领域模型与类设计2.1 四个角色对应四个类还是用权限字段区分很多初版源码喜欢写User、Merchant、Rider、Admin四个完全独立的类每个类里重复存用户名、密码、联系方式。这种写法的直接后果是登录时要写四个分支而且后面如果出现“用户同时也是商家”的场景就得再写一个类去继承两个父类菱形继承的坑全踩一遍。更合理的做法是让基类只做身份认证不同角色通过一个role枚举来区分业务行为用独立的服务类实现。我一般会这样设计enum class Role { CUSTOMER, MERCHANT, RIDER, ADMIN }; struct Account { std::string username; std::string password; std::string phone; Role role; }; class User { public: User(const Account acc) : account_(acc) {} virtual ~User() default; const std::string getUsername() const { return account_.username; } Role getRole() const { return account_.role; } bool verifyPassword(const std::string pw) const { return account_.password pw; } private: Account account_; };这段代码的逻辑在于User只负责保存账号信息并校验密码不包含具体业务动作。后续Customer、Merchant等类都可以继承User也可以不继承——直接用User加Role判断再把业务函数放进对应的服务类比如OrderService、MenuService。这样做的好处是第一文件读写只需要处理Account这一种结构第二未来想加“管理员也是用户”的场景不需要重写登录逻辑。参数层面注意verifyPassword目前是明文比较。真实系统里密码至少要做一次哈希但课程设计或演示项目里明文存储往往被老师批评“不安全”。如果你想让答辩更好看可以加一个简单的哈希函数哪怕只是std::hashstd::string配合固定盐值也能在代码里写一句“密码已加密存储”的注释。2.2 从用户到订单的关联购物车与订单对象外卖系统的核心是订单。一个订单里包含多个菜品项每项有菜品、数量、单价。设计时不要直接把菜品对象塞进订单里因为菜品信息比如价格商家随时可能改而订单下单时的价格必须固定不变。正确做法是把“下单时快照”存进订单项。struct CartItem { std::string dishId; std::string dishName; double price; // 下单时价格快照 int quantity; }; struct Order { std::string orderId; std::string customerName; std::string merchantName; std::vectorCartItem items; double totalPrice; std::string status; // pending, accepted, delivering, completed, cancelled std::string createTime; };这里值得注意的点是status用字符串而非枚举。字符串的好处是序列化和反序列化直接存文件时不需要做枚举到字符串的映射表坏处是拼写错误很难发现。所以我一般会定义一组常量namespace OrderStatus { constexpr const char* PENDING pending; constexpr const char* ACCEPTED accepted; constexpr const char* DELIVERING delivering; constexpr const char* COMPLETED completed; constexpr const char* CANCELLED cancelled; }这份状态表的迁移关系我会在第 4 章详细讲。现在先记住一点订单对象必须能独立存在不要持有User的指针或引用否则用户退出登录后订单数据就悬空了。只存用户名和商家名这类标识字符串是最简单也最稳妥的做法。2.3 文件存储的最小设计一个结构体对应一行不做数据库的外卖系统文件就是唯一的持久层。常见做法是把所有订单存在orders.txt每行一个字段用|分隔。为什么用|而不是逗号因为菜品名称里几乎不会出现|但可能出现英文逗号或中文逗号用|能少踩很多解析的坑。// 序列化 Order 为一行字符串 std::string serializeOrder(const Order order) { std::ostringstream oss; oss order.orderId | order.customerName | order.merchantName | order.status | order.totalPrice | order.createTime |; for (const auto item : order.items) { oss item.dishId , item.dishName , item.price , item.quantity ;; } return oss.str(); }加载时按|分割得到前六个字段最后一个字段再按;分割成每个购物车项每一项再按,分割。这套解析逻辑不复杂但容易出现边界问题如果菜品名里意外包含,或;解析就会错位。所以更稳妥的做法是每个订单单独占一行并且限制菜品名称里不允许出现|;,\n这几个字符。在录入菜品时做一个校验函数这比在解析时做防御要简单得多。文件存储的完整读写函数我放在第 3 章给出这里先确立“一行一对象”的序列化风格。3. 把购物车和订单跑起来C 外卖管理系统的最小可复现流程3.1 用 vector 和 map 组织内存数据控制台版外卖系统不需要引入数据库内存容器用std::vector存订单列表用std::mapstd::string, Dish按菜品 ID 索引菜品是标准做法。map的查找复杂度是 O(log n)对一个小型系统来说完全够用。如果菜品数量上千改成std::unordered_map也无妨但要注意遍历时需要有序输出时map更方便。struct Dish { std::string dishId; std::string merchant; std::string name; double price; bool available; }; class MenuService { public: void addDish(const Dish dish) { dishes_[dish.dishId] dish; } bool removeDish(const std::string dishId) { auto it dishes_.find(dishId); if (it dishes_.end()) return false; dishes_.erase(it); return true; } std::vectorDish getDishesByMerchant(const std::string merchant) const { std::vectorDish result; for (const auto [id, dish] : dishes_) { if (dish.merchant merchant dish.available) { result.push_back(dish); } } return result; } private: std::mapstd::string, Dish dishes_; };这里需要注意getDishesByMerchant返回的是菜品副本而非引用因为要防止外部直接修改容器内部数据。添加菜品前还应检查dishId是否已存在否则后写入的会覆盖先前的菜品。更合理的做法是addDish返回bool由调用方决定是提示“已存在”还是覆盖。3.2 购物车用临时对象还是持久化结构购物车本质上是一个std::vectorCartItem放在当前登录用户的会话里就可以。但控制台程序没有“会话”这个概念所以常见做法是设计一个Session结构保存当前登录用户名、角色和一个购物车对象。struct Session { bool loggedIn false; std::string username; Role role; std::vectorCartItem cart; }; bool addToCart(Session session, const Dish dish, int quantity) { if (quantity 0) { std::cout 数量必须大于0\n; return false; } for (auto item : session.cart) { if (item.dishId dish.dishId) { item.quantity quantity; return true; } } session.cart.push_back({dish.dishId, dish.name, dish.price, quantity}); return true; }这里做了两个防御数量小于等于 0 直接拒绝同一菜品重复添加时合并数量。细心的读者会发现price是从Dish对象里取出的当前价格这正好符合快照原则——购物车里记录的是“加入购物车那一刻”的价格。但快照还不够真正下单时应该把购物车的每一项再和当前菜品价格比对一次防止商家在用户下单前改了价。这个校验放在生成订单的函数里更合适。3.3 生成订单订单号、时间与价格计算订单号不能只用自增整数因为文件持久化之后一旦删除某条记录自增序号就会撞号。我会用时间戳加用户名的组合来生成订单号std::string generateOrderId(const std::string username) { auto now std::chrono::system_clock::now(); auto time std::chrono::system_clock::to_time_t(now); std::tm tm_buf; #if defined(_WIN32) localtime_s(tm_buf, time); #else localtime_r(time, tm_buf); #endif std::ostringstream oss; oss std::put_time(tm_buf, %Y%m%d%H%M%S) _ username; return oss.str(); }这段代码用了跨平台的localtime_s/localtime_r分支因为 Windows 和 Linux 的本地时间函数签名不同很多 C 初学者在这上面栽过跟头。订单号包含时间到秒理论上同一秒内同一用户下多单会有重复但演示系统里概率极低。如果追求严谨可以再加一个随机数后缀。生成订单的完整流程如下检查购物车是否为空计算总价并遍历菜品列表确认每个购物项仍然存在且在售生成订单对象状态设为pending把订单序列化到orders.txt清空购物车下单代码的关键步骤bool placeOrder(Session session, OrderService orderSvc) { if (session.cart.empty()) { std::cout 购物车为空无法下单\n; return false; } Order order; order.orderId generateOrderId(session.username); order.customerName session.username; order.status OrderStatus::PENDING; order.createTime currentTimeString(); order.items session.cart; double total 0.0; for (const auto item : order.items) { total item.price * item.quantity; } order.totalPrice total; if (!orderSvc.saveOrder(order)) return false; session.cart.clear(); std::cout 下单成功订单号 order.orderId \n; return true; }注意total用的是购物车快照价格而不是实时去菜品表里查。这是刻意为之因为订单历史必须反映下单时的价格否则审计对账会产生混乱。要想更强健可以在saveOrder前调用一个validateOrderItems函数把购物车里的每一项与当前菜品价格进行比较差值超过一定阈值就拒绝下单这在参数上可以做成一个可选开关。3.4 文件读写追加写入与整体回写saveOrder有两种实现方式。最简单的是打开文件以追加模式写一行但这样无法处理“取消订单”后从文件删除的问题。所以我更倾向于“启动时全量加载退出时全量回写”的模型class OrderService { public: void loadFromFile(const std::string path) { std::ifstream fin(path); if (!fin) return; std::string line; while (std::getline(fin, line)) { if (line.empty()) continue; Order order deserializeOrder(line); orders_.push_back(order); } } bool saveToFile(const std::string path) { std::ofstream fout(path, std::ios::trunc); if (!fout) return false; for (const auto order : orders_) { fout serializeOrder(order) \n; } return true; } private: std::vectorOrder orders_; };这套模式最简单也最安全内存是唯一数据源文件只是持久化快照每次退出或每次变更后调用一次保存。它的性能瓶颈是订单数达到万级以后全量写文件会变慢但外卖管理系统演示程序通常只有几十上百条订单完全没问题。如果你想把作业做得更有说服力可以在loadFromFile失败时创建备份文件orders.bak用来防止写坏数据后的恢复。deserializeOrder的逆解析函数是对称的这里给一个分步拆解版本Order deserializeOrder(const std::string line) { Order order; std::istringstream iss(line); std::string token; std::vectorstd::string parts; while (std::getline(iss, token, |)) { parts.push_back(token); } order.orderId parts[0]; order.customerName parts[1]; order.merchantName parts[2]; order.status parts[3]; order.totalPrice std::stod(parts[4]); order.createTime parts[5]; // parts[6] 是购物车项序列 if (parts.size() 7) { std::istringstream itemStream(parts[6]); std::string itemToken; while (std::getline(itemStream, itemToken, ;)) { if (itemToken.empty()) continue; std::istringstream it(itemToken); std::string dishId, name, priceStr, qtyStr; std::getline(it, dishId, ,); std::getline(it, name, ,); std::getline(it, priceStr, ,); std::getline(it, qtyStr, ,); order.items.push_back({dishId, name, std::stod(priceStr), std::stoi(qtyStr)}); } } return order; }必须检查parts.size() 7因为老版本数据文件可能没有购物车项这一列或者文件被手动编辑过。解析时读到的字段数量不对不要直接崩溃而是打印一行警告并跳过该条数据。这个防御性写法在答辩时会成为加分项因为大多数同学的代码遇到坏数据行会直接std::stod抛异常退出。4. 订单状态机与多角色操作C 外卖管理系统从“能下”到“能管”4.1 状态转移表谁能把订单从 pending 改到 completed订单状态是整个系统的业务核心。一个设计良好的外卖系统状态迁移必须满足以下规则当前状态可执行操作目标状态允许角色pending接单accepted商家merchantpending取消cancelled用户/商家/管理员accepted开始配送delivering骑手riderdelivering完成订单completed骑手/管理员accepted取消cancelled商家/管理员这张表要实现成代码核心是一个转移矩阵。用 if-else 写会又长又容易漏常见做法是用std::map嵌套判断class OrderStateMachine { public: using Transition std::vectorstd::pairRole, const char*; OrderStateMachine() { transitions_ { {OrderStatus::PENDING, { {Role::MERCHANT, OrderStatus::ACCEPTED}, {Role::CUSTOMER, OrderStatus::CANCELLED}, {Role::ADMIN, OrderStatus::CANCELLED} }}, {OrderStatus::ACCEPTED, { {Role::RIDER, OrderStatus::DELIVERING}, {Role::MERCHANT, OrderStatus::CANCELLED}, {Role::ADMIN, OrderStatus::CANCELLED} }}, {OrderStatus::DELIVERING, { {Role::RIDER, OrderStatus::COMPLETED}, {Role::ADMIN, OrderStatus::COMPLETED} }} }; } bool canTransition(const std::string current, Role role, const char* target) const { auto it transitions_.find(current); if (it transitions_.end()) return false; for (const auto [r, t] : it-second) { if (r role std::strcmp(t, target) 0) return true; } return false; } private: std::mapstd::string, Transition transitions_; };注意CANCELLED和COMPLETED是终态一旦进入不可再迁移转移表中不包含这两个状态作为起点。这样用户就无法把已取消的订单重新点成“配送中”保证状态机是严格单向的。4.2 商家端上架菜品、接单、拒单商家登录后能看到所有状态为pending且商家名匹配的订单。拒绝订单的操作本质上是把状态改为cancelled但要在订单里标记一下是谁取消的否则用户看到订单被取消却不知道原因。可以在Order结构里增加一个cancelReason字段序列化时多加一个管道字段。因为前面的存储格式是“顺序字段”加字段必须放在末尾这样旧数据还能用。商家接单流程的伪代码bool acceptOrder(OrderService svc, Order order) { if (!OrderStateMachine().canTransition(order.status, Role::MERCHANT, OrderStatus::ACCEPTED)) { std::cout 当前状态不能接单\n; return false; } order.status OrderStatus::ACCEPTED; svc.updateOrder(order); std::cout 接单成功\n; return true; }updateOrder的实现就是找到订单号所在位置替换整个对象。这里面有个隐藏问题订单列表在内存中的顺序可能和文件读取顺序一致但查找时要遍历整个std::vector复杂度 O(n) 对演示系统没问题但如果你想把项目写成“高并发高性能”写在简历上就应该换成std::mapstd::string, Order按订单号索引。我一般会建议保持 vector 不变因为状态机只涉及小规模数据遍历查找的代码更直观面试官问起来也能坦诚说明复杂度。4.3 骑手端配送中的订单与状态回写骑手角色只做两件事查看自己接的配送单、更新配送状态。为了不让骑手看到所有用户订单需要按订单状态筛选accepted状态且尚未被某个骑手抢的订单可以在订单里加一个riderName字段。默认值为空字符串骑手接单时填入自己的用户名然后状态改为delivering。bool riderAcceptDeliver(Session session, Order order) { if (order.status ! OrderStatus::ACCEPTED) { std::cout 订单未处于待配送状态\n; return false; } if (!order.riderName.empty()) { std::cout 订单已被骑手 order.riderName 接走\n; return false; } order.riderName session.username; order.status OrderStatus::DELIVERING; return true; }这种“抢单”模型只需要一个用户名判断不涉及锁。如果未来要做多线程版本就得给订单加std::mutex或者用数据库的行锁来保证同一订单不会分配给两个骑手。在单线程控制台程序里判断riderName是否为空已经足够。4.4 管理员视角强制取消与统计数据管理员拥有最高权限可以强制取消任何状态下的订单终态除外。此外一个外卖管理系统通常需要给管理员展示统计信息今日订单数、总营收、各商家的订单量排行。统计功能不需要额外写类直接遍历订单列表累加即可。void printAdminStats(const std::vectorOrder orders) { std::mapstd::string, int merchantOrderCount; std::mapstd::string, double merchantRevenue; for (const auto order : orders) { if (order.status OrderStatus::COMPLETED) { merchantOrderCount[order.merchantName]; merchantRevenue[order.merchantName] order.totalPrice; } } std::cout 商家订单统计 \n; for (const auto [merchant, count] : merchantOrderCount) { std::cout merchant : count 单营收 merchantRevenue[merchant] 元\n; } }这里只统计completed状态因为pending和delivering的订单还不能确认收入。如果你希望把客单价也算出来可以再除以订单数。这个函数没有做数据量级优化但如果订单数达到十万重复遍历时可以考虑用 SQL 或离线聚合但在课程设计场景下结论是“完全没必要”。5. 数据一致性、线程安全与扩展方向C 外卖管理系统踩坑复盘5.1 文件写入半截与损坏先写临时文件再替换你一定遇到过这种情况程序在saveToFile执行到一半时崩溃或断电orders.txt只剩半行下次启动deserializeOrder直接解析失败。最容易的解决方案是“原子写”也就是先写到orders.tmp写完后用std::rename覆盖原文件。bool saveToFileAtomic(const std::string path) { std::string tmpPath path .tmp; std::ofstream fout(tmpPath, std::ios::trunc); if (!fout) return false; for (const auto order : orders_) { fout serializeOrder(order) \n; } fout.flush(); fout.close(); // C17 及以后可以使用 std::filesystem::rename但老项目用 rename 更通用 if (std::rename(tmpPath.c_str(), path.c_str()) ! 0) { return false; } return true; }注意std::rename在 Windows 上如果目标文件已被其他程序打开会失败所以写文件前要确保没有任何流对象还在占用。另外flush后紧跟close能确保缓冲区内容真正落盘不能只靠close隐式刷新这里是一个隐蔽的坑。5.2 多线程与并发操作什么时候需要锁C 外卖管理系统的“多线程”有两种层次。第一种是多个服务端线程同时处理请求比如用 socket 实现网络版第二种是只有一个控制台进程但将来可能加一个自动保存定时器。在第二种场景下orders_容器如果被后台线程读取、主线程写入就会产生数据竞争。如果一定要加锁我的做法是在OrderService内部加一个mutable std::mutex每个公开方法入口加锁。但要注意如果getOrder返回的是引用外部拿到引用后锁已经释放一样竞态。所以线程安全版本的查询接口应该返回Order的副本std::optionalOrder findOrderById(const std::string orderId) const { std::lock_guardstd::mutex lock(mutex_); for (const auto order : orders_) { if (order.orderId orderId) return order; } return std::nullopt; }这里用了std::optional需要 C17。如果你的开发环境还是 Visual C 6.0 或老版本 GCC就用bool加输出参数代替。实际上课程设计很少会要求你写多线程我建议在有把握的前提下提一句“单线程版本已保证数据一致性扩展多线程时需要在 OrderService 内部加锁”。5.3 从控制台到图形界面怎么让这套 C 外卖管理系统源码“可进化”你搜到的标题里包含“源码PPT”意味着这套系统多半要用来展示和答辩。如果评委问“怎么把它改成带界面的系统”最优答案不是立刻去学 Qt而是把业务逻辑层和表现层解耦。本文设计的MenuService、OrderService、OrderStateMachine都是纯逻辑类不包含任何std::cout和cin这是刻意为之。控制台输入输出全部放在main.cpp的菜单函数里所以你完全可以用 Qt / wxWidgets 替换掉菜单函数界面调用placeOrder、acceptOrder这些方法把文件存储替换成 SQLite只需要重写OrderService的loadFromFile和saveToFile内部实现把每个角色入口封装成CustomerPanel、MerchantPanel、RiderPanel三个类界面版直接生成对应面板对象这里有一张简单的替换对照表能帮你快速梳理改造点层当前实现图形界面版替换方案表现层printMenu()/getInput()Qt 的QMainWindow 信号槽业务层OrderService/MenuService保持不变存储层文本文件orders.txtSQLite / MySQL会话层Session结构UserSession单例对象业务层保持不变的前提是它的接口里不出现控制台类型这一点从设计之初就要注意。很多同学的代码会在业务函数里直接写std::cin x导致后来封装界面时不得不把整个函数拆烂。5.4 给源码加上一份能扛住提问的 PPT 结构标题自带“源码PPT”那 PPT 的内容结构其实也应该围绕上面拆出来的层次走。我建议按着五页来需求分析四个角色、六种状态、六个用例图体系结构分层图业务逻辑、存储、界面分离核心代码讲解状态机、序列化、购物车合并逻辑演示流程商家上架 - 用户下单 - 商家接单 - 骑手配送 - 完成总结与改进增加数据库、密码哈希、多线程并发测试重点在第四页的演示流程PPT 上放一张状态迁移表让评委一眼看懂业务闭环。代码截图不要贴一大段贴 5 到 10 行关键代码比如状态转移的canTransition旁边配一句话解释作者如何做到角色权限校验。这种设计比一段一段贴代码更容易拿高分。6. 验证与调试三分钟自测这套 C 外卖管理系统能否交付6.1 造一份干净的测试数据覆盖所有状态路径写代码最简单验证代码却往往被忽视。我会准备一份test_data.txt预置两个商家、一个用户、一个骑手、一个管理员以及三笔不同状态的订单。数据文件构造示例# 测试账号文件 accounts.txt user1|123456|customer shopA|abc123|merchant rider1|rider123|rider admin|admin123|admin这里#开头的行被加载逻辑忽略。你要确认自己的加载代码是否支持空行和注释行如果暂不支持就在loadFromFile里加一句if (line.empty() || line[0] #) continue;这个技巧能让你手工测试时注释掉某些账号或订单不用真的删除文件行。接下来按下面的测试路径走一遍用户浏览商家菜品加入购物车下单 - 检查订单状态为pending文件里多出一行商家登录查看pending订单接单 - 状态变为accepted骑手登录查看accepted订单接单配送 - 状态变为delivering骑手确认送达 - 状态变为completed管理员查看统计确认该商户订单数和营收增加在每步操作后手动查看orders.txt确认对应行的状态字段正确更新。如果你在保存时忘了更新文件那么重启程序后一定会出现订单状态倒退回pending的诡异问题这是课程设计里最常见的 bug。6.2 用命令行和错误用例验证边界不需要额外安装测试框架把程序跑起来后依次输入这些异常场景// 异常输入用例 int quantity -1; // 添加购物车预期拒绝 int quantity 0; // 预期拒绝 std::string dishId nonexist; // 加入不存在的菜品 ID预期提示 std::string orderId blank; // 查询不存在的订单预期提示这些用例不需要写成单元测试但要在答辩前手动跑一遍。最常见的崩溃点有两个一是std::stod或std::stoi解析异常数据它们会抛出std::invalid_argument需要在整个main外面包一层try-catchint main() { try { // 原有逻辑 } catch (const std::exception e) { std::cerr 发生异常: e.what() std::endl; return 1; } }二是菜单循环中用户输入了非数字字符导致std::cin choice进入失败状态后续所有输入都失效。标准的修复方法是每次读取后检查cin失败则清空并重试int choice; std::cin choice; if (std::cin.fail()) { std::cin.clear(); std::cin.ignore(std::numeric_limitsstd::streamsize::max(), \n); std::cout 输入无效请重新选择\n; continue; }6.3 最后一个技巧给状态迁移加上日志输出状态机是否工作单靠肉眼去数文件里的字段太慢。我会在updateOrder里加一行日志记录void logOrderChange(const Order before, const Order after) { std::cout [LOG] after.orderId : before.status - after.status by currentSession.username \n; }这行输出在演示时能让评委直观看到每次操作的效果同时也能在调试时快速定位是哪个角色执行了非法迁移。如果你不想在正式输出里显示日志可以加一个bool DEBUG true的编译期开关用#ifdef控制。加上这个小机制之后整套 C 外卖管理系统的源码就不仅“能跑”而且“可验证”。这样的交付物配合 PPT 演示已经超过绝大多数课程设计的水准了。本文还有配套的精品资源点击获取
返回列表