C++ practical course · lesson 20

C++ 结课项目:命令行信息管理系统

这一次不再跟着老师逐行模仿。学生从需求清单开始,选择数据结构和算法,完成一个可以真正交给别人使用的命令行信息管理系统。

Easy explanation

项目不是把所有学过的语法堆在一起。

一个好项目先解决清楚的问题,再选择恰好够用的工具。图书管理、成绩管理、任务管理和游戏角色管理都可以成为选题,但必须明确用户是谁、保存什么记录、最常执行哪些操作、出现错误时怎样恢复。没有需求边界,代码会不断增加却始终无法完成。

结课项目要经历分析、设计、实现、测试和演示。每一阶段都留下可检查成果:需求清单、数据模型、菜单草图、测试表和说明文档。功能不是越多越好,稳定完成核心增删改查、搜索排序、统计和保存,比十个半成品功能更有价值。

目标 1把自然语言需求拆成可验证功能
目标 2为记录设计类并选择合适容器
目标 3整合排序、查找、统计和文件存储
目标 4完成测试记录、说明文档和项目演示
C++ 结课项目:命令行信息管理系统通俗学习插图
结课项目把对象、容器、算法、文件和测试连接成完整产品。

Core concepts

先把四个核心知识点说清楚。

01

最小可用版本

add / list / find / save

先完成最小闭环,再增加修改、删除、排序和统计,始终保持程序可运行。

02

分层职责

UI -> Service -> Repository

菜单负责交互,业务层负责规则,存储层负责文件,避免所有代码塞进 main。

03

稳定标识

int id;

每条记录使用唯一编号,显示名称可以重复或修改,关联和删除仍然可靠。

04

完成定义

requirements + tests + demo

功能只有通过约定测试、保存成功并能演示,才算完成。

optional<Item> findById(int id);

接口明确表达记录可能不存在,不用特殊假对象。

sort(items.begin(), items.end(), comparator);

排序规则独立,菜单只负责选择规则。

bool save(const vector<Item>& items);

存储函数返回成功状态,界面根据结果提示用户。

assert(service.remove(999) == false);

把找不到记录等失败情况写进固定测试。

Complete examples

两个例子,把知识变成能运行的程序。

Example 1

例子一:项目骨架与职责边界

Book 只保存一本书的数据,Library 负责唯一编号、借阅规则、查找和容器,main 负责菜单。文件读写还可以放入单独 Repository。

先写类接口和空函数,让程序能编译,再一次完成一个用例。这样任何时候都有可运行版本。

模型、服务和主循环
struct Book
{
    int id;
    string title;
    string author;
    bool borrowed = false;
};

class Library
{
private:
    vector<Book> books;
    int nextId = 1;
public:
    int add(string title, string author);
    bool remove(int id);
    Book* find(int id);
    vector<Book> search(string keyword) const;
    void sortByTitle();
    bool load(const string& path);
    bool save(const string& path) const;
};

int main()
{
    Library library;
    library.load("books.txt");
    runMenu(library);
    library.save("books.txt");
}
启动:加载 12 本书菜单:新增、查看、搜索、借阅、归还、统计、退出退出:保存成功

Example 2

例子二:从需求写出验收测试

“删除图书”不是一句代码,而是一组行为:存在时删除并返回 true,不存在返回 false,删除后无法查到,保存重启后仍然不存在。

把这些行为写成测试,再实现函数直到全部通过。每新增功能都先写成功和失败用例,项目会比只靠手工点击稳定得多。

用例驱动项目完成
void testRemoveBook()
{
    Library library;
    int id = library.add("C++ 入门", "TeacherMa");

    assert(library.find(id) != nullptr);
    assert(library.remove(id) == true);
    assert(library.find(id) == nullptr);
    assert(library.remove(id) == false);
}

void testEmptySearch()
{
    Library library;
    assert(library.search("missing").empty());
}
testRemoveBook:通过testEmptySearch:通过全部核心测试通过

Algorithm thinking

把项目切成一条条可以完成和验收的用户故事。

不要写“完成图书系统”这种巨大任务。改成:用户可以添加一本合法图书;用户可以按编号找到图书;不存在编号会提示;数据重启后仍保留。每条故事都能在短时间内实现和测试,完成后再进入下一条。

算法和数据结构要服务需求。主记录数量不大且经常排序展示,可以用 vector;需要按编号频繁查询可增加 unordered_map 索引;撤销操作可用 stack;待处理预约可用 queue。必须解释选择理由,而不是为了展示知识强行使用所有容器。

  1. 1
    确定范围

    选择一个主题,写出必须功能、可选功能和明确不做的功能。

  2. 2
    设计数据与接口

    画类图、选择唯一编号、列出类方法和文件格式。

  3. 3
    迭代实现

    先做最小闭环,每增加一个功能立即测试并保持可运行。

  4. 4
    验收与演示

    使用固定示例数据走完整流程,展示正常、边界和错误恢复。

Common mistakes

这些错误很常见,学会自己排查。

1

一开始追求全部功能

核心流程还没运行就增加颜色、动画和复杂权限,项目容易永远完不成。

2

所有逻辑放进 main

菜单、规则、排序和文件混在一起,无法独立测试和修改。

3

只演示成功路线

不存在编号、空列表、坏文件和重复数据必须成为正式演示的一部分。

4

最后一天才保存文件

持久化会影响编号和模型,应尽早建立最小保存加载闭环。

本课通用调试法

先准备一个最小输入,只保留能够重现问题的几行数据;再在关键步骤输出变量值,确认程序究竟在哪一步偏离预期。编译错误从第一条开始处理,运行错误则比较“实际结果”和“期望结果”。修复后别只重跑原来的例子,还要增加空输入、边界值和错误输入,防止问题换一个形式再次出现。

Homework

第 20 课课后作业:从模仿到独立完成。

建议按顺序完成。前四题帮助巩固语法和基本操作,第五到第七题要求把多个知识点组合起来,第八题是小项目。每道题都要先写输入、处理、输出三行计划,再开始敲代码;程序运行后至少测试正常情况、边界情况和错误情况。

1

项目提案

选择题目,写用户、问题、八项核心功能、三项可选功能和不做范围。

2

数据模型

画类图,标出字段、方法、唯一编号、继承或组合,并解释每个容器选择。

3

菜单原型

先用假数据完成菜单循环、输入验证、返回和退出,确认交互清楚。

4

核心 CRUD

完成新增、查看、修改、删除,覆盖重复编号、找不到目标和空字段。

5

算法功能

加入至少两种排序、一种搜索和三项统计,说明时间复杂度。

6

文件闭环

实现启动加载和退出保存,处理首次运行、空文件和坏记录。

7

测试报告

列出至少二十个测试,记录输入、期望、实际和修复过的问题。

8

最终交付

提交源代码、README、示例数据、类图、测试报告,并完成五分钟现场演示。

本课验收标准:项目可从干净目录编译运行;具备可靠输入、完整 CRUD、排序搜索统计、文件保存加载和错误恢复;至少二十个测试有记录;学生能独立解释类、容器和算法选择。

提交内容应包括源代码、三组测试输入与输出、一个曾经出现的错误及修复方法。最后请用自己的话解释本课最重要的概念;如果只能照着代码念,还需要再独立重写一次核心例子。

Lesson 20 summary

独立完成项目,意味着能做选择、验证结果并解释原因。

requirementsCRUDarchitecturepersistencetests

完成本项目后,学生已经具备继续学习竞赛算法、游戏开发或更大型 C++ 工程的基础。下一阶段应选择一个方向,通过更长项目继续积累调试和设计经验。