实战:图书管理系统(类+继承+多态)
图书管理系统,几乎是每个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 就行,现有代码完全不用改。