学习路线位置:创建型模式收官之战。前四天分别解决了「全局唯一」「一个产品一个工厂」「一族产品配一个工厂」「分步构建复杂对象」,今天解决创建对象的最后一个难题——复制一个现成的对象。
原型模式(Prototype Pattern):用一份已经创建好的对象当作「模板」(原型),通过 clone() 复制出一个个新对象。客户端既不需要写 new,也不需要知道具体类名,就能源源不断地获得「和原型一模一样」的独立实例。
一句话定义:让对象自己复制自己,把「如何复制」封装在对象内部,调用方只依赖抽象接口。
先看一个真实场景:图形编辑器需要「复制粘贴」图形。没有原型模式时,复制逻辑只能写在客户端,靠类型判断 + 手工逐字段拷贝:
// 反例:没有原型模式时的"复制"逻辑
Shape* duplicateShape(const Shape& src) {
if (src.type() == "Circle") { // 按类型分支
const auto& c = static_cast<const Circle&>(src);
auto* copy = new Circle(c.radius(), c.color());
copy->setTags(c.tags()); // 手工逐字段复制,字段一多极易遗漏
return copy;
}
if (src.type() == "Rectangle") {
const auto& r = static_cast<const Rectangle&>(src);
return new Rectangle(r.width(), r.height(), r.color());
}
// 每新增一种图形,这里就要加一个分支!
return nullptr;
}
这段代码有四个问题:
duplicateShape 都要被修改,加一个分支。Circle、Rectangle 的名字,违反依赖倒置(DIP),也无法面向抽象编程。需求继续增加(复制时还要复制动画、样式、子对象、撤销快照……),这个函数会越来越臃肿、越来越容易出错。根本原因:「复制」这件本该由对象自己负责的事,被外包给了客户端。于是我们需要原型模式——让每个对象自己实现 clone(),把复制逻辑收回对象内部。
原型模式的核心只有一句话:「复制」由对象自己完成,而不是由外部代码按类型手工拷贝。
每个可复制的对象实现一个 clone() 方法,返回一份「和它内容相同、但完全独立」的新对象。客户端要做的事只有一件:「给我一份和这个一样的」——它不需要关心对象到底是什么类、内部有多少字段、深拷贝还是浅拷贝,这些细节全部封装在对象自己内部。
生活类比:复印机。你把一份填好的表格放进复印机,按一下按钮,出来一份一模一样的——你不必知道表格是怎么排版印刷的,也不必重新填写一遍。又像细胞分裂:一个细胞复制出另一个细胞,新细胞自带全部内部结构。再想想 Word 的「另存为模板」:从一个做好的文档复制出无数新文档,每个再微调。
软件工程落点:clone() 本质上是「多态的构造函数」,也叫「虚拟构造函数」。普通构造函数是静态绑定的——你必须写出具体类名(new Circle(...))才能创建对象;而 clone() 是虚函数、动态绑定——你手里只有基类指针,调用 ptr->clone() 依然能复制出完整的派生对象。这就是原型模式最迷人的地方:创建动作(new)被推迟、被多态化,创建与使用彻底分离。
原型模式只有三个核心角色(第四个是可选增强):
┌─────────────────────┐
│ Prototype │ 抽象接口:声明 clone()
│ + clone(): Proto │ 谁调用谁:Client 调用它
└──────────┬──────────┘
▲ 继承(实现 clone 的复制细节)
┌──────────┴──────────┐ ┌──────────────────────┐
│ ConcretePrototype │ │ ConcretePrototype2 │
│ + clone(): Proto │ │ + clone(): Proto │
└──────────┬──────────┘ └──────────┬───────────┘
│ clone() 返回自己的拷贝 │
┌──────────┴─────────────────────────┴───────────┐
│ Client(客户端) │
│ 只依赖 Prototype 抽象,不依赖任何具体类 │
└────────────────────────────────────────────────┘
clone() 纯虚函数,是整个模式的「接口契约」。clone(),真正的复制逻辑(深拷贝还是浅拷贝、复制哪些字段)由它自己决定。clone() 获得新对象。谁依赖谁?Client → Prototype,依赖抽象;谁创建谁?具体原型创建自己(自复制)。std::map<std::string, std::unique_ptr<Prototype>>),需要时按名字取出克隆。它让「运行时才知道类型」的创建成为可能,与工厂模式配合效果极佳。扩展点在哪里?新增一种可复制类型 = 新增一个子类 + 实现 clone(),既有代码一行都不用改——这正是开闭原则要求的形态。
用「图形画布」演示:画布上要批量生成图形,全部通过克隆原型得到。C++17 完整可编译:
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
// 原型接口:定义"克隆"操作,所有可复制对象都继承它
class Shape {
public:
virtual ~Shape() = default;
virtual std::unique_ptr<Shape> clone() const = 0; // 克隆:返回独立拷贝
virtual void draw() const = 0;
};
// 圆形:具体原型
class Circle : public Shape {
public:
Circle(int radius, std::string color)
: m_radius(radius), m_color(std::move(color)) {}
std::unique_ptr<Shape> clone() const override {
// 用拷贝构造生成新对象;std::string 是值语义,自动深拷贝
return std::make_unique<Circle>(*this);
}
void draw() const override {
std::cout << "绘制" << m_color << "圆形,半径 " << m_radius << "\n";
}
private:
int m_radius;
std::string m_color;
};
// 矩形:另一个具体原型
class Rectangle : public Shape {
public:
Rectangle(int width, int height, std::string color)
: m_width(width), m_height(height), m_color(std::move(color)) {}
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Rectangle>(*this);
}
void draw() const override {
std::cout << "绘制" << m_color << "矩形," << m_width << "x" << m_height << "\n";
}
private:
int m_width;
int m_height;
std::string m_color;
};
int main() {
std::vector<std::unique_ptr<Shape>> canvas; // 画布只持有抽象接口
Circle redCircle(5, "红色"); // 原型 1:只创建一次
Rectangle blueRect(3, 4, "蓝色"); // 原型 2
// 客户端完全不关心具体类型,统一通过 clone() 复制
canvas.push_back(redCircle.clone());
canvas.push_back(redCircle.clone()); // 再复制一个红色小圆
canvas.push_back(blueRect.clone());
for (const auto& s : canvas) {
s->draw(); // 多态调用,无需知道具体类型
}
return 0;
}
逐段解释:
Shape 是抽象原型,clone() 返回 std::unique_ptr<Shape>——用智能指针管理所有权,杜绝裸 new/delete 和内存泄漏。Circle::clone() 里一行 std::make_unique<Circle>(*this) 搞定一切:它调用 Circle 的拷贝构造函数,把半径、颜色完整复制到新对象。unique_ptr<Circle> 返回时隐式转换为 unique_ptr<Shape>。main() 里没有任何 if (type == ...),也没有出现具体类的分支——新增 Triangle 类只需实现 clone(),main 一行不用改。程序输出:
绘制红色圆形,半径 5
绘制红色圆形,半径 5
绘制蓝色矩形,3x4
⚠ 深拷贝陷阱:本例成员都是值类型(int / std::string),默认拷贝构造就是深拷贝。但如果类里有裸指针成员(如 int* m_data),默认拷贝只是复制指针地址,两个对象会共享同一块内存——析构时双重释放、改一个全变。此时必须在 clone() 里手动深拷贝(new 一块新内存并拷贝内容)。现代 C++ 的最佳实践是用 RAII 容器(std::vector、std::string、std::unique_ptr)替代裸指针,让拷贝语义自动正确。
做一个 Qt 6 图形小工具:左边工具栏每种图形维护一个「原型」,点击按钮就克隆一个新图形放进 QGraphicsScene——这正是 Qt 官方 Diagram Scene 示例复制粘贴功能的简化版,也是原型模式在 Qt 世界最典型的用法。
shapeitem.h(原型接口 + 两个具体原型):
#ifndef SHAPEITEM_H
#define SHAPEITEM_H
#include <QColor>
#include <QGraphicsItem>
#include <QPainter>
#include <memory>
#include <utility>
// 原型接口:所有可克隆图形项的抽象基类(也是 QGraphicsItem 子类)
class ShapeItem : public QGraphicsItem {
public:
explicit ShapeItem(QColor color) : m_color(std::move(color)) {}
~ShapeItem() override = default;
// 克隆接口:返回一份独立拷贝(多态复制)
virtual std::unique_ptr<ShapeItem> clone() const = 0;
QRectF boundingRect() const override { return QRectF(-30, -30, 60, 60); }
void paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*) override {
painter->setBrush(m_color);
painter->setPen(QPen(Qt::black, 2));
drawShape(painter); // 由子类决定画什么形状
}
protected:
virtual void drawShape(QPainter* painter) const = 0;
QColor m_color{Qt::white}; // 原型的状态:颜色
};
// 圆形:具体原型
class CircleItem : public ShapeItem {
public:
explicit CircleItem(QColor color) : ShapeItem(std::move(color)) {}
std::unique_ptr<ShapeItem> clone() const override {
return std::make_unique<CircleItem>(*this); // 拷贝构造 = 克隆
}
protected:
void drawShape(QPainter* painter) const override {
painter->drawEllipse(QRectF(-25, -25, 50, 50));
}
};
// 矩形:具体原型
class RectItem : public ShapeItem {
public:
explicit RectItem(QColor color) : ShapeItem(std::move(color)) {}
std::unique_ptr<ShapeItem> clone() const override {
return std::make_unique<RectItem>(*this);
}
protected:
void drawShape(QPainter* painter) const override {
painter->drawRect(QRectF(-25, -25, 50, 50));
}
};
#endif // SHAPEITEM_H
main.cpp:
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QPushButton>
#include <QRandomGenerator>
#include <QVBoxLayout>
#include <QWidget>
#include "shapeitem.h"
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
// 原型库:每种图形只保留一个原型,点击按钮即克隆出新实例
CircleItem circleProto(Qt::red);
RectItem rectProto(Qt::blue);
QGraphicsScene scene;
QGraphicsView view(&scene);
view.setRenderHint(QPainter::Antialiasing);
QPushButton btnCircle("克隆圆形");
QPushButton btnRect("克隆矩形");
QPushButton btnClear("清空场景");
QWidget window;
auto* layout = new QVBoxLayout(&window);
layout->addWidget(&view);
layout->addWidget(&btnCircle);
layout->addWidget(&btnRect);
layout->addWidget(&btnClear);
// 点击"克隆圆形":从原型复制一个新圆并放入场景
QObject::connect(&btnCircle, &QPushButton::clicked, &scene, [&] {
auto item = circleProto.clone(); // ① 原型模式:克隆
item->setPos(QRandomGenerator::global()->bounded(0, 300),
QRandomGenerator::global()->bounded(0, 300));
scene.addItem(item.release()); // ② 场景接管所有权(Qt 负责 delete)
});
QObject::connect(&btnRect, &QPushButton::clicked, &scene, [&] {
auto item = rectProto.clone();
item->setPos(QRandomGenerator::global()->bounded(0, 300),
QRandomGenerator::global()->bounded(0, 300));
scene.addItem(item.release());
});
QObject::connect(&btnClear, &QPushButton::clicked, &scene,
&QGraphicsScene::clear);
window.resize(420, 480);
window.show();
return app.exec();
}
CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(prototype_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_standard_project_setup() # Qt 6 专用:自动处理 AUTOMOC 等
qt_add_executable(prototype_demo
main.cpp
shapeitem.h
)
target_link_libraries(prototype_demo PRIVATE Qt6::Widgets)
find_package(Qt5 ...)、set(CMAKE_AUTOMOC ON) + 普通 add_executable,链接 Qt5::Widgets;Qt 6 的 qt_standard_project_setup() 和 qt_add_executable() 更省心。本示例没有 Q_OBJECT 宏,moc 不是必需的。clone() 返回 unique_ptr(RAII 保证不泄漏);scene.addItem(item.release()) 把裸指针交给场景——QGraphicsScene 采用 Qt 的 parent-child 机制接管删除。两个所有权模型(unique_ptr 和场景父子树)在此处精确交接。以 Qt 示例为例,完整运行流程:
circleProto、rectProto——每种图形只在内存中存在一份「母本」。clicked 触发 lambda 槽函数。circleProto.clone() 被调用——由于虚函数动态绑定,实际执行 CircleItem 的拷贝构造函数,在堆上生成一个内容相同、内存独立的新对象(深拷贝 m_color 等成员)。scene.addItem(item.release()) 交给场景,场景触发重绘,视图中出现一个新圆形。纯 C++ 示例的流程更简洁:创建原型 → clone()(拷贝构造 + 隐式转基类指针)→ 存入 vector<unique_ptr<Shape>> → 多态 draw() → 程序结束 RAII 自动释放全部对象。
canvas.push_back(redCircle.clone()) 里没有出现任何具体类名,复制细节(拷贝哪些字段、深浅拷贝)全部封装在对象内部。main() 只依赖 Shape / ShapeItem 抽象接口。clone(),既有代码零修改。对比 §2 的反例——那里每加一种图形都要改 duplicateShape 的分支,这正是开闭原则「对扩展开放、对修改关闭」的直观体现。Prototype,而不是依赖 Circle、Rectangle 具体类;具体类型由对象自身(或原型管理器)决定。std::string、std::vector 的拷贝构造)组合完成——成员越「值语义」,克隆越安全。clone():克隆一份、修改克隆体、断言原型不受影响(独立性);也可以用一个原型批量生成测试夹具,再也不用在测试代码里写一堆构造参数。没有原型模式,复制逻辑会退化成「类型分支 + 手工逐字段拷贝」的泥潭:
// 反例:类型分支复制,新增字段后极易漏拷贝
if (type == "Circle") copy = new Circle(c.radius(), c.color());
else if (type == "Rect") copy = new Rect(r.width(), r.height());
else if (type == "Line") copy = new Line(...);
// 新加了一个"阴影"属性:这里忘了拷贝 → 静默 bug
// 第 6 种图形出现时,这个函数又要改一次
new 某某类,功能直接无法实现。copy = &src 或复制指针成员,两个「复制品」共享同一块资源,改一个全变、析构时还可能双重释放——内存错误与状态错乱齐飞。这些问题的根源都是同一个:复制责任放错了位置。把它收回对象内部(clone),一切迎刃而解。
✅ 适合使用(3~5 个场景):
❌ 不适合使用(2~4 个场景):
new 或拷贝构造就够,clone 是画蛇添足。⚠ 过度设计提醒:不要为了「炫技」给所有类加 clone()。判断标准很朴素——只有当代码里真的出现了「按类型写复制逻辑」或「需要复制运行时才知道类型的对象」时,才引入原型模式。设计模式是工具,不是装饰品。
最容易和原型混淆的是两个创建型模式,外加 C++ 自带的拷贝构造:
| 模式 | 核心动作 | 创建方式 | 典型场景 |
|---|---|---|---|
| 原型 Prototype | 复制 | 现有对象自我复制(clone),可保留当前状态 | 复制粘贴、模板派生、运行时类型复制 |
| 工厂方法 Factory Method | 创建 | 工厂子类决定 new 哪个具体类(从零创建) | 一个产品一个工厂,创建逻辑子类化 |
| 建造者 Builder | 组装 | 分步设置参数,最后一步产出成品 | 构造过程复杂、参数有顺序依赖 |
clone() 是虚函数、动态绑定,能经基类指针复制出完整的派生对象。所以 clone() 本质就是「多态的拷贝构造」。deep=true 深拷贝整棵子树,deep=false 只克隆节点本身。这是 Qt 框架里最教科书式的原型模式应用,XML 文档的复制、移动操作都建立在它之上。clone() 供复制粘贴使用——与我们今天的 Qt 实战完全同构。阅读这些示例时留意:它们如何深拷贝数据成员、如何处理场景所有权。答:工厂方法由工厂决定「创建哪个具体类」,但仍是从零创建(出厂状态);原型模式由现有对象自我复制,客户端连「要创建什么类」都不需要知道,且复制品保留原型的当前状态。工厂适合首次创建,原型适合「基于实例再生成」。二者还可组合:注册表里的原型 + 工厂风格的对外接口。
答:拷贝构造是静态绑定。通过基类指针/引用拷贝派生对象时,只会调用基类的拷贝构造,派生部分被「切片」丢弃。clone() 是虚函数、动态绑定到实际类型,能复制出完整的派生对象;同时返回 unique_ptr,把所有权语义一并封装好。
答:当对象拥有独占资源(裸指针指向的堆内存、文件句柄等)时必须深拷贝,否则两个对象共享资源,析构时双重释放,或一处修改影响另一处。值语义成员(int、std::string、std::vector)默认拷贝即深拷贝,无需额外处理——这也是现代 C++ 鼓励用 RAII 容器替代裸指针的原因。
答:为 QGraphicsItem 子类实现 clone() 返回 unique_ptr,克隆时深拷贝全部数据成员;加入场景时用 scene.addItem(item.release()) 把所有权交给场景,由场景负责销毁(Qt parent-child 机制)。注意:release() 之后绝不能再碰这个指针,所有权已转移。
答:用字符串名字登记一组原型(std::map<std::string, std::unique_ptr<Prototype>>),按名字取出克隆。它消除了客户端里「按类型决定创建方式」的 if/switch,配合配置文件或插件系统可实现「运行时才知道类型」的创建——这是原型模式区别于工厂模式的独有价值。
需求:报表模板中心
ReportTemplate:含 clone() 纯虚函数、标题 title、样式 style、数据行 std::vector<std::string> rows。ExcelReport、PdfReport。std::map<std::string, std::unique_ptr<ReportTemplate>> 做原型注册表,登记「月度销售报表」「年度总结报表」等模板。createReport(name, newTitle):从注册表克隆模板、修改标题、返回新报表;修改克隆体的数据行后,验证原型模板不受影响。提示:① clone() 内写 std::make_unique<ExcelReport>(*this),rows 用 vector 自动深拷贝;② 注册表里存的是原型本身,取用时必须走 proto->clone(),千万不要把原型对象直接交出去。