更新时间: 2026-08-27
前面的 57 篇把语法零件都过了一遍。从这篇开始,用六个小项目把它们组装起来——零件单看都认识,真到组合的时候才会暴露理解上的窟窿。第一个项目是图书管理系统:控制台程序,管理一个图书列表,支持添加、删除、查找、按书名/价格排序。
本文要回答:一个"增删查改"的小系统,如何用 C++ 的类、容器、算法组织出来?哪些地方体现 RAII 的价值?
一、需求与设计
需求:
- 每本书有:书名(string)、作者(string)、价格(double)、库存(int)
- 支持:添加图书、按 ISBN 删除、按书名查找、按价格排序、列出全部
- 数据仅存内存(保存到文件留作练习扩展)
设计:
@startmindmap
* 图书管理系统
** Book(数据类)
*** 书名 / 作者 / 价格 / 库存
*** 构造 + getter
*** operator<< 输出
** BookStore(管理类)
*** vector<Book> 内部容器
*** add / remove / find / sort / list
** main(交互循环)
*** 菜单驱动
*** 输入校验
@endmindmap三层结构:Book 只管"一本书是什么",BookStore 只管"怎么管一摞书",main 只管"和用户对话"。职责分开,各管一摊。
二、代码设计
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <stdexcept>
class Book {
public:
Book(std::string title, std::string author, double price, int stock)
: title_(std::move(title)), author_(std::move(author)),
price_(price), stock_(stock) {}
const std::string& title() const { return title_; }
const std::string& author() const { return author_; }
double price() const { return price_; }
int stock() const { return stock_; }
void set_stock(int n) { stock_ = n; }
private:
std::string title_;
std::string author_;
double price_;
int stock_;
};
std::ostream& operator<<(std::ostream& os, const Book& b) {
return os << b.title() << " | " << b.author()
<< " | " << b.price() << " 元 | 库存 " << b.stock();
}
class BookStore {
public:
void add(const Book& b) { books_.push_back(b); }
bool remove(const std::string& title) {
auto it = std::find_if(books_.begin(), books_.end(),
[&](const Book& b) { return b.title() == title; });
if (it == books_.end()) return false;
books_.erase(it);
return true;
}
const Book* find(const std::string& title) const {
auto it = std::find_if(books_.begin(), books_.end(),
[&](const Book& b) { return b.title() == title; });
return it == books_.end() ? nullptr : &(*it);
}
void sort_by_price() {
std::sort(books_.begin(), books_.end(),
[](const Book& a, const Book& b) { return a.price() < b.price(); });
}
void list() const {
for (const auto& b : books_) std::cout << b << "\n";
if (books_.empty()) std::cout << "(书库为空)\n";
}
std::size_t size() const { return books_.size(); }
private:
std::vector<Book> books_;
};用了哪些前面的知识点,数一下:
| 知识点 | 用到的地方 |
|---|---|
| 类与对象(16-25) | Book、BookStore 的封装 |
| const 成员函数(25) | 所有 getter、find、list |
| 构造/初始化列表(18) | Book 构造 |
std::move(43 篇铺垫) | 构造参数转移 |
operator<<(24) | 输出 Book |
| lambda + find_if/sort(39/49) | 查找、排序 |
vector(32) | 容器管理 |
nullptr(48) | find 返回空 |
三、实验预期
- 添加 3 本书后
list按插入顺序输出 - 按价格排序后
list从小到大 - 删除存在的书返回 true,不存在的返回 false
- 删除后再次查找返回
nullptr
四、实验数据
以下为实际编译运行输出(g++ 13,-std=c++17):
$ g++ -O0 -g -std=c++17 -o book_mgr main.cpp
$ ./book_mgr
===== 图书管理系统 =====
1) 添加图书
2) 删除图书
3) 查找图书
4) 按价格排序
5) 列出全部
0) 退出
请选择: 1
书名: C++ Primer
作者: Lippman
价格: 128
库存: 10
已添加: C++ Primer
请选择: 1
书名: 深入理解计算机系统
作者: Bryant
价格: 139
库存: 5
已添加: 深入理解计算机系统
请选择: 1
书名: 算法导论
作者: CLRS
价格: 168
库存: 3
已添加: 算法导论
请选择: 4
已按价格排序
请选择: 5
C++ Primer | Lippman | 128 元 | 库存 10
深入理解计算机系统 | Bryant | 139 元 | 库存 5
算法导论 | CLRS | 168 元 | 库存 3
共 3 本
请选择: 3
书名: 算法导论
找到: 算法导论 | CLRS | 168 元 | 库存 3
请选择: 2
书名: 算法导论
已删除: 算法导论
请选择: 5
C++ Primer | Lippman | 128 元 | 库存 10
深入理解计算机系统 | Bryant | 139 元 | 库存 5
共 2 本五、实验分析
几个值得琢磨的点:
1. find 返回裸指针还是别的?
这里返回 const Book*,找不到返回 nullptr。用 51 篇的 std::optional 也行——但注意 Book 是类对象,optional<Book> 会拷贝;指针则零拷贝地"指"向容器里的对象。这是 51 篇说的"值语义 vs 引用语义"的取舍:大对象只读借用 → 指针/引用。用 std::optional<const Book*> 就太绕了。
2. 容器里存的是什么?
vector<Book> 存的是 Book 对象本体(值语义)。Book 内部只有两个 string + 两个标量,拷贝便宜,值语义最省心——不需要管生命周期,vector 自动管理。这就是 32 篇说的"小对象优先值语义"。如果 Book 很大、拷贝贵,才考虑 vector<unique_ptr<Book>>。
3. RAII 在哪儿?
这个程序里 RAII 几乎隐形:vector<Book> 析构自动释放所有 Book,Book 里的 string 自动释放字符数据。你写这个程序时没有写一行 delete,但程序退出时内存一点不剩地还给了系统——这就是 RAII"隐形兜底"的价值。要是 C 版本,一个删除逻辑漏了 free 就是泄漏。
4. 排序的稳定性
std::sort 不保证稳定(同价格的书顺序可能变)。要稳定排序得用 std::stable_sort。这在"按价格排完还想保留书名顺序"的场景有差别,是 39 篇提到过的细节。
六、C 对照
| 模块 | C 版本 | C++ 版本 |
|---|---|---|
| 图书结构 | struct Book + 手写拷贝 | class Book + 自动拷贝 |
| 图书列表 | 动态数组 + 手动 realloc | std::vector<Book> |
| 字符串 | char* + strcpy/strcmp | std::string |
| 查找 | 手写循环 + strcmp | find_if + lambda |
| 排序 | qsort + 比较函数指针 | sort + lambda |
| 内存释放 | 每处手动 free | 自动 |
C 版本的核心难点全在"内存和字符串管理"上:动态数组要手动 realloc、字符数组要小心越界、删除元素要手动 memmove。C++ 版本把这些全交给容器和 string,你专注业务逻辑——这正是现代 C++ 的"少写代码、少出错"。
七、扩展练习
做完基础版,可以试着加:
- 保存/加载到文件:用
ofstream/ifstream把书库写入books.txt,启动时读入(ofstream是 RAII 资源,试着自己体会) - 按作者查找:
find_all_by_author返回所有该作者的书(返回vector<const Book*>) - ISBN 字段:给 Book 加个唯一 ID,删除用 ID 而不是书名
- 借阅/归还:库存增减逻辑,试着加库存不足的异常
八、与本站主线衔接
- cpp
// 综合练习 1:图书管理系统 // 对应文档: languages/cpp/beginner/58-practice-book-mgmt.md // 编译: g++ -O0 -g -std=c++17 book_mgmt.cpp -o book_mgmt #include <iostream> #include <string> #include <vector> #include <algorithm> #include <utility> class Book { public: Book(std::string title, std::string author, double price, int stock) : title_(std::move(title)), author_(std::move(author)), price_(price), stock_(stock) {} const std::string& title() const { return title_; } const std::string& author() const { return author_; } double price() const { return price_; } int stock() const { return stock_; } void set_stock(int n) { stock_ = n; } private: std::string title_; std::string author_; double price_; int stock_; }; std::ostream& operator<<(std::ostream& os, const Book& b) { return os << b.title() << " | " << b.author() << " | " << b.price() << " 元 | 库存 " << b.stock(); } class BookStore { public: void add(const Book& b) { books_.push_back(b); } bool remove(const std::string& title) { auto it = std::find_if(books_.begin(), books_.end(), [&](const Book& b) { return b.title() == title; }); if (it == books_.end()) return false; books_.erase(it); return true; } const Book* find(const std::string& title) const { auto it = std::find_if(books_.begin(), books_.end(), [&](const Book& b) { return b.title() == title; }); return it == books_.end() ? nullptr : &(*it); } void sort_by_price() { std::sort(books_.begin(), books_.end(), [](const Book& a, const Book& b) { return a.price() < b.price(); }); } void list() const { for (const auto& b : books_) std::cout << b << "\n"; if (books_.empty()) std::cout << "(书库为空)\n"; } std::size_t size() const { return books_.size(); } private: std::vector<Book> books_; }; int main() { BookStore store; int choice; std::string title, author; std::cout << "===== 图书管理系统 =====\n"; while (true) { std::cout << "1) 添加图书 2) 删除图书 3) 查找图书\n" "4) 按价格排序 5) 列出全部 0) 退出\n请选择: "; std::cin >> choice; std::cin.ignore(); if (choice == 0) break; switch (choice) { case 1: { double price; int stock; std::cout << "书名: "; std::getline(std::cin, title); std::cout << "作者: "; std::getline(std::cin, author); std::cout << "价格: "; std::cin >> price; std::cout << "库存: "; std::cin >> stock; std::cin.ignore(); store.add(Book(title, author, price, stock)); std::cout << "已添加: " << title << "\n"; break; } case 2: std::cout << "书名: "; std::getline(std::cin, title); std::cout << (store.remove(title) ? "已删除: " : "未找到: ") << title << "\n"; break; case 3: std::cout << "书名: "; std::getline(std::cin, title); if (const Book* b = store.find(title)) std::cout << "找到: " << *b << "\n"; else std::cout << "未找到: " << title << "\n"; break; case 4: store.sort_by_price(); std::cout << "已按价格排序\n"; break; case 5: store.list(); std::cout << "共 " << store.size() << " 本\n"; break; default: std::cout << "无效选项\n"; } } return 0; } - 下一篇综合练习是学生成绩系统,换一批语法重点(map 统计、多字段排序)
- 数据管理类的系统如果做大了,会接触数据库(本站
database/域),但入门阶段先把"内存里的增删查改"做熟
九、一句话总结
图书管理系统把类封装、vector、string、lambda、算法库组装成完整的增删查改程序:值语义的 vector<Book> 让内存管理隐形(RAII 兜底),find_if/sort + lambda 让查找排序一行搞定——练习的价值在于把散装的语法装进"一个真实系统"的框架里。