C++停车场收费系统实战:面向对象设计、STL容器与文件持久化
1. 项目概述与核心价值最近在整理硬盘里的老项目翻出来一个当年练手做的C停车场收费系统。这个项目虽然不是什么企业级应用但麻雀虽小五脏俱全从控制台交互、文件读写、时间计算到简单的计费逻辑都涵盖了非常适合C初学者用来理解面向对象编程和项目实战。网上类似的源码很多但要么逻辑混乱要么注释不清我这个版本是我自己边学边做反复重构了几次自认为在结构清晰度和代码可读性上还算“独一无二”。今天就把这个项目的完整源码和设计思路分享出来希望能给正在学习C、想找个完整小项目练手的你一些启发。这个系统模拟了一个小型停车场的核心业务流程车辆入场登记、出场计费结算、以及基础的数据查询统计。它完全运行在控制台使用文件来持久化存储停车记录避免了数据库的复杂性让你能聚焦于C核心语法和面向对象设计。通过这个项目你能实战演练类的设计如Vehicle、ParkingRecord、STL容器的使用vector、map、文件流操作fstream以及时间处理ctime等关键知识点。无论你是刚学完C基础语法还是想巩固面向对象思想这个项目都是一个不错的起点。2. 系统整体设计与核心类解析2.1 需求分析与功能模块划分做任何项目第一步都是理清需求。对于一个最基础的停车场收费系统我们需要模拟以下场景一辆车开进来记录它的车牌号和入场时间车开走时根据停放时长和预设的费率计算停车费并生成记录管理员需要能查看当前在场车辆、历史记录以及营收统计。基于此我将系统划分为四大核心模块车辆与记录管理模块这是系统的数据核心需要定义车辆Vehicle和停车记录ParkingRecord的数据结构并管理它们的生命周期。计费策略模块这是业务逻辑的核心需要封装不同的计费规则比如按小时计费、有免费时长、夜间费率不同等。这里我采用策略模式的思想便于未来扩展。数据持久化模块因为没连接数据库我们需要将车辆入场记录、出场结算记录保存到本地文件中确保程序重启后数据不丢失。用户交互模块提供控制台菜单让用户模拟管理员可以通过输入数字选项来执行各种操作。这样的模块化设计使得代码结构清晰各司其职也方便你分步实现和调试。2.2 核心类设计详解接下来我们深入看看几个关键类的设计。这是面向对象编程的精华所在。2.2.1 Vehicle类车辆的抽象车辆类很简单主要就是封装车牌号这个属性。为什么不把入场时间也放进来因为一辆车可能有多条停车记录多次进出入场时间是属于某一次具体的停车行为的。所以Vehicle类更像是一个实体标识。class Vehicle { private: std::string licensePlate; // 车牌号 public: Vehicle(const std::string plate) : licensePlate(plate) {} std::string getLicensePlate() const { return licensePlate; } // 重载运算符便于在容器中查找 bool operator(const Vehicle other) const { return licensePlate other.licensePlate; } };注意这里重载了运算符这样我们就可以直接把Vehicle对象放入std::vector并使用std::find来查找非常方便。这是STL应用的一个小技巧。2.2.2 ParkingRecord类一次停车行为的完整描述停车记录类是系统的核心数据单元它关联了一辆车和一次具体的停车事件。class ParkingRecord { private: Vehicle vehicle; std::time_t entryTime; // 入场时间戳 std::time_t exitTime; // 出场时间戳-1表示尚未出场 double fee; // 停车费 bool isPaid; // 是否已支付 public: ParkingRecord(const Vehicle v, std::time_t entry) : vehicle(v), entryTime(entry), exitTime(-1), fee(0.0), isPaid(false) {} // 结算费用 void calculateFee(double hourlyRate) { if (exitTime -1) return; // 未出场 double durationHours difftime(exitTime, entryTime) / 3600.0; // 确保最小计费单位比如不足1小时按1小时算 durationHours ceil(durationHours); fee durationHours * hourlyRate; } void setExitTime(std::time_t exit) { exitTime exit; } std::time_t getEntryTime() const { return entryTime; } // ... 其他getter方法 };实操心得时间处理是停车场系统的关键。这里使用std::time_t通常是自1970年1月1日以来的秒数来存储时间计算时长时用difftime函数。注意difftime返回的是秒要转换成小时。另外ceil函数用于向上取整实现“不足一小时按一小时计”的常见计费规则。exitTime初始化为-1是一个状态标识用来判断车辆是否已出场。2.2.3 BillingStrategy类计费策略的抽象为了让计费规则灵活可变我定义了一个计费策略基类。这是设计模式中“策略模式”的简单应用。class BillingStrategy { public: virtual ~BillingStrategy() default; // 传入入场和出场时间返回计算出的费用 virtual double calculateFee(std::time_t entry, std::time_t exit) const 0; };然后我们可以派生出具体的计费策略比如最简单的按小时计费class HourlyBilling : public BillingStrategy { private: double ratePerHour; // 每小时费率 int freeMinutes; // 免费时长分钟 public: HourlyBilling(double rate, int free 15) : ratePerHour(rate), freeMinutes(free) {} double calculateFee(std::time_t entry, std::time_t exit) const override { double totalSeconds difftime(exit, entry); double totalMinutes totalSeconds / 60.0; // 扣除免费时长 double chargeableMinutes totalMinutes - freeMinutes; if (chargeableMinutes 0) return 0.0; // 转换为小时并向上取整 double chargeableHours ceil(chargeableMinutes / 60.0); return chargeableHours * ratePerHour; } };为什么用策略模式想象一下如果停车场节假日打折或者夜间收费更便宜。如果没有策略模式你就得在ParkingRecord::calculateFee里写一堆if-else。有了它你只需要在系统初始化时传入不同的BillingStrategy子类对象比如HolidayBilling核心业务代码一行都不用改。这体现了“对扩展开放对修改关闭”的原则。3. 核心功能实现与代码拆解3.1 数据管理使用STL容器与文件持久化系统运行时我们需要在内存中管理两类主要数据当前在场的车辆便于快速查找和出场结算和所有的历史停车记录用于查询统计。我选择使用std::map和std::vector。class ParkingSystem { private: std::mapstd::string, ParkingRecord activeParkings; // 车牌号 - 停车记录未出场 std::vectorParkingRecord allRecords; // 所有历史记录 std::unique_ptrBillingStrategy billingStrategy; // 计费策略 const std::string dataFile “parking_data.txt”; // 数据文件 // ... };activeParkings 用std::map存储正在停放的车辆。键Key是车牌号std::string值Value是对应的ParkingRecord对象。选择map是因为车牌号是唯一的并且我们需要根据车牌号快速查找车辆进行出场操作map的查找时间复杂度是O(log n)效率很高。allRecords 用std::vector存储所有记录包括已出场的。因为历史记录主要是遍历查询vector的连续内存访问效率高。billingStrategy 使用std::unique_ptr智能指针来管理计费策略对象避免内存泄漏。数据持久化是让程序“记住”状态的关键。我设计了两个简单的函数loadData()和saveData()使用std::fstream来读写文件。void ParkingSystem::saveData() { std::ofstream outFile(dataFile); if (!outFile) { std::cerr “无法保存数据到文件” std::endl; return; } for (const auto record : allRecords) { // 将每条记录的关键信息车牌、入场时间、出场时间、费用格式化输出到文件 outFile record.getVehicle().getLicensePlate() “,” record.getEntryTime() “,” record.getExitTime() “,” record.getFee() “\n”; } outFile.close(); }loadData()函数则是按行读取文件解析出各个字段重新构建ParkingRecord对象并加入allRecords。对于出场时间为-1的记录还需要将其放入activeParkings中。踩坑记录文件读写最容易出问题的地方是格式。必须确保写入和读取的格式严格一致。我选择用逗号分隔CSV格式简单可靠。另外每次程序退出或关键操作后调用saveData()程序启动时调用loadData()这样才能保证数据一致性。记得处理文件打开失败的情况给用户明确的错误提示。3.2 车辆入场与出场流程实现这是用户最常交互的两个功能我们来看看具体实现。3.2.1 车辆入场 (vehicleEntry)bool ParkingSystem::vehicleEntry(const std::string licensePlate) { // 1. 检查车辆是否已在场内 if (activeParkings.find(licensePlate) ! activeParkings.end()) { std::cout “错误车牌号 ” licensePlate “ 的车辆已在停车场内” std::endl; return false; } // 2. 创建车辆和记录对象 Vehicle newVehicle(licensePlate); std::time_t now std::time(nullptr); // 获取当前时间 ParkingRecord newRecord(newVehicle, now); // 3. 存入活跃记录map activeParkings[licensePlate] newRecord; // 4. 同时存入总记录vector此时出场时间为-1 allRecords.push_back(newRecord); std::cout “车辆 ” licensePlate “ 入场成功时间” std::ctime(now); return true; }流程非常清晰查重 - 创建 - 存储 - 反馈。std::time(nullptr)获取当前系统的日历时间。std::ctime(now)可以将时间戳转换成易读的字符串格式输出。3.2.2 车辆出场与计费 (vehicleExit)出场逻辑比入场稍复杂因为它涉及查找、计费、更新状态和移出活跃列表。bool ParkingSystem::vehicleExit(const std::string licensePlate) { // 1. 查找车辆 auto it activeParkings.find(licensePlate); if (it activeParkings.end()) { std::cout “错误未找到车牌号为 ” licensePlate “ 的在场车辆” std::endl; return false; } // 2. 获取记录设置出场时间 ParkingRecord record it-second; std::time_t exitTime std::time(nullptr); record.setExitTime(exitTime); // 3. 计算费用 if (billingStrategy) { double fee billingStrategy-calculateFee(record.getEntryTime(), exitTime); record.setFee(fee); // 假设ParkingRecord有setFee方法 record.markAsPaid(); // 标记为已支付 } else { std::cerr “计费策略未设置” std::endl; return false; } // 4. 显示账单 std::cout “\n 停车账单 \n”; std::cout “车牌号” licensePlate “\n”; std::cout “入场时间” std::ctime(record.getEntryTime()); std::cout “出场时间” std::ctime(exitTime); std::cout “应付费用” std::fixed std::setprecision(2) record.getFee() “ 元\n”; std::cout “\n”; // 5. 从活跃map中移除 activeParkings.erase(it); // 6. 保存数据可选也可定期保存 saveData(); return true; }注意事项这里有一个关键点allRecords里的记录和activeParkings里的记录在出场前指向的是同一个对象吗在我的设计里不是。allRecords.push_back(newRecord)是值拷贝activeParkings[licensePlate] newRecord也是值拷贝。出场时我们修改的是activeParkings中那个副本然后这个副本被移除了。allRecords里的那个副本还是旧状态这是一个严重的Bug。为了解决这个问题我后来修改了设计让activeParkings存储指向allRecords中元素的指针或迭代器或者只使用一个容器通过状态字段来区分。这是初期设计考虑不周导致的也提醒我们数据结构的设计需要深思熟虑。3.3 查询与统计功能一个基本的系统还需要数据查看功能。我实现了三个查询查看当前在场车辆遍历activeParkings这个map即可。按车牌查询历史记录遍历allRecords这个vector匹配车牌号。统计今日营收遍历allRecords筛选出出场时间在今天内的记录累加费用。这里重点讲一下今日营收统计它涉及时间范围的判断。double ParkingSystem::calculateDailyRevenue() const { double total 0.0; std::time_t now std::time(nullptr); std::tm* localNow std::localtime(now); int todayYear localNow-tm_year; int todayMonth localNow-tm_mon; int todayDay localNow-tm_mday; for (const auto record : allRecords) { if (record.isPaid()) { // 只统计已支付的 std::time_t exitTime record.getExitTime(); if (exitTime ! -1) { std::tm* localExit std::localtime(exitTime); // 比较年、月、日是否相同 if (localExit-tm_year todayYear localExit-tm_mon todayMonth localExit-tm_mday todayDay) { total record.getFee(); } } } } return total; }技术细节std::localtime函数将time_t时间戳转换为本地日历时间的结构体tm从中可以取出年、月、日、时、分、秒等字段。注意tm_year是从1900开始的年数tm_mon月份是0-110代表一月。比较日期时就是比较这三个字段是否完全相同。另外std::localtime返回的是指向静态内存的指针不是线程安全的但在我们这个单线程控制台程序里没有问题。4. 主程序架构与用户交互4.1 控制台菜单驱动为了让用户能操作需要一个简单的文本菜单。我使用一个while循环来持续显示菜单并处理用户输入。void runParkingSystem() { ParkingSystem system; system.loadData(); // 启动时加载数据 // 设置计费策略例如每小时5元前15分钟免费 system.setBillingStrategy(std::make_uniqueHourlyBilling(5.0, 15)); int choice 0; do { printMenu(); std::cin choice; std::cin.ignore(); // 清除输入缓冲区中的换行符 switch (choice) { case 1: { // 车辆入场 std::string plate; std::cout “请输入车牌号”; std::getline(std::cin, plate); system.vehicleEntry(plate); break; } case 2: { // 车辆出场 std::string plate; std::cout “请输入车牌号”; std::getline(std::cin, plate); system.vehicleExit(plate); break; } case 3: // 查看在场车辆 system.showActiveVehicles(); break; case 4: // 查询历史记录 // ... 输入车牌查询 break; case 5: // 统计营收 std::cout “今日总营收” system.calculateDailyRevenue() “ 元\n”; break; case 6: // 保存并退出 system.saveData(); std::cout “数据已保存再见\n”; break; default: std::cout “无效选项请重新输入\n”; } std::cout “\n按回车键继续...”; std::cin.get(); // 暂停一下让用户看清结果 } while (choice ! 6); }交互细节这里有两个容易忽略的输入处理技巧。第一std::cin choice之后输入缓冲区会留下一个换行符\n。如果接下来直接用std::getline(std::cin, plate)读取车牌号getline会立刻读到这个换行符得到一个空字符串。所以需要用std::cin.ignore()忽略掉它。第二在每个操作结束后我用std::cin.get()暂停程序等用户按回车后再清屏或直接显示主菜单这样用户就有时间看清操作结果体验更好。4.2 错误处理与输入验证一个健壮的程序必须处理各种非法输入。对于菜单选择要处理非数字输入对于车牌号可以做简单的格式检查比如不能为空。int getMenuChoice() { int choice; while (true) { std::cout “请选择操作 (1-6): “; if (std::cin choice) { if (choice 1 choice 6) { std::cin.ignore(std::numeric_limitsstd::streamsize::max(), ‘\n’); return choice; } else { std::cout “输入错误请输入1-6之间的数字\n”; } } else { // 输入的不是数字清除错误状态和缓冲区 std::cin.clear(); std::cin.ignore(std::numeric_limitsstd::streamsize::max(), ‘\n’); std::cout “输入错误请输入一个有效的数字\n”; } } }这个getMenuChoice函数实现了完整的输入验证和错误恢复。std::cin choice如果失败比如用户输入了字母cin会进入错误状态后续所有输入操作都会失败。所以需要用std::cin.clear()清除错误状态再用std::cin.ignore清空输入缓冲区然后重新提示用户输入。std::numeric_limitsstd::streamsize::max()表示忽略缓冲区中直到换行符的所有字符。5. 项目编译、运行与扩展思考5.1 环境搭建与编译命令这个项目是纯C标准库实现不依赖任何第三方库因此可以在任何支持C11及以上标准的编译器中运行。我推荐使用GCC(MinGW-w64) 或Clang在命令行编译或者使用Visual Studio创建控制台项目。使用GCC/Clang命令行编译假设你的源码文件是main.cpp,parkingsystem.cpp,parkingsystem.h等。# 编译所有.cpp文件生成可执行文件parking_system.exe (Windows) 或 parking_system (Linux/macOS) g -stdc11 -o parking_system main.cpp parkingsystem.cpp vehicle.cpp billing.cpp # 或者使用clang clang -stdc11 -o parking_system main.cpp parkingsystem.cpp vehicle.cpp billing.cpp-stdc11指定使用C11标准确保智能指针等功能可用。-o指定输出文件名。在Visual Studio中新建一个“控制台应用”项目。将所有的.h和.cpp文件添加到项目中。确保项目属性中“C语言标准”设置为“ISO C11标准”或更高。直接点击“本地Windows调试器”运行即可。5.2 运行效果与测试建议编译成功后运行程序你会看到一个简单的文本菜单。建议你按以下流程测试覆盖主要功能分支正常流程输入车牌京A12345入场 - 再次输入同一车牌应提示“车辆已在场” - 输入另一车牌沪B67890入场 - 选择“查看在场车辆”应看到两辆车 - 为京A12345办理出场应正确计算并显示费用 - 再次查看在场车辆应只剩沪B67890- 查询京A12345的历史记录应能看到完整记录和费用 - 统计营收应包含刚收取的费用。边界测试测试免费时长停车14分钟出场费用应为0。测试不足一小时按一小时计停车61分钟按2小时计费。异常测试出场时输入一个不存在的车牌号程序应给出友好提示而不是崩溃。数据持久化测试录入几辆车后选择“保存并退出”。重新启动程序选择“查看在场车辆”或“查询历史记录”检查数据是否成功加载。5.3 可能的扩展方向与进阶思考这个基础版本完成了核心功能但离一个“实用”的系统还有距离。你可以尝试以下扩展这会让你的C和软件设计能力再上一个台阶修复数据结构缺陷如前所述当前activeParkings和allRecords存储了数据副本导致状态不一致。进阶实现让activeParkings存储指向allRecords中元素的指针ParkingRecord*或迭代器std::vectorParkingRecord::iterator。出场时通过指针/迭代器直接修改allRecords中的对象然后从activeParkings中移除该指针/迭代器。这涉及到对STL迭代器失效规则的深入理解。引入更复杂的计费策略实现DailyMaxBilling每日封顶、NightBilling夜间半价等策略。这需要你设计更复杂的BillingStrategy子类并在ParkingSystem中能够动态切换或组合策略。改用数据库存储将文件存储替换为SQLite数据库。你可以学习使用C的SQLite API如sqlite3库。定义ParkingRecords表使用SQL语句进行增删改查。这能让你接触真实的数据持久化方案。增加图形化界面使用Qt或Dear ImGui等库为系统制作一个简单的图形界面。把控制台的菜单和输入输出换成按钮、文本框和表格。这会是一个全新的挑战但成就感十足。实现多线程与网络通信高级模拟多个入口/出口同时操作。你可以用std::thread创建多个线程模拟并发入场出场。此时对activeParkings和allRecords的访问就需要加锁如std::mutex这是学习并发编程的好例子。更进一步可以设计一个客户端-服务器模型服务器运行核心逻辑多个控制台或GUI客户端通过网络连接操作。这个停车场收费系统项目就像一块很好的“毛坯房”。基础框架我已经搭好了里面的装修、扩建、甚至改建都留给了你。每解决一个上述的扩展问题你都会对C和软件工程有更深的理解。编程学习最好的方式就是从一个能运行的小项目开始然后不断地“折腾”它在解决问题的过程中成长。希望这份源码和解析能成为你“折腾”之旅的一个可靠起点。