C++实战:从零构建宾馆预定系统,掌握文件存储与业务逻辑设计

📅 2026/7/14 4:24:31 👁️ 阅读次数 📝 编程学习
C++实战:从零构建宾馆预定系统,掌握文件存储与业务逻辑设计

1. 项目概述:从零构建一个真实的宾馆预定系统

最近在带几个新人做项目复盘,发现很多朋友对C++的理解还停留在书本上的语法和算法题,一提到用C++做个“能用的”东西,比如一个宾馆预定系统,就有点无从下手。这其实挺可惜的,C++在构建需要高性能、稳定性和精细内存管理的桌面应用或后台服务时,依然有不可替代的优势。这个“基于C++的城市宾馆预定系统”项目,就是一个绝佳的练手机会,它能让你把类、文件I/O、STL容器、多模块编程这些知识点串起来,形成一个完整的、可交付的应用程序。

简单来说,我们要做的是一个运行在命令行终端(Console)上的管理系统。它需要管理多家宾馆的信息、大量的客房资源,并处理用户的预定、入住、退房等一系列业务流程。听起来是不是有点像大学数据库课的大作业?但区别在于,我们不依赖任何图形界面库或网络框架,纯粹用C++标准库和文件操作来实现数据的持久化存储,这更能考验你对核心语言特性和系统编程的理解。这个系统适合有一定C++基础(至少学过类和STL)、想通过一个综合性项目提升工程能力的朋友。通过它,你不仅能巩固语法,更能学会如何设计程序结构、处理异常、保证数据一致性,这些才是企业级开发中更看重的“内功”。

2. 系统核心需求分析与整体架构设计

在动手写第一行代码之前,我们必须把系统要干什么、怎么干想清楚。盲目开始编码,最后往往会得到一个结构混乱、难以维护的“屎山”。好的设计是成功的一半。

2.1 核心业务需求拆解

一个宾馆预定系统,无论界面多么花哨,其核心业务逻辑是相对稳定的。我们可以从三个主要角色(管理员、前台、客户)的视角来梳理需求:

  1. 信息管理:这是系统的基石。我们需要维护两类核心数据:
    • 宾馆信息:宾馆名称、地址、星级、联系电话等。一个城市可能有多个合作宾馆。
    • 客房信息:隶属于某个宾馆,包括房型(如标准间、大床房、套房)、房间号、状态(空闲、已预定、已入住)、价格、可入住人数等。
  2. 预定业务:这是系统的核心流程。客户提供姓名、证件号、联系电话、期望的入住/离店日期、房型偏好。系统需要:
    • 查询可用房:根据日期和房型,快速找出所有符合条件的空闲客房。
    • 生成预定单:锁定选中的房间,生成一个唯一的预定号,记录客户信息、预定时间、房价、押金等。
    • 管理预定状态:支持预定取消(释放房间)、预定查询(凭预定号或客户信息)。
  3. 入住与退房:将预定转化为实际交易。
    • 入住办理:客户凭预定号办理入住,系统将房间状态从“已预定”改为“已入住”,并记录实际入住时间。
    • 退房结账:计算住宿费用(考虑是否超时退房),进行支付结算,释放房间状态为“空闲”,并生成消费记录。
  4. 统计与查询:为管理提供数据支持。
    • 客房状态总览:实时查看所有宾馆各房型的占用情况。
    • 经营统计:按日、月统计各宾馆的入住率、营收情况。
    • 历史记录查询:查询客户的历史入住记录或所有的预定/入住流水。

注意:在实际项目中,需求会复杂得多(如钟点房、续住、团队预定、会员折扣等)。但我们这个教学实例需要抓住主干,避免过度设计。先实现一个稳定可用的核心,后续功能可以在此基础上迭代。

2.2 技术选型与整体架构

明确了“做什么”,接下来是“用什么做”和“怎么做”。鉴于这是一个本地化、单机版的命令行程序,我们的技术栈非常纯粹:

  • 核心语言:C++11/14标准。使用现代C++中好用的特性,如auto、基于范围的for循环、智能指针(std::unique_ptr)来管理资源,但避免过于前沿的特性以保证兼容性和可读性。
  • 数据存储文本文件(如.csv)或二进制文件。这是本项目与使用数据库(如MySQL)的项目最大的区别。我们需要自己设计数据格式,并实现序列化(将对象写入文件)和反序列化(从文件读取并重建对象)的逻辑。这虽然增加了工作量,但能让你深刻理解数据持久化的原理。
  • 数据结构:大量使用STL容器,如std::vectorstd::mapstd::unordered_map来在内存中高效组织和管理数据(宾馆、客房、订单等对象)。
  • 架构设计:采用经典的分层架构,将不同的关注点分离,使代码更清晰、易于维护。
    • 数据层(Model):定义核心的数据类,如HotelRoomReservationCustomer。这些类是系统的“骨骼”。
    • 业务逻辑层(Service/Manager):包含HotelManagerReservationService等类。它们封装了所有业务规则和操作,例如“办理预定”时,需要检查房间状态、生成订单、更新房间状态、保存数据,这一系列操作在一个服务方法内完成,保证事务性。
    • 表示层(View):即我们的命令行界面。它负责接收用户输入,调用相应的业务逻辑,并将结果以文本形式展示出来。这一层应该尽可能“薄”,只处理输入输出,不包含业务规则。

整个系统的运行流程可以概括为:程序启动时,从数据文件加载所有数据到内存的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); }; #endif

2. 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,可以避免枚举值污染全局命名空间,并且是强类型的,不能隐式转换为整数,更安全。房间的状态变更(bookcheckIncheckOut)封装在类内部,并加入了简单的状态校验,这是保证业务逻辑正确性的第一道防线。

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); }; #endif

3.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中,需要实现loadAllDatasaveAllData。以加载房间数据为例:

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); // 退房会返回结算信息 // ... 其他查询方法 }; #endif

bookRoom方法的实现逻辑是业务核心:

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格式。它简单直观,调试方便。我们之前类定义中的toFileStringfromFileString就是为此服务的。

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方法中,按顺序加载所有数据文件(宾馆、客房、客户、订单)。
  • 操作时保存:有两种策略:
    1. 实时保存:每次增删改操作后,立即调用对应的saveXxx()方法。优点是数据丢失风险小,缺点是频繁IO操作影响性能,且在一次业务操作中修改多个文件时,若中途崩溃可能导致数据不一致。
    2. 定时/退出时保存:在内存中操作,程序正常退出时或定时(如每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 ./HotelBookingSystem

6.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. 编写单元测试,单独测试toFileStringfromFileString的互逆性。
查询结果不对,找不到明明存在的房间1.findAvailableRooms逻辑有误,日期判断出错。
2. 房间状态更新不及时或错误。
3. 数据加载时,键(Key)生成规则不一致(如hotelId_roomNumber拼接时多了空格)。
1. 在findAvailableRooms函数内打印中间结果,逐步调试。
2. 检查bookcheckIncheckOut函数中的状态转换逻辑。
3. 对比内存中容器的键和查询时使用的键是否完全一致。
用户输入日期后程序逻辑异常1. 日期字符串格式不符合预期。
2.stringToTime函数无法解析,抛出异常未被捕获。
1. 在输入后立即打印日期字符串进行验证。
2. 用try-catch块包裹日期转换和业务逻辑,给用户友好的错误提示。
在多处修改数据后,出现状态不一致业务逻辑不是原子操作,部分成功部分失败。1. 将涉及多个数据修改的操作封装在DataManager的一个方法中,在该方法内进行所有修改,最后统一保存。如果任何一步失败,则进行回滚(恢复原状)。
2. 引入简单的日志,记录每个关键操作,便于回溯。

我个人在实现这个系统时的最深体会是数据一致性是生命线。尤其是在没有数据库事务支持的情况下,自己管理文件存储,必须对“读-改-写”这个流程保持高度警惕。一个实用的技巧是,在开发初期,为每个核心类重载operator<<,方便打印其完整状态,调试时会省力很多。另外,不要试图在第一版就实现所有功能,先把“预定-入住-退房”这个核心闭环跑通,确保数据能正确保存和加载,之后再迭代增加管理员功能、统计报表、更复杂的查询等,这样能有效控制复杂度,每一步都走得稳。