03.C++设计模式-原型模式

原型模式(Prototype Pattern)是一种创建型设计模式,它的核心思想是用一个已经创建的实例作为原型,通过复制该原型来创建新的对象,而不必通过 new 直接实例化。下面我会从定义、结构、应用场景、C++实现、深/浅拷贝问题等方面为你详细讲解。


1. 定义与意图

意图: 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

简单说,就是给系统增加一个"克隆"能力,让一个对象可以自我复制。当直接创建对象的代价较高(比如初始化很耗时、依赖外部资源、或者类数量庞大)时,原型模式可以让客户端代码不依赖具体类就能生成新对象。

GoF 中的描述: 通过"原型"实例指定要创建的对象类型,然后拷贝该原型创建更多的同类型对象。


2. 结构图与参与者

原型模式通常包含以下几种角色:

  • Prototype(抽象原型类)

    声明一个克隆自身的接口,通常是 clone() 方法。

  • ConcretePrototype(具体原型类)

    实现 clone() 方法,完成自身的复制。

  • Client(客户端)

    让一个原型对象克隆自己,从而得到一个新的对象。客户端只需知道抽象原型接口,不需要知道具体类。

此外,很多时候会引入一个原型管理器(Prototype Registry),用来存储和查找已有的原型实例,客户端通过键值来获取原型副本。


3. 工作原理

原型模式的核心就是对象的拷贝。拷贝分为两种:

  • 浅拷贝(Shallow Copy)

    只复制对象本身,不复制其引用的成员。也就是说,拷贝对象中的指针/引用与原对象指向同一块内存。对于值类型成员没问题,但对动态分配的资源(如堆内存、文件句柄等)会造成多个对象共享同一资源,可能导致多次释放、重复修改等问题。

  • 深拷贝(Deep Copy)

    不仅复制对象本身,还递归复制其引用的所有对象。拷贝出的对象与原对象完全独立,互不影响。

原型模式通常要求实现深拷贝 ,保证克隆体完全独立。

在 C++ 中,需要特别注意拷贝构造函数和 clone() 的实现。


4. 应用场景

什么时候使用原型模式?

  1. 对象创建代价很高

    初始化一个对象需要耗费大量资源(如复杂的计算、数据库读取、网络请求、大量配置等),而之后的对象与某个已有对象"相似"。这时可以先创建一个原型,后续通过克隆来快速生成。

  2. 系统要独立于它的产品类

    客户端不需要知道具体产品类的名字,只需要通过原型克隆。这在框架设计中很常见,例如游戏中的怪物生成器,只需向原型管理器注册怪物原型,运行时根据配置文件或关卡数据动态克隆。

  3. 当一个类有很多子类,且这些子类的差异仅在状态组合

    不需要为每种组合写一个子类,可以先构造具有特定状态的原型,然后克隆它们。

  4. 动态加载与配置

    配合原型管理器,可以在程序运行时动态注册、删除原型,实现高度灵活的对象创建。例如:在图形编辑器中,用户选定一个图形,然后通过克隆该图形来"复制-粘贴"。

  5. 避免大量工厂类

    当使用工厂方法模式可能导致大量平行工厂类时,原型模式可以替代,将工厂角色合并到对象自身的 clone() 方法中。


5. C++ 代码示例

下面给出一个完整的 C++ 例子,包含抽象原型、具体原型、深拷贝处理以及原型管理器。

5.1 抽象原型类

cpp 复制代码
#include <iostream>
#include <string>
#include <memory>
#include <unordered_map>

// 抽象原型
class Shape {
public:
    virtual ~Shape() = default;
    virtual std::unique_ptr<Shape> clone() const = 0; // 克隆接口
    virtual void draw() const = 0;
};

这里使用 std::unique_ptr 来表达所有权转移,确保克隆后由调用者管理内存。

5.2 具体原型类(以圆形和矩形为例)

cpp 复制代码
// 具体原型 ------ 圆形
class Circle : public Shape {
private:
    int x, y;
    int radius;
    std::string color;

public:
    Circle(int x, int y, int r, const std::string& c)
        : x(x), y(y), radius(r), color(c) {}

    // 拷贝构造函数 (深拷贝)
    Circle(const Circle& other)
        : x(other.x), y(other.y), radius(other.radius), color(other.color) {
        std::cout << "Circle deep-copied.\n";
    }

    std::unique_ptr<Shape> clone() const override {
        // 利用拷贝构造函数进行深拷贝
        return std::make_unique<Circle>(*this);
    }

    void draw() const override {
        std::cout << "Circle at (" << x << ", " << y 
                  << ") radius=" << radius << " color=" << color << std::endl;
    }
};

// 具体原型 ------ 矩形
class Rectangle : public Shape {
private:
    int x, y;
    int width, height;
    std::string color;

public:
    Rectangle(int x, int y, int w, int h, const std::string& c)
        : x(x), y(y), width(w), height(h), color(c) {}

    Rectangle(const Rectangle& other)
        : x(other.x), y(other.y), width(other.width), 
          height(other.height), color(other.color) {
        std::cout << "Rectangle deep-copied.\n";
    }

    std::unique_ptr<Shape> clone() const override {
        return std::make_unique<Rectangle>(*this);
    }

    void draw() const override {
        std::cout << "Rectangle at (" << x << ", " << y 
                  << ") width=" << width << " height=" << height 
                  << " color=" << color << std::endl;
    }
};

5.3 原型管理器(可选)

cpp 复制代码
class ShapeRegistry {
private:
    std::unordered_map<std::string, std::unique_ptr<Shape>> prototypes;

public:
    // 注册原型
    void addPrototype(const std::string& key, std::unique_ptr<Shape> prototype) {
        prototypes[key] = std::move(prototype);
    }

    // 通过键克隆一个对象
    std::unique_ptr<Shape> createShape(const std::string& key) const {
        auto it = prototypes.find(key);
        if (it != prototypes.end()) {
            return it->second->clone();
        }
        return nullptr;
    }
};

5.4 客户端使用

cpp 复制代码
int main() {
    // 创建原型管理器并注册原型
    ShapeRegistry registry;
    registry.addPrototype("big red circle", 
        std::make_unique<Circle>(10, 20, 30, "red"));
    registry.addPrototype("small blue rect", 
        std::make_unique<Rectangle>(0, 0, 40, 20, "blue"));

    // 根据需求克隆对象
    auto shape1 = registry.createShape("big red circle");
    if (shape1) shape1->draw();

    auto shape2 = registry.createShape("small blue rect");
    if (shape2) shape2->draw();

    // 克隆另一个圆形
    auto shape3 = registry.createShape("big red circle");
    if (shape3) shape3->draw();

    return 0;
}

输出:

复制代码
Circle deep-copied.
Circle at (10, 20) radius=30 color=red
Rectangle deep-copied.
Rectangle at (0, 0) width=40 height=20 color=blue
Circle deep-copied.
Circle at (10, 20) radius=30 color=red

可以看出,每次 createShape() 都调用 clone() 并触发了拷贝构造函数,实现深拷贝。

5.5 深拷贝关键点

如果 CircleRectangle 内部含有动态分配的资源(比如 char* 或指向其它对象的指针),深拷贝必须:

  • 在拷贝构造函数中为新对象分配独立的资源;
  • 复制原对象资源的内容;
  • 确保析构函数能正确释放自己的资源,避免重复释放。

在 C++11 之后,推荐使用 RAII 和智能指针(如 std::unique_ptrstd::string)来管理资源,可以大幅简化深拷贝工作。大多数标准库类型(vector, string 等)的拷贝构造本身就是深拷贝,我们只需要确保成员对象都正确实现了拷贝语义。


6. 优缺点

优点:

  • 减少子类构造:可以不通过大量工厂类或子类来创建配置复杂的对象。
  • 动态配置:运行时可以随时增删原型,比编译时静态生成更灵活。
  • 隐藏对象创建细节:客户端只与抽象原型接口打交道,降低耦合。
  • 效率提升:在初始化成本高的情况下,克隆比重新初始化快得多。

缺点:

  • 深拷贝复杂:对象有多重引用或循环引用时,实现深拷贝可能很困难。
  • 每个子类必须实现 clone:如果系统中有大量具体类,每个都要正确实现 clone 方法,增加维护成本。
  • 破坏单例/不可复制语义:某些对象设计上不应被复制,却因为原型模式不得不暴露克隆接口。

7. 与其它模式的关系

  • 抽象工厂模式 可以用原型模式来实现,工厂通过克隆产品原型来生产新品。
  • 组合模式装饰模式 经常与原型一起使用,你可能会克隆一个复杂组合结构。
  • 原型模式有时可以替代 备忘录模式 的部分功能,将对象状态保存在克隆体中,但它们的意图和复杂度不同。

8. 原型模式、拷贝构造函数和复制赋值运算符

它们不是平级替代的关系,而是目的与手段的关系。

下面我从几个关键维度来剖析它们的异同。


8.1 核心定义与意图的不同

  • 拷贝构造函数 (Copy Constructor)

    • 意图: 是一种语言层面的对象构造机制。它定义了如何用同类型的已存在对象来初始化一个新对象。核心关注点是"正确复制资源"。
    • 特点: 是静态的、编译时确定的。ClassName obj2(obj1);
    • 解决的问题: 主要是深拷贝与浅拷贝问题,防止悬垂指针或多次释放同一块内存。
  • 复制赋值运算符 (Copy Assignment Operator)

    • 意图: 是一种语言层面的对象赋值机制。它定义了如何将同类型的已存在对象的值,复制给另一个同样已存在的对象。核心关注点是"赋值"和"释放旧资源"。
    • 特点: 是静态的、编译时确定的。obj2 = obj1;
    • 解决的问题: 同上,但额外要处理自我赋值和释放已有资源的问题。
  • 原型模式 (Prototype Pattern)

    • 意图: 是一种设计层面的对象创建模式。它通过一个原型实例来指定要创建对象的类型,并通过复制这个原型来创建新对象。
    • 特点: 目标是解耦。让客户端代码不知道具体要创建什么类,只知道要克隆的那个"原型"对象。
    • 解决的问题: 类太多、初始化复杂、希望运行时动态配置对象种类等设计问题,而不是单纯的内存拷贝问题。

8.2 多态性:原型模式的关键区别

这是原型模式区别于前两者最本质的特征。拷贝构造函数和复制赋值运算符都不具备多态性

cpp 复制代码
#include <memory>

class Base {
public:
    // 普通拷贝构造:从同类型对象构造
    Base(const Base& other) { /* ... */ }
    
    // 这是一个"虚拷贝构造"的替代方案,即原型模式的核心
    virtual std::unique_ptr<Base> clone() const = 0;
};

class Derived : public Base {
public:
    Derived(const Derived& other) : Base(other) { /* ... */ }
    
    // 实现原型模式的clone接口
    std::unique_ptr<Base> clone() const override {
        // 先调用自身的拷贝构造,再封装成基类指针返回
        return std::make_unique<Derived>(*this);
    }
};

int main() {
    // 1. 多态创建:客户端只知道Base*,却能正确克隆出Derived对象
    std::unique_ptr<Base> prototype = std::make_unique<Derived>();
    std::unique_ptr<Base> cloneObj = prototype->clone(); // 这是原型模式

    // 2. 拷贝构造做不到这一点
    // Base* bPtr = new Derived();
    // Base* bPtr2 = new Base(*bPtr); 
    // 错误!因为编译器只知道 *bPtr 是 Base&,会调用Base的拷贝构造,发生"对象切片"
    
    return 0;
}

结论: 原型模式通过 clone() 虚函数实现了多态复制。你可以在完全不知道对象具体类型的情况下,通过基类指针或引用来复制它。这是任何非虚的拷贝构造或赋值运算符都做不到的。


8.3 与"组合模式"的协同工作

原型模式能递归处理复杂结构,而单纯的拷贝构造通常只在单层类中自动工作。

考虑之前图形编辑器的例子:

cpp 复制代码
class CompoundGraphic : public Graphic {
    std::vector<std::unique_ptr<Graphic>> children;
public:
    // 拷贝构造函数只能处理同类型
    CompoundGraphic(const CompoundGraphic& other) {
        for (const auto& child : other.children) {
            // 在这里,child是std::unique_ptr<Graphic>,我们不知道每个child具体是Dot还是Circle
            // 但我们可以调用 clone(),因为这是Graphic基类的接口
            children.push_back(child->clone()); // 原型模式决定性的一刻!
        }
    }
    // ...
};

在这里,CompoundGraphic自己的拷贝构造函数 内部,使用了其成员的原型模式接口。单纯的拷贝构造无法完成这种异构集合的深拷贝。


8.4 对比总结表

特性 拷贝构造 / 复制赋值 原型模式 (clone)
核心意图 正确复制值 多态创建对象,解耦具体类
多态性 (非虚函数,静态绑定) (虚函数,动态绑定)
应用层次 语言特性,用于类的实现 设计模式,用于系统架构
客户端依赖 依赖具体类名 依赖抽象原型接口 (Base*)
动态配置 不支持 支持(运行时注册原型)
典型场景 类的内部实现细节 框架、对象工厂、编辑器
相互关系 clone()方法的实现基础 利用拷贝构造来实现,并赋予其多态能力

简单类比:
拷贝构造函数 是造砖的模具,决定了每块砖怎么造出来。
原型模式是拿一块"样品砖"给流水线,流水线(客户端)不需要知道这是什么类型的砖,只要它能"自我复制"就行。而"自我复制"这个能力,很多时候就是用拷贝构造函数这个"模具"实现的。

因此,你在项目中的思维路径应该是:

  1. 设计时: 如果发现系统需要"用一个对象来快速创建一批相似对象",或者"客户端不想用一堆if-else和new来对付各种子类",就应该考虑引入原型模式
  2. 实现时: 为每个具体子类实现 clone() 函数,而这个函数的内部实现 ,最佳实践就是利用C++的拷贝构造函数 return std::make_unique<Derived>(*this);
相关推荐
神仙别闹1 小时前
基于QT(C++)实现线性表的建立、插入、删除、查找等基本操作
java·c++·qt
salipopl2 小时前
C/C++ 中 volatile 关键字详解:原理、作用与实际应用
开发语言·c++
张赫轩(不重名)2 小时前
图论3:连通性问题(复杂度均为 O(N + M) )
c++·算法·图论·拓扑学
AIminminHu2 小时前
(让 C++ 程序长出大脑:从“语音遥控器”到具身智能 Agent 的进化之路)------OpenGL渲染与几何内核那点事------(二-1-(15))
开发语言·c++·agent·具身智能
君义_noip2 小时前
CSP-J 2025 入门级 第一轮(初赛) 完善程序(1)
c++·算法·信息学奥赛·csp 第一轮
哭泣方源炼蛊3 小时前
AtCoder Beginner Contest 456 E补题(分层图 + 有向环检测 )
c++·算法·深度优先·图论·拓扑学
Yuk丶3 小时前
UE4 与 UE5:技术差异深度解析
c++·ue5·游戏引擎·ue4·游戏程序·虚幻
故事和你914 小时前
洛谷-数据结构2-1-二叉堆与树状数组1
开发语言·数据结构·c++·算法·动态规划·图论
海参崴-4 小时前
C++ STL篇 红黑树的模拟实现
开发语言·c++