15、C++项目实战:超市收银系统
超市收银系统,听起来是不是挺接地气的?其实它背后藏着不少C++的核心知识点。我个人觉得,做这种贴近生活的项目,学起来最有动力。你想想看,每次去超市结账,收银员扫一下条码,价格就出来了,还能算折扣、找零钱——这些功能,我们今天就用C++来实现。
这个项目我做了好几次迭代。最早在大学时写过一个控制台版本,后来在工作中又重构过。嗯,今天我把最精华的部分提炼出来,带你一步步搭建一个完整的收银系统。
系统需求分析
先别急着写代码。我习惯先想清楚要做什么。超市收银系统,核心功能就这几个:
- 商品管理:添加、删除、修改商品信息
- 购物车:扫描商品、计算总价
- 折扣处理:满减、打折、会员价
- 结算:找零、打印小票
说白了,就是模拟一个真实的收银流程。我在项目中遇到过最头疼的事,就是折扣逻辑写得太死板,后来改需求改到崩溃。所以这次我们设计得灵活一点。
整体架构设计
先画一张图,看看整个系统的脉络。这样你心里就有谱了。
从图里能看出来,整个系统分三层。主控模块负责调度,中间三个模块各司其职,底层是数据存储。这种分层设计,我在实际项目中用了很多次,好处是改一个模块不影响其他模块。
商品管理模块
商品是收银系统的基础。每个商品得有编号、名称、单价、库存这些信息。我习惯用结构体来定义:
struct Product {
string id; // 商品编号
string name; // 商品名称
double price; // 单价
int stock; // 库存数量
string category; // 商品类别
};
为什么用结构体而不是类?因为商品数据本身没有复杂的行为,结构体更轻量。当然,如果你需要封装更多逻辑,用类也行。我个人偏好是:数据简单就用结构体,有复杂行为就用类。
商品管理模块的核心操作,我封装成了一个类:
class ProductManager {
private:
vector<Product> products;
public:
void addProduct(const Product& p) {
products.push_back(p);
}
Product* findProduct(const string& id) {
for (auto& p : products) {
if (p.id == id) return &p;
}
return nullptr;
}
bool updateStock(const string& id, int delta) {
Product* p = findProduct(id);
if (p == nullptr) return false;
if (p->stock + delta < 0) return false;
p->stock += delta;
return true;
}
};
购物车模块
购物车是收银系统的核心。顾客拿了一堆东西,总得有个地方暂存。购物车里的每一项,除了商品信息,还得记录数量:
struct CartItem {
Product product;
int quantity;
double subtotal() const {
return product.price * quantity;
}
};
这里有个细节:subtotal() 是实时计算的。为什么不直接存小计金额?因为商品价格可能会变(比如促销活动),实时计算更准确。嗯,这也是我在项目中踩过的坑——以前存了快照价格,结果促销结束后价格没更新,顾客投诉了。
购物车类的设计:
class ShoppingCart {
private:
vector<CartItem> items;
public:
void addItem(const Product& p, int qty = 1) {
for (auto& item : items) {
if (item.product.id == p.id) {
item.quantity += qty;
return;
}
}
items.push_back({p, qty});
}
double getTotal() const {
double total = 0.0;
for (const auto& item : items) {
total += item.subtotal();
}
return total;
}
void clear() {
items.clear();
}
};
折扣处理
折扣是收银系统里最灵活的部分。不同的超市有不同的促销策略。我建议用策略模式来处理:
class DiscountStrategy {
public:
virtual double apply(double total) = 0;
virtual ~DiscountStrategy() = default;
};
// 满减策略
class FullReduction : public DiscountStrategy {
private:
double threshold; // 满多少
double reduction; // 减多少
public:
FullReduction(double t, double r) : threshold(t), reduction(r) {}
double apply(double total) override {
return (total >= threshold) ? total - reduction : total;
}
};
// 打折策略
class PercentageDiscount : public DiscountStrategy {
private:
double rate; // 折扣率,如0.8表示8折
public:
PercentageDiscount(double r) : rate(r) {}
double apply(double total) override {
return total * rate;
}
};
为什么用虚函数?因为不同的折扣策略可以随时切换。我曾经在项目里看到有人用一堆if-else处理折扣,后来加了十几种促销活动,代码变得没法维护。用策略模式,新增一种折扣只需要写一个新类,不改原有代码。
结算模块
结算就是收钱、找零、打小票。这部分逻辑比较直接:
class Checkout {
public:
struct Receipt {
vector<CartItem> items;
double total;
double discount;
double finalAmount;
double cashGiven;
double change;
string time;
};
Receipt processPayment(ShoppingCart& cart,
DiscountStrategy* discount,
double cash) {
Receipt receipt;
receipt.total = cart.getTotal();
if (discount) {
receipt.finalAmount = discount->apply(receipt.total);
receipt.discount = receipt.total - receipt.finalAmount;
} else {
receipt.finalAmount = receipt.total;
receipt.discount = 0;
}
receipt.cashGiven = cash;
receipt.change = cash - receipt.finalAmount;
// 记录交易时间
auto now = chrono::system_clock::now();
receipt.time = formatTime(now);
return receipt;
}
};
主控流程
把上面几个模块串起来,就是完整的收银流程:
int main() {
ProductManager pm;
ShoppingCart cart;
// 初始化商品数据
pm.addProduct({"001", "苹果", 5.0, 100});
pm.addProduct({"002", "牛奶", 12.5, 50});
pm.addProduct({"003", "面包", 8.0, 30});
// 模拟扫描商品
cart.addItem(*pm.findProduct("001"), 3); // 3个苹果
cart.addItem(*pm.findProduct("002"), 2); // 2瓶牛奶
// 应用折扣:满30减5
FullReduction discount(30, 5);
// 结算
Checkout checkout;
auto receipt = checkout.processPayment(cart, &discount, 50.0);
// 打印小票
printReceipt(receipt);
return 0;
}
你看,整个流程很清晰。主控函数只负责调度,具体逻辑都交给各个模块。这就是面向对象设计的好处——各司其职,互不干扰。
避坑指南
做这个项目时,有几个地方容易出问题,我帮你列出来:
- 浮点数精度:金额计算用double会有精度问题。我建议用整数表示分(如12.5元存为1250分),最后再除以100显示。
- 库存并发:如果多个收银台同时操作,库存会出错。实际项目中要用锁或事务来保证一致性。
- 折扣叠加:多个折扣同时生效时,顺序很重要。是先打折再满减,还是先满减再打折?这个得跟业务方确认清楚。
我曾经在项目里遇到过最离谱的bug:满100减20和8折同时生效,结果算出来的金额是负数。排查了半天,发现是折扣叠加顺序写反了。从那以后,我每次写折扣逻辑都会画个流程图。
扩展思考
这个收银系统虽然简单,但扩展性很好。你可以给它加上:
- 会员系统:不同会员等级享受不同折扣
- 商品条码扫描:用扫码枪输入商品编号
- 数据持久化:把商品和交易记录存到文件或数据库
- 图形界面:用Qt或MFC做个窗口程序
每一步扩展,都能让你对C++的理解更深一层。我个人觉得,项目实战最大的价值就在这里——你学到的不是孤立的语法,而是如何用代码解决真实问题。
好了,收银系统的核心内容就这些。代码我已经贴出来了,你可以直接跑起来试试。有什么问题,欢迎随时交流。