实战:图书管理系统(类+继承+多态)

图书管理系统,几乎是每个C++学习者都会碰到的经典项目。说实话,我当年学多态的时候,也是靠这个项目才真正开窍的。你想想看,一个系统里有书、有杂志、有DVD,它们都是"资料",但借阅规则、展示方式又不一样——这不就是活生生的继承和多态场景吗?

项目需求分析

我们先理清楚要做什么。这个系统需要管理多种类型的文献资料,支持借阅、归还、查询等基本操作。我个人习惯在写代码前,先把类的关系图画出来,这样心里有底。

核心需求:

  • 支持图书、杂志、DVD三种资料类型
  • 每种资料有独立的借阅规则
  • 支持按标题、作者、类型查询
  • 记录借阅历史和当前状态

类结构设计

这里我用了一个基类 Material,然后派生出三个子类。为什么这么设计?因为所有资料都有共同属性(标题、编号、状态),但具体行为不同。嗯,这里要注意:基类一定要把析构函数声明为虚函数,否则删除子类对象时会有内存泄漏。我曾经在这个坑里摔过一次,排查了半天才发现是析构函数没加 virtual。

class Material {
protected:
    string title;
    string id;
    bool isBorrowed;
    time_t borrowDate;

public:
    Material(const string& t, const string& i) 
        : title(t), id(i), isBorrowed(false) {}
    
    virtual ~Material() = default;
    
    virtual void displayInfo() const = 0;
    virtual int getMaxBorrowDays() const = 0;
    virtual double calcFine(int overdueDays) const = 0;
    
    bool borrow() {
        if (isBorrowed) return false;
        isBorrowed = true;
        borrowDate = time(nullptr);
        return true;
    }
    
    bool returnMaterial() {
        if (!isBorrowed) return false;
        isBorrowed = false;
        return true;
    }
};

你看,基类里定义了三个纯虚函数,这就是多态的关键。每个子类必须实现自己的版本,借阅规则自然就分开了。

子类实现

图书的借阅期是30天,杂志15天,DVD只有7天。罚款计算也不一样:图书每天0.5元,杂志0.3元,DVD 1元。这些规则直接写在子类里,清晰明了。

class Book : public Material {
private:
    string author;
    string isbn;

public:
    Book(const string& t, const string& a, const string& i, const string& isbn)
        : Material(t, i), author(a), isbn(isbn) {}
    
    void displayInfo() const override {
        cout << "图书: " << title << " | 作者: " << author;
        cout << " | " << (isBorrowed ? "已借出" : "可借阅") << endl;
    }
    
    int getMaxBorrowDays() const override { return 30; }
    
    double calcFine(int overdueDays) const override {
        return overdueDays * 0.5;
    }
};

class Magazine : public Material {
private:
    int issueNumber;

public:
    Magazine(const string& t, int issue, const string& i)
        : Material(t, i), issueNumber(issue) {}
    
    void displayInfo() const override {
        cout << "杂志: " << title << " 第" << issueNumber << "期";
        cout << " | " << (isBorrowed ? "已借出" : "可借阅") << endl;
    }
    
    int getMaxBorrowDays() const override { return 15; }
    
    double calcFine(int overdueDays) const override {
        return overdueDays * 0.3;
    }
};

class DVD : public Material {
private:
    double duration; // 时长(分钟)

public:
    DVD(const string& t, double dur, const string& i)
        : Material(t, i), duration(dur) {}
    
    void displayInfo() const override {
        cout << "DVD: " << title << " 时长: " << duration << "分钟";
        cout << " | " << (isBorrowed ? "已借出" : "可借阅") << endl;
    }
    
    int getMaxBorrowDays() const override { return 7; }
    
    double calcFine(int overdueDays) const override {
        return overdueDays * 1.0;
    }
};

小技巧:在实际项目中,罚款规则可能会频繁变动。我建议把罚款计算策略单独抽出来,用策略模式管理。不过对于这个入门项目,写在子类里更直观。

多态的魅力

为什么说多态好用?你看下面的代码,一个 vector<unique_ptr<Material>> 就能管理所有类型的资料。遍历的时候,每个对象自动调用自己的 displayInfo(),完全不用写 if-else 判断类型。

class Library {
private:
    vector<unique_ptr<Material>> materials;

public:
    void addMaterial(unique_ptr<Material> m) {
        materials.push_back(move(m));
    }
    
    void showAllMaterials() const {
        for (const auto& m : materials) {
            m->displayInfo();
        }
    }
    
    Material* findById(const string& id) {
        for (const auto& m : materials) {
            // 这里假设 Material 类有 getId() 方法
            if (m->getId() == id) return m.get();
        }
        return nullptr;
    }
};

说白了,多态就是"同样的接口,不同的实现"。你调用 displayInfo(),图书显示作者和ISBN,杂志显示期号,DVD显示时长——各显神通。

系统流程图

下面这张图展示了整个系统的核心流程,从添加资料到借阅归还,一目了然。

图书管理系统核心流程 用户操作 操作类型 借阅操作 归还操作 查询操作 检查可借性 更新状态 计算罚款 图例 操作节点 判断分支 具体操作

避坑指南

我曾经踩过的坑:

  • 切片问题:千万别把子类对象按值传给基类参数。用指针或引用,否则多态就失效了。
  • 虚析构:基类析构函数不加 virtual,删除子类对象时只会调用基类析构,子类资源就泄漏了。
  • 拷贝控制:如果类里有动态分配的资源,记得实现拷贝构造和赋值操作符,或者直接用 unique_ptr 管理。

完整测试代码

int main() {
    Library lib;
    
    // 添加各种资料
    lib.addMaterial(make_unique<Book>("C++ Primer", "Lippman", "B001", "978-7-111-"));
    lib.addMaterial(make_unique<Magazine>("程序员", 202403, "M001"));
    lib.addMaterial(make_unique<DVD>("设计模式解析", 120.0, "D001"));
    
    // 展示所有资料(多态调用)
    lib.showAllMaterials();
    
    // 借阅操作
    Material* m = lib.findById("B001");
    if (m) {
        m->borrow();
        cout << "借阅成功!最大借阅天数: " << m->getMaxBorrowDays() << endl;
    }
    
    return 0;
}

运行结果会显示每种资料的不同信息,这就是多态在运行时发挥的作用。你想想看,如果没有多态,你得写多少个 if-else 来判断类型?维护起来得多痛苦。

扩展建议:如果你想让系统更健壮,可以加入异常处理。比如借阅已借出的资料时抛出异常,而不是返回 false。我个人习惯在业务逻辑层用异常,在底层用返回值。

好了,这个图书管理系统虽然简单,但把C++面向对象的三大特性——封装、继承、多态——都用上了。代码量不大,但结构清晰,扩展起来也方便。以后想加个"电子书"类型,直接继承 Material 就行,现有代码完全不用改。

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