原型模式:模式动机与定义
说实话,原型模式是我个人非常喜欢的一个创建型模式。为什么?因为它解决了一个很实际的问题——当你需要创建大量相似对象时,直接 new 的成本太高了。
我记得有一次在做一个游戏引擎,需要生成成千上万的子弹对象。每个子弹的属性都差不多,只是位置和速度不同。如果每次都 new 一个对象,再逐个设置属性,性能开销大得吓人。后来我改用原型模式,先创建一个"样板子弹",然后直接克隆它,再微调几个参数。性能提升了好几个数量级。
原型模式的定义其实很简单:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。说白了,就是先造一个样板,后面需要的时候直接复制它。
核心思想:不通过 new 创建对象,而是通过克隆已有对象来创建新对象。
角色与结构
原型模式的结构非常清晰,就三个角色:
| 角色 | 职责 | 我见过的坑 |
|---|---|---|
| Prototype(抽象原型) | 声明克隆接口 | 很多人忘记声明虚析构函数 |
| ConcretePrototype(具体原型) | 实现克隆方法 | 深拷贝没处理好指针成员 |
| Client(客户端) | 调用克隆方法创建新对象 | 误以为克隆和 new 完全等价 |
嗯,这里要注意一点:原型模式的核心在于克隆方法的实现。你想想看,如果克隆方法写错了,那后面所有基于它的对象都会出问题。
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 也没问题。工具要用在合适的地方。