【设计模式系列 (五) 】原型模式

发布时间:2026/9/30 20:45:36

【设计模式系列 (五) 】原型模式 ⭐️在这个怀疑的年代我们依然需要信仰。个人主页 YYYing.⭐️设计模式系列专栏设计模式系列系列上期内容【设计模式系列 (四) 】建造者模式系列下期内容暂无目录1. 概述2. 结构3. 核心矛盾浅克隆 vs 深克隆4. 实现一浅克隆proto/ex14.1 抽象原型4.2 具体原型4.3 客户端验证共享5. 实现二深克隆proto/ex25.1 这段代码里的两个坑5.2 深克隆的另一种实现序列化6. 为什么 clone() 不直接返回具体类型7. 优缺点与适用环境7.1 典型用法原型管理器8. 面试专题8.1 开场题说说原型模式吧8.2 高频追问清单8.3 什么时候不该用原型结语创建型模式里原型模式是最容易被低估的一个。它的核心只有两个字克隆。本文代码取自 design-pattern-cpp 仓库的design-pattern/proto/使用 C11。为阅读方便片段省略了头文件保护宏。1. 概述WikipediaThe prototype pattern is a creational design pattern in software development. It is used whenthe type of objects to create is determined by a prototypical instance, which is cloned to produce new objects.原型模式是软件开发中的一种创造设计模式。当要创建的对象类型由原型实例确定时使用该实例被克隆以生成新对象。GoFSpecify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.使用原型实例指定要创建的对象的种类并通过复制此原型来创建新对象。翻译过来一句话用原型实例指定要创建的对象种类并通过复制这个原型来创建新对象。两个关键词① 原型实例prototypical instance创建对象不再通过new 类名()而是从一个已经存在的实例身上复制。② 克隆clone原型模式解决的问题是创建代价高对象的构造需要查数据库、发网络请求、做大量计算 → 造一个就够剩下的复制运行时才知道要造哪种子类手里只有一个基类指针→ 让它自己 clone 自己比写一堆if/else判断类型再 new 优雅需要一个与现有对象状态相同、后续独立演化的副本 → 比如游戏关卡编辑器里的复制粘贴、撤销栈里的对象快照。2. 结构角色职责Prototype抽象原型声明clone()接口。所有原型必须实现它ConcretePrototype具体原型实现clone()返回自己的一个副本Client客户端调用clone()得到新对象不关心具体类型结构很简单一个虚函数 一次拷贝构造。难的是拷贝到底拷多深。3. 核心矛盾浅克隆 vs 深克隆这是原型模式真正要考的东西。C 默认的拷贝构造是逐成员拷贝逐字节复制对指针成员只复制地址本身成员类型默认拷贝的行为后果基本类型 / 值语义成员复制一份值✅ 两个对象互不影响指针 / 引用成员只复制地址❌ 两个对象指向同一份资源于是有了两个概念浅克隆Shallow Clone只复制值成员和指针本身指针指向的资源仍是共享的。改副本会影响原型两个对象析构时还会重复释放同一块内存。深克隆Deep Clone连指针指向的对象也复制一份副本与原型完全独立。一句话浅克隆共享资源深克隆复制资源。4. 实现一浅克隆proto/ex14.1 抽象原型namespace proto { class Prototyppe // 仓库里的原拼写多了一个 p { public: virtual Prototyppe* clone() 0; }; }4.2 具体原型仓库里用CC_SYNTHESIZE系列宏自动生成 getter/setter先看一眼宏展开才读得懂代码/** 定义受保护的变量并提供get、set方法 */ #define CC_SYNTHESIZE(varType, varName, funName)\ protected: varType varName;\ public: varType get##funName(void) const { return varName; }\ public: void set##funName(varType var){ varName var; }Level的成员里有std::vectorMonster*和Rewards*两个指针成员这正是浅克隆会出问题的地方class Level : public Prototyppe { CC_SYNTHESIZE(std::string, name, Name); CC_SYNTHESIZE(std::string, descrip, Descrip); CC_SYNTHESIZE_CR_GET(std::vectorMonster*, monsters, Monsters); // 返回引用才能 push_back CC_SYNTHESIZE(Rewards*, rew, Rew); CC_SYNTHESIZE_SET(bool, release, Release); // 只给 set不需要 get public: Prototyppe* clone() override { // 使用默认拷贝构造进行浅克隆 Level* copy new Level(*this); copy-setRelease(false); return copy; } Level() { release true; this-rew nullptr; } ~Level() { if (!release) return; // ← 副本不负责释放资源 if (this-rew) delete rew; for (auto one : monsters) delete one; monsters.clear(); } };关键在于release这个标记clone()造出的副本把release置为false于是它析构时直接 return不碰那些共享的Rewards和Monster。这是浅克隆避免 double free的典型权宜写法——资源只有原型自己释放副本只借用。代价也很明显副本一旦自己新增了Monster/Rewards就泄漏了。4.3 客户端验证共享Level* proto new Level(); proto-setName(普通山庄); proto-setDescrip(这是一个比较危险的山庄经常有山蜇出没.); proto-getMonsters().push_back(new Monster(10, 20)); proto-setRew(new Rewards(10)); ​ Level* copy dynamic_castLevel*(proto-clone()); copy-setDescrip(我修改了描述); // 值成员互不影响 std::cout proto: proto-getDescrip() std::endl; // 原描述 std::cout copy: copy-getDescrip() std::endl; // 我修改了描述 ​ copy-getRew()-setGold(30); // 指针成员改副本 改原型 std::cout proto: proto-getRew()-getGold() std::endl; // 30 ← 被带跑了 std::cout copy: copy-getRew()-getGold() std::endl; // 30copy-getRew()-setGold(30)这一行就是浅克隆的罪证只改了副本的奖励原型的金币也跟着变成 30。顺带一提Prototyppe*要dynamic_castLevel*才能拿回具体类型——这个问题下一节会讲怎么优雅解决。5. 实现二深克隆proto/ex2思路绕过默认拷贝构造自己写一套copy()/release()让拷贝构造和赋值运算符共用同一份深拷贝逻辑。class Level : public Prototyppe { CC_SYNTHESIZE(std::string, name, Name); CC_SYNTHESIZE(std::string, descrip, Descrip); CC_SYNTHESIZE_CR_GET(std::vectorMonster*, monsters, Monsters); CC_SYNTHESIZE(Rewards*, rew, Rew); private: void copy(const Level l) { this-name l.name; this-descrip l.descrip; // 值成员直接赋 if (l.rew) { this-rew new Rewards(*l.rew); // ★ 指针成员重新 new 一份 } for (auto m : l.monsters) { this-monsters.push_back(new Monster(*m)); // ★ 容器里的指针同样逐个深拷贝 } } void release() { if (this-rew) { delete rew; this-rew nullptr; } for (auto one : monsters) delete one; monsters.clear(); } public: Prototyppe* clone() override { // 使用拷贝构造进行克隆 return new Level(*this); } Level() { this-rew nullptr; } ~Level() { release(); } // 重写拷贝构造 Level(const Level l) { copy(l); } // 重载赋值运算符 Level operator(const Level l) { release(); // 先释放资源防止覆盖前泄漏 copy(l); // 再复制 return *this; } };三个要点copy()是深拷贝的唯一实现点拷贝构造和赋值运算符都复用它。这样两份逻辑不会走偏改一处即可。operator必须先 release 再 copy否则旧资源会被直接覆盖造成泄漏。clone()里的new Level(*this)走的就是被重写过的拷贝构造所以自动变成深克隆——clone()本身不需要懂深还是浅。此时再跑一遍客户端的验证代码原型打印 10副本打印 30两者彻底独立。5.1 这段代码里的两个坑① 自赋值没有防护Level operator(const Level l) { release(); // 如果 this l这一步已经把资源删了 copy(l); // 再去读已释放的内存 → UB return *this; }标准写法要在release()之前判断if (this l) return *this;② 异常安全copy()进行到一半抛异常比如new失败对象会停在已经 release、只拷了一半的半残状态析构时还可能二次释放。工程里更稳的做法是把裸指针换成智能指针std::unique_ptrRewards、std::vectorstd::unique_ptrMonster拷贝构造交给 RAII 管深克隆只需要在clone()里显式std::make_unique一份。5.2 深克隆的另一种实现序列化先把原型序列化成字节流再从字节流反序列化出新对象。优点不用给每个类手写copy()天然深拷贝对类的新增成员自动生效缺点引入序列化框架、性能差、遇到不可序列化的成员文件描述符、句柄、锁就歇菜。Java 的Object.clone()要求实现Cloneable接口本质上也是同一类思路的产物。C 里没有语言级的 clone 支持只能自己实现。6. 为什么clone()不直接返回具体类型4.3 节提到过仓库里返回Prototyppe*客户端要dynamic_cast才能拿回Level*。其实 C 支持协变返回类型covariant return typeLevel* clone() override { return new Level(*this); } // 合法派生类可以收窄返回类型派生类把返回类型改写为更具体的类型是允许的前提是返回指针或引用这样客户端就不用dynamic_cast了。C 里这是零成本的。但更值得注意的是原型模式的根本价值——多态地复制Prototyppe* p getSomePrototype(); // 运行时才知道是 Level 还是 Boss Prototyppe* copy p-clone(); // 克隆出来的必然是正确的那一种这件事拷贝构造函数做不到拷贝构造是编译期静态绑定的Prototyppe p someLevel;会发生对象切片slicing派生部分被切掉。想在只有一个基类指针的前提下复制出正确的派生类对象只能是虚函数——这就是clone()存在的意义。7. 优缺点与适用环境主要优点创建新对象不必知道具体类型客户端与具体类解耦逃避构造函数的约束——不需要为每种原型写一堆工厂类减少类的数量可以动态地增删原型运行期注册原型即可见下对创建代价高的对象复制比重新构造便宜得多。主要缺点每个类都必须实现 clone()而且深克隆要处理每一层引用关系引用环、共享成员实现复杂深克隆的成本可能接近甚至超过重新构造需要权衡必须小心处理浅克隆带来的资源重复释放问题与所有创建型模式一样克隆细节对客户端透明出问题时排查链更长。适用环境对象之间大同小异差异只是若干属性的取值对象的创建代价大数据准备、网络、复杂计算需要动态加载/注册原型运行期才知道要造什么配合原型管理器。7.1 典型用法原型管理器仓库里的ClassFactory工厂篇讲过的自注册反射机制就是同一思路的另一个形态。如果要做原型管理器通常就是一张表class PrototypeManager { std::mapstd::string, Prototyppe* protos; // 注册表名字 → 原型 public: void add(const std::string key, Prototyppe* p) { protos[key] p; } Prototyppe* create(const std::string key) { return protos[key]-clone(); } // 克隆出副本 };注意注册表里存的是原型样板取出来的一定是clone()的副本而不是原型本身。否则所有客户端会共用同一个对象就退化成全局变量了。8. 面试专题8.1 开场题说说原型模式吧考察点是概念、角色、应用场景按这个顺序答概念原型模式用原型实例指定创建对象的种类并通过拷贝这些原型来创建新对象。它属于创建型模式。角色抽象原型 Prototype 声明clone()具体原型 ConcretePrototype 实现clone()返回自己的副本客户端通过调用clone()得到新对象不关心具体类型。使用场景对象创建代价高要查库、走网络、复杂计算时对象之间只有少量属性不同时运行时才知道要创建哪种子类、手里只有基类指针时。核心难点浅克隆与深克隆的取舍。可扩展把浅/深克隆的区别、和拷贝构造/工厂模式的区别接上见下。8.2 高频追问清单追问答法浅克隆和深克隆的区别必问浅克隆只复制值成员和指针本身指针指向的资源仍是共享的深克隆连指针指向的对象一起复制两者完全独立。C 默认拷贝构造就是浅克隆深克隆怎么实现必问一是自己重写拷贝构造 重载赋值运算符集中到copy()里处理每一层引用二是序列化/反序列化通用但要引入框架且性能差原型模式和拷贝构造函数有什么区别拷贝构造是编译期静态绑定的用基类对象接派生类对象会发生对象切片clone()是虚函数能多态地复制——拿基类指针就能克隆出正确的派生类对象原型模式和工厂模式有什么区别工厂关注造哪个用类名/参数决定原型关注复制哪一个已有的用原型实例决定。工厂要新增产品时通常要加工厂类原型只需要多加一个原型对象甚至运行期动态注册为什么clone()返回基类指针为了让客户端通过基类接口调用实现多态复制。C 支持协变返回类型派生类可以把返回类型收窄为具体类型从而免掉dynamic_cast浅克隆时怎么防止 double free引用计数、智能指针推荐、或像仓库里那样用一个release标记让副本不参与资源释放原型对象的析构谁负责原型本身由创建它的人负责克隆出的副本由客户端负责。所有权关系必须写清楚否则就是上一问的 double free深克隆会遇到什么麻烦引用环A 引用 B、B 又引用 A会导致无限递归多个成员共享同一个对象时深拷贝要保证共享关系也被正确复制而不是各拷一份原型模式有什么缺点每个类都要实现clone()深克隆实现复杂、成本可能不比重新构造低容易踩浅克隆的资源重复释放什么场景不适合用原型对象结构简单、构造开销不大时直接new更直观对象里有不该被复制的资源连接、句柄、锁时深拷贝语义难以定义8.3 什么时候不该用原型判断标准就一条这个对象复制起来真的比重新造便宜吗成员只有几个值类型、构造函数就是几个赋值 → 直接new或拷贝构造clone()是纯成本对象持有外部资源数据库连接、文件句柄、互斥锁→ 深拷贝语义说不清楚浅拷贝又会共享别硬套只想复用一段初始化逻辑 → 那是一个工厂或构造辅助函数的事不是原型模式。原型模式的价值在于把复制一个复杂对象这件事封装成一次虚函数调用。对象不复杂的时候它换不来任何东西。结语我是YYYing后面还有更精彩的内容希望各位能多多关注支持一下主包。无限进步我们下次再见
延伸阅读

更多相关文章

2026/9/30 20:40:35

BL350不是芯片型号:工业级双核SoC的实时性本质与协同设计

1. BL350不是芯片型号,而是工业级SoC的系统级代号 很多人第一次看到“BL350”时,下意识会把它当成某款ARM Cortex-M4F芯片的型号——比如误以为是意法半导体STM32F4系列的变种,或是NXP i.MX RT1050的别名。我刚接触这个代号时也犯过同样错误&…

2026/9/30 20:40:35

英文论文Turnitin AI检测率高达60%?留学生如何安全降AI与避坑指南

熬夜两个月敲出来的英文论文,提交前测了一下,论文查重率倒是安全,但Turnitin的AI检测率居然飙到了60%。 那种委屈和恐慌,我当年发第一篇SCI时深有体会。明明是自己一行行码的字,却因为句式固化被判定疑似代写。 为了顺…

2026/9/30 21:46:01

UltraEdit 设置默认打开格式:用 TaoToken 统一管理配置文件关联

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/30 21:41:01

零基础 Vibe Coding 教程:MCP 服务介绍与 TaoToken 统一 Key 接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/29 11:07:23

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/9/29 21:48:03

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 7:00:49

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 0:01:22

MATLAB+Yalmip+CPLEX实战:综合能源系统优化调度全流程解析

做综合能源系统优化调度这活儿,最痛苦的不是建模本身,而是模型写完之后不知道该怎么求解。看论文里轻飘飘一句“采用Yalmip调用CPLEX求解”,自己上手时却往往卡在环境配置、变量声明、约束写法和求解状态判读上,一耗就是两三天。这…

2026/9/30 0:01:22

I3C比I2C快10倍?RK3576实战:速率、DTS配置与混合总线避坑指南

I3C 比 I2C 快 10 倍?这句话在嵌入式群里传了很久,每次都能吵出一堆截图。前段时间我正好在 RK3576 上调板级 I3C 接口,从控制器寄存器一路摸到 Linux DTS 配置,踩了不少坑,也把这笔速度账彻底算明白了。本文就用 RK35…

2026/9/30 0:01:22

字符串转对象:JSON.parse、new Function与URLSearchParams

“字符串转对象”这几个字,我在技术群里见过的问法至少有十几种:有人拿着一串{a:1,b:2}说 JSON.parse 直接报错,有人要从 URL 里抠出参数,还有人只是想把abc变成能挂属性的东西。js 这门语言里,字符串和对象之间的转换…

2026/9/29 3:53:39

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/30 18:00:04

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/30 10:28:53

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑