🎯 导读:本文讲解原型模式——不用
new从头构建对象,而是像复印机一样直接”克隆”已有对象!适合需要频繁创建相似对象的场景。
🖨️ 原型模式(Prototype Pattern) 是创建型设计模式中最特别、也最符合人类日常直觉的一个。
如果说”工厂模式”是按图纸在流水线上从零开始制造产品 🏭,那么”原型模式”就是 复印机 📠 或 细胞分裂 🧬。它的核心思想极其简单:不要用 new 从头构建一个复杂的对象,而是拿一个已经存在的、配置好的对象作为”原型(Prototype)”,直接把它”克隆(Clone)”一份出来。
💡 先提醒一句:原型模式的重点不是“完全不调用构造函数”,而是避免每次都走那套复杂的初始化流程。在 C++ 里,克隆通常依赖拷贝构造或自定义复制逻辑来完成。
📚 1. 客户端(使用者)需要知道哪些类?
在这个模式下,客户端程序员的负担被降到了极低:
✅ 你需要知道的类:
| 序号 | 类名 | 说明 |
|---|---|---|
| 1️⃣ | 抽象原型接口Prototype Interface |
通过基类接口调用 clone(),接收克隆出来的对象 |
| 2️⃣ | 原型管理器/注册表Prototype Registry |
存放配置好的原型(如 std::map),按名字获取克隆体 🗂️ |
❌ 你不需要/绝不应该知道的类:
- 具体的实现类(Concrete Prototype): 在理想的设计下,业务层不需要直接依赖
Goblin、Dragon这样的具体子类。它只需要面向抽象接口和注册表编程。
📌 补充说明:如果是“组装系统”的代码(例如初始化注册表的启动代码),它通常仍然要显式创建具体原型对象;但真正的业务使用方可以不知道这些细节。
🎮 2. 生动的实战场景:游戏里的”怪物生成器”
假设你在开发一款大型动作角色扮演游戏(ARPG)。游戏里有成百上千种怪物,比如”持盾哥布林”、”火焰飞龙”等。
❌ 痛点(没有原型模式)
每次要在屏幕上刷出一只怪,你都要:
1 | new Goblin(); // 创建对象 |
刷 100 只哥布林,这段初始化代码就要跑 100 遍;如果初始化过程本身很复杂,就会明显消耗 CPU 时间,甚至还会夹杂模型加载、资源配置等重复工作。⏱️💸
✅ 解法(原型模式)
在游戏加载关卡时,我们只精心捏出一只完美的”持盾哥布林”对象,把它作为原型(种子) 🌱 放进注册表里。
当游戏开始,需要刷怪时,我们直接对着这只原型哥布林按下 Ctrl+C 和 Ctrl+V(调用 clone())。
🚀 这样就能快速批量生成一批初始状态相同的哥布林对象。 如果对象构建成本本来就很高,这种做法会特别划算。
📊 UML 类图
classDiagram
class IMonster {
<>
+clone()
}
class Goblin {
-hp: int
-armor: int
-weapon: string
+clone()
}
class Dragon {
-hp: int
-fireDamage: int
+clone()
}
class MonsterRegistry {
-prototypes: map
+register(name, monster)
+create(name)
}
IMonster <|-- Goblin : 实现
IMonster <|-- Dragon : 实现
MonsterRegistry ..> IMonster : 克隆
💻 3. C++ 现代代码生动演绎
在 C++ 中,原型模式常常可以理解为:通过多态接口,把“复制自己”这件事封装起来。很多实现最终会落到拷贝构造函数上,但也可以落到自定义复制逻辑上。我们来看代码:
1 |
|
🔰 初学者版本:不用智能指针的原型模式
💡 给初学者的说明:上面的代码使用了
std::unique_ptr(智能指针),这是现代 C++ 的推荐做法。但如果你还不熟悉智能指针,下面这个版本使用原始指针,更容易看清“谁申请、谁释放”的过程。
1 |
|
🔑 两个版本的关键区别
| 特性 | 智能指针版本 | 原始指针版本 |
|---|---|---|
| 内存管理 | 自动释放 ✅ | 手动 delete ⚠️ |
| 代码复杂度 | 稍复杂 | 更简单 |
| 安全性 | 不会内存泄漏 | 容易忘记释放 💥 |
| 学习曲线 | 需要懂智能指针 | 只需懂 new/delete |
📌 建议:先理解原始指针版本,再学习智能指针版本。实际项目中强烈推荐使用智能指针!
⚠️ 4. C++ 程序员必须面对的”深渊”:深拷贝与浅拷贝
原型模式在 C++ 中有一个极其致命的坑,也就是面试必考题:深拷贝(Deep Copy)vs. 浅拷贝(Shallow Copy) 🕳️
在上面的代码中,return std::make_unique<Goblin>(*this); 使用了编译器自动生成的默认拷贝构造函数。
✅ 安全的情况
如果你的类里只有普通变量(如 int、double)和具备值语义的标准类型(如 std::string、std::vector),默认拷贝通常是安全的 🛡️。
更准确地说,这是因为这些标准类型自己已经正确处理了资源复制;并不是所有“默认拷贝”都天然等于“深拷贝”。
🔥 致命危险
如果你的类里面有一个裸指针(比如 int* data),默认拷贝只会复制这个指针的地址(浅拷贝)。
1 | 原型对象 ──► [内存块 A] ◄── 克隆对象 |
结果就是:原型和克隆体共享了同一块内存!一旦原型被销毁,克隆体里的指针就变成了悬空指针(Dangling Pointer) 💀,程序直接崩溃!
💡 解决办法
如果你的原型类管理了堆内存,你必须手动重写类的拷贝构造函数,在里面为克隆体重新 new 一块独立的内存并复制数据(实现深拷贝)。
⚖️ 5. 优缺点总结
✅ 优点:
| 优点 | 说明 |
|---|---|
| 🚀 创建相似对象更高效 | 避免每次都重复走复杂的初始化流程;当对象构建成本较高时,收益会很明显 |
| 🔄 动态配置 | 相比于死板的工厂类,原型模式可以在运行时动态地增加或删除原型种类 |
❌ 缺点:
| 缺点 | 说明 |
|---|---|
| 😵 实现复杂 | 为每一个类配备一个完美的 clone() 方法有时极其困难(尤其是当类内部存在复杂的循环引用、或者持有不可拷贝的资源如网络 Socket、文件句柄时) |
🎯 总结
💡 在 C++ 中,理解“对象复制语义”——尤其是拷贝构造、资源管理和深浅拷贝——就抓住了原型模式的关键。
你对 C++ 的深拷贝和浅拷贝机制足够熟悉了吗?如果不熟悉,推荐阅读:🌊 浅拷贝 vs 🏗️ 深拷贝 —— 一文看懂!
📚 这篇文章用生动的”复印钥匙”比喻,配合完整代码示例,帮你彻底搞懂深拷贝与浅拷贝的区别!
- 本文作者: 迪丽惹Bug
- 本文链接: https://lyroom.github.io/2026/04/29/原型模式-对象复印机/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!