从零到一:C++飞机订票系统项目复盘,聊聊我踩过的那些坑(文件操作、全局变量设计)
最近完成了一个C++飞机订票系统的项目,整个过程可以说是"痛并快乐着"。作为一个C++学习者,这个项目让我深刻体会到了理论知识和实际开发之间的差距。今天就来分享几个让我熬夜调试的典型问题,希望能帮到正在做类似项目的你。
1. 文件操作的那些坑
文件操作看似简单,但实际开发中处处是陷阱。我在这个项目中至少踩了三个大坑。
1.1 文件打开模式的迷思
刚开始我天真地以为ios::out就能搞定所有写入需求,结果数据被反复覆盖。后来才发现不同场景需要不同的打开模式组合:
// 错误示范 - 每次都会清空文件 fstream fs("data.txt", ios::out); // 正确做法 - 追加模式 fstream fs("data.txt", ios::app | ios::out); // 读取时 fstream fs("data.txt", ios::in);实际开发中发现,最安全的做法是:
- 写入新数据:
ios::app | ios::out - 覆盖写入:
ios::trunc | ios::out - 读取数据:
ios::in
1.2 重复读取导致的数据混乱
我的航班管理类中有多个函数都需要读取文件数据,最初的设计是这样的:
void ControlFlight::AddFlights() { ReadfromFile_Flights(); // 读取文件 // ...添加新航班 WritetoFile_Flights(); // 写入文件 } void ControlFlight::DeleteFlights() { ReadfromFile_Flights(); // 再次读取 // ...删除航班 WritetoFile_Flights(); }问题来了:每次操作都重新读取文件,如果连续调用多个函数,容器中的数据会不断累积。解决方案是在读取函数开头清空容器:
void ControlFlight::ReadfromFile_Flights() { mv_Flights.clear(); // 关键一步! // ...读取数据 }1.3 文件流状态管理
另一个坑是没检查文件流状态。有用户反馈程序崩溃,最后发现是因为文件不存在。现在我会这样处理:
fstream fs("data.txt", ios::in); if (!fs) { cerr << "文件打开失败!创建新文件..." << endl; fs.open("data.txt", ios::out); fs.close(); return; }2. 全局变量的设计陷阱
订单号管理是我遇到的另一个头疼问题。
2.1 全局变量的重置问题
我最初的设计是用全局变量C_OrderNumber来生成订单号:
int C_OrderNumber = 0; // 全局变量 void ClientActions::BookTicket() { ReadfromFile_Orders(); C_OrderNumber++; // 自增 // ...其他操作 }但很快发现,每次调用ReadfromFile_Orders()都会重新计算订单号,导致订单号重复。解决方案是在每个函数结束时重置全局变量:
void ClientActions::BookTicket() { ReadfromFile_Orders(); C_OrderNumber++; // ...订票逻辑 WritetoFile_Orders(); C_OrderNumber = 0; // 重置 }2.2 更优雅的解决方案
后来我意识到,全局变量终究不是最佳实践。改进方案是使用类的静态成员:
class ClientActions { private: static int s_OrderCounter; // ... public: static int GetNextOrderNumber() { return ++s_OrderCounter; } };初始化时从文件读取最大值,这样就避免了全局变量的管理问题。
3. vector操作的注意事项
STL容器用起来方便,但也有不少坑。
3.1 erase的迭代器陷阱
删除航班时,我最初这样写:
for (size_t i = 0; i < mv_Flights.size(); i++) { if (shouldDelete(mv_Flights[i])) { mv_Flights.erase(mv_Flights.begin() + i); } }这会导致迭代器失效。正确做法是:
for (auto it = mv_Flights.begin(); it != mv_Flights.end(); ) { if (shouldDelete(*it)) { it = mv_Flights.erase(it); } else { ++it; } }3.2 性能优化
当数据量增大时,频繁的vector操作会成为性能瓶颈。我做了以下优化:
- 减少不必要的拷贝:使用移动语义
- 预留空间:
mv_Flights.reserve(100) - 考虑改用list:当频繁插入删除时
4. 项目架构的演进
随着功能增加,最初的架构开始显得力不从心。
4.1 从面向过程到面向对象
第一版代码把所有逻辑都写在main.cpp里,很快就变得难以维护。重构后的架构:
├── ControlFlight.h/cpp // 航班管理 ├── ManagerActions.h/cpp // 管理员操作 ├── ClientActions.h/cpp // 客户操作 └── Main.cpp // 入口4.2 数据持久化的改进
最初使用纯文本存储,后来发现几个问题:
- 没有数据校验
- 没有备份机制
- 并发访问会出问题
改进方案:
- 添加数据校验逻辑
- 定期生成备份文件
- 考虑使用SQLite等轻型数据库
4.3 异常处理的重构
最初的错误处理全是cout输出,用户体验很差。改进后:
try { // 业务逻辑 } catch (const FileException& e) { // 专门处理文件错误 ShowErrorDialog(e.what()); } catch (const std::exception& e) { // 通用错误处理 LogError(e.what()); }5. 测试与调试经验
调试过程让我收获了不少实战经验。
5.1 单元测试的重要性
为关键功能编写测试用例:
void TestFlightManagement() { ControlFlight cf; // 测试添加 cf.AddTestFlight("TEST123"); assert(cf.FindFlight("TEST123")); // 测试删除 cf.DeleteTestFlight("TEST123"); assert(!cf.FindFlight("TEST123")); }5.2 日志系统的价值
添加简单的日志功能帮助巨大:
class Logger { public: static void Log(const string& msg) { ofstream log("system.log", ios::app); log << GetCurrentTime() << " - " << msg << endl; } };5.3 用户输入验证
最初的版本几乎没有输入验证,导致各种异常。后来添加了:
bool ValidateTimeFormat(const string& time) { // 检查时间格式HH:MM regex pattern(R"(\d{2}:\d{2})"); return regex_match(time, pattern); }6. 性能优化实战
当航班数据达到上千条时,性能问题开始显现。
6.1 查询优化
最初的航班查询是线性搜索:
for (const auto& flight : mv_Flights) { if (flight.m_Flight_Number == target) { return flight; } }优化方案:
- 使用
std::unordered_map建立索引 - 对常用查询字段建立多级索引
6.2 内存管理
发现内存占用过高后,我做了以下改进:
- 使用
std::string_view替代字符串拷贝 - 对大型数据使用智能指针
- 实现延迟加载机制
6.3 多线程尝试
虽然最终没有采用,但我实验性地添加了多线程支持:
std::future<void> result = std::async(std::launch::async, [&](){ // 后台加载数据 LoadFlightData(); }); // 主线程继续其他工作7. 项目总结与反思
回顾整个项目,有几个关键收获:
- 设计先于编码:前期设计不充分导致多次重构
- 测试驱动开发:越早开始写测试,后期越轻松
- 代码可维护性:良好的命名和注释节省了大量调试时间
- 性能考量:数据量小时忽略的问题,在规模增长后都会暴露
如果重做这个项目,我会:
- 采用更现代的C++特性(如C++17的
std::filesystem) - 实现真正的GUI界面而非控制台
- 加入网络功能实现多终端访问
这个项目虽然基础,但涵盖了C++开发的多个核心概念。对于想深入学习C++的朋友,我的建议是:先完成一个这样的综合项目,再回头系统学习STL、内存管理等高级主题,效果会比单纯看书好得多。