C++设计模式之模板方法模式(TemplateMethod)

发布时间:2026/10/21 15:34:55

C++设计模式之模板方法模式(TemplateMethod) 前言模板方法模式Template Method Pattern是 GoF 二十三种设计模式中的行为型模式之一。它的核心思想只有一句话在一个非虚函数里把算法的骨架固定下来把其中会变化的步骤声明为虚函数交给子类去实现。基类控制「先做什么、后做什么、什么条件下做」子类只负责「每一步具体怎么做」。这个模式在 C 里有一个绕不开的命名混淆它和 C 语言里的「模板template」没有关系。模板方法是运行期多态的产物靠虚函数和继承实现C 的template是编译期泛型机制两者可以配合使用但不是一回事。中文资料里常把前者写成「模板方法模式」后者写成「模板/泛型编程」读的时候要看清语境。另一个常见误解是把「模板方法」理解成「一个被反复调用的普通函数」。它之所以叫模板是因为算法流程像一张固定模板步骤的数量和顺序由基类写死子类只能填空不能改结构。如果子类可以整体替换流程那只是普通的虚函数重写约束就消失了。本文讲清模式的结构、C 的落地写法包括非虚接口惯用法、与策略模式的区别以及常见的实现错误。代码以 C17 为基准未经过编译运行行为按标准语义推演。一、结构固定的骨架与可变的步骤模式里有两类参与者。抽象基类定义模板方法它通常是非虚的、公开的模板方法依次调用若干「基本操作」这些基本操作是受保护的虚函数可以是纯虚必须实现也可以带默认实现称为钩子hook。具体子类重写基本操作但不重写模板方法。一个典型的调用链是client - Base::templateMethod() 非虚固定顺序- step1() 虚子类实现- step2() 虚子类实现- hook() 虚有默认实现子类可选覆盖这种分工带来两个好处。第一算法流程只有一份修改流程只需改基类不会出现「五个子类各自抄了一遍流程、改的时候漏了一个」的情况。第二子类被限制在步骤级别无法破坏步骤之间的不变式例如「必须先打开再写入」「写完必须关闭」。反过来它也有代价继承是强耦合子类必须接受基类定义的全部步骤运行期多态有虚调用开销如果步骤划分得不合理子类会被迫实现一些自己根本不关心的空函数。步骤粒度是这个模式设计时最需要斟酌的地方。二、C 落地非虚接口与访问控制C 实现模板方法时有几个语言层面的细节决定了它是否安全。第一模板方法不要声明为virtual。一旦它是虚函数子类就可以整体重写骨架的约束形同虚设。常见写法是公开的非虚函数甚至可以加final之外的修饰非虚函数本来就不能被重写但可以被子类用同名函数隐藏见坑点。第二析构函数必须是virtual。只要类打算通过基类指针delete派生对象基类析构就必须是虚的否则是未定义行为undefined behavior, UB。这一点与模板方法本身无关但只要用了继承多态就必须遵守。第三基本操作放在protected而不是public。外部调用方只应该看到模板方法这一个入口把步骤暴露为公有等于允许调用方绕过骨架直接调用某一步顺序约束就没了。更严格的做法是「非虚接口non-virtual interface, NVI」公有非虚函数做入口基本操作声明为private virtual由基类的非虚函数调用。派生类可以重写私有虚函数但外部包括其他派生类无法直接调用它。第四绝不能在构造或析构函数里调用虚函数并指望派生出效果。构造基类子对象时对象的动态类型还是基类虚调用只会调到基类版本若调用的是纯虚函数行为未定义。析构阶段同理。需要子类参与的初始化应该提供一个显式的init()或build()步骤由派生类构造完成后调用。下面是一个完整的报表导出示例流程固定为「写头 → 写正文 → 可选写脚注 → 写结束标记」具体格式由子类决定。// 适用于 C17#include iostream#include stringclass ReportGenerator {public:virtual ~ReportGenerator() default;// 模板方法非虚固定算法骨架void generate() const {write_header();write_body();if (want_footer()) {write_footer();}write_end();}protected:virtual void write_header() const 0;virtual void write_body() const 0;virtual void write_footer() const 0;// 钩子有默认实现子类可选择覆盖virtual bool want_footer() const { return true; }private:// 私有步骤子类无法覆盖也无法从外部调用void write_end() const {std::cout end \n;}};class PlainTextReport : public ReportGenerator {protected:void write_header() const override {std::cout [plain]\n;}void write_body() const override {std::cout line 1\nline 2\n;}void write_footer() const override {std::cout (no footer)\n;}bool want_footer() const override {return false; // 不输出脚注}};class CsvReport : public ReportGenerator {protected:void write_header() const override {std::cout id,name\n;}void write_body() const override {std::cout 1,alpha\n2,beta\n;}void write_footer() const override {std::cout rows2\n;}// 不覆盖 want_footer使用基类默认的 true};void export_report(const ReportGenerator r) {r.generate(); // 只依赖基类接口}int main() {PlainTextReport text;CsvReport csv;export_report(text);export_report(csv);return 0;}这段代码里generate()的调用顺序由基类唯一决定PlainTextReport通过覆盖want_footer()跳过脚注CsvReport直接继承默认钩子。write_end()是私有非虚函数子类既不能改也不能调保证结束标记一定按基类的方式输出。三、与策略模式的区别以及静态版本模板方法模式和策略模式经常被拿来比较因为两者都在「让变化的部分可替换」。区别在于变化的时机和绑定方式维度模板方法模式策略模式变化单元算法中的若干步骤整个算法绑定方式继承 虚函数运行期组合 接口对象运行期关系基类与子类是 is-a上下文与策略是 has-a扩展方式新增子类新增策略类无需改上下文复用骨架由基类复用算法可独立复用、可动态切换耦合度高子类依赖基类步骤划分低只依赖策略接口如果需求是「流程固定、个别步骤不同」模板方法合适如果需求是「整个算法可以整体替换甚至运行期切换」策略模式更合适。实践中两者可以组合模板方法里的某个步骤内部持有一个策略对象。C 还有第三种选择用奇异递归模板模式curiously recurring template pattern, CRTP把模板方法做成编译期多态// 适用于 C17template typename Derivedclass StaticReportGenerator {public:void generate() const {self().write_header();self().write_body();self().write_end();}private:const Derived self() const {return static_castconst Derived(*this);}};class PlainReport : public StaticReportGeneratorPlainReport {public:void write_header() const { /* ... */ }void write_body() const { /* ... */ }void write_end() const { /* ... */ }};CRTP 版本没有虚函数表调用可以在编译期内联代价是类型必须在编译期确定无法通过基类引用持有不同子类也不能放进同一个容器里做运行期分派。它适合性能敏感、类型集合固定的场景不属于 GoF 的原始模式但常被当作「静态模板方法」使用。常见坑点❌ 把generate()声明为virtual让子类可以整体重写。✅ 模板方法保持非虚只把步骤声明为虚函数。❌ 基类析构函数不写virtual却通过基类指针delete派生对象。✅ 写virtual ~ReportGenerator() default;否则是 UB。❌ 在基类构造函数里调用write_header()这种纯虚函数。✅ 构造期间动态类型是基类纯虚调用是 UB把初始化拆成显式的init()步骤。❌ 把write_header()等步骤声明为public。✅ 放protected或采用 NVI公有非虚入口 私有虚步骤。❌ 派生类重写步骤时漏写override签名差一个const就变成新函数。✅ 所有重写都写override让编译器检查签名。❌ 每个子类各自复制一遍generate()的流程再各自加步骤。✅ 流程只留在基类差异用钩子表达而不是复制骨架。❌ 把「模板方法模式」和 C 的template混为一谈用模板参数代替虚函数。✅ 前者是运行期多态后者是编译期泛型按是否需要运行期替换来选择。❌ 钩子的默认实现返回一个「看似安全」的值却没有文档说明。✅ 钩子要有明确语义和默认行为说明例如want_footer()默认true。总结角色职责C 落点抽象基类定义模板方法与步骤接口公有非虚generate()受保护虚步骤模板方法固定步骤顺序与条件非虚禁止子类重写基本操作子类必须实现的步骤纯虚函数钩子子类可选覆盖的扩展点带默认实现的虚函数具体子类实现步骤不碰流程overridevirtual析构模板方法模式的本质是「用继承固定流程、用虚函数开放步骤」。写的时候盯住三件事模板方法非虚、析构虚、步骤受保护。若发现子类需要改流程而不是填步骤说明模式选错了应该考虑策略模式或重新划分职责。
延伸阅读

更多相关文章

2026/10/21 15:34:55

社交工程评论注入:职场社交平台钓鱼攻击的攻防解析

还记得那次值班时接到的求助电话吗?一位同事在职业社交平台收到一条很自然的评论:“刚看到一份行业报告,顺手分享给您,链接已私信。”他点开、登录、以为一切正常,第二天邮箱就开始向外发送他根本没写过的“内部通知”…

2026/10/21 15:29:47

Azure数据科学实战避坑指南:DP-100背后的工程决策逻辑

简介:本资源为微软DP-100认证考试(Designing and Implementing a Data Science Solution on Azure)官方级备考大纲PDF,面向Azure数据科学工程师、机器学习实践者及计划考取该认证的技术人员。内容覆盖考试全部核心能力域&#xff…

2026/10/21 15:29:47

DH-ITC602-CEVRB3A卡口电警实战施工指南

简介:本资源为DH-ITC602-CEVRB3A系列高清像机卡口式电警系统的标准化施工方案文档,面向智能交通领域工程设计、现场施工与系统运维技术人员,解决电子警察设备部署不规范、联调失败、防雷接地缺失及视频红灯识别误判等典型实施风险。文档完整覆…

2026/10/21 20:45:30

springboot整合Knife4j

一、前言 knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案(在非Java项目中也提供了前端UI的增强解决方案),前身是swagger-bootstrap-ui,取名knife4j是希望她能像一把匕首一样小巧,轻量,并且功能强悍! 二、快速使用 2.1 依赖…

2026/10/21 20:45:30

软件定义汽车---目的、实现途径与架构演进

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 假若你的生活不够好,不够努力,那么,加油努力吧,不要抱怨,起而行,迎头赶上,方是正途。假若你已经拥有很多,却依然活得不快乐,那么,让自己慢…

2026/10/21 20:45:30

接口超时之后,FDE为什么不能直接再试一次

FDE岗位实务指南 第18篇上一篇的四道检查管的是"动作发出之前"。这一篇处理动作发出之后的硬情况:请求发出去了,响应没回来。页面转圈,然后提示超时。此刻用户的直觉动作只有一个:再点一次。产品团队的直觉通常也一样&…

2026/10/21 20:45:30

Windows系统计时器处理

Windows系统计时器处理 时钟中断 时钟中断属于可屏蔽中断(INTR)的一种,可以被 CLI 延迟,也可以被中断控制器屏蔽。关中断期间,时钟中断会处于 pending 状态,等开中断后再处理。 时钟中断的产生 产生时钟中断…

2026/10/21 20:40:28

从Andorid攻城狮转Java后端开发(2)

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

2026/10/21 4:53:08

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/21 0:00:35

交通流仿真实战:IDM跟驰与MOBIL换道模型参数调优与避坑指南

简介:这份资源是面向交通工程学习者与Python开发者的微观交通流仿真项目,核心实现智能驾驶模型(IDM)下的车辆跟驰与换道行为,可用于理解速度、密度与流量之间的动态关系,适合具备Python基础、希望深入交通模…

2026/10/21 0:00:35

揭穿智能体表面顺从:用追踪代码重建任务执行确定性

1. 表面顺从不是智能体的“礼貌”,而是任务执行链路上的隐性断点最近在调试一个跨平台长周期任务调度系统时,我反复遇到一种令人困惑的现象:智能体在用户指令发出后,几乎秒级响应“已收到”“正在处理”“请稍候”,UI上…

2026/10/21 0:00:35

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事:AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里,按宣传口径,本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”,而是这话背后的含金量——它把过去只能…

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

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

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