简介:这份C++课程设计资源聚焦酒店客房管理系统的完整实现,面向高校计算机专业学生及需要完成课程设计或巩固面向对象编程的学习者。项目以Room类为核心,覆盖客房信息录入、查询、修改与删除等业务,并涉及std::vector、std::map等STL容器选型、智能指针资源管理、工厂与策略等设计模式,以及异常处理机制,适合作为C++综合实践参考。压缩包为7z格式,共30个文件,约24.48MB,包含cpp源码、vcxproj工程文件、sln解决方案、txt说明文档,以及exe可执行程序、pdb调试符号和ipch、tlog等Visual Studio中间文件,便于直接编译运行与调试。目前已有3911人学习下载。读者可从中获得一套可运行的课程设计工程结构、客房状态流转的实现思路与排错参考,适合对照源码理解类设计、容器操作和模块划分,快速搭建自己的管理系统方案。
1. 酒店客房管理系统:从课程设计到能跑通的 C++ 工程
很多人对“C++课程设计之酒店客房管理系统”的第一反应是:不就是个增删改查吗?但真正动手写过的都知道,这东西翻车的地方从来不在“查”,而在“退房时房态怎么同步”“订单和房间怎么解耦”“文件存盘后重启数据还在不在”。我带过几届课程设计,见过太多人前两天兴致勃勃建了十几个类,第三天卡在“为什么我的房间状态改了但订单里还是旧数据”,最后交上去一个只能跑一次的 Demo。
这个标题背后其实是一套完整的 C++ 工程训练:面向对象建模、容器选型、文件持久化、状态机设计、边界条件处理。它适合刚学完 C++ 语法、想找一个能写进简历的完整项目练手的人,也适合已经工作但想回头补一补“怎么把类设计得不那么臭”的开发者。下面我按自己实际做这类系统的路径,从建模到落盘一步步拆开讲,代码可以直接抄,参数可以按需改。
2. 先定数据模型:房间、订单、客人三张表怎么切
2.1 为什么我不建议把“客人”直接塞进“房间”类
新手最容易犯的错是让Room类里直接放一个Guest对象,觉得“房间住着客人,天经地义”。但酒店业务里,客人退房后房间还在,客人信息要留档,订单要能追溯。如果Room持有Guest,退房时你得手动清空,稍不留神就出现“房间空了但客人还挂在上面”的玄学 bug。
常见做法是拆成三个独立实体:Room只管房态和房型,Guest只管身份信息,Order把两者关联起来并记录时间区间。这样退房只是把Order标记为已完成,Room状态改回空闲,Guest记录保留用于历史查询。三者通过 ID 关联,而不是对象嵌套。
// Room.h —— 只描述房间本身,不持有客人 #pragma once #include <string> enum class RoomStatus { FREE, OCCUPIED, MAINTENANCE }; class Room { public: Room(int id, const std::string& type, double price) : id_(id), type_(type), price_(price), status_(RoomStatus::FREE) {} int getId() const { return id_; } RoomStatus getStatus() const { return status_; } void setStatus(RoomStatus s) { status_ = s; } double getPrice() const { return price_; } std::string getType() const { return type_; } private: int id_; std::string type_; // 单人间 / 标准间 / 套房 double price_; // 每晚单价 RoomStatus status_; };这段代码的关键点是status_用枚举而不是bool isFree,因为后面你会遇到“维修中”这种第三态。price_放在Room里而不是Order里,是因为房价随房型走,订单只记录入住时的快照价格,避免调价后历史订单金额被篡改。
2.2 订单类的时间区间与状态流转
Order是整个系统的核心,它要回答三个问题:谁住的、住哪间、从什么时候到什么时候。我用checkIn和checkOut两个time_t时间戳,配合一个OrderStatus枚举。
// Order.h #pragma once #include <ctime> #include <string> enum class OrderStatus { BOOKED, CHECKED_IN, CHECKED_OUT, CANCELLED }; class Order { public: Order(int orderId, int roomId, const std::string& guestId, std::time_t checkIn, std::time_t checkOut) : orderId_(orderId), roomId_(roomId), guestId_(guestId), checkIn_(checkIn), checkOut_(checkOut), status_(OrderStatus::BOOKED), actualCheckOut_(0) {} // 退房时写入实际离开时间,用于计算是否超时 void complete(std::time_t actualOut) { actualCheckOut_ = actualOut; status_ = OrderStatus::CHECKED_OUT; } bool isActive() const { return status_ == OrderStatus::BOOKED || status_ == OrderStatus::CHECKED_IN; } int getRoomId() const { return roomId_; } OrderStatus getStatus() const { return status_; } std::time_t getCheckIn() const { return checkIn_; } std::time_t getCheckOut() const { return checkOut_; } private: int orderId_; int roomId_; std::string guestId_; std::time_t checkIn_; std::time_t checkOut_; std::time_t actualCheckOut_; OrderStatus status_; };参数说明:checkIn和checkOut是预订时填的计划时间,actualCheckOut_是退房时真实写入的时间。为什么要分开?因为超时退房要加收费用,而计划时间不能改。isActive()用来判断订单是否还占着房间,预订和已入住都算活跃,已退房和已取消不算。
2.3 用 vector 还是 map 存房间:我踩过的坑
一开始我用std::vector<Room>存所有房间,查房态要遍历,房间少的时候没问题。但课程设计通常要求支持“按房号快速查房态”,房间数上百后每次遍历就很蠢。后来换成std::map<int, Room>,房号做 key,查找 O(log n),而且天然按房号排序,打印房间列表时不用再排。
但注意:map的 value 是Room对象,取出来修改要拿引用,不能拿拷贝。我见过有人写auto room = rooms[101]; room.setStatus(...),改的是临时拷贝,原 map 里纹丝不动,查半天查不出问题。
#include <map> std::map<int, Room> rooms; rooms.emplace(101, Room(101, "标准间", 288.0)); // 正确:拿引用改 auto& r = rooms.at(101); r.setStatus(RoomStatus::OCCUPIED); // 错误示范:auto r = rooms[101]; 改的是副本emplace比insert少一次拷贝构造,房间多的时候有感知。at比[]安全,房号不存在会抛异常而不是静默插入一个默认房间。
3. 核心业务逻辑:入住、退房、查询怎么写才不臭
3.1 入住流程:先查房态再改状态,顺序不能反
入住操作看起来简单,但顺序错了就会出并发问题。正确顺序是:检查房间是否存在 → 检查房态是否空闲 → 创建订单 → 把房间状态改为占用。如果先改状态再创建订单,创建失败时房间就白白被占了。
// HotelManager.cpp 片段 bool HotelManager::checkIn(int roomId, const std::string& guestId, std::time_t checkIn, std::time_t checkOut) { auto it = rooms_.find(roomId); if (it == rooms_.end()) { std::cerr << "房号不存在: " << roomId << std::endl; return false; } if (it->second.getStatus() != RoomStatus::FREE) { std::cerr << "房间当前不可入住,状态非空闲" << std::endl; return false; } // 先落订单 int orderId = nextOrderId_++; orders_.emplace(orderId, Order(orderId, roomId, guestId, checkIn, checkOut)); // 再改房态 it->second.setStatus(RoomStatus::OCCUPIED); return true; }逻辑说明:nextOrderId_是成员变量,每次自增保证订单号唯一。orders_用map<int, Order>存,订单号做 key。注意这里没有做“同一客人重复入住同一房间”的校验,实际业务里应该加,但课程设计阶段可以先留个 TODO。
参数怎么改:如果要做“预订”和“入住”分离,可以把checkIn拆成book()和confirmCheckIn()两个函数,book只创建订单不改房态,confirmCheckIn才改房态。这样能支持提前预订。
3.2 退房结算:费用计算与房态回滚
退房要算钱,算钱就要处理时间差。我用difftime算实际住几晚,不足一晚按一晚算。
double HotelManager::checkOut(int roomId, std::time_t actualOut) { // 找到该房间当前活跃订单 for (auto& pair : orders_) { Order& o = pair.second; if (o.getRoomId() == roomId && o.isActive()) { o.complete(actualOut); // 计算住宿天数,向上取整 double seconds = std::difftime(actualOut, o.getCheckIn()); int nights = static_cast<int>(seconds / 86400); if (seconds - nights * 86400 > 0) nights += 1; if (nights <= 0) nights = 1; double total = nights * rooms_.at(roomId).getPrice(); // 房态回滚为空闲 rooms_.at(roomId).setStatus(RoomStatus::FREE); return total; } } std::cerr << "未找到该房间的活跃订单" << std::endl; return -1.0; }这里有个血泪经验:difftime返回的是秒,除以 86400 得到天数,但整数除法会截断。如果客人住了 1.5 天,截断后是 1 天,少收钱。所以必须判断余数,有余数就加一天。nights <= 0的兜底是防止客人刚入住就退房导致天数为零。
房态回滚放在算完钱之后,保证即使算钱抛异常,房态也不会被错误释放。实际工程里应该用 RAII 或者事务,但课程设计用这个顺序够了。
3.3 查询功能:按房型、按状态、按客人三种过滤
查询是课程设计里最容易被低估的部分。老师往往要求“能按多种条件查”,但很多人只写一个遍历打印。我一般会抽一个queryRooms函数,用 lambda 做过滤条件。
std::vector<Room> HotelManager::queryRooms( std::function<bool(const Room&)> filter) const { std::vector<Room> result; for (const auto& pair : rooms_) { if (filter(pair.second)) { result.push_back(pair.second); } } return result; } // 调用示例:查所有空闲的标准间 auto freeStandard = queryRooms([](const Room& r) { return r.getStatus() == RoomStatus::FREE && r.getType() == "标准间"; });用std::function的好处是过滤逻辑可以随便换,不用改queryRooms本身。参数说明:filter接收const Room&,返回bool。注意返回的是vector<Room>拷贝,房间多的时候有开销,可以改成返回 ID 列表,但课程设计阶段拷贝更直观。
4. 文件持久化:重启后数据不丢的落盘方案
4.1 为什么我用文本格式而不是二进制
很多人觉得二进制“高级”,但课程设计里文本格式才是后悔药。二进制存盘一旦结构体对齐变了,旧文件就读不出来,而且没法用记事本直接看。我用 CSV 风格的文本,每行一条记录,字段用逗号分隔。
房间文件rooms.txt格式:
101,标准间,288.0,0 102,单人间,188.0,1最后一位是状态码:0 空闲,1 占用,2 维修。
订单文件orders.txt格式:
1,101,G001,1700000000,1700086400,2,1700080000依次是订单号、房号、客人ID、计划入住、计划退房、状态码、实际退房时间。
4.2 存盘与加载的代码实现
// 存盘 void HotelManager::saveToFile(const std::string& roomFile, const std::string& orderFile) { std::ofstream rf(roomFile); for (const auto& pair : rooms_) { const Room& r = pair.second; rf << r.getId() << "," << r.getType() << "," << r.getPrice() << "," << static_cast<int>(r.getStatus()) << "\n"; } rf.close(); std::ofstream of(orderFile); for (const auto& pair : orders_) { const Order& o = pair.second; of << o.getOrderId() << "," << o.getRoomId() << "," << o.getGuestId() << "," << o.getCheckIn() << "," << o.getCheckOut() << "," << static_cast<int>(o.getStatus()) << "," << o.getActualCheckOut() << "\n"; } of.close(); }加载时用std::getline逐行读,再用std::stringstream按逗号切分。注意getline默认以换行符分隔,但 Windows 下文件可能有\r,切分前要处理掉。
// 加载房间 void HotelManager::loadRooms(const std::string& filename) { std::ifstream f(filename); std::string line; while (std::getline(f, line)) { if (!line.empty() && line.back() == '\r') line.pop_back(); std::stringstream ss(line); std::string idStr, type, priceStr, statusStr; std::getline(ss, idStr, ','); std::getline(ss, type, ','); std::getline(ss, priceStr, ','); std::getline(ss, statusStr, ','); int id = std::stoi(idStr); double price = std::stod(priceStr); RoomStatus st = static_cast<RoomStatus>(std::stoi(statusStr)); rooms_.emplace(id, Room(id, type, price)); rooms_.at(id).setStatus(st); } }参数说明:std::stoi和std::stod会抛异常,如果文件被手动改坏,程序会崩。稳妥做法是包一层try-catch,跳过坏行并打印警告。课程设计里可以不做,但要知道这个边界。
4.3 自动存盘的时机:别等退出才写
我见过有人只在main返回前调一次saveToFile,结果程序中途崩溃,一上午的测试数据全没了。我的习惯是每次写操作(入住、退房、加房)之后立刻存盘。虽然频繁 IO 有开销,但课程设计的数据量完全扛得住,而且能保证任何时刻断电最多丢最后一次操作。
// 在 checkIn 成功返回前调用 saveToFile("rooms.txt", "orders.txt");如果嫌每次写太频繁,可以加一个dirty_标志,每 N 次操作或每隔 30 秒存一次。但课程设计阶段,简单直接比优化更重要。
5. 避坑与排查:课程设计里最容易翻车的 5 个点
5.1 现象:退房后房间还是“占用”,重启才恢复
原因:退房时只改了订单状态,忘了把Room的status_改回FREE。或者改了但存盘时没写房间文件,只写了订单文件。
解决:退房函数里必须同时操作orders_和rooms_,并且两个文件都要存。排查时先看内存里的房态,再看文件里的状态码,定位是逻辑漏了还是落盘漏了。
5.2 现象:订单号重复,map 插入失败但程序不报错
原因:nextOrderId_没有持久化,每次重启都从 0 开始,而旧订单还在文件里。emplace遇到重复 key 会静默失败,不抛异常。
解决:加载订单时把最大订单号读出来,初始化nextOrderId_ = maxId + 1。或者用insert的返回值判断是否插入成功,失败就打印错误。
5.3 现象:时间计算出来是负数或巨大值
原因:std::time_t在不同平台可能是 32 位或 64 位,手动构造时间戳时填了错误的年月日。或者difftime的参数顺序反了,actualOut减checkIn写成了checkIn减actualOut。
解决:统一用std::time(nullptr)获取当前时间,不要手写时间戳。difftime(end, start)记住是 end 减 start。打印时间用std::localtime转成可读格式,方便核对。
5.4 现象:中文房型名存盘后变成乱码
原因:Windows 默认 GBK 编码,Linux 默认 UTF-8,文件读写时编码不一致。或者std::string存中文没问题,但用printf输出时编码不匹配。
解决:课程设计里统一用英文房型名最省事,比如Standard、Single、Suite。如果必须用中文,确保源文件、编译器、终端三处编码一致,VS 里加/utf-8编译选项。
5.5 现象:查询结果里出现了已退房的订单
原因:isActive()判断写成了status_ != CHECKED_OUT,漏了CANCELLED也算不活跃。或者遍历订单时没过滤房号,把其他房间的订单也带出来了。
解决:isActive()明确写成status_ == BOOKED || status_ == CHECKED_IN,白名单比黑名单安全。查询时先按房号过滤,再按状态过滤,两个条件都要。
6. 进阶技巧:用状态机把房态流转管起来
写到后面你会发现,房态和订单状态的组合越来越多:空闲房可以预订,预订房可以入住或取消,入住房可以退房,维修房不能预订。这些规则散落在各个if-else里,改一处漏一处。我后来用一个简单的状态转移表来管。
| 当前房态 | 操作 | 目标房态 | 前置条件 |
|---|---|---|---|
| FREE | 预订 | FREE | 创建 BOOKED 订单 |
| FREE | 直接入住 | OCCUPIED | 创建 CHECKED_IN 订单 |
| OCCUPIED | 退房 | FREE | 订单转 CHECKED_OUT |
| FREE | 报修 | MAINTENANCE | 无活跃订单 |
| MAINTENANCE | 修复 | FREE | 无 |
把这张表写成代码,就是一个canTransfer函数:
bool HotelManager::canTransfer(RoomStatus from, const std::string& action) { static const std::map<std::pair<RoomStatus, std::string>, RoomStatus> table = { {{RoomStatus::FREE, "book"}, RoomStatus::FREE}, {{RoomStatus::FREE, "checkin"}, RoomStatus::OCCUPIED}, {{RoomStatus::OCCUPIED, "checkout"}, RoomStatus::FREE}, {{RoomStatus::FREE, "maintain"}, RoomStatus::MAINTENANCE}, {{RoomStatus::MAINTENANCE, "repair"}, RoomStatus::FREE}, }; return table.find({from, action}) != table.end(); }这样每次操作前先查表,不合法就拒绝,逻辑集中在一处,加新状态只改表不改散落的判断。参数说明:action用字符串而不是枚举,是为了方便打印日志,实际工程里可以用枚举加转换函数。
验证方法:写一个单元测试,遍历所有房态和操作的组合,断言只有表里允许的才能通过。课程设计不要求测试,但你自己跑一遍能省很多调试时间。
我自己的习惯是,每次加一个新功能前,先问“这个操作会改哪几个状态”,然后去表里加一行。如果发现表里加不进去,说明业务逻辑有冲突,得先理清再写代码。这个习惯帮我省了至少三次重构。希望帮到你。
本文还有配套的精品资源,点击获取