ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++超市商品管理系统课程设计:继承多态、基类指针与文件持久化

C++超市商品管理系统课程设计:继承多态、基类指针与文件持久化 简介C《超市商品管理系统》课程设计报告面向高校计算机、软件工程等专业的面向对象程序设计课程学习者可用于课程设计选题参考、报告撰写模板借鉴与期末项目复盘。文档完整梳理了超市商品管理系统的需求分析、类层次设计与算法实现涉及商品基类与食品、化妆品、日用品、饮料四大子类的继承关系模板机制的引入以及购买、添加、修改、删除、按类别与厂家查询、按价格与库存量排序统计等功能模块并配有主函数流程图、算法步骤说明和关键代码解释报告结构遵循摘要、需求分析、系统设计、软件实现、总结的规范格式。资源包仅含1个doc文档约524KB代码在VC6.0环境下编译验证便于直接参考排版与实现思路。目前已有1534人学习适合需要快速搭建课程设计框架、理解继承与模板综合运用的初学者与进阶学生。1. 一个把链表、继承和文件读写揉在一起的超市商品管理系统很多人第一次拿到 C 课程设计题目时第一反应是“用数组加几个 if-else 不就完了”。但这份《超市商品管理系统》的设计走的是另一条路它要求把食品、化妆品、日用品、饮料四类商品抽出一个共同的Goods基类再通过继承派生具体商品类仓库统一用vectorGoods*存放基类指针配合infile.txt做落地存储。这套结构看起来朴素却把面向对象里最容易踩坑的三件事一次性摆到了台面上——指针容器的内存归属、虚函数接口的调用时机、以及文件序列化的读写顺序对齐。它的定位很明确面向对象程序设计课程的综合实践适合已经能写独立函数、但对类层次和容器组合还停留在课本例题阶段的开发者。读通这套代码你拿到的不是一个能商用的库存系统而是一份把“基类指针 派生对象 文件持久化”串成完整闭环的最小样本。2. Goods 基类与四类商品的继承体系怎么切课程设计的类图里Goods在最上层下面挂Food、Drink、Daily_Supplies、Cosmetics四个派生类。这个层次的切分依据是商品的“类别属性”而不是商品的存储方式或销售方式。换句话说如果哪天要加“家电类”只需要再派生一个类System里的购买、删除、查询逻辑一行都不用改——这正是把类别做成继承体系而不是做成一个category字段的真正收益。2.1 Goods 基类的接口设计与数据成员基类承担的是“所有商品都有什么”这件事。从报告正文看商品共有的信息是商品名称、品牌、价格、生产厂家、库存量。对应到代码里这些成员放在Goods中并由一组Get_*/Set_*方法暴露给外部访问// goods.h —— 所有商品的抽象基类 #ifndef GOODS_H_ #define GOODS_H_ #include string #include iostream #include fstream using namespace std; class Goods { public: // 读取与写入文件供 System::Read / System::Write 调用 virtual void Read(ifstream in) 0; virtual void Write(ofstream out) 0; // 控制台录入与展示供 Add / Show 使用 virtual void Set() 0; virtual void Show() 0; // 基本属性的统一读写入口 string Get_Name() const { return name; } string Get_Kind() const { return kind; } double Get_Price() const { return price; } int Get_Num() const { return num; } void Set_Price(double p) { price p; } void Set_Num(int n) { num n; } virtual ~Goods() {} // 基类指针删除派生对象时必须虚析构 protected: string name; // 商品名称 string brand; // 品牌 string kind; // 类别食品 / 饮料 / 日常用品 / 化妆品 string maker; // 生产厂家 double price; // 单价 int num; // 库存量 }; #endif这里有几个点值得单独说。第一Read/Write/Set/Show声明为纯虚函数意味着Goods不能直接实例化任何商品都必须落到具体类别上这从类型层面堵住了“类别不明”的商品进入仓库。第二Get_Name()这类访问器写成const成员函数是为了让System::Statistics()里遍历something[i]时不会意外修改商品状态。第三虚析构函数不是可选项——仓库用的是vectorGoods*一旦new Food出来的对象通过基类指针delete没有虚析构就会只调基类析构派生类里额外申请的字符串资源可能泄漏。2.2 派生类怎么复用基类又注入差异四个派生类的代码结构高度相似以Food为例// food.h #ifndef FOOD_H_ #define FOOD_H_ #include goods.h class Food : public Goods { public: void Set() override { kind 食品; cout 请输入食品名称: ; cin name; cout 请输入品牌: ; cin brand; cout 请输入单价: ; cin price; cout 请输入生产厂家: ; cin maker; cout 请输入库存量: ; cin num; } void Show() override { cout 类别: kind 名称: name 品牌: brand 单价: price 厂家: maker 库存: num endl; } void Read(ifstream in) override { in name brand price maker num; } void Write(ofstream out) override { out kind name brand price maker num endl; } }; #endif派生类里唯一真正“不同”的地方就是kind的赋值和Show的展示顺序。这种写法看着有点重复但课程设计阶段的取舍是对的把差异显式写在每个派生类里比在基类里塞一堆if (kind 食品)更符合“多态”的训练目标。要注意的是Read和Write的字段顺序必须严格对齐——Write里写的是“类别 名称 品牌 单价 厂家 库存”Read里就必须按同样顺序读否则infile.txt一旦跨版本就会整列错位。方法是否纯虚责任谁调用Set()是控制台交互录入一条商品System::Add()Show()是格式化输出一条商品购买/查询/统计Read(ifstream)是从文件流反序列化一条商品System::Read()Write(ofstream)是把一条商品序列化到文件流System::Write()Get_Num()/Set_Num()否库存量的读与改购买/修改2.3 Ask() 工厂函数与 new 出来的对象归属System::Add()里有一段关键代码string kind Ask(); // 询问用户输入类别返回中文字符串 Goods *temp nullptr; if (kind 食品) temp new Food; if (kind 饮料) temp new Drink; if (kind 日常用品) temp new Daily_Supplies; if (kind 化妆品) temp new Cosmetics; if (temp nullptr) return 0; // 类别非法直接返回 temp-Set(); // 派生类的 Set 被调用 int a Have(temp-Get_Kind(), temp-Get_Name()); if (a -1) { something.push_back(temp); // 归属权移交给 vector } else { delete temp; // 已存在必须手动释放 return 0; } return 1;这段代码是整份设计的核心模式用Ask()把“类别字符串”翻译成“派生类指针”再由push_back完成所有权转移。很多刚接触多态指针容器的同学容易在这里漏掉delete temp结果是每次重复添加商品就泄漏一个派生类对象。另一个常见误用是在Ask()里直接返回Goods对象而不是指针那样会发生对象切片派生类里新增的成员和重写的虚函数全部丢失something.push_back进去的全是基类副本。3. vector infile.txt仓库存储与持久化的取舍这份设计没有用链表而是用vectorGoods*做仓库。报告里提到“使用的了数据结构当中的链表结构”但从给出的Read/Write代码看实际容器是vectorerase(something.begin() a)和something[i]-Write(out)都是 vector 的典型用法。这不影响功能反而更安全——vector 的随机访问让Have()返回下标后可以直接定位不需要维护链表指针。3.1 为什么用指针容器而不是对象容器如果写成vectorGoods something那么所有商品都只能是Goods类型。C 值语义不支持多态push_back(new Food)会触发对象切片Show()永远调基类版本。所以这里必须用vectorGoods*让容器里存的是地址虚函数表指针完整保留。代价是内存管理必须自己扛。常见做法是在System的析构函数里统一清理// system.h 中 System 的析构函数 System::~System() { for (size_t i 0; i something.size(); i) { delete something[i]; // 释放每个派生类对象 } something.clear(); }如果你用 C11 及以后的环境可以把容器换成vectorunique_ptrGoodspush_back用std::move转移erase时对象自动析构。但课程设计里通常不允许改动核心结构所以老老实实写好析构才是稳妥方案。3.2 Read / Write 的序列化协议持久化部分就是一段极简的纯文本协议。Write先写商品条数再逐条写商品Read先读条数再循环读入void System::Write() { ofstream out; out.open(infile.txt); if (!out.is_open()) return; // 文件打不开直接返回避免写出空文件 out something.size() endl; // 第一行商品总条数 for (size_t i 0; i something.size(); i) { something[i]-Write(out); // 每条商品调各自派生类版本 } out.close(); } void System::Read() { ifstream in; in.open(infile.txt); if (!in.is_open()) return; // 首次运行没有文件正常 int count 0; in count; // 先读条数 for (int i 0; i count; i) { string kind; in kind; // 读类别决定 new 哪个派生类 Goods *temp nullptr; if (kind 食品) temp new Food; if (kind 饮料) temp new Drink; if (kind 日常用品) temp new Daily_Supplies; if (kind 化妆品) temp new Cosmetics; if (temp) { temp-Read(in); // 派生类自己读剩下的字段 something.push_back(temp); } } in.close(); }这里有一个必须注意的协议细节Write时每条商品的第一个字段是kindRead时也必须先单独读kind用它决定new哪个类再把流交给派生类的Read读剩余字段。如果你把kind放进派生类的Read里读和工厂判断逻辑就会打架。文件位置字段类型说明第 1 行countint商品总条数决定 Read 循环次数后续每行kindstring类别标识用于工厂 new同行namestring商品名称同行brandstring品牌同行pricedouble单价同行makerstring生产厂家同行numint库存量提示字段之间用空格分隔所以商品名称、品牌、厂家都不能含空格。如果实际业务里出现“可口可乐 罐装”这种带空格的名称这套纯文本协议会直接错列需要换成带长度前缀或 CSV 转义。3.3 Have() 线性查找与去重逻辑Have(kind, name)承担了几乎全部功能模块的前置校验返回下标或 -1int System::Have(string kind, string name) { for (size_t i 0; i something.size(); i) { // 类别和名称同时匹配才算命中 if (something[i]-Get_Kind() kind something[i]-Get_Name() name) { return static_castint(i); } } return -1; }复杂度是 O(n)在几百条商品规模下完全够用。它的价值在于把“存在性判断”收敛到一个函数里购买前查它、添加前查重、删除前查它、修改前查它、查询前查它。五处调用点的行为因此保持一致。常见的误用是只在添加时查重删除时不查就直接erase结果按下标删掉了另一条商品。4. 六大功能函数的实现细节与返回值约定六个功能函数——Buy、Add、Delect、Modefied、Find、Statistics——在主菜单里被Star()分发。它们之间的协作不是各自为政而是围绕Have()和something容器形成了一套稳定的返回值约定。4.1 购买函数的库存扣减与边界判断购买逻辑先把“商品存在”和“库存充足”两个条件分开判断int System::Buy() { string kind Ask(); string name; cout 请输入购买商品名称: ; cin name; int a Have(kind, name); if (a -1) return 0; // 商品不存在 something[a]-Show(); // 显示商品信息供确认 int buy_num; cout 请输入购买数量: ; cin buy_num; if (buy_num something[a]-Get_Num()) { int left something[a]-Get_Num() - buy_num; something[a]-Set_Num(left); // 扣减库存 cout 本次花费: buy_num * something[a]-Get_Price() 元 endl; return 1; } return -1; // 库存不足 }返回值三态1表示购买成功0表示商品不存在-1表示库存不足。主菜单根据返回值给不同提示。这套约定在六个函数里基本一致是课程设计中值得保留的习惯。容易忽略的是buy_num本身没有做“必须大于 0”的校验用户输入0或负数时库存会被反向增加。补一行if (buy_num 0) return -1;就能堵住。4.2 删除与修改的下标稳定性删除用的是eraseint System::Delect() { string kind Ask(); string name; cout 请输入要删除的商品名称: ; cin name; int a Have(kind, name); if (a ! -1) { delete something[a]; // 先释放对象防止泄漏 something.erase(something.begin() a); // 再移除指针 return 1; } return 0; }顺序不能反。如果先erase再delete指针已经不在容器里你手上的a下标也已经失效。另一个坑是erase之后所有后续元素下标前移如果外部缓存了旧下标必须同步失效。修改函数不涉及下标变化只改字段相对安全int System::Modefied() { string kind Ask(); string name; cout 请输入要修改的商品名称: ; cin name; int a Have(kind, name); if (a -1) return 0; int choice; cout 1.修改价格 2.修改库存 endl; cin choice; if (choice 1) { double price; cin price; something[a]-Set_Price(price); } else if (choice 2) { int num; cin num; something[a]-Set_Num(num); } return 1; }4.3 统计排序与 operator 的方向坑统计函数把商品的关键字段抽到item结构体里再排序输出struct item { string name; double price; int num; }; // 全局 operator 决定 sort 的排序依据 bool operator(const item g1, const item g2) { return g1.num g2.num; // 按库存从小到大 }这里有一个和原始需求不一致的地方任务书要求“按从大到小排序”而这份代码的operator和报告正文都写成从小到大。要改成从大到小最直接的做法是把比较方向反过来bool operator(const item g1, const item g2) { return g1.num g2.num; // 语义上是小于效果上是降序 }或者保留原比较器在调用sort时传入反向迭代器sort(st.rbegin(), st.rend()); // 结果数组整体倒序统计维度原始字段排序比较器输出列价格统计item.price按 price 比较名称 价格库存统计item.num按 num 比较名称 库存全量统计全部字段不排序全部详细信息注意operator一旦定义成按num比较价格统计就不能复用它。稳妥写法是不定义全局比较器改为在Statistics()内部用 lambda 指定比较字段避免两个统计维度互相干扰。统计里还有一处边界问题for (unsigned int i 0; i something.size(); i)里打印st[i]如果某次st和something长度不一致比如中途有商品被删就会越界。统计前先重新填充st且打印循环用st.size()作为上界才是正确写法。5. 从 VC6.0 迁到现代编译器的兼容处理与验证这份代码的原生环境是 VC6.0它对 C 标准的支持停留在 1998 年之前。把同样的源码丢进 g 或 Visual Studio 2019 时有几类报错几乎必然出现处理方式可以直接抄。5.1 头文件与命名空间的适配VC6.0 时代常用iostream.h这种带.h的写法现代编译器必须改成#include iostream // 不是 iostream.h #include fstream #include vector #include string #include algorithm // sort 需要 using namespace std;VC6.0 的for循环变量作用域是函数级的for (int i 0; ...)之后还能访问i。标准 C 里i只属于循环体。如果后续代码引用了循环外的i编译会报未声明需要把变量提到循环外。5.2 override 与虚析构的处理老代码里虚函数重写没有override关键字。加上它能让编译器帮你检查签名是否真的匹配class Food : public Goods { public: void Set() override; // 签名不符时编译器直接报错 void Show() override; void Read(ifstream in) override; void Write(ofstream out) override; };同时确保Goods的析构函数声明为virtual。如果漏了用-Wall编译时会提示delete非虚析构类的派生对象这是真实存在的未定义行为。5.3 用一组固定用例验证读写一致性编译通过只是第一步持久化协议最容易在“写进去再读出来”这个来回中出问题。建议准备一组最小验证用例# 1. 编译假设所有头文件和源文件在同一目录 g -stdc11 -Wall -o market main.cpp system.cpp # 2. 首次运行文件不存在应进入空仓库 ./market # 3. 手动造一条最小合法数据 printf 1\n食品 可乐 可口可乐 3.5 可口可乐公司 100\n infile.txt # 4. 再次运行检查统计功能能否读出这条记录 ./market运行后重点看三件事统计输出里的商品名称、单价、库存是否和文件里的数值一致购买一次后再退出infile.txt第一行条数是否仍为 1、库存是否减少连续添加两条同名同类别商品第二条是否被Have()拦截而不会写入文件。这三步覆盖了序列化对齐、库存扣减落盘、以及去重逻辑三条最容易出错的路径。如果第二步行数变成了 2 而文件里应该只有 1 条多半是Write之前没有对容器去重或者Read时因为字段错位重复 push 了对象。本文还有配套的精品资源点击获取
返回列表