30、综合项目实战:图书管理系统或学生成绩管理系统
终于走到这一步了。说实话,每次我带新人做这个综合项目时,都能看到他们眼睛一亮——前面学了一堆零散的知识点,终于能串起来了。我自己当年学C++时,也是做完这个项目后,才真正感觉自己「入门了」。
这个项目,说白了就是把你学过的面向对象、STL、文件操作、多线程这些东西,揉进一个完整的系统里。我选的是图书管理系统来讲,因为它的业务逻辑更直观。你如果喜欢做成绩管理,换一下数据模型就行,架构完全通用。
项目需求概览
先看看我们要做什么:
| 功能模块 | 说明 |
|---|---|
| 图书管理 | 增删改查图书信息(书名、作者、ISBN、库存) |
| 用户管理 | 读者注册、登录、权限控制 |
| 借还书 | 借书、还书、逾期计算 |
| 数据持久化 | 程序退出后数据不丢失,用文件存储 |
| 并发支持 | 多用户同时操作时数据安全 |
嗯,这里要注意:不要一上来就写代码。我见过太多人直接开干,写到一半发现类设计有问题,回头重构,浪费时间。先画图,再动手。
整体架构设计
我习惯用分层架构来做这种系统。下面这张图展示了核心模块之间的关系:
你想想看,每一层只负责自己的事。UI层只管展示和接收输入,业务层只管逻辑判断,数据层只管读写文件。这样改一个地方,不会牵一发动全身。
核心类设计
我直接给出类的骨架,你感受一下:
// 图书实体类
class Book {
public:
string isbn;
string title;
string author;
int totalCopies;
int borrowedCopies;
// 序列化到字符串,方便文件存储
string serialize() const;
static Book deserialize(const string& data);
};
// 用户实体类
class User {
public:
string userId;
string name;
vector<string> borrowedBooks; // 借的书的ISBN列表
double fine; // 罚款金额
string serialize() const;
static User deserialize(const string& data);
};
// 图书管理器
class BookManager {
private:
unordered_map<string, Book> books; // ISBN -> Book
mutable shared_mutex mtx; // 读写锁
public:
bool addBook(const Book& book);
bool removeBook(const string& isbn);
Book* findBook(const string& isbn);
bool borrowBook(const string& isbn);
bool returnBook(const string& isbn);
void saveToFile(const string& filename);
void loadFromFile(const string& filename);
};
关键点:我用 shared_mutex 来实现读写锁。读操作可以并发,写操作必须独占。这在多线程场景下性能提升很明显。我之前做过一个项目,用普通 mutex 锁整个容器,结果并发一高就卡死,换成 shared_mutex 后吞吐量翻了三倍。
多线程安全:借书操作的完整流程
借书这个操作,看起来简单,但多线程下容易出问题。你想想看:两个线程同时检查库存,都发现还剩1本,然后都执行借出,库存就变成-1了。这就是经典的竞态条件。
我的做法是这样的:
bool BookManager::borrowBook(const string& isbn) {
// 写操作,用独占锁
unique_lock lock(mtx);
auto it = books.find(isbn);
if (it == books.end()) return false;
if (it->second.borrowedCopies >= it->second.totalCopies) {
return false; // 库存不足
}
it->second.borrowedCopies++;
return true;
}
注意:千万不要在锁外面检查库存、在锁里面修改。我曾经犯过这个错——先读库存,发现够,然后加锁,再修改。结果两个线程在「读库存」那一步都通过了,最后都执行了修改。这个bug在测试时很难复现,上线后才暴露,教训深刻。
文件操作:数据持久化
我选择用文本文件 + 自定义序列化格式。为什么不用二进制?因为文本文件出错了还能手动编辑修复,调试起来方便。
void BookManager::saveToFile(const string& filename) {
shared_lock lock(mtx); // 读操作,共享锁
ofstream ofs(filename);
if (!ofs) {
cerr << "无法打开文件: " << filename << endl;
return;
}
for (const auto& [isbn, book] : books) {
ofs << book.serialize() << "\n";
}
}
void BookManager::loadFromFile(const string& filename) {
unique_lock lock(mtx); // 写操作,独占锁
ifstream ifs(filename);
if (!ifs) return; // 文件不存在,不报错
books.clear();
string line;
while (getline(ifs, line)) {
if (line.empty()) continue;
Book book = Book::deserialize(line);
books[book.isbn] = book;
}
}
序列化格式我设计成这样的:
// 每行一条记录,字段用 | 分隔
// 图书: B|ISBN|书名|作者|总库存|已借出
// 用户: U|用户ID|姓名|已借书ISBN列表(逗号分隔)|罚款
// 示例数据:
B|978-7-111-12345-6|C++ Primer|Lippman|5|2
B|978-7-111-67890-1|Effective Modern C++|Meyers|3|1
U|U001|张三|978-7-111-12345-6|0.0
U|U002|李四|978-7-111-12345-6,978-7-111-67890-1|5.5
小技巧:用 | 做分隔符,是因为书名里可能有逗号,但很少有条形符。如果你用CSV格式,书名带逗号就解析错了。这个坑我踩过,后来统一改用 | 再也没出过问题。
STL容器选型心得
这个项目里我用到了几个STL容器,说说我的选择理由:
| 容器 | 用途 | 为什么选它 |
|---|---|---|
unordered_map |
按ISBN查书、按ID查用户 | O(1)查找,比map快。不需要排序 |
vector |
用户借书记录列表 | 数量不大,随机访问多,插入删除少 |
shared_mutex |
读写锁 | 读多写少的场景,性能最优 |
string |
所有文本数据 | 现代C++ string 够快,别用char*了 |
说实话,选容器这事,很多人纠结半天。我的建议是:先用最直观的,性能不够再优化。比如你一开始用 map 完全没问题,等数据量到十万级了,再考虑换成 unordered_map 也不迟。
主程序流程
最后看看主函数怎么把这些串起来:
int main() {
BookManager bookMgr;
UserManager userMgr;
// 启动时加载数据
bookMgr.loadFromFile("books.dat");
userMgr.loadFromFile("users.dat");
// 启动后台线程:每天检查逾期
thread overdueThread([&]() {
while (true) {
this_thread::sleep_for(chrono::hours(24));
userMgr.checkOverdueBooks(bookMgr);
userMgr.saveToFile("users.dat");
}
});
// 主线程:处理用户交互
while (true) {
showMenu();
int choice = getChoice();
switch (choice) {
case 1: handleBorrow(bookMgr, userMgr); break;
case 2: handleReturn(bookMgr, userMgr); break;
case 3: handleAddBook(bookMgr); break;
// ... 其他选项
case 0:
// 退出前保存数据
bookMgr.saveToFile("books.dat");
userMgr.saveToFile("users.dat");
return 0;
}
}
overdueThread.detach(); // 或者 join,看设计
return 0;
}
核心思路:后台线程做定时任务(检查逾期),主线程做交互。退出时一定要保存数据,不然用户白干了。我见过有人程序崩溃没来得及保存,用户借书记录全丢了,那场面...嗯,你懂的。
总结一下这个项目教会你什么
- 面向对象:用类封装数据和操作,用继承实现不同用户类型(普通用户、管理员)
- STL:unordered_map 做快速查找,vector 做列表,shared_mutex 做并发控制
- 文件操作:序列化/反序列化,程序启动加载、退出保存
- 多线程:后台线程做定时任务,读写锁保证数据安全
- 分层架构:UI、业务、数据三层分离,好维护好扩展
做完这个项目,你基本就能应付大部分C++开发岗位的面试了。别问我怎么知道的——我面过的人里,能把这种综合项目讲清楚的,最后都拿到了offer。
公众号:蓝海资料掘金营,微信deep3321