原型模式:模式动机与定义

说实话,原型模式是我个人非常喜欢的一个创建型模式。为什么?因为它解决了一个很实际的问题——当你需要创建大量相似对象时,直接 new 的成本太高了。

我记得有一次在做一个游戏引擎,需要生成成千上万的子弹对象。每个子弹的属性都差不多,只是位置和速度不同。如果每次都 new 一个对象,再逐个设置属性,性能开销大得吓人。后来我改用原型模式,先创建一个"样板子弹",然后直接克隆它,再微调几个参数。性能提升了好几个数量级。

原型模式的定义其实很简单:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。说白了,就是先造一个样板,后面需要的时候直接复制它。

核心思想:不通过 new 创建对象,而是通过克隆已有对象来创建新对象。

角色与结构

原型模式的结构非常清晰,就三个角色:

角色 职责 我见过的坑
Prototype(抽象原型) 声明克隆接口 很多人忘记声明虚析构函数
ConcretePrototype(具体原型) 实现克隆方法 深拷贝没处理好指针成员
Client(客户端) 调用克隆方法创建新对象 误以为克隆和 new 完全等价

嗯,这里要注意一点:原型模式的核心在于克隆方法的实现。你想想看,如果克隆方法写错了,那后面所有基于它的对象都会出问题。

Prototype + clone(): Prototype* ConcretePrototypeA - data: int + clone(): Prototype* ConcretePrototypeB - data: string + clone(): Prototype* Client 调用 prototype->clone()

C++深拷贝与浅拷贝实现

说到原型模式,就绕不开深拷贝和浅拷贝这个话题。我见过太多人在这里栽跟头了。

浅拷贝的问题

先看一个反面教材:

class ShallowCopy {
public:
    int* data;
    
    ShallowCopy(int val) {
        data = new int(val);
    }
    
    // 默认拷贝构造函数——浅拷贝
    // 两个对象的 data 指向同一块内存!
};

int main() {
    ShallowCopy obj1(42);
    ShallowCopy obj2 = obj1;  // 浅拷贝
    
    delete obj1.data;  // obj2.data 变成悬空指针!
    return 0;
}

为什么会这样?因为默认的拷贝构造函数只是简单地把指针值复制过去。两个对象的 data 指向同一块内存。一个释放了,另一个就完蛋了。

我曾经在一个项目中用了浅拷贝,结果程序运行时随机崩溃。查了两天才发现是浅拷贝导致的内存重复释放。从那以后,只要类里有指针成员,我必写深拷贝。

深拷贝的正确实现

class DeepCopy {
public:
    int* data;
    
    DeepCopy(int val) {
        data = new int(val);
    }
    
    // 深拷贝构造函数
    DeepCopy(const DeepCopy& other) {
        data = new int(*other.data);  // 重新分配内存
    }
    
    // 赋值操作符也要深拷贝
    DeepCopy& operator=(const DeepCopy& other) {
        if (this != &other) {
            delete data;                    // 释放旧内存
            data = new int(*other.data);    // 分配新内存
        }
        return *this;
    }
    
    ~DeepCopy() {
        delete data;
    }
};

说白了,深拷贝就是"你的是你的,我的是我的"。每个对象都拥有自己独立的内存空间。

原型模式中的克隆实现

class Prototype {
public:
    virtual ~Prototype() {}
    virtual Prototype* clone() const = 0;
};

class ConcretePrototype : public Prototype {
private:
    int* data;
    std::string name;
    
public:
    ConcretePrototype(int val, const std::string& n) 
        : data(new int(val)), name(n) {}
    
    // 深拷贝克隆
    Prototype* clone() const override {
        return new ConcretePrototype(*this);  // 调用拷贝构造函数
    }
    
    // 拷贝构造函数(深拷贝)
    ConcretePrototype(const ConcretePrototype& other)
        : data(new int(*other.data)), name(other.name) {}
    
    ~ConcretePrototype() {
        delete data;
    }
};

个人习惯:在 clone() 方法中直接调用拷贝构造函数,而不是手动复制每个成员。这样代码更简洁,也更容易维护。如果以后新增了成员变量,只需要修改拷贝构造函数就行。

使用场景

原型模式不是万能的,但在某些场景下特别好用。我总结了几种典型情况:

  • 对象创建成本高:比如需要从数据库加载数据、需要复杂计算的对象。克隆比 new 快得多。
  • 对象状态只有细微差别:比如游戏中的怪物,大部分属性相同,只有位置不同。
  • 需要避免工厂类层次结构:当具体产品类型太多时,用原型模式比用工厂模式更灵活。
  • 运行时动态指定对象类型:你可以在运行时注册原型,然后根据需要克隆。

举个例子,我在做配置管理系统时,需要创建大量配置对象。每个配置对象有几十个属性,但大部分属性值都一样。如果用 new 再逐个设置,代码又长又慢。用原型模式,先创建一个默认配置作为原型,然后克隆并修改个别属性,效率高多了。

避坑指南:如果你的对象里包含循环引用或者共享资源(比如文件句柄、网络连接),深拷贝可能会变得很复杂。这时候可以考虑用智能指针 + 引用计数,或者干脆不用原型模式。

嗯,最后说一句:原型模式虽然好用,但不要滥用。如果你的对象很简单,成员都是基本类型,那直接用 new 也没问题。工具要用在合适的地方。


公众号:蓝海资料掘金营,微信deep3321