【C++/Qt 进阶】设计模式系列(4):原型模式——用“克隆“代替“重新构造“

系列文章:

  1. 单例模式:【C++/Qt 进阶】设计模式系列(1):单例模式------饿汉式内存泄漏真相、指针单例存在的意义、五种实现终极对比-CSDN博客
  2. 工厂模式与抽象工厂模式:【C++/Qt 进阶】设计模式系列(1):单例模式------饿汉式内存泄漏真相、指针单例存在的意义、五种实现终极对比-CSDN博客
  3. 建造者模式:【C++/Qt 进阶】设计模式系列(3): 建造者模式------把"复杂对象的组装"从业务代码里剥离出来-CSDN博客
  4. 原型模式(本篇)
  5. 适配器模式
  6. 桥接模式(持续更新中)

写在前面

上一讲我们借助建造者模式解决了"复杂对象分步装配"的难题------零件稳定,组合灵活。但仍有另一类问题,建造者模式无能为力:

"我手头已有近乎完美的对象,仅需局部修改生成新对象。若重新 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 中既可存放原型,也可注册创建函数,实现"一键克隆或新建"的统一接口。

相关推荐
Python图像识别1 小时前
56-【2027毕设】YOLO11X光安检危险物品检测系统 - Python完整源码+PyQt5界面+训练模型+数据集
python·qt·课程设计
无忧.芙桃2 小时前
C++语言原理与实践(十三):stack、queue 和 priority_queue的介绍与使用
开发语言·c++·栈·队列
博风3 小时前
11、设计模式应用举例:迭代器模式:使用多个集合存储对象
设计模式·迭代器模式
TDTC_XQ3 小时前
模板类的写法
linux·c++
CCCCCCCCharlie4 小时前
深入解析I/O底层:文件描述符与缓冲区真相
linux·开发语言·c++·算法
把我格子衫拿来4 小时前
鼠标点到哪里才算选中?Qt 与 VTK 的拾取坐标、DPI 与体素定位
python·qt·vtk·坐标变换·三维交互
自信150413057594 小时前
重生之从0开始学习c++之string(下)
c++·学习·stl
wuminyu5 小时前
Continuation::freeze与thaw的x86_64汇编Stack Chunk拆解源码分析
java·linux·c语言·汇编·jvm·c++
kunicoding5 小时前
C++ 策略模式实战:写一个可以“一键换输出“的日志系统
开发语言·c++·策略模式