C++实战:从零构建宾馆预定系统,掌握文件存储与业务逻辑设计
1. 项目概述:从零构建一个真实的宾馆预定系统
最近在带几个新人做项目复盘,发现很多朋友对C++的理解还停留在书本上的语法和算法题,一提到用C++做个“能用的”东西,比如一个宾馆预定系统,就有点无从下手。这其实挺可惜的,C++在构建需要高性能、稳定性和精细内存管理的桌面应用或后台服务时,依然有不可替代的优势。这个“基于C++的城市宾馆预定系统”项目,就是一个绝佳的练手机会,它能让你把类、文件I/O、STL容器、多模块编程这些知识点串起来,形成一个完整的、可交付的应用程序。
简单来说,我们要做的是一个运行在命令行终端(Console)上的管理系统。它需要管理多家宾馆的信息、大量的客房资源,并处理用户的预定、入住、退房等一系列业务流程。听起来是不是有点像大学数据库课的大作业?但区别在于,我们不依赖任何图形界面库或网络框架,纯粹用C++标准库和文件操作来实现数据的持久化存储,这更能考验你对核心语言特性和系统编程的理解。这个系统适合有一定C++基础(至少学过类和STL)、想通过一个综合性项目提升工程能力的朋友。通过它,你不仅能巩固语法,更能学会如何设计程序结构、处理异常、保证数据一致性,这些才是企业级开发中更看重的“内功”。
2. 系统核心需求分析与整体架构设计
在动手写第一行代码之前,我们必须把系统要干什么、怎么干想清楚。盲目开始编码,最后往往会得到一个结构混乱、难以维护的“屎山”。好的设计是成功的一半。
2.1 核心业务需求拆解
一个宾馆预定系统,无论界面多么花哨,其核心业务逻辑是相对稳定的。我们可以从三个主要角色(管理员、前台、客户)的视角来梳理需求:
- 信息管理:这是系统的基石。我们需要维护两类核心数据:
- 宾馆信息:宾馆名称、地址、星级、联系电话等。一个城市可能有多个合作宾馆。
- 客房信息:隶属于某个宾馆,包括房型(如标准间、大床房、套房)、房间号、状态(空闲、已预定、已入住)、价格、可入住人数等。
- 预定业务:这是系统的核心流程。客户提供姓名、证件号、联系电话、期望的入住/离店日期、房型偏好。系统需要:
- 查询可用房:根据日期和房型,快速找出所有符合条件的空闲客房。
- 生成预定单:锁定选中的房间,生成一个唯一的预定号,记录客户信息、预定时间、房价、押金等。
- 管理预定状态:支持预定取消(释放房间)、预定查询(凭预定号或客户信息)。
- 入住与退房:将预定转化为实际交易。
- 入住办理:客户凭预定号办理入住,系统将房间状态从“已预定”改为“已入住”,并记录实际入住时间。
- 退房结账:计算住宿费用(考虑是否超时退房),进行支付结算,释放房间状态为“空闲”,并生成消费记录。
- 统计与查询:为管理提供数据支持。
- 客房状态总览:实时查看所有宾馆各房型的占用情况。
- 经营统计:按日、月统计各宾馆的入住率、营收情况。
- 历史记录查询:查询客户的历史入住记录或所有的预定/入住流水。
注意:在实际项目中,需求会复杂得多(如钟点房、续住、团队预定、会员折扣等)。但我们这个教学实例需要抓住主干,避免过度设计。先实现一个稳定可用的核心,后续功能可以在此基础上迭代。
2.2 技术选型与整体架构
明确了“做什么”,接下来是“用什么做”和“怎么做”。鉴于这是一个本地化、单机版的命令行程序,我们的技术栈非常纯粹:
- 核心语言:C++11/14标准。使用现代C++中好用的特性,如
auto、基于范围的for循环、智能指针(std::unique_ptr)来管理资源,但避免过于前沿的特性以保证兼容性和可读性。 - 数据存储:文本文件(如.csv)或二进制文件。这是本项目与使用数据库(如MySQL)的项目最大的区别。我们需要自己设计数据格式,并实现序列化(将对象写入文件)和反序列化(从文件读取并重建对象)的逻辑。这虽然增加了工作量,但能让你深刻理解数据持久化的原理。
- 数据结构:大量使用STL容器,如
std::vector、std::map、std::unordered_map来在内存中高效组织和管理数据(宾馆、客房、订单等对象)。 - 架构设计:采用经典的分层架构,将不同的关注点分离,使代码更清晰、易于维护。
- 数据层(Model):定义核心的数据类,如
Hotel、Room、Reservation、Customer。这些类是系统的“骨骼”。 - 业务逻辑层(Service/Manager):包含
HotelManager、ReservationService等类。它们封装了所有业务规则和操作,例如“办理预定”时,需要检查房间状态、生成订单、更新房间状态、保存数据,这一系列操作在一个服务方法内完成,保证事务性。 - 表示层(View):即我们的命令行界面。它负责接收用户输入,调用相应的业务逻辑,并将结果以文本形式展示出来。这一层应该尽可能“薄”,只处理输入输出,不包含业务规则。
- 数据层(Model):定义核心的数据类,如
整个系统的运行流程可以概括为:程序启动时,从数据文件加载所有数据到内存的STL容器中;用户通过命令行菜单选择操作,表示层调用业务层相应的方法;业务方法修改内存中的容器数据,并在操作成功后(或定期)将更改写回数据文件。
3. 核心类的详细设计与实现要点
有了架构蓝图,我们就可以开始“浇筑”核心的类了。这是面向对象编程中最有意思的部分,设计得好,后续编码会事半功倍。
3.1 实体类设计:定义系统的数据结构
我们首先设计几个核心的实体类,它们就像数据库里的表结构。
1. Customer(客户类)这个类相对简单,主要用于记录客户的基本信息。
// Customer.h #ifndef CUSTOMER_H #define CUSTOMER_H #include <string> class Customer { private: std::string id; // 客户ID,可以用身份证号或系统生成的唯一ID std::string name; std::string phone; // 可以扩展:邮箱、会员等级、积分等 public: Customer(const std::string& id, const std::string& name, const std::string& phone); // Getter 和 Setter 方法 std::string getId() const { return id; } std::string getName() const { return name; } // ... 其他Getter/Setter // 序列化方法:将对象转化为字符串,便于存储 std::string toFileString() const; // 反序列化方法:从字符串解析并构建对象 static Customer fromFileString(const std::string& data); }; #endif2. Room(客房类)客房是系统的核心资源,其状态变迁驱动着业务流程。
// Room.h #ifndef ROOM_H #define ROOM_H #include <string> enum class RoomStatus { AVAILABLE, BOOKED, OCCUPIED, MAINTENANCE }; // 使用枚举类更安全 class Room { private: std::string roomNumber; // 房间号,如 "101" std::string hotelId; // 所属宾馆ID std::string type; // 房型,如 "Standard" RoomStatus status; double pricePerNight; int capacity; // 可入住人数 public: Room(const std::string& num, const std::string& hotel, const std::string& type, double price, int cap); bool isAvailable() const { return status == RoomStatus::AVAILABLE; } bool book() { if (isAvailable()) { status = RoomStatus::BOOKED; return true;} return false;} bool checkIn() { if (status == RoomStatus::BOOKED) { status = RoomStatus::OCCUPIED; return true;} return false;} bool checkOut() { if (status == RoomStatus::OCCUPIED) { status = RoomStatus::AVAILABLE; return true;} return false;} // 序列化/反序列化方法 std::string toFileString() const; static Room fromFileString(const std::string& data); }; #endif实操心得:使用
enum class代替传统的enum,可以避免枚举值污染全局命名空间,并且是强类型的,不能隐式转换为整数,更安全。房间的状态变更(book,checkIn,checkOut)封装在类内部,并加入了简单的状态校验,这是保证业务逻辑正确性的第一道防线。
3. Reservation(预定单类)预定单是连接客户和客房的纽带,也是一个核心的业务实体。
// Reservation.h #ifndef RESERVATION_H #define RESERVATION_H #include <string> #include <chrono> // 用于处理时间 class Reservation { private: std::string reservationId; // 系统生成的唯一预定号,如 ORDER20240520001 std::string customerId; std::string roomNumber; std::string hotelId; std::chrono::system_clock::time_point createTime; // 预定创建时间 std::chrono::system_clock::time_point checkInDate; std::chrono::system_clock::time_point checkOutDate; double totalPrice; double deposit; // 押金 enum class Status { CONFIRMED, CANCELLED, CHECKED_IN, COMPLETED } status; public: Reservation(const std::string& cid, const std::string& rid, const std::string& hid, const std::string& inDateStr, const std::string& outDateStr, double price); // 生成唯一ID的静态方法 static std::string generateReservationId(); // 将时间点转换为字符串(用于存储和显示) static std::string timeToString(const std::chrono::system_clock::time_point& tp); static std::chrono::system_clock::time_point stringToTime(const std::string& str); // 序列化/反序列化 std::string toFileString() const; static Reservation fromFileString(const std::string& data); }; #endif注意事项:处理日期和时间是业务系统的一个难点。这里使用C++11的
<chrono>库,虽然学习曲线稍陡,但比手动解析字符串要严谨和强大得多。generateReservationId可以设计成基于时间戳和随机数的组合,确保全局唯一。
4. Hotel(宾馆类)宾馆类主要存储宾馆的静态信息。
// Hotel.h #ifndef HOTEL_H #define HOTEL_H #include <string> #include <vector> // 注意:这里不直接包含Room,而是通过ID关联 class Hotel { private: std::string id; std::string name; std::string address; int starRating; // 星级 std::string phone; public: Hotel(const std::string& id, const std::string& name, const std::string& addr, int star, const std::string& phone); // Getter/Setter // 序列化/反序列化 std::string toFileString() const; static Hotel fromFileString(const std::string& data); }; #endif3.2 管理类与服务类设计:组织业务逻辑
实体类定义好了“数据是什么”,管理类则负责“数据怎么存、怎么找”,服务类负责“业务怎么做”。
1. DataManager(数据管理器)—— 单例模式应用这是一个全局的数据管理中心,采用单例模式确保整个程序只有一份数据实例。
// DataManager.h #ifndef DATAMANAGER_H #define DATAMANAGER_H #include <unordered_map> #include <vector> #include <memory> #include "Hotel.h" #include "Room.h" #include "Customer.h" #include "Reservation.h" class DataManager { private: static DataManager* instance; // 单例指针 DataManager(); // 构造函数私有化 std::unordered_map<std::string, Hotel> hotels; // key: hotelId std::unordered_map<std::string, Room> rooms; // key: hotelId_roomNumber std::unordered_map<std::string, Customer> customers; // key: customerId std::unordered_map<std::string, Reservation> reservations; // key: reservationId // 文件路径 std::string hotelFile = "hotels.dat"; std::string roomFile = "rooms.dat"; // ... 其他文件 public: ~DataManager(); // 删除拷贝构造和赋值,确保唯一性 DataManager(const DataManager&) = delete; DataManager& operator=(const DataManager&) = delete; // 获取单例对象的静态方法 static DataManager& getInstance(); // 数据访问接口 Hotel* getHotel(const std::string& id); Room* getRoom(const std::string& hotelId, const std::string& roomNumber); bool addRoom(const Room& room); std::vector<Room*> findAvailableRooms(const std::string& hotelId, const std::string& type, const std::chrono::system_clock::time_point& start, const std::chrono::system_clock::time_point& end); // ... 其他大量的数据查询和操作方法 // 数据持久化 bool loadAllData(); bool saveAllData(); bool saveHotels(); bool saveRooms(); // ... 其他保存方法 }; #endif在DataManager.cpp中,需要实现loadAllData和saveAllData。以加载房间数据为例:
bool DataManager::loadRooms() { std::ifstream file(roomFile); if (!file.is_open()) { std::cerr << "Warning: Cannot open room file, maybe first run." << std::endl; return false; } std::string line; rooms.clear(); // 清空现有数据 while (std::getline(file, line)) { if (line.empty()) continue; try { Room room = Room::fromFileString(line); std::string key = room.getHotelId() + "_" + room.getRoomNumber(); rooms[key] = room; } catch (const std::exception& e) { std::cerr << "Error parsing room line: " << line << " - " << e.what() << std::endl; } } file.close(); return true; }踩坑记录:单例模式的析构函数很重要!需要在
~DataManager()中调用saveAllData(),确保程序异常退出时数据也能尽可能保存。另外,数据文件读写一定要做好异常处理,文件不存在、格式错误是常见情况,程序不能因此崩溃。
2. ReservationService(预定服务类)这个类封装了“预定”这个核心业务流程。它依赖于DataManager来获取和更新数据。
// ReservationService.h #ifndef RESERVATIONSERVICE_H #define RESERVATIONSERVICE_H #include <string> class ReservationService { public: struct BookingQuery { std::string customerId; std::string customerName; std::string customerPhone; std::string hotelId; std::string roomType; std::string checkInDateStr; std::string checkOutDateStr; }; struct BookingResult { bool success; std::string reservationId; std::string message; double totalPrice; double deposit; }; BookingResult bookRoom(const BookingQuery& query); bool cancelReservation(const std::string& reservationId); bool checkIn(const std::string& reservationId); BookingResult checkOut(const std::string& reservationId); // 退房会返回结算信息 // ... 其他查询方法 }; #endifbookRoom方法的实现逻辑是业务核心:
ReservationService::BookingResult ReservationService::bookRoom(const BookingQuery& query) { BookingResult result; result.success = false; // 1. 参数基础校验 if (query.checkInDateStr.empty() || query.checkOutDateStr.empty() || query.hotelId.empty()) { result.message = "Invalid input: missing required fields."; return result; } // 2. 日期转换与校验(检查离店日期是否晚于入住日期) auto checkInDate = Reservation::stringToTime(query.checkInDateStr); auto checkOutDate = Reservation::stringToTime(query.checkOutDateStr); if (checkOutDate <= checkInDate) { result.message = "Invalid date: check-out date must be after check-in date."; return result; } // 3. 查找或创建客户 DataManager& dm = DataManager::getInstance(); Customer* customer = dm.getCustomer(query.customerId); if (!customer && !query.customerName.empty()) { // 新客户,创建并保存 std::string newId = query.customerId.empty() ? generateCustomerId(query.customerPhone) : query.customerId; customer = &dm.addCustomer(Customer(newId, query.customerName, query.customerPhone)); } if (!customer) { result.message = "Customer information is invalid or missing."; return result; } // 4. 查找可用房间 auto availableRooms = dm.findAvailableRooms(query.hotelId, query.roomType, checkInDate, checkOutDate); if (availableRooms.empty()) { result.message = "No available room found for your criteria."; return result; } // 5. 选择房间(这里简化处理,选第一间) Room* selectedRoom = availableRooms[0]; // 6. 计算价格(简化:价格*天数) int nights = std::chrono::duration_cast<std::chrono::hours>(checkOutDate - checkInDate).count() / 24; double totalPrice = selectedRoom->getPricePerNight() * nights; double deposit = totalPrice * 0.5; // 押金为总价的50% // 7. 创建预定单 std::string resId = Reservation::generateReservationId(); Reservation newRes(customer->getId(), selectedRoom->getRoomNumber(), query.hotelId, query.checkInDateStr, query.checkOutDateStr, totalPrice); newRes.setDeposit(deposit); // 8. 更新房间状态 if (!selectedRoom->book()) { result.message = "Room status conflict, booking failed."; return result; } // 9. 保存预定单 if (!dm.addReservation(newRes)) { selectedRoom->checkOut(); // 回滚房间状态 result.message = "Failed to save reservation."; return result; } // 10. 保存数据到文件 if (!dm.saveRooms() || !dm.saveReservations()) { // 这里处理更复杂,可能需要事务回滚。简化处理:记录错误日志。 std::cerr << "Warning: Booking succeeded but data may not be saved persistently." << std::endl; } // 11. 返回成功结果 result.success = true; result.reservationId = resId; result.totalPrice = totalPrice; result.deposit = deposit; result.message = "Booking successful! Your reservation ID is: " + resId; return result; }核心技巧:注意上述流程中的“回滚”思想(第9步)。在涉及多个数据修改的操作中,如果后续步骤失败,应该尝试撤销前面已成功的步骤,以保持数据一致性。在更严谨的实现中,会引入“事务”的概念。
4. 数据持久化:文件读写与序列化实战
不使用数据库,我们就必须自己搞定数据的“存”和“取”。这里有两个主流选择:文本格式(如CSV、JSON)和二进制格式。
4.1 文本格式 vs 二进制格式
| 特性 | 文本格式 (如 CSV) | 二进制格式 |
|---|---|---|
| 可读性 | 高,直接用文本编辑器查看和调试 | 低,乱码,无法直接阅读 |
| 实现难度 | 低,使用std::getline和字符串分割即可 | 中,需处理字节对齐、数据类型转换 |
| 存储空间 | 较大,数字也以字符形式存储 | 较小,直接存储二进制值 |
| 读写速度 | 较慢,涉及字符串解析 | 较快,直接内存拷贝 |
| 数据安全 | 低,容易被意外修改 | 相对高 |
| 扩展性 | 好,增加字段容易,兼容性好 | 差,数据结构变动后旧文件无法读取 |
对于教学项目,推荐使用CSV格式。它简单直观,调试方便。我们之前类定义中的toFileString和fromFileString就是为此服务的。
4.2 CSV序列化实现示例
以Room类为例:
// Room.cpp #include "Room.h" #include <sstream> std::string Room::toFileString() const { std::ostringstream oss; // 约定字段顺序:roomNumber, hotelId, type, status, pricePerNight, capacity oss << roomNumber << "," << hotelId << "," << type << "," << static_cast<int>(status) << "," // 枚举转int存储 << pricePerNight << "," << capacity; return oss.str(); } Room Room::fromFileString(const std::string& data) { std::istringstream iss(data); std::string token; std::vector<std::string> tokens; while (std::getline(iss, token, ',')) { tokens.push_back(token); } if (tokens.size() != 6) { throw std::runtime_error("Invalid room data format: " + data); } std::string roomNumber = tokens[0]; std::string hotelId = tokens[1]; std::string type = tokens[2]; RoomStatus status = static_cast<RoomStatus>(std::stoi(tokens[3])); double price = std::stod(tokens[4]); int capacity = std::stoi(tokens[5]); Room room(roomNumber, hotelId, type, price, capacity); room.status = status; // 注意:这里直接设置状态,因为构造函数默认是AVAILABLE return room; }避坑指南:CSV解析要特别注意逗号转义问题。如果字段内容本身包含逗号,简单的
std::getline(iss, token, ',')就会出错。在实际项目中,可以考虑使用成熟的库(如fast-cpp-csv-parser),或者约定使用其他分隔符(如|或\t),或者将字段用双引号包裹。这里为了简化,我们假设输入数据不包含逗号。
4.3 数据加载与保存的时机
- 启动时加载:在
DataManager的构造函数或loadAllData方法中,按顺序加载所有数据文件(宾馆、客房、客户、订单)。 - 操作时保存:有两种策略:
- 实时保存:每次增删改操作后,立即调用对应的
saveXxx()方法。优点是数据丢失风险小,缺点是频繁IO操作影响性能,且在一次业务操作中修改多个文件时,若中途崩溃可能导致数据不一致。 - 定时/退出时保存:在内存中操作,程序正常退出时或定时(如每5分钟)调用
saveAllData()。优点是性能好,操作原子性强(全部成功或全部失败),缺点是意外崩溃会丢失最后一次保存后的所有数据。折中方案:对于关键业务操作(如预定、入住、退房),采用实时保存;对于不重要的信息更新,可以采用定时保存。在我们的教学项目中,为了简单可靠,可以在DataManager析构函数和每个修改数据的Service方法末尾调用保存。
- 实时保存:每次增删改操作后,立即调用对应的
5. 命令行用户界面的设计与实现
一个友好的CLI(命令行界面)是项目用户体验的关键。我们需要一个清晰的菜单驱动界面。
5.1 主菜单与模块化控制
// Main.cpp #include <iostream> #include <limits> // 用于清除输入缓冲区 #include "ReservationService.h" #include "DataManager.h" void clearInputBuffer() { std::cin.clear(); std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); } void displayMainMenu() { std::cout << "\n========== 城市宾馆预定系统 ==========\n"; std::cout << "1. 客房查询与预定\n"; std::cout << "2. 办理入住\n"; std::cout << "3. 办理退房\n"; std::cout << "4. 预定查询/取消\n"; std::cout << "5. 客房管理(管理员)\n"; std::cout << "6. 统计报表\n"; std::cout << "0. 退出系统\n"; std::cout << "=====================================\n"; std::cout << "请选择操作 (0-6): "; } void handleReservation() { std::cout << "\n--- 客房查询与预定 ---\n"; // 1. 获取用户输入的查询条件(城市、入住日期、离店日期、房型) std::string city, checkInDate, checkOutDate, roomType; std::cout << "请输入城市: "; std::getline(std::cin, city); std::cout << "请输入入住日期 (YYYY-MM-DD): "; std::getline(std::cin, checkInDate); // ... 获取其他输入 // 2. 调用 DataManager 查询符合条件的宾馆和房间 // 3. 列表展示查询结果 // 4. 用户选择房间后,收集客户信息,调用 ReservationService::bookRoom // 5. 显示预定结果 } int main() { // 初始化数据 if (!DataManager::getInstance().loadAllData()) { std::cerr << "Failed to load initial data. System may not work properly.\n"; } int choice = -1; while (choice != 0) { displayMainMenu(); std::cin >> choice; clearInputBuffer(); // 清除换行符 switch (choice) { case 1: handleReservation(); break; case 2: /* 调用办理入住函数 */ break; case 3: /* 调用办理退房函数 */ break; case 4: /* 调用预定查询函数 */ break; case 5: /* 进入管理员子菜单 */ break; case 6: /* 显示统计报表 */ break; case 0: std::cout << "感谢使用,再见!\n"; break; default: std::cout << "无效选择,请重新输入。\n"; } } // 程序结束前保存数据 DataManager::getInstance().saveAllData(); return 0; }5.2 输入验证与错误处理
命令行界面最大的挑战是处理用户各种“奇怪”的输入。必须进行严格的验证。
bool isValidDate(const std::string& dateStr) { // 简单正则匹配 YYYY-MM-DD,实际项目中应使用更严格的检查,并考虑闰年等 std::regex datePattern(R"(\d{4}-\d{2}-\d{2})"); if (!std::regex_match(dateStr, datePattern)) return false; // 可以进一步解析年、月、日,检查范围 // ... return true; } std::string getValidatedInput(const std::string& prompt, const std::function<bool(const std::string&)>& validator) { std::string input; while (true) { std::cout << prompt; std::getline(std::cin, input); if (validator(input)) { return input; } else { std::cout << "输入无效,请重新输入。\n"; } } } // 使用示例 std::string date = getValidatedInput("请输入入住日期 (YYYY-MM-DD): ", isValidDate);6. 项目编译、测试与常见问题排查
6.1 项目组织与编译
建议的目录结构:
HotelBookingSystem/ ├── CMakeLists.txt # CMake构建脚本 ├── src/ │ ├── Main.cpp │ ├── Model/ │ │ ├── Customer.h/.cpp │ │ ├── Hotel.h/.cpp │ │ ├── Room.h/.cpp │ │ └── Reservation.h/.cpp │ ├── Service/ │ │ ├── DataManager.h/.cpp │ │ └── ReservationService.h/.cpp │ └── Utils/ │ └── DateHelper.h/.cpp # 日期处理工具函数 ├── data/ # 数据文件目录 │ ├── hotels.dat │ ├── rooms.dat │ └── ... └── build/ # 编译输出目录(由CMake生成)使用CMake管理项目是现代C++项目的标准做法。一个简单的CMakeLists.txt:
cmake_minimum_required(VERSION 3.10) project(HotelBookingSystem) set(CMAKE_CXX_STANDARD 14) # 使用C++14标准 # 包含头文件目录 include_directories(${PROJECT_SOURCE_DIR}/src) # 查找所有源文件 file(GLOB_RECURSE SOURCES "src/*.cpp") # 生成可执行文件 add_executable(HotelBookingSystem ${SOURCES})在项目根目录下执行:
mkdir build && cd build cmake .. make ./HotelBookingSystem6.2 核心功能测试用例
编写简单的测试代码来验证核心逻辑,可以放在一个单独的Test.cpp里,或者直接在main函数初始化后调用。
void runBasicTests() { DataManager& dm = DataManager::getInstance(); // 测试1:添加宾馆和房间 Hotel h1("H001", "和平饭店", "上海南京东路", 5, "021-63216888"); dm.addHotel(h1); Room r1("101", "H001", "Standard", 500.0, 2); dm.addRoom(r1); assert(dm.getHotel("H001") != nullptr); assert(dm.getRoom("H001", "101")->isAvailable()); // 测试2:预定房间 ReservationService rs; ReservationService::BookingQuery q; q.customerName = "张三"; q.customerPhone = "13800138000"; q.hotelId = "H001"; q.roomType = "Standard"; q.checkInDateStr = "2024-05-20"; q.checkOutDateStr = "2024-05-22"; auto result = rs.bookRoom(q); assert(result.success == true); assert(dm.getRoom("H001", "101")->isAvailable() == false); // 房间应被预定 // 测试3:查询预定 // ... 更多测试 std::cout << "All basic tests passed!\n"; }6.3 常见问题与排查技巧实录
在开发过程中,你几乎一定会遇到下面这些问题:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 程序崩溃,提示“Segmentation fault” | 1. 访问了空指针或野指针。 2. 容器(如 vector)越界访问。3. 对象已被销毁(如返回了局部变量的引用)。 | 1. 使用调试器(如GDB)运行程序,查看崩溃时的调用栈。 2. 在所有指针访问前加断言 assert(ptr != nullptr)。3. 检查函数返回值,确保返回的是对象的拷贝或智能指针管理的对象。 |
| 数据修改后,重启程序发现没保存 | 1.saveAllData未被调用(程序异常终止)。2. 文件写入路径错误或权限不足。 3. 序列化/反序列化逻辑错误,数据没正确写入。 | 1. 在DataManager析构函数和每个修改操作后立即保存并检查返回值。2. 打印完整的文件路径,检查文件是否生成,内容是否正确。 3. 编写单元测试,单独测试 toFileString和fromFileString的互逆性。 |
| 查询结果不对,找不到明明存在的房间 | 1.findAvailableRooms逻辑有误,日期判断出错。2. 房间状态更新不及时或错误。 3. 数据加载时,键(Key)生成规则不一致(如 hotelId_roomNumber拼接时多了空格)。 | 1. 在findAvailableRooms函数内打印中间结果,逐步调试。2. 检查 book,checkIn,checkOut函数中的状态转换逻辑。3. 对比内存中容器的键和查询时使用的键是否完全一致。 |
| 用户输入日期后程序逻辑异常 | 1. 日期字符串格式不符合预期。 2. stringToTime函数无法解析,抛出异常未被捕获。 | 1. 在输入后立即打印日期字符串进行验证。 2. 用 try-catch块包裹日期转换和业务逻辑,给用户友好的错误提示。 |
| 在多处修改数据后,出现状态不一致 | 业务逻辑不是原子操作,部分成功部分失败。 | 1. 将涉及多个数据修改的操作封装在DataManager的一个方法中,在该方法内进行所有修改,最后统一保存。如果任何一步失败,则进行回滚(恢复原状)。2. 引入简单的日志,记录每个关键操作,便于回溯。 |
我个人在实现这个系统时的最深体会是:数据一致性是生命线。尤其是在没有数据库事务支持的情况下,自己管理文件存储,必须对“读-改-写”这个流程保持高度警惕。一个实用的技巧是,在开发初期,为每个核心类重载operator<<,方便打印其完整状态,调试时会省力很多。另外,不要试图在第一版就实现所有功能,先把“预定-入住-退房”这个核心闭环跑通,确保数据能正确保存和加载,之后再迭代增加管理员功能、统计报表、更复杂的查询等,这样能有效控制复杂度,每一步都走得稳。