1. 项目概述与核心价值
最近在带几个刚入门的C++学员做项目,发现很多朋友在学完基础语法后,面对“做一个项目”这个任务时,常常会感到无从下手。他们要么是觉得课本上的例子太简单,要么是看到开源社区那些动辄几万行代码的庞然大物直接被劝退。其实,从“知道语法”到“能写项目”之间,缺的往往就是一个结构清晰、功能完整、但又不过于复杂的实战案例。这个“简易购物车系统”就是为此而生的。
它听起来很普通,对吧?一个购物车,无非就是加商品、删商品、算总价。但恰恰是这种我们日常生活中习以为常的功能,背后涵盖了C++面向对象编程的几乎所有核心概念:类的封装、继承与多态、STL容器的灵活运用、文件操作实现数据持久化,以及控制台交互的逻辑设计。你不需要去纠结复杂的图形界面或者网络通信,就能在一个可控的范围内,把C++的基础知识串起来,形成一个完整的编程思维。这就是它的核心价值——一个绝佳的、承上启下的综合性练手项目。
通过实现它,你能真切地体会到如何将抽象的需求(用户想买东西)转化为具体的代码结构(商品类、购物车类),如何管理程序的状态(购物车里的商品列表),以及如何与用户进行交互(一个简洁的菜单系统)。这比单独练习十道算法题或者看十篇设计模式的文章,收获要大得多。接下来,我们就一步步拆解,看看如何从零开始,构建一个五脏俱全的C++购物车系统。
2. 系统整体设计与核心思路拆解
在动手写代码之前,花点时间做设计是绝对值得的。这能避免你写到一半发现结构混乱,推倒重来的窘境。我们的目标是“简易”,但“简易”不等于“随意”。一个好的设计应该职责清晰、易于扩展。
2.1 核心类设计:从现实世界到代码世界
购物车系统最核心的两个实体就是“商品”和“购物车”。我们需要用类(Class)来抽象它们。
商品类 (Product): 这是系统的基础单元。它需要封装商品的内在属性。最基本的属性应该包括:
id: 商品唯一标识(字符串或整数)。这是区分不同商品的关键,比如“P001”。name: 商品名称。price: 商品单价(浮点数,考虑精度可以用double)。stock: 库存数量(整数)。
为什么需要id而不只用name?因为名称可能重复,而id是唯一的,便于精确查找和管理。这个类的主要职责就是存储数据和提供安全的访问接口(通过getter和setter方法)。我们会把数据成员设为private,这是封装思想的直接体现。
购物车类 (ShoppingCart): 这是系统的业务逻辑核心。它管理的是一个商品集合,以及在这个集合上的各种操作。
- 核心数据成员:一个容器,用于存放“商品及其购买数量”。这里就有个关键选择:用什么容器?
std::vector简单,但查找特定商品效率是O(n)。std::map或std::unordered_map(以商品id为键,购买数量和商品信息为值)可以将查找、更新操作降到O(1)或O(log n),更为合适。我们选择std::map<std::string, std::pair<Product, int>>,键是商品ID,值是一个pair,包含商品对象和购买数量。 - 核心成员函数:
addItem(const Product& product, int quantity): 添加商品。需检查库存是否充足。removeItem(const std::string& productId, int quantity): 移除商品。需处理完全移除和部分移除的情况。updateItemQuantity(const std::string& productId, int newQuantity): 修改商品数量。calculateTotal(): 计算购物车内所有商品的总价。displayCart(): 清晰展示购物车内容(商品名、单价、数量、小计)。clearCart(): 清空购物车。
购物车类的职责是管理商品集合和执行业务规则(如库存校验)。
2.2 数据流与程序结构:单次运行与持久化
一个完整的系统还需要考虑数据从哪里来,到哪里去。
- 商品数据初始化:程序启动时,需要有一批商品可供选择。我们可以硬编码在程序里,但更好的做法是从文件(如
products.txt或products.csv)中读取。这引入了文件输入流(ifstream)的使用。 - 购物车数据持久化(可选但建议实现):用户本次未结算的购物车,能否下次启动程序时恢复?这需要将购物车状态(商品ID和数量)保存到文件(如
cart_save.txt),并在程序启动时检查并加载。这引入了文件输出流(ofstream)和更复杂的状态管理。 - 程序主循环:一个典型的控制台程序结构是“菜单驱动”。通过
while循环持续显示功能菜单,根据用户输入的数字或字符调用不同的函数,直到用户选择退出。
设计模式的一点思考:在这个简易系统中,你可以隐约看到“模型-视图-控制器”(MVC)模式的影子。Product和ShoppingCart是模型(Model),负责数据和业务逻辑;控制台菜单的显示和用户输入处理是视图(View)和控制器(Controller)的混合体。虽然我们不做严格分离,但保持这种意识对后续理解更复杂的架构有帮助。
3. 核心模块实现与关键技术点解析
理论说完,我们进入实战环节。我会用代码片段展示关键实现,并解释为什么这么做。
3.1 Product类的实现:封装与接口
// Product.h #ifndef PRODUCT_H #define PRODUCT_H #include <string> class Product { private: std::string id; std::string name; double price; int stock; public: // 构造函数 Product(const std::string& id = "", const std::string& name = "", double price = 0.0, int stock = 0); // Getter 和 Setter (提供对私有成员的受控访问) std::string getId() const; void setId(const std::string& newId); std::string getName() const; void setName(const std::string& newName); double getPrice() const; void setPrice(double newPrice); int getStock() const; void setStock(int newStock); // 显示商品信息 void display() const; // 减少库存(购物车添加时调用) bool reduceStock(int quantity); // 增加库存(购物车移除时调用) void increaseStock(int quantity); }; #endif // PRODUCT_H// Product.cpp #include “Product.h” #include <iostream> #include <iomanip> // 用于格式化输出 Product::Product(const std::string& id, const std::string& name, double price, int stock) : id(id), name(name), price(price), stock(stock) {} // ... Getter 和 Setter 的实现略 ... void Product::display() const { std::cout << std::left << std::setw(10) << id << std::setw(20) << name << “¥” << std::setw(10) << std::fixed << std::setprecision(2) << price << std::setw(10) << stock << “件” << std::endl; } bool Product::reduceStock(int quantity) { if (quantity > 0 && quantity <= stock) { stock -= quantity; return true; // 扣减成功 } return false; // 库存不足,扣减失败 } void Product::increaseStock(int quantity) { if (quantity > 0) { stock += quantity; } }关键点解析:
- 头文件保护:
#ifndef/#define/#endif防止头文件被多次包含,是编写多文件项目的必备好习惯。 - 成员初始化列表:在构造函数中,使用初始化列表(
: id(id), name(name)...)来初始化成员变量,这比在构造函数体内赋值更高效(对于非内置类型,避免了一次默认构造+一次赋值)。 - const成员函数:
getId(),display()等不修改对象状态的函数被声明为const。这是一个重要的语义约定,保证了函数的“只读”性,也让代码更安全。 - 库存管理方法:
reduceStock和increaseStock封装了库存变更的逻辑,并进行了简单的校验(如数量需为正数)。reduceStock返回布尔值,让调用者知道操作是否成功,这是一种清晰的错误反馈机制。
3.2 ShoppingCart类的实现:STL容器与业务逻辑
这是系统的中枢,我们重点看如何用std::map来管理购物车项。
// ShoppingCart.h #ifndef SHOPPINGCART_H #define SHOPPINGCART_H #include “Product.h” #include <map> #include <string> class ShoppingCart { private: // key: 商品ID, value: pair<商品对象, 购买数量> std::map<std::string, std::pair<Product, int>> items; public: ShoppingCart() = default; // 核心操作 bool addItem(const Product& product, int quantity); bool removeItem(const std::string& productId, int quantity = -1); // -1 表示移除全部该商品 bool updateItemQuantity(const std::string& productId, int newQuantity); // 计算与展示 double calculateTotal() const; void displayCart() const; void clearCart(); // 获取购物车状态(用于持久化) const std::map<std::string, std::pair<Product, int>>& getItems() const; // 从状态加载(用于持久化) void loadItems(const std::map<std::string, std::pair<Product, int>>& loadedItems); // 检查购物车是否为空 bool isEmpty() const; }; #endif // SHOPPINGCART_H// ShoppingCart.cpp #include “ShoppingCart.h” #include <iostream> #include <iomanip> bool ShoppingCart::addItem(const Product& product, int quantity) { if (quantity <= 0) { std::cout << “添加数量必须大于0!” << std::endl; return false; } // 检查库存 if (!product.reduceStock(quantity)) { std::cout << “商品 [” << product.getName() << “] 库存不足!当前库存:” << product.getStock() + quantity << “, 尝试添加:” << quantity << std::endl; return false; } std::string pid = product.getId(); auto it = items.find(pid); if (it != items.end()) { // 购物车中已有此商品,增加数量 it->second.second += quantity; } else { // 购物车中无此商品,新增一项 // 注意:这里存储的是product的副本。更复杂的实现可能需要存储指针或引用,但副本更安全,避免了外部商品对象被修改的影响。 items[pid] = std::make_pair(product, quantity); } std::cout << “已成功添加 ” << quantity << “ 件 [” << product.getName() << “] 到购物车。” << std::endl; return true; } bool ShoppingCart::removeItem(const std::string& productId, int quantity) { auto it = items.find(productId); if (it == items.end()) { std::cout << “购物车中未找到商品ID:” << productId << std::endl; return false; } Product& productInCart = it->second.first; int& currentQty = it->second.second; if (quantity == -1 || quantity >= currentQty) { // 移除全部 productInCart.increaseStock(currentQty); // 库存加回 items.erase(it); std::cout << “已从购物车移除所有商品 [” << productInCart.getName() << “]。” << std::endl; } else if (quantity > 0 && quantity < currentQty) { // 移除部分 productInCart.increaseStock(quantity); currentQty -= quantity; std::cout << “已从购物车移除 ” << quantity << “ 件 [” << productInCart.getName() << “]。” << std::endl; } else { std::cout << “移除数量无效!” << std::endl; return false; } return true; } double ShoppingCart::calculateTotal() const { double total = 0.0; for (const auto& item : items) { total += item.second.first.getPrice() * item.second.second; } return total; } void ShoppingCart::displayCart() const { if (items.empty()) { std::cout << “购物车是空的。” << std::endl; return; } std::cout << “\n========== 购物车详情 ==========” << std::endl; std::cout << std::left << std::setw(10) << “ID” << std::setw(20) << “商品名” << std::setw(10) << “单价” << std::setw(10) << “数量” << std::setw(12) << “小计” << std::endl; std::cout << “----------------------------------------------” << std::endl; for (const auto& item : items) { const Product& p = item.second.first; int qty = item.second.second; double subtotal = p.getPrice() * qty; std::cout << std::left << std::setw(10) << p.getId() << std::setw(20) << p.getName() << “¥” << std::setw(9) << std::fixed << std::setprecision(2) << p.getPrice() << std::setw(10) << qty << “¥” << std::setw(10) << std::fixed << std::setprecision(2) << subtotal << std::endl; } std::cout << “----------------------------------------------” << std::endl; std::cout << std::right << std::setw(50) << “总计:¥” << std::fixed << std::setprecision(2) << calculateTotal() << std::endl; std::cout << “==============================================\n” << std::endl; }关键点解析与避坑指南:
std::map的使用:我们选择map而非unordered_map,因为商品ID通常需要有序展示(虽然这里影响不大),且数据量不大,两者性能差异可忽略。map的find操作返回迭代器,通过判断it != items.end()来检查元素是否存在,这是标准用法。- 库存同步是难点:这是购物车逻辑中最容易出错的地方。注意在
addItem中,我们先调用product.reduceStock,成功后才更新购物车。这个顺序不能反,否则可能出现库存扣减失败但购物车已添加的脏数据。同理,在removeItem中,先恢复库存,再修改或删除购物车项。这保证了库存数据与购物车数据的一致性。 - 对象的存储方式:
items[pid] = std::make_pair(product, quantity);这里存储的是product的副本。这意味着,即使外部原始的product对象库存被修改,购物车里存的这个副本的库存信息并不会自动更新(除非我们通过ID去商品总列表中重新查找)。在简易系统中,这可以接受。更严谨的做法是存储商品的指针(或智能指针std::shared_ptr<Product>),指向商品总列表中的同一个对象,这样库存信息就能实时同步。但这会引入指针管理和生命周期的复杂度,作为进阶练习可以考虑。 - 格式化输出:使用
<iomanip>头文件中的std::setw,std::left,std::fixed,std::setprecision来对齐表格和控制小数位数,能让控制台输出看起来专业很多。
4. 系统集成与主程序逻辑搭建
有了核心类,我们需要一个“导演”来把它们组织起来,这就是主函数(main.cpp)要做的事情。主程序逻辑通常包括初始化、主循环和清理。
4.1 商品库的初始化与文件读取
我们不应该把商品信息硬编码在main函数里。最佳实践是从文件加载。
假设有一个products.txt文件,每行格式为:ID,Name,Price,Stock
P001,苹果 iPhone 15,6999.00,50 P002,小米 14 Pro,4999.00,100 P003,华为 MateBook X Pro,8999.00,30 P004,索尼 WH-1000XM5,2599.00,80 P005,可口可乐 500ml,3.00,200// 在 main.cpp 或一个专门的工具函数中 #include <fstream> #include <sstream> #include <vector> std::vector<Product> loadProductsFromFile(const std::string& filename) { std::vector<Product> productList; std::ifstream inFile(filename); if (!inFile.is_open()) { std::cerr << “错误:无法打开商品文件 ” << filename << “, 将使用默认商品列表。” << std::endl; // 可以在这里添加一些默认商品 productList.push_back(Product(“P001”, “测试商品A”, 100.0, 10)); return productList; } std::string line; while (std::getline(inFile, line)) { std::stringstream ss(line); std::string id, name, priceStr, stockStr; // 使用 getline 配合逗号分隔符读取 if (std::getline(ss, id, ‘,’) && std::getline(ss, name, ‘,’) && std::getline(ss, priceStr, ‘,’) && std::getline(ss, stockStr)) { try { double price = std::stod(priceStr); int stock = std::stoi(stockStr); productList.push_back(Product(id, name, price, stock)); } catch (const std::invalid_argument& e) { std::cerr << “警告:跳过格式错误的行:” << line << std::endl; } } } inFile.close(); std::cout << “成功从 ” << filename << “ 加载了 ” << productList.size() << “ 件商品。” << std::endl; return productList; }关键点:
std::ifstream用于读取文件。std::getline可以指定分隔符,非常适合读取CSV格式。std::stringstream将字符串转换为其他类型(std::stod,std::stoi)。- 一定要检查文件是否成功打开(
is_open()),并进行错误处理。 - 使用
try-catch捕获转换异常,增强程序健壮性。
4.2 主菜单与用户交互循环
这是程序的“脸面”,要清晰、友好。
// main.cpp 主函数部分 int main() { // 1. 初始化 std::vector<Product> allProducts = loadProductsFromFile(“products.txt”); ShoppingCart cart; // (可选)加载已保存的购物车 // loadCartFromFile(cart, “cart_save.txt”); int choice = 0; bool running = true; while (running) { displayMainMenu(); std::cout << “请输入您的选择:”; std::cin >> choice; // 清除输入缓冲区,防止错误输入导致死循环 std::cin.clear(); std::cin.ignore(std::numeric_limits<std::streamsize>::max(), ‘\n’); switch (choice) { case 1: // 浏览所有商品 displayAllProducts(allProducts); break; case 2: // 添加商品到购物车 handleAddItem(cart, allProducts); break; case 3: // 查看购物车 cart.displayCart(); break; case 4: // 从购物车移除商品 handleRemoveItem(cart); break; case 5: // 修改商品数量 handleUpdateItem(cart); break; case 6: // 结算 handleCheckout(cart); break; case 7: // 清空购物车 cart.clearCart(); std::cout << “购物车已清空。” << std::endl; break; case 0: // 退出 running = false; std::cout << “感谢使用,再见!” << std::endl; // (可选)退出前保存购物车 // saveCartToFile(cart, “cart_save.txt”); break; default: std::cout << “无效的选择,请重新输入。” << std::endl; } std::cout << std::endl; } return 0; }交互处理函数示例 (handleAddItem):
void handleAddItem(ShoppingCart& cart, std::vector<Product>& allProducts) { std::string pid; int quantity; std::cout << “请输入要添加的商品ID:”; std::cin >> pid; // 在商品列表中查找 auto it = std::find_if(allProducts.begin(), allProducts.end(), [&pid](const Product& p) { return p.getId() == pid; }); if (it == allProducts.end()) { std::cout << “未找到ID为 ” << pid << “ 的商品。” << std::endl; return; } std::cout << “请输入要添加的数量:”; std::cin >> quantity; // 调用购物车的添加功能 cart.addItem(*it, quantity); }关键点与避坑指南:
- 输入验证与缓冲清理:用户输入是“脏”的。
std::cin >> choice;如果用户输入了字母,会导致cin进入错误状态,后续所有输入都会失败,程序可能陷入死循环。cin.clear()清除错误状态,cin.ignore(...)清空输入缓冲区直到换行符,这是处理这类问题的标准做法。 - 菜单功能分离:将每个菜单项对应的具体操作封装成函数(如
handleAddItem),保持main函数简洁清晰。这也符合“单一职责原则”。 - 商品查找:在
allProducts中查找商品使用了std::find_if算法和Lambda表达式,这是现代C++更优雅的写法。你也可以用简单的for循环。 - 结算逻辑:
handleCheckout函数应该展示最终清单、总价,然后询问确认。确认后,可以生成订单(例如打印到文件或屏幕),然后清空购物车。注意,结算时不应再回滚库存,因为添加商品时库存已经扣减。
5. 进阶功能探讨与扩展方向
一个基础的购物车系统已经完成了。但如果你想让它更像一个“项目”,而不是“作业”,可以考虑以下扩展方向,每一个都能深入练习特定的C++知识点。
5.1 数据持久化:让购物车“记住”状态
我们之前提到了保存和加载购物车。实现它需要将ShoppingCart中的items映射序列化到文件。
保存到文件:
void saveCartToFile(const ShoppingCart& cart, const std::string& filename) { std::ofstream outFile(filename); if (!outFile.is_open()) { std::cerr << “无法保存购物车到文件 ” << filename << std::endl; return; } const auto& items = cart.getItems(); for (const auto& item : items) { const Product& p = item.second.first; int qty = item.second.second; outFile << p.getId() << “,” << qty << “\n”; // 只存ID和数量 } outFile.close(); std::cout << “购物车已保存。” << std::endl; }从文件加载:
void loadCartFromFile(ShoppingCart& cart, const std::string& filename, const std::vector<Product>& allProducts) { std::ifstream inFile(filename); if (!inFile.is_open()) { return; // 文件不存在是正常的(第一次运行) } std::map<std::string, int> loadedData; // 临时存储从文件读取的ID和数量 std::string line; while (std::getline(inFile, line)) { std::stringstream ss(line); std::string pid, qtyStr; if (std::getline(ss, pid, ‘,’) && std::getline(ss, qtyStr)) { try { int qty = std::stoi(qtyStr); loadedData[pid] = qty; } catch (...) { /* 忽略错误行 */ } } } inFile.close(); // 根据ID从商品库中找到对应商品,并加入购物车 for (const auto& data : loadedData) { auto it = std::find_if(allProducts.begin(), allProducts.end(), [&data](const Product& p) { return p.getId() == data.first; }); if (it != allProducts.end()) { cart.addItem(*it, data.second); // 注意:这里addItem会再次检查库存! } } }注意:加载时调用
cart.addItem会再次检查库存。如果上次保存后商品库存发生了变化(比如被管理员修改),可能导致添加失败。这是一个需要根据业务逻辑权衡的设计点。
5.2 使用智能指针管理商品对象
如前所述,存储商品副本可能导致信息不同步。我们可以改用std::shared_ptr<Product>。
- 商品库改为:
std::vector<std::shared_ptr<Product>> allProducts; - 购物车
items类型改为:std::map<std::string, std::pair<std::shared_ptr<Product>, int>> - 添加商品时,传入的是智能指针:
cart.addItem(productPtr, quantity);
这样,购物车和商品库共享同一个商品对象,库存信息完全同步。但你需要学习智能指针的用法,并小心避免循环引用(在这个简单模型里不会发生)。
5.3 引入简单的折扣策略
这可以练习策略模式(Strategy Pattern)或简单的继承多态。
- 定义一个
Discount基类,有virtual double apply(double originalPrice, int quantity)纯虚函数。 - 派生
PercentageDiscount(百分比折扣)、FixedDiscount(固定金额减免)、BuyXGetYFree(买X赠Y)等子类。 Product类可以关联一个Discount指针(或智能指针)。- 在
ShoppingCart::calculateTotal()中,遍历商品时调用其折扣策略的apply方法计算折后价。
5.4 简单的用户登录与多购物车
这可以引入User类和UserManager类。每个User对象拥有一个自己的ShoppingCart实例。用户登录后,操作的就是当前用户的购物车。这涉及到用户数据的文件存储、密码验证(简易的可以明文,实际应用必须加密)等。
6. 开发环境配置、编译与调试心得
很多初学者卡在第一步:环境。这里分享一些最直接的实操建议。
1. 编译器选择:
- Windows:强烈推荐使用MinGW-w64中的
g++。可以通过MSYS2或直接下载MinGW-w64安装包获取。它是GNU工具链在Windows上的实现,稳定且标准。 - macOS:安装Xcode Command Line Tools,自带
clang++,非常优秀。 - Linux:使用系统包管理器安装
g++(如sudo apt install g++)。
2. 集成开发环境(IDE)或编辑器:
- Visual Studio Code (VSCode) + C/C++扩展:轻量、灵活,需要自己配置
tasks.json(编译)和launch.json(调试)。对于本项目,一个简单的tasks.json配置如下:{ “version”: “2.0.0”, “tasks”: [ { “type”: “cppbuild”, “label”: “C/C++: g++.exe 生成活动文件”, “command”: “C:\\mingw64\\bin\\g++.exe”, // 你的g++路径 “args”: [ “-fdiagnostics-color=always”, “-g”, “${fileDirname}\\*.cpp”, // 编译当前目录所有.cpp文件 “-o”, “${fileDirname}\\${fileBasenameNoExtension}.exe”, “-std=c++11” // 或更高标准 ], “options”: { “cwd”: “${fileDirname}” }, “problemMatcher”: [“$gcc”], “group”: “build” } ] } - Visual Studio (Windows):功能强大,开箱即用。创建“空项目”,把
.cpp和.h文件添加进去即可。调试体验一流。 - CLion (跨平台):专业的C++ IDE,智能提示、重构、调试功能都很强大,但需要付费或教育许可。
3. 多文件项目的编译: 如果你的项目像我们这样分成了main.cpp,Product.cpp,ShoppingCart.cpp,在命令行(终端)中编译的命令是:
g++ -std=c++11 -o shopping_cart main.cpp Product.cpp ShoppingCart.cpp-std=c++11指定使用C++11标准,-o shopping_cart指定输出的可执行文件名。
4. 调试实战心得:
- 善用
cout进行“打印调试”:在关键函数入口、变量修改后,输出变量的值。这是最朴素但最有效的调试手段。 - 学习使用调试器(GDB/LLDB):在VSCode或VS中设置断点,单步执行,查看变量值。当程序崩溃或逻辑复杂时,调试器能帮你快速定位问题。
- 在VSCode中,确保
launch.json配置正确,编译时加了-g参数生成调试信息。 - 遇到“段错误(segmentation fault)”,十有八九是访问了非法内存(如空指针、数组越界)。用调试器查看崩溃时的调用栈。
- 在VSCode中,确保
- 本项目的常见调试场景:
- 添加商品失败:检查
loadProductsFromFile是否成功读取了商品,商品ID是否匹配,库存检查逻辑是否正确。 - 总价计算错误:在
calculateTotal循环内打印每一项的单价和数量,检查浮点数计算精度问题(商业软件会用decimal类型或以分为单位存储整数)。 - 菜单输入后程序行为异常:大概率是输入缓冲区清理问题,回顾我们
main函数中cin.clear()和cin.ignore()的用法。
- 添加商品失败:检查
5. 版本控制入门: 强烈建议使用Git来管理你的代码。即使是一个人开发,Git也能让你安心地尝试新功能而不怕搞坏原有代码。在项目根目录执行:
git init git add . git commit -m “初始提交:完成购物车基础功能”学习基本的git add,git commit,git branch,git merge操作,这是现代开发的必备技能。
把这个项目做下来,你收获的不仅仅是一个可以运行的程序,更是一套如何用C++思考和解决实际问题的思维框架。从类设计、STL使用、文件IO到简单的用户交互,每一个环节都踩一遍,你对C++的理解会扎实很多。接下来,你可以基于第5部分的扩展方向,选择一两个深入下去,把它变成你简历上一个值得细说的个人项目。编程最好的学习方式就是动手,遇到问题就查、就调试,这个过程积累的经验,才是最宝贵的。