C++超市商品管理系统:从数据结构到文件IO的完整项目实践
1. 项目概述与核心价值
最近在整理一些早年的项目代码,翻到了这个“超市商品管理系统”。这大概是很多C++初学者在掌握了基础语法和数据结构后,第一个会尝试去做的、有点规模的综合练习。它不像“学生信息管理系统”那么千篇一律,又比“通讯录”复杂一些,能很好地串联起文件操作、链表/容器管理、排序查找、简单界面等核心知识点。今天,我就把这个项目的设计思路和C++实现细节,结合我后来在实际工作中积累的经验,重新梳理一遍,希望能给正在学习C++、想找个项目练手的朋友们提供一个清晰的、可落地的参考模板。
这个系统的核心目标很简单:模拟一个小型超市的商品进销存管理。你需要能录入新商品(进货)、记录商品销售(出货)、查询库存、根据各种条件(如名称、类别、库存量)进行排序和筛选,并且所有数据在程序关闭后不能丢失,下次启动要能加载回来。听起来功能不少,但用C++来实现,恰恰能让我们避开一些高级框架的“黑箱”,从底层去理解一个管理系统的数据流动和业务逻辑是如何被代码组织和驱动的。我会从最朴素的文件存储开始,逐步引入更高效的数据结构,并讨论一些实际编码中容易踩的坑。
2. 系统整体架构与设计思路拆解
2.1 需求分析与模块划分
接到“商品管理系统”这个题目,第一步不是急着写代码,而是把模糊的需求具体化。我们需要明确系统到底要管理商品的哪些信息,以及提供哪些操作。
核心数据模型(商品类Commodity):一个商品至少包含以下属性:商品编号(唯一标识)、商品名称、类别(如食品、日用品)、进货单价、销售单价、当前库存数量、生产日期、保质期等。为了简化,我们先聚焦于最核心的字段。这里有一个设计关键点:商品编号的生成策略。是让用户手动输入,还是系统自动生成?自动生成能避免重复,但需要设计一个不重复的算法(比如基于时间戳或递增序列)。在我们的教学项目中,可以采用“前缀+递增数字”的方式,例如SP00001。
核心功能模块:
- 商品信息管理模块:负责商品的增、删、改、查。这是系统的基石。
- 进货管理模块:增加特定商品的库存数量,并可以记录本次进货的详细信息(如进货时间、供应商,可选)。
- 销售管理模块:减少特定商品的库存数量,并生成销售记录(商品、数量、销售时间、总金额)。这里要处理库存不足的校验。
- 查询统计模块:支持按编号、名称、类别查询商品;按库存量、销售额等进行排序;统计总库存价值、畅销商品等。
- 数据持久化模块:负责将内存中的商品数据保存到文件,以及从文件加载数据到内存。这是保证数据不丢失的关键。
架构选型思考:对于初学者,我建议采用最经典的“控制台菜单驱动+面向过程为主”的模式。这能让你更专注于逻辑本身。进阶一点,可以采用“类封装数据+全局函数操作”的方式。再进一步,可以尝试MVC(模型-视图-控制器)的雏形,将数据、界面、控制逻辑分离。我们今天的实现会介于第二种和第三种之间,既体现面向对象的思想,又保持足够的简洁性。
2.2 数据结构选型:数组、链表还是STL容器?
这是C++项目设计早期最重要的决策之一,直接影响后续所有操作的效率和代码复杂度。
- 原生数组:最简单,但大小固定。超市商品数量是动态变化的,使用数组需要处理扩容问题(手动
new/delete),非常麻烦且易出错,不推荐。 - 单向/双向链表:这是数据结构课的经典练习题。你可以自己实现一个
Node结构体和链表操作函数(插入、删除、遍历)。它的优势是插入删除效率高(O(1)),但随机访问效率低(O(n)),且代码量较大,需要小心处理指针,防止内存泄漏。如果你旨在深刻理解指针和数据结构,这是一个绝佳的练习。 - STL容器(
vector或list):这是生产环境中的首选。std::vector(动态数组)在内存中是连续的,支持随机访问(O(1)),尾部插入删除快,但中间插入删除慢。std::list(双向链表)在任何位置插入删除都快,但不支持随机访问。对于商品管理系统,大部分操作是遍历、查找和基于查找结果的修改,随机访问需求不高。但vector的缓存友好性通常使其在遍历时效率高于list。
我的选择与理由:在这个项目中,我推荐使用std::vector<Commodity>。原因如下:
- 简单安全:无需手动管理内存,避免内存泄漏和野指针。
- 访问模式匹配:我们最频繁的操作是“遍历所有商品进行显示或查找”,
vector的连续内存布局对这种操作极其友好。 - 性能足够:超市商品数量通常在几千到几万级别,在这个量级下,
vector中间插入删除的O(n)开销是可以接受的。况且,我们可以通过优化(如先查找位置再插入)来减少性能损失。 - 与文件IO配合方便:可以将整个
vector视为二进制块进行读写(需谨慎处理类内指针),或者更稳妥地,逐个元素序列化。
当然,如果你的商品数量极大(如十万级以上),且需要频繁在中间插入删除,那么可以考虑std::list,或者更高级的std::map(以商品编号为键)来优化查找效率。但作为入门项目,vector是平衡复杂度和性能的最佳起点。
注意:使用
vector存储对象时,确保你的Commodity类遵循“三之法则”(如果定义了析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,就应该定义全部三个),或者使用C++11以后的“五之法则”(加上移动构造和移动赋值),以保证在vector扩容发生对象拷贝或移动时的正确性。最简单的做法是,让Commodity只包含基本类型和std::string,使用编译器自动生成的默认拷贝/移动语义。
3. 核心类的设计与实现细节
3.1 Commodity类的定义
我们首先设计核心的Commodity类。这里展示一个相对完整的版本,包含构造函数、Getter/Setter以及一个关键的函数:显示信息。
// Commodity.h #ifndef COMMODITY_H #define COMMODITY_H #include <string> #include <iostream> class Commodity { private: std::string id; // 商品编号,如 "SP10001" std::string name; // 商品名称 std::string category; // 商品类别 double purchasePrice; // 进货单价 double salePrice; // 销售单价 int stock; // 当前库存 // 可以扩展生产日期、保质期等,这里用字符串简单表示 std::string productionDate; int shelfLifeDays; // 保质期天数 public: // 构造函数 Commodity(const std::string& id, const std::string& name, const std::string& category, double pPrice, double sPrice, int stock, const std::string& pDate, int shelfLife); // 默认构造函数,用于容器初始化或文件加载 Commodity() = default; // Getter 和 Setter std::string getId() const { return id; } void setId(const std::string& newId) { id = newId; } std::string getName() const { return name; } void setName(const std::string& newName) { name = newName; } // ... 其他属性的Getter/Setter int getStock() const { return stock; } // 修改库存,提供进货和销售两个接口更安全 void increaseStock(int amount) { stock += amount; } bool decreaseStock(int amount) { // 销售时调用 if (stock >= amount) { stock -= amount; return true; // 销售成功 } return false; // 库存不足 } // 计算商品价值(库存 * 销售单价) double getTotalValue() const { return stock * salePrice; } // 显示商品信息 void display() const { std::cout << "编号: " << id << " | 名称: " << name << " | 类别: " << category << " | 进价: " << purchasePrice << " | 售价: " << salePrice << " | 库存: " << stock << " | 生产日期: " << productionDate << " | 保质期: " << shelfLifeDays << "天\n"; } // 判断是否过期(简单逻辑,需要当前日期作为参数) bool isExpired(const std::string& currentDate) const; // 实现略 }; #endif // COMMODITY_H设计要点解析:
- 封装性:所有数据成员设为
private,通过公共接口(Getter/Setter)访问。这保护了数据完整性,例如库存stock不能直接被赋值为负数,必须通过increaseStock和decreaseStock方法,后者还进行了库存不足的校验。 const正确性:对于不修改对象状态的成员函数(如getId,display),务必声明为const。这是良好的习惯,也是某些STL操作所要求的。- 默认构造函数:我们提供了
= default的默认构造函数。这是因为std::vector<Commodity>在resize或某些初始化操作时,需要元素类型是可默认构造的。如果你的类成员都是内置类型或具有默认构造函数的类(如std::string),= default就足够了。
3.2 库存管理类(CommodityManager)的设计
这是系统的中枢,负责管理一个Commodity对象的集合(我们选择vector),并提供所有核心业务操作。
// CommodityManager.h #ifndef COMMODITY_MANAGER_H #define COMMODITY_MANAGER_H #include "Commodity.h" #include <vector> #include <string> class CommodityManager { private: std::vector<Commodity> commodities; // 下一个可用的商品编号,用于自动生成ID int nextIdNumber; // 内部辅助函数 std::string generateId(); // 生成形如"SP10001"的ID std::vector<Commodity>::iterator findById(const std::string& id); std::vector<Commodity>::const_iterator findById(const std::string& id) const; public: CommodityManager() : nextIdNumber(10001) {} // 编号从10001开始 // 核心业务操作 bool addCommodity(const Commodity& comm); // 添加商品 bool deleteCommodity(const std::string& id); // 删除商品 Commodity* getCommodity(const std::string& id); // 查询商品(返回指针,可修改) const Commodity* getCommodity(const std::string& id) const; // 只读查询 // 批量操作 void displayAll() const; void purchase(const std::string& id, int amount); // 进货 bool sell(const std::string& id, int amount); // 销售,返回是否成功 // 查询与排序 std::vector<Commodity*> searchByName(const std::string& keyword); std::vector<const Commodity*> searchByName(const std::string& keyword) const; void sortByStock(bool ascending = true); void sortBySalePrice(bool ascending = true); // 数据持久化 bool saveToFile(const std::string& filename) const; bool loadFromFile(const std::string& filename); // 获取统计信息 int getTotalCommodityTypes() const { return commodities.size(); } double getTotalInventoryValue() const; }; #endif // COMMODITY_MANAGER_H关键设计解析:
- 迭代器的使用:
findById返回迭代器,这是STL中定位元素的标准方式。我们提供了const和非const两个版本,以适配不同的使用场景(修改或只读)。 - 函数重载:
getCommodity和searchByName都提供了const和非const版本,这是提供灵活性和保证const正确性的常用技巧。 - 自动ID生成:
generateId()函数利用nextIdNumber成员生成唯一ID,并在每次成功添加商品后递增。这比让用户输入更可靠。注意,从文件加载数据后,需要正确设置nextIdNumber(通常是已存在最大ID的数字部分+1)。 - 返回指针而非引用或对象:
getCommodity返回指针,允许返回nullptr表示未找到,这比抛出异常或返回一个“空对象”对初学者更友好。调用者必须检查指针是否为空。
4. 关键功能模块的C++实现
4.1 商品查找功能的实现
查找是几乎所有操作(删、改、进货、销售)的基础。我们来实现核心的findById函数。
// CommodityManager.cpp 片段 std::vector<Commodity>::iterator CommodityManager::findById(const std::string& id) { for (auto it = commodities.begin(); it != commodities.end(); ++it) { if (it->getId() == id) { return it; } } return commodities.end(); // 未找到,返回尾后迭代器 } // const版本 std::vector<Commodity>::const_iterator CommodityManager::findById(const std::string& id) const { for (auto it = commodities.begin(); it != commodities.end(); ++it) { if (it->getId() == id) { return it; } } return commodities.end(); } Commodity* CommodityManager::getCommodity(const std::string& id) { auto it = findById(id); if (it != commodities.end()) { return &(*it); // 解引用迭代器得到对象,再取地址 } return nullptr; }这里使用了迭代器进行线性查找(O(n))。如果商品数量很多,可以考虑改用std::unordered_map<std::string, Commodity>来存储,将ID作为键,这样查找效率可以提升到O(1)。但为了教学清晰,我们先用vector。
4.2 进货与销售逻辑的实现
进货和销售本质上是库存的增减,但包含业务规则。
bool CommodityManager::sell(const std::string& id, int amount) { Commodity* pComm = getCommodity(id); if (!pComm) { std::cerr << "错误:商品编号 " << id << " 不存在!\n"; return false; } if (amount <= 0) { std::cerr << "错误:销售数量必须为正数!\n"; return false; } return pComm->decreaseStock(amount); // 调用Commodity类的安全减库存方法 } void CommodityManager::purchase(const std::string& id, int amount) { Commodity* pComm = getCommodity(id); if (!pComm) { std::cerr << "错误:商品编号 " << id << " 不存在,无法进货!\n"; return; } if (amount <= 0) { std::cerr << "错误:进货数量必须为正数!\n"; return; } pComm->increaseStock(amount); std::cout << "进货成功。商品 " << pComm->getName() << " 库存增加 " << amount << ",当前库存为 " << pComm->getStock() << "。\n"; }注意sell函数返回bool表示成功与否,而purchase直接是void。这是因为销售可能失败(库存不足),而进货(增加库存)在我们当前的逻辑里总是成功的。这是一种简单的错误处理方式。更健壮的系统可能需要记录每次进货和销售的流水。
4.3 数据持久化:文件读写
这是让程序“记住”数据的关键。我们采用文本文件(如CSV格式)进行存储,因为它人类可读,便于调试。
bool CommodityManager::saveToFile(const std::string& filename) const { std::ofstream outFile(filename); if (!outFile.is_open()) { std::cerr << "无法打开文件 " << filename << " 进行写入!\n"; return false; } // 可以保存表头 outFile << "ID,Name,Category,PurchasePrice,SalePrice,Stock,ProductionDate,ShelfLifeDays\n"; for (const auto& comm : commodities) { outFile << comm.getId() << "," << comm.getName() << "," << comm.getCategory() << "," << comm.getPurchasePrice() << "," << comm.getSalePrice() << "," << comm.getStock() << "," << comm.getProductionDate() << "," << comm.getShelfLifeDays() << "\n"; } outFile.close(); // 还需要保存nextIdNumber,可以单独一行,或者用另一个文件 std::ofstream idFile("next_id.dat"); idFile << nextIdNumber; idFile.close(); std::cout << "数据已保存至 " << filename << "\n"; return true; } bool CommodityManager::loadFromFile(const std::string& filename) { std::ifstream inFile(filename); if (!inFile.is_open()) { std::cerr << "无法打开文件 " << filename << " 进行读取,可能首次运行。\n"; return false; // 文件不存在不一定是错误,可能是第一次运行 } commodities.clear(); // 加载前清空现有数据 std::string line; std::getline(inFile, line); // 跳过表头 while (std::getline(inFile, line)) { std::stringstream ss(line); std::string token; std::vector<std::string> tokens; while (std::getline(ss, token, ',')) { tokens.push_back(token); } if (tokens.size() >= 8) { // 确保有足够字段 // 注意:从字符串转换到数值类型需要处理异常 try { Commodity comm( tokens[0], // id tokens[1], // name tokens[2], // category std::stod(tokens[3]), // purchasePrice std::stod(tokens[4]), // salePrice std::stoi(tokens[5]), // stock tokens[6], // productionDate std::stoi(tokens[7]) // shelfLifeDays ); commodities.push_back(comm); } catch (const std::exception& e) { std::cerr << "解析行时出错: " << line << ",错误: " << e.what() << "\n"; continue; // 跳过这一行错误数据 } } } inFile.close(); // 加载nextIdNumber std::ifstream idFile("next_id.dat"); if (idFile.is_open()) { idFile >> nextIdNumber; idFile.close(); } else { // 如果找不到id文件,根据已加载商品的最大ID推算 nextIdNumber = 10001; for (const auto& comm : commodities) { std::string idStr = comm.getId().substr(2); // 假设ID格式为"SPxxxxx" try { int num = std::stoi(idStr); if (num >= nextIdNumber) nextIdNumber = num + 1; } catch (...) { // 忽略转换错误 } } } std::cout << "数据已从 " << filename << " 加载,共 " << commodities.size() << " 条记录。\n"; return true; }重要提示:上述文件解析代码是简化版,没有处理字段内包含逗号、换行符等复杂情况。在实际项目中,建议使用专门的CSV解析库,或者采用更简单的格式(如用
|分隔)。std::stod和std::stoi可能抛出std::invalid_argument或std::out_of_range异常,务必进行异常处理,防止因文件格式错误导致程序崩溃。
5. 用户界面与主程序循环
对于控制台程序,一个清晰的文本菜单是必不可少的。主程序结构通常是一个while循环,根据用户输入调用CommodityManager的相应方法。
// main.cpp 框架 #include "CommodityManager.h" #include <iostream> #include <limits> // 用于清空输入缓冲区 void displayMenu() { 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 << "7. 按库存排序\n"; std::cout << "8. 保存数据\n"; std::cout << "9. 加载数据\n"; std::cout << "0. 退出系统\n"; std::cout << "请选择操作: "; } void clearInputBuffer() { std::cin.clear(); // 清除错误状态 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 忽略缓冲区剩余字符直到换行 } int main() { CommodityManager manager; // 程序启动时尝试自动加载数据 manager.loadFromFile("commodities.csv"); int choice = 0; do { displayMenu(); std::cin >> choice; clearInputBuffer(); // 清除数字后的换行符,避免影响后续getline switch (choice) { case 1: manager.displayAll(); break; case 2: { // 收集用户输入,创建Commodity对象,调用manager.addCommodity std::string name, category, pDate; double pPrice, sPrice; int shelfLife; std::cout << "请输入商品名称: "; std::getline(std::cin, name); // ... 获取其他输入 // 注意:ID由系统自动生成,不应由用户输入 Commodity newComm(manager.generateId(), name, category, pPrice, sPrice, 0, pDate, shelfLife); if(manager.addCommodity(newComm)) { std::cout << "添加成功!\n"; } else { std::cout << "添加失败(可能ID重复?)。\n"; } break; } case 3: { std::string keyword; std::cout << "请输入商品名称关键字: "; std::getline(std::cin, keyword); auto results = manager.searchByName(keyword); if(results.empty()) { std::cout << "未找到相关商品。\n"; } else { for(auto* p : results) { p->display(); } } break; } case 4: { std::string id; int amount; std::cout << "请输入要进货的商品编号: "; std::cin >> id; clearInputBuffer(); std::cout << "请输入进货数量: "; std::cin >> amount; clearInputBuffer(); manager.purchase(id, amount); break; } // ... 实现其他case case 8: manager.saveToFile("commodities.csv"); break; case 9: manager.loadFromFile("commodities.csv"); break; case 0: std::cout << "感谢使用,再见!\n"; break; default: std::cout << "无效选择,请重新输入。\n"; } } while (choice != 0); // 退出前自动保存 manager.saveToFile("commodities.csv"); return 0; }交互细节心得:混合使用std::cin >>和std::getline()是新手常踩的坑。cin >>会留下换行符在缓冲区,紧接着的getline()会读到空行。我的解决方法是:在每次使用cin >>读取数值后,立即调用一个clearInputBuffer()函数来清空缓冲区。这个函数在读取错误输入后的恢复中也很有用。
6. 进阶优化与扩展思路
一个基础版本完成后,可以考虑以下方向进行深化,这会让你的项目更有竞争力。
6.1 使用STL算法提升代码质量
不要自己手写循环查找、排序。使用<algorithm>头文件中的现成算法,代码更简洁、更安全、通常也更高效。
// 使用std::find_if替代手写循环查找 auto CommodityManager::findById(const std::string& id) -> std::vector<Commodity>::iterator { return std::find_if(commodities.begin(), commodities.end(), [&id](const Commodity& comm) { return comm.getId() == id; }); } // 使用std::sort配合lambda表达式进行排序 void CommodityManager::sortByStock(bool ascending) { std::sort(commodities.begin(), commodities.end(), [ascending](const Commodity& a, const Commodity& b) { return ascending ? (a.getStock() < b.getStock()) : (a.getStock() > b.getStock()); }); } // 使用std::copy_if进行条件筛选 std::vector<const Commodity*> CommodityManager::searchByName(const std::string& keyword) const { std::vector<const Commodity*> results; std::copy_if(commodities.begin(), commodities.end(), std::back_inserter(results), [&keyword](const Commodity& comm) { // 大小写不敏感查找,提升用户体验 std::string lowerName = comm.getName(); std::string lowerKey = keyword; std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower); std::transform(lowerKey.begin(), lowerKey.end(), lowerKey.begin(), ::tolower); return lowerName.find(lowerKey) != std::string::npos; }); return results; }6.2 引入简单的图形界面(可选)
如果你已经厌倦了黑框框,可以尝试用一些轻量级的库来增加图形界面,这会让项目看起来更“完整”。
- EasyX:一个为C++初学者设计的图形库,在Windows下安装简单,适合绘制简单的按钮和文本框来构建界面。
- Qt:功能强大的跨平台GUI框架。用Qt Creator可以拖拽设计界面,但需要学习信号槽机制,对于初学者项目来说稍显重量级。不过,如果你能把核心的
CommodityManager类写好,Qt界面只是调用这些接口,是一个很好的架构练习。 - ImGui:一个即时模式的图形界面库,需要配合OpenGL或DirectX等图形后端。它更偏向于工具和游戏调试界面,但写起来很直接。
我的建议是,先确保控制台版本的所有逻辑正确、健壮,然后再考虑为其套一个图形界面。将业务逻辑(CommodityManager)和界面逻辑分离,是优秀的软件设计习惯。
6.3 增加更复杂的业务逻辑
- 销售流水与统计:创建
SalesRecord类,记录每一笔销售的详情(商品ID、数量、时间、销售员等)。在CommodityManager中增加一个std::vector<SalesRecord>。这样可以实现日销售额统计、畅销商品排行等功能。 - 会员与折扣:引入
Member类,设计折扣规则。销售时判断是否为会员,并应用相应的折扣策略。 - 库存预警:在
Commodity类中增加一个“最低库存阈值”字段。每次销售或定期检查时,提示哪些商品库存低于阈值,需要进货。 - 多线程安全:如果你的程序未来可能涉及并发访问(虽然控制台程序很少见),需要考虑在
CommodityManager的关键方法上加锁(如使用std::mutex),防止数据竞争。
7. 常见问题与调试技巧实录
在实现这个项目的过程中,你几乎一定会遇到下面这些问题。
7.1 文件读写相关
- 问题:程序运行后,数据没有被保存,或者加载时出现乱码、程序崩溃。
- 排查:
- 检查文件路径和权限。程序是否有权在当前位置创建和写入文件?
- 在
saveToFile和loadFromFile函数中,在每个关键步骤后添加调试输出(std::cout),看执行到哪一步出了问题。 - 用文本编辑器直接打开生成的CSV文件,检查格式是否正确。特别注意最后一个字段后面是否有多余的逗号或空格。
- 确保
loadFromFile时,字符串到数字的转换(std::stoi,std::stod)被try-catch块包围,防止因文件损坏导致程序退出。
- 技巧:可以编写一个简单的测试函数,创建几个商品对象,保存到文件,再加载回来,然后比较加载前后的对象数据是否一致。这是单元测试的思想。
7.2 内存与指针相关
- 问题:程序运行一段时间后崩溃,或者出现不可预知的行为。
- 排查:
- 如果你用了原生指针和
new/delete:确保每个new都有对应的delete。使用valgrind(Linux/Mac)或Visual Studio的内存诊断工具来检测内存泄漏。 - 如果你用了
vector和对象:确保你的Commodity类遵循“三/五之法则”。一个常见的错误是类中有指针成员(例如char* name),但只进行了浅拷贝。当vector扩容时,对象被拷贝,原对象的指针被删除,导致拷贝对象的指针悬空。最佳实践是使用std::string代替char*,让标准库管理内存。 - 检查所有返回指针的函数(如
getCommodity),调用者是否检查了指针是否为nullptr后再使用。
- 如果你用了原生指针和
7.3 输入处理相关
- 问题:在输入数字后,接下来的字符串输入被跳过。
- 原因与解决:这就是前面提到的
cin >>和getline()的混用问题。牢记在读取数值后调用clearInputBuffer()。 - 问题:用户输入了非数字字符,但程序期待一个数字,导致死循环或错误。
- 解决:更健壮的输入处理是读取一整行到字符串,然后用
std::stringstream进行解析和错误检查。
int getIntegerInput(const std::string& prompt) { int value; std::string input; while (true) { std::cout << prompt; std::getline(std::cin, input); std::stringstream ss(input); if (ss >> value && ss.eof()) { // 成功转换且没有多余字符 return value; } std::cout << "输入无效,请输入一个整数。\n"; } }7.4 项目编译与构建
- 问题:多个
.cpp文件如何一起编译? - 解决:对于小项目,可以直接使用命令行:
对于稍大点的项目,建议学习使用构建工具,如CMake。创建一个g++ -std=c++11 main.cpp Commodity.cpp CommodityManager.cpp -o supermarket.exeCMakeLists.txt文件会让你的项目更规范,也更容易在不同平台和IDE间迁移。
在项目目录下执行cmake_minimum_required(VERSION 3.10) project(SupermarketSystem) set(CMAKE_CXX_STANDARD 11) add_executable(supermarket main.cpp Commodity.cpp CommodityManager.cpp )cmake .和make即可。
把这个项目从头到尾实现一遍,你会对C++的类、对象、STL容器、文件IO、程序结构有一个非常扎实的理解。它就像一块基石,之后无论你是想深入研究数据结构(比如自己实现一个哈希表来替换findById的线性查找),还是想学习图形界面开发,都有了可靠的立足点。编程最好的学习方式就是动手去做,在调试和解决问题的过程中,那些书本上的概念会变得无比清晰。
