系列文章:
- 单例模式:【C++/Qt 进阶】设计模式系列(1):单例模式------饿汉式内存泄漏真相、指针单例存在的意义、五种实现终极对比-CSDN博客
- 工厂模式与抽象工厂模式:【C++/Qt 进阶】设计模式系列(1):单例模式------饿汉式内存泄漏真相、指针单例存在的意义、五种实现终极对比-CSDN博客
- 建造者模式:【C++/Qt 进阶】设计模式系列(3): 建造者模式------把"复杂对象的组装"从业务代码里剥离出来-CSDN博客
- 原型模式(本篇)
- 适配器模式
- 桥接模式(持续更新中)
写在前面
上一讲我们借助建造者模式解决了"复杂对象分步装配"的难题------零件稳定,组合灵活。但仍有另一类问题,建造者模式无能为力:
"我手头已有近乎完美的对象,仅需局部修改生成新对象。若重新
new一次再逐个set字段,不仅效率低下,还极易遗漏或出错,该怎么办?"
这正是许多 Qt 工程师都曾遭遇的典型场景------撤销/重做(Undo/Redo)。
cpp
// 反人类写法:每次操作都"重造"一份状态
void onUserClick() {
auto* prev = m_doc; // 仅保留指针无用,深拷贝需手动实现
m_doc = new DocState(
m_doc->width(),
m_doc->height(),
m_doc->layers(), // 假设包含 1000 个 Layer
m_doc->cursor(),
m_doc->selection()
);
m_history.push(prev);
}
当对象包含上千个层级、数十个状态字段及复杂的嵌套结构时,通过 new + 拷贝构造 手动复制,几乎不可维护。
一、30 秒看懂原型模式
| 维度 | 工厂模式(造新车) | 原型模式(复印车) |
|---|---|---|
| 创建方式 | 使用 new 构造 |
复制已有对象 |
| 关注重点 | 选择类型 | 保留当前状态 |
| 适用场景 | 产品族 / 套件 | 撤销重做 / 对象池 / 配置快照 |
| 性能优势 | 字段少、构造简单 | 字段多、构造昂贵 |
一句话总结:以现有对象为模板,调用一次
clone()即可获得一份完整且独立的新对象。在 C++ 中,
clone()的本质是调用拷贝构造函数 (T(const T&)),将繁琐的逐字段赋值封装于统一接口。
二、UML 类图与角色
以下为纯 C++/Qt 视角 下的原型模式结构图,结合 QObject 派生关系、Q_DISABLE_COPY 和 QExplicitlySharedDataPointer 等习惯绘制。
四大核心角色:
| 角色 | 职责 | C++/Qt 中的对应实现 |
|---|---|---|
| 抽象原型(Prototype) | 定义 clone() 接口的纯虚基类 |
class ICloneable { virtual clone() = 0; } |
| 具体原型(ConcretePrototype) | 实现拷贝构造与 clone(),内部调用 new T(*this) |
CanvasState |
| 客户端(Client) | 调用 clone() 获取新对象,无需关心构造细节 |
MainWindow::onUndo() |
| 原型管理器(PrototypeManager) | 使用 QHash<key, prototype> 缓存原型,按键克隆 |
PrototypeRegistry |
💡 在 C++ 中,
clone()是真正的虚函数------由编译器通过 vtable 动态分发,无需额外标记。返回类型采用std::unique_ptr<ICloneable>,确保基类指针能安全接管新对象的所有权,避免裸指针泄漏。
三、基础代码实现(C++/Qt 版)
以下以"可撤销的画布状态"为例,演示原型模式的标准实现。
3.1 抽象原型接口
cpp
// icloneable.h
#include <memory>
class ICloneable {
public:
virtual ~ICloneable() = default;
virtual std::unique_ptr<ICloneable> clone() const = 0;
};
使用
std::unique_ptr作为返回值,强制调用方接管所有权,提升安全性,杜绝内存泄漏风险。
3.2 具体原型:画布文档状态
cpp
// canvasstate.h
#include <QString>
#include <QList>
#include <QColor>
#include <memory>
#include "icloneable.h"
class CanvasState : public ICloneable {
public:
struct Layer {
QString name;
QColor color;
QList<QPointF> points;
};
CanvasState() = default;
explicit CanvasState(QString name) : m_name(std::move(name)) {}
// 核心:拷贝构造实现深拷贝
CanvasState(const CanvasState& other) {
m_name = other.m_name;
m_width = other.m_width;
m_height = other.m_height;
m_layers = other.m_layers; // QList 为值语义,元素自动深拷贝
}
// clone() 内部调用拷贝构造
std::unique_ptr<ICloneable> clone() const override {
return std::make_unique<CanvasState>(*this);
}
// 业务方法
void addLayer(const Layer& l) { m_layers.append(l); }
void setName(const QString& n) { m_name = n; }
int layerCount() const { return m_layers.size(); }
private:
QString m_name;
int m_width = 1920;
int m_height = 1080;
QList<Layer> m_layers; // 嵌套对象随 QList 自动深拷贝
};
3.3 原型管理器(可选)
cpp
// prototyperegistry.h
#include <QHash>
#include <QString>
#include <memory>
#include "icloneable.h"
class PrototypeRegistry {
public:
// 注册原型
static void registerPrototype(const QString& key,
std::unique_ptr<ICloneable> proto) {
registry()[key] = std::move(proto);
}
// 通过克隆创建新对象
static std::unique_ptr<ICloneable> createByClone(const QString& key) {
auto& r = registry();
auto it = r.find(key);
if (it == r.end()) return nullptr;
return it.value()->clone();
}
private:
static QHash<QString, std::unique_ptr<ICloneable>>& registry() {
static QHash<QString, std::unique_ptr<ICloneable>> r;
return r;
}
};
看到这个 QHash + clone() 的结构是否觉得眼熟?与前几讲的注册式工厂几乎如出一辙 ------区别仅在于:工厂通过 new 创建实例,而原型管理器则通过 clone() 复制已有对象。
3.4 客户端:撤销/重做场景
cpp
// main.cpp
#include "canvasstate.h"
#include "prototyperegistry.h"
#include <QStack>
#include <QDebug>
int main() {
// 1) 注册一个"空白文档"原型
auto blank = std::make_unique<CanvasState>("Untitled");
PrototypeRegistry::registerPrototype("BlankDoc", std::move(blank));
// 2) 客户端复制一份作为初始文档
auto doc = PrototypeRegistry::createByClone("BlankDoc");
auto* canvas = static_cast<CanvasState*>(doc.get());
// 3) 撤销栈:每次修改前克隆当前状态存档
QStack<std::unique_ptr<ICloneable>> undoStack;
auto snapshot = canvas->clone(); // ← 原型模式正式登场
undoStack.push(std::move(snapshot));
// 4) 用户绘制几笔
canvas->addLayer({"Layer1", Qt::red, {{10,10},{20,20}}});
canvas->addLayer({"Layer2", Qt::blue, {{30,30}}});
// 5) 撤销:直接使用栈顶克隆体恢复状态
auto* restored = static_cast<CanvasState*>(undoStack.top().get());
qDebug() << "撤销后 layer 数:" << restored->layerCount(); // 输出 0
return 0;
}
编译运行命令:
bash
g++ -std=c++17 -fPIC main.cpp -o prototype_demo $(pkg-config --cflags --libs Qt6Core)
./prototype_demo
四、通用模板场景
模板 1:对象池(Pool)
适用场景:构造成本高(如数据库连接、缓冲区、复杂配置对象),希望"用完归还,供后续复用"。
cpp
class ConnectionPool {
public:
// 获取连接:从池中取出一个"原型连接"克隆并返回
std::unique_ptr<Connection> acquire() {
if (m_idle.isEmpty()) {
// 池为空,创建一个基准连接再克隆
auto base = std::make_unique<Connection>();
return std::unique_ptr<Connection>(
static_cast<Connection*>(base->clone().release())
);
}
auto head = std::move(m_idle.front());
m_idle.pop();
return head;
}
// 归还连接:将当前状态快照放回池中
void release(std::unique_ptr<Connection> c) {
m_idle.push(std::move(c));
}
private:
QQueue<std::unique_ptr<Connection>> m_idle;
};
收益 :高频创建/销毁的对象避免重复 new 开销,显著提升性能。
模板 2:配置快照(Snapshot)
适用场景:定时保存系统配置快照,用于崩溃恢复或版本回滚。
cpp
class ConfigSnapshot : public ICloneable {
QHash<QString, QVariant> m_kv;
public:
std::unique_ptr<ICloneable> clone() const override {
return std::make_unique<ConfigSnapshot>(*this); // QHash 自带深拷贝
}
void set(const QString& k, const QVariant& v) { m_kv[k] = v; }
QVariant get(const QString& k) const { return m_kv.value(k); }
};
// 每 5 分钟克隆一次当前配置并存档
ConfigSnapshot snap;
snap.set("mode", "prod");
m_history.append(snap.clone()); // 核心:克隆完整状态
模板 3:撤销/重做(Undo/Redo)
即 3.4 节示例的提炼版,三行搞定核心逻辑:
cpp
// 修改前:拍快照入栈
m_undoStack.push(m_state->clone());
// 修改后:清空重做栈
m_redoStack.clear();
// 撤销:弹出快照并替换当前状态
m_state = std::move(m_undoStack.pop());
💡 复用策略 :上述三个模板均基于同一模式------实现
clone()方法,并在需要复制时调用它 。可将Connection、ConfigSnapshot替换为实际项目中的类,快速复用。
五、优缺点分析
5.1 优点
✅ 性能更优 :相比 new 后逐个设置属性,克隆通常更快,尤其字段较多时。
✅ 绕过构造限制:某些对象无公开构造函数(如缓存中、工厂返回的),克隆是唯一获取新实例的方式。
✅ 简化创建流程 :客户端只需调用 clone(),无需关心构造细节。
✅ 天然支持撤销与快照:克隆生成独立、完整的状态副本,完美契合历史记录需求。
5.2 缺点
❌ 深拷贝实现复杂:含指针、句柄或资源成员时,必须手动编写拷贝逻辑,易遗漏。
❌ 循环引用导致栈溢出 :对象图存在环时,递归 clone() 会无限递归,引发崩溃。
❌ 小对象下开销反而更高 :字段极少(≤3 个)时,clone() 的调用和初始化成本可能超过直接 new。
❌ 依赖拷贝语义细节:C++ 拷贝构造与移动构造语义微妙差异,在迁移到 C++20/23 时需谨慎处理。
六、与其他创建型模式对比
| 模式 | 关注点 | 适用场景 | 与原型的区别 |
|---|---|---|---|
| 单例 | 全局唯一实例 | 日志器、配置中心 | 解决"数量"问题 |
| 简单工厂 | 一句话创建对象 | 产品少且稳定 | 关注"造哪个" |
| 工厂方法 | 子类决定创建类型 | 产品族频繁扩展 | 同上 |
| 抽象工厂 | 一次性创建一组相关对象 | 主题/界面套件 | 同上 |
| Builder | 分步组装复杂对象 | 零件固定但组合多变 | 关注"装配顺序" |
| Prototype | 克隆已有对象 | 构造昂贵 / 撤销重做 | 关注"复制当前状态" |
核心区别 :工厂系列聚焦于"创建新对象 ",而原型模式专注于"复制现有对象"。
七、最佳实践与避坑指南
7.1 必须遵守
🔸 clone() 内部应调用拷贝构造:避免手动逐字段赋值,防止遗漏。
🔸 优先使用 Qt 值语义容器 (QList、QHash、QString):它们自带深拷贝,无需额外处理。
🔸 资源类成员使用 RAII 管理 (std::unique_ptr、QScopedPointer):确保拷贝时正确转移所有权。
🔸 提供 const 修饰的 clone():强调"克隆不修改原对象",增强语义清晰性。
7.2 常见踩坑
❌ 坑 1:浅拷贝导致共享内存
cpp
// 错误示例:仅复制指针地址
class BadClone {
int* m_buf;
public:
std::unique_ptr<ICloneable> clone() const override {
auto p = std::make_unique<BadClone>();
p->m_buf = m_buf; // ← 两个对象共享同一块内存
return p;
}
};
// 后果:一方修改,另一方同步变化;析构后悬空。
✅ 正确做法 :要么动态分配新内存并复制数据(如 new int[*size] + memcpy),要么改用 std::vector<int>,让拷贝构造自动处理。
❌ 坑 2:clone() 缺失 override 修饰
cpp
// 错误:基类未声明为 virtual,子类无法参与多态
class Base { /* virtual clone() */ };
class Derived : public Base {
Derived* clone() const { return new Derived(*this); } // 未加 override!
};
✅ 正确写法 :基类声明 virtual,子类显式使用 override 并返回基类指针类型。
❌ 坑 3:混淆 clone() 与拷贝构造
clone() 强调"创建一个独立的新对象 ",而拷贝构造强调"初始化一个新对象 "。两者在返回类型、是否 virtual、是否 const 等方面均有差异,不可混用。
❌ 坑 4:对含环结构直接 clone()
cpp
struct Node { Node* next; }; // 链表含环
Node* head = ...; // 构成环
// head->clone() → 无限递归 → 栈溢出 💥
✅ 正确方案 :使用 std::unordered_set<Node*> 记录已访问节点,遇到环则停止深拷贝,转而共享指针。
八、写在最后
原型模式是应对"复制当前完整状态 "场景的最优解。在 Qt 项目中,任何涉及撤销/重做、对象池、定时快照的实现,几乎都离不开它。
与工厂模式的关系:许多项目会将原型管理器(PrototypeRegistry)与注册式工厂(Factory)合并 ------同一个
QHash中既可存放原型,也可注册创建函数,实现"一键克隆或新建"的统一接口。