一、项目概述:为什么要做这个图书管理系统?

说实话,我见过太多学C++的同学,语法背得滚瓜烂熟,一到写项目就懵了。为什么?因为知识点是散的,就像一堆零件堆在桌上,不知道怎么组装成一台机器。

这个图书管理系统,就是咱们的「组装练习」。它会用到类与对象、继承、多态、文件I/O、STL,甚至设计模式。做完它,你就能把这些知识点串起来了。

我在公司带新人时,经常让他们先写一个类似的小系统。不是为了炫技,而是为了理解「面向对象到底在解决什么问题」。你想想看,如果只是写几个函数也能跑,为什么要用类?为什么要继承?

嗯,这个项目会给你答案。

二、项目需求:我们要做什么?

2.1 功能清单

一个简单的图书管理系统,至少需要这些功能:

  • 图书管理:添加、删除、修改、查询图书
  • 借阅管理:借书、还书、查看借阅记录
  • 用户管理:管理员和普通读者两种角色
  • 数据持久化:程序关闭后数据不丢失(文件I/O)
  • 统计功能:统计图书数量、借阅次数等

2.2 技术栈

技术点 用途
类与对象 封装图书、用户、借阅记录等实体
继承 管理员和读者继承自基类User
多态 统一的接口处理不同用户的操作
文件I/O 读写图书和用户数据到文件
STL vector存储图书列表,map管理用户
设计模式 单例模式管理图书馆实例

三、架构设计:怎么搭架子?

3.1 整体架构图

先别急着写代码。我习惯先画图,把结构理清楚。下面这张图展示了系统的核心模块和它们之间的关系:

图书管理系统架构图 Library(单例) BookManager UserManager BorrowManager 数据层:books.txt | users.txt | records.txt Book User(基类) Admin(派生) Reader(派生) 图例:实线=组合关系,虚线=数据流向

3.2 核心类设计

咱们先定义几个核心类。我习惯从最底层的实体类开始写:

// Book.h - 图书实体类
class Book {
private:
    string m_id;          // 图书编号
    string m_title;       // 书名
    string m_author;      // 作者
    bool m_borrowed;      // 是否已借出
    int m_borrowCount;    // 借阅次数
    
public:
    Book() : m_borrowed(false), m_borrowCount(0) {}
    Book(string id, string title, string author) 
        : m_id(id), m_title(title), m_author(author), 
          m_borrowed(false), m_borrowCount(0) {}
    
    // getter/setter 略...
    virtual void display() const;  // 多态:显示图书信息
};
我的习惯:构造函数里用初始化列表,而不是在函数体内赋值。这样效率更高,尤其是对string这种类型。以前我踩过坑,在函数体内赋值会导致先默认构造再赋值,多了一次操作。

3.3 继承与多态:用户体系

用户分为管理员和普通读者。它们有很多共同属性(姓名、ID、密码),但权限不同。这时候继承就派上用场了:

// User.h - 用户基类
class User {
protected:
    string m_name;
    string m_userId;
    string m_password;
    
public:
    virtual void showMenu() = 0;  // 纯虚函数:不同用户显示不同菜单
    virtual ~User() {}            // 虚析构,防止内存泄漏
    
    string getName() const { return m_name; }
    string getUserId() const { return m_userId; }
    bool checkPassword(const string& pwd) const { return m_password == pwd; }
};

// Admin.h - 管理员
class Admin : public User {
public:
    void showMenu() override {
        cout << "1. 添加图书  2. 删除图书  3. 查看所有图书" << endl;
        cout << "4. 查看所有用户  5. 退出" << endl;
    }
};

// Reader.h - 读者
class Reader : public User {
public:
    void showMenu() override {
        cout << "1. 查询图书  2. 借书  3. 还书" << endl;
        cout << "4. 查看我的借阅记录  5. 退出" << endl;
    }
};
关键点:showMenu() 是纯虚函数,强迫派生类实现。这样在代码里,我们可以用一个User指针调用showMenu(),实际执行哪个版本由对象类型决定——这就是多态。

四、单例模式:图书馆只有一个

一个系统里只能有一个「图书馆」实例。如果有人不小心new了两个Library对象,数据就乱了。单例模式就是解决这个问题的:

// Library.h - 单例模式
class Library {
private:
    vector<Book> m_books;
    map<string, User*> m_users;  // key: userId, value: User指针
    string m_bookFile;
    string m_userFile;
    
    // 构造函数私有化,外部不能直接new
    Library() : m_bookFile("books.txt"), m_userFile("users.txt") {
        loadData();  // 启动时加载数据
    }
    
    // 禁止拷贝
    Library(const Library&) = delete;
    Library& operator=(const Library&) = delete;
    
public:
    static Library& getInstance() {
        static Library instance;  // C++11保证线程安全
        return instance;
    }
    
    void loadData();   // 从文件加载数据
    void saveData();   // 保存数据到文件
    void addBook(const Book& book);
    void removeBook(const string& bookId);
    Book* findBook(const string& bookId);
    // ... 其他方法
};
注意:单例的析构函数要小心。我曾经在一个项目里,单例里持有文件句柄,程序退出时没有正确关闭,导致数据丢失。建议在析构函数里调用saveData(),或者用RAII机制管理资源。

五、文件I/O:让数据活下来

程序关了数据就没了?那不行。咱们得把数据存到文件里。我习惯用简单的文本格式,方便调试:

void Library::saveData() {
    ofstream out(m_bookFile);
    if (!out) {
        cerr << "无法打开文件: " << m_bookFile << endl;
        return;
    }
    
    for (const auto& book : m_books) {
        // 格式:ID|书名|作者|是否借出|借阅次数
        out << book.getId() << "|" 
            << book.getTitle() << "|" 
            << book.getAuthor() << "|"
            << book.isBorrowed() << "|"
            << book.getBorrowCount() << endl;
    }
    out.close();
}

void Library::loadData() {
    ifstream in(m_bookFile);
    if (!in) return;  // 文件不存在就跳过
    
    string line;
    while (getline(in, line)) {
        // 解析每一行,构造Book对象
        // 用stringstream或find+substr分割字符串
        // ... 解析代码略
    }
    in.close();
}
避坑指南:我曾经用二进制格式存数据,结果换了个平台就读不出来了。后来统一用文本格式,虽然慢一点,但可读性强,跨平台也没问题。对于小项目,这点性能损失可以忽略。

六、STL容器:选对工具干对活

咱们用到了两个STL容器:

  • vector<Book>:存储图书列表。为什么用vector?因为图书数量会动态变化,vector支持随机访问,查找也方便。
  • map<string, User*>:管理用户。用用户ID作为key,查找用户时O(log n)复杂度,比遍历vector快多了。

你可能会问:为什么不用unordered_map?嗯,对于几十个用户的小系统,map就够用了。unordered_map虽然更快,但需要提供哈希函数,有点杀鸡用牛刀的感觉。

七、完整流程:从启动到退出

咱们捋一遍程序的完整生命周期:

  1. 启动:Library::getInstance() 被调用,触发构造函数,自动加载文件数据。
  2. 登录:用户输入ID和密码,从m_users中查找并验证。
  3. 显示菜单:根据用户类型,通过多态调用showMenu()。
  4. 操作循环:用户选择功能,调用对应的Manager方法。
  5. 退出:程序结束前,Library的析构函数自动调用saveData()。
核心思想:整个系统围绕Library这个单例展开。所有操作都通过它来协调。这样设计的好处是——数据集中管理,不会出现多个副本不一致的问题。

八、总结:你学到了什么?

这个项目虽然小,但五脏俱全。你想想看,我们用了:

  • 类与对象:把图书、用户抽象成类
  • 继承:Admin和Reader继承User
  • 多态:通过虚函数实现不同用户的菜单
  • 文件I/O:数据持久化,程序重启后数据还在
  • STL:vector和map管理数据
  • 设计模式:单例模式保证Library唯一

说实话,这个架构拿到小公司里直接就能用。我当年第一个正式项目,就是从类似的结构开始的。当然,生产环境会更复杂,要考虑并发、数据库、网络等等。但核心思想是一样的——面向对象的封装、继承、多态,加上合适的设计模式,能让代码清晰、可维护。

下一章,咱们会真正动手写代码。我会带着你一步步实现每个类,从Book到Library,从文件读写到用户交互。准备好了吗?

公众号:蓝海资料掘金营,微信deep3321