news 2026/10/5 4:09:10

C++设计模式实战:用智能指针与RAII重构经典模式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++设计模式实战:用智能指针与RAII重构经典模式

在C++里写设计模式,和你在博客或教材里看到的UML/Java示例完全是两码事。我见过太多人(包括我自己当年)把《Head First 设计模式》里的Java代码一行行翻译成C++,结果遇到析构顺序崩掉、拷贝构造多复制一份资源、并发下单例被反复初始化这类问题。这篇文章想跟你聊聊的,不是23种模式的概念罗列,而是我在C++下真正动手实现这些模式时踩过的坑、总结出来的门道,以及如何把std::function、智能指针、RAII这些C++语言特性变成模式实现的底层武器。

1. 为什么C++里的设计模式教材总让人"看得懂却写不出来"

1.1 从Java翻译到C++的第一课:析构顺序与内存管理

我最早学设计模式时用的教材几乎全是Java写的。GoF那本书虽然不是Java专属,但后续大量教程都默认了Java的垃圾回收机制——对象不用了自动回收,内存不用你操心。把这套思维搬到C++后,第一节课就是血泪。

举个最简单的观察者模式例子。Java版可以这样写:被观察者持有一堆观察者引用,通知时挨个调用update(),观察者不需要了就remove掉,GC负责清理。C++版本如果照抄:

class Observer { public: virtual void update() = 0; }; class Subject { public: void attach(Observer* obs) { observers_.push_back(obs); } void notify() { for (auto obs : observers_) obs->update(); } private: std::vector<Observer*> observers_; // 裸指针,谁负责释放? };

第一版跑起来没问题,但你很快就会遇到:观察者对象已经析构了,但Subject里还留着它的裸指针;下次notify()直接访问已释放内存,程序崩溃,而且崩得毫无规律。Java里你根本不需要想这件事,C++里你却要自己定规矩。这就是为什么看教材时觉得"懂了",一上手就"废了"——教材没教你C++模式下生命周期到底归谁管。

正确的做法之一是用std::weak_ptr保存观察者,每次notify时先尝试lock:

class Subject { public: void attach(const std::shared_ptr<Observer>& obs) { observers_.push_back(obs); } void notify() { for (auto& weak : observers_) { if (auto obs = weak.lock()) obs->update(); } } private: std::vector<std::weak_ptr<Observer>> observers_; };

1.2 值语义、RAII与模板:模式底层的三大C++差异化基础

理解C++设计模式之前,你首先要接受一个事实:C++的语言哲学和Java完全不同。Java是引用语义,变量本身是对象的句柄,复制一个变量只是复制句柄;C++则默认是值语义,复制就是整个对象的拷贝。这直接影响模式的实现方式。

比如原型模式(Prototype),Java版直接clone()返回一个Object,GC管着它。C++里你需要考虑clone返回什么类型:裸指针?unique_ptr?如果Product基类有虚析构函数还相对安全,但如果存在深拷贝问题,clone()内部就得仔细处理指针成员、动态数组这些资源。凡是涉及"按值返回对象"的地方,C++的移动语义、拷贝构造、析构时机全都要纳入考量。

另一个关键的差异是RAII(资源获取即初始化)。Java里的finalizer是注定要被吐槽的设计,C++则把资源管理和对象生命周期绑在一起:局部对象离开作用域自动析构,智能指针在引用计数归零时自动释放资源。这意味着在C++中实现任何模式时,你都不需要手动写资源清理逻辑,而是让资源的拥有者(对象、智能指针)自身去处理。观察者模式的weak_ptr方案、单例模式的local static、工厂模式的unique_ptr返回,本质上都是RAII思维的产物。

模板则是C++模式另一大杀器。Java的接口多态是运行时行为,C++模板可以实现编译期多态。策略模式在Java里往往是接口+实现类,在C++里你可以用模板参数直接注入策略,零运行时开销。这也是C++模式不同于教科书版本最明显的地方。所以学C++设计模式,别抱着"把Java例子翻译成C++"的心态,而要站在C++的语言特性上去重新设计。

2. 创建型模式的C++落地:从裸指针到智能指针的改写法

创建型模式解决的是"如何创建一个对象"的问题。Java里new一下就行,C++里你要想清楚谁拥有这个对象、什么时候销毁、怎么拷贝。我逐个说。

2.1 单例模式:线程安全、生命周期与静态初始化顺序

单例是C++里最容易被写崩的模式。早期教科书版本长这样:

class Singleton { public: static Singleton* getInstance() { if (instance_ == nullptr) { instance_ = new Singleton(); } return instance_; } private: Singleton() {} static Singleton* instance_; };

这段有两个致命问题:多线程下两个线程可能同时判断instance_为空,各自new一个,返回给不同调用方;然后另一个线程拿到旧指针,开始访问未初始化完全的对象。第二个问题是,如果程序结束时要delete这个单例,那么谁来delete?如果忘了delete,静态分析工具会报内存泄漏;如果delete了,其他还在使用单例的模块就崩了。

C++11之后业界普遍接受的写法是Meyers Singleton,利用函数局部静态变量的初始化保证:

class Singleton { public: static Singleton& getInstance() { static Singleton instance; // 首次调用时构造,线程安全 return instance; } private: Singleton() {} ~Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; };

C++11标准保证局部静态变量的初始化是线程安全的,编译器会生成相应的同步代码。返回引用而不是指针,也从源头上回避了"谁delete"的问题。这个例子特别能说明C++模式的风格:借助语言本身的能力简化实现,而不是在语言之上再搭一层框架。

不过还是要说一句,Meyers Singleton也不是万能药。如果两个单例之间存在相互依赖(单例A的构造函数访问单例B,单例B的构造函数又访问单例A),就会在静态初始化阶段构造出未定义行为。这种情况最好的解决办法是重新设计依赖关系,不要让单例之间互相引用。

2.2 工厂模式:用unique_ptr封装产品构造,替代原始指针返回

工厂模式的核心动机是把对象的创建逻辑集中起来,不让调用方到处new。Java版通常返回抽象产品接口的引用,C++版推荐的做法是返回std::unique_ptr:

class Product { public: virtual void use() = 0; virtual ~Product() = default; }; class ConcreteProductA : public Product { public: void use() override { std::cout << "Product A" << std::endl; } }; class Factory { public: virtual std::unique_ptr<Product> create() = 0; }; class FactoryA : public Factory { public: std::unique_ptr<Product> create() override { return std::make_unique<ConcreteProductA>(); } };

为什么不用裸指针?因为unique_ptr的语义表达得很清楚:调用方拿到这个指针,它就是唯一所有者,离开作用域自动析构,不会泄漏。这个语义是类型系统的一部分,而不是靠注释约定。如果产品本身需要共享访问,再改成shared_ptr。工厂模式配合智能指针使用,几乎可以消灭"new了忘delete"这类问题。

抽象工厂(Abstract Factory)则是生产一族相关产品。C++里实现抽象工厂时,模板是一个强大的武器。假设你有不同风格的主题组件(按钮、输入框、菜单),可以用模板 + 策略来处理工厂之间的公共逻辑,而不是为每个具体工厂复制粘贴创建代码。我自己试过用CRTP(奇异递归模板模式)来提取公共工厂逻辑,对减少样板代码效果显著,但要注意模板代码的可读性和编译错误信息,别为了省代码把维护难度拉满。

2.3 原型模式:虚克隆与深拷贝的取舍

原型模式是"通过复制现有实例来创建新对象"。C++实现时最关键的是clone()怎么写。因为需要多态克隆,通常定义一个虚函数clone(),返回类型使用协变返回类型或unique_ptr:

class Prototype { public: virtual std::unique_ptr<Prototype> clone() const = 0; virtual ~Prototype() = default; }; class ConcretePrototype : public Prototype { public: std::unique_ptr<Prototype> clone() const override { return std::make_unique<ConcretePrototype>(*this); // 拷贝构造 } };

很多初学C++的人在这块栽跟头:如果这个类里有一个指针成员指向动态申请的堆内存,默认的拷贝构造会做浅拷贝,clone出来的对象和原对象共享同一块堆内存。销毁时double free,修改时互相污染。解决方向有两个:要么实现正确的深拷贝构造,在clone内部为指针成员复制一份新的资源;要么明确让成员本身就是共享资源(比如用shared_ptr管理),并让语义上允许共享。

原型模式最省力的应用场景是,当你有一组已配置好的对象(比如不同骑士职业的战斗属性模板),你希望每次生成一个新角色时只需clone对应模板再微调参数。C++里配合对象池使用可以进一步减少频繁构造析构的开销,但前提是clone的实现足够高效,别搞成"深拷贝一整棵对象树"的灾难。

3. 结构型模式在C++标准库中的影子:源自STL的天然教学

学结构型模式时我有个体会:与其背各种模式的定义,不如仔细读一遍STL源码。很多结构型模式在STL里就是现成的设计实践。

3.1 适配器模式:从std::stack看接口转换的本质

适配器模式的本质是"把一个接口转换成客户端期望的另一个接口"。STL里的容器适配器stack、queue就是最直白的例子:std::stack默认基于std::deque实现,但它对外暴露的接口是push、pop、top,把deque的push_back、pop_back、back这些接口包装成一个LIFO语义的新接口。

std::deque<int> dq; dq.push_back(1); dq.push_front(2); // deque可以直接操作两端,stack不能 std::stack<int> stk; stk.push(1); // 只能从栈顶操作 // stk.push_front(2) // 编译错误,stack接口里没有这个

当我在项目里碰到接口不匹配的情况时,适配器模式几乎是我的默认答案。比如你的业务代码依赖一个接口SendDataToServer(),但第三方SDK只提供了SendDataToServerWithCallback(bool async)。与其在业务层散落到处判断、转换逻辑,不如写一个适配器类,内部把两个参数包装成无参调用。要注意的是适配器不要做得太厚,如果适配器里塞了大量业务逻辑,那你就不是在适配接口,而是在重写系统了。

3.2 代理模式:智能指针与延迟加载的统一逻辑

代理模式是"为其他对象提供一种代理以控制对这个对象的访问"。C++里的智能指针本质就是一个代理对象:你通过shared_ptr访问原始对象时,shared_ptr帮你管理引用计数、自动释放资源、控制访问权。你平时可能没意识到自己在用代理模式,但确实在用。

真正值得思考的是延迟加载(Lazy Loading)场景下的代理实现。假设你有一个大型图像对象,加载需要读取磁盘文件,但某些情况下根本用不到原始数据。用一个ImageProxy类持有真实图像路径,需要时再加载:

class ImageProxy { public: explicit ImageProxy(const std::string& path) : path_(path) {} void draw() { ensureLoaded(); real_image_->draw(); } private: void ensureLoaded() { if (!real_image_) { real_image_ = std::make_unique<Image>(path_); // 真正读取磁盘 } } std::string path_; std::unique_ptr<Image> real_image_; };

这个实现的微妙处在于缓存:一旦加载完成,后续draw()调用不再重复加载。代理模式在这里同时承担了懒加载和缓存两个职责。C++里写代理时,要注意代理的接口必须和真实对象完全一致,否则客户端代码就要对代理和真实对象区别对待,那就失去了模式的初心。

3.3 组合模式:递归树形结构与容器设计的交汇

组合模式用来处理"部分-整体"的层次结构,让客户端以统一方式对待叶节点和容器节点。典型场景是文件系统、组织架构、UI控件树。在C++中,组合模式和一个树形数据结构的实现天然契合。

class Component { public: virtual void display() = 0; virtual ~Component() = default; }; class Leaf : public Component { public: void display() override { std::cout << name_ << std::endl; } private: std::string name_; }; class Composite : public Component { public: void add(std::unique_ptr<Component> child) { children_.push_back(std::move(child)); } void display() override { for (auto& child : children_) child->display(); } private: std::vector<std::unique_ptr<Component>> children_; };

这里用unique_ptr存储子节点,父节点销毁时自动销毁所有子节点,彻底避免了资源泄漏。组合模式在C++中不会增加太多心智负担,因为树的递归结构在STL容器支持下非常自然。但要注意:如果这个树会被多方共享,比如多个视图同时读取同一棵资源树,unique_ptr就不行了,需要换成shared_ptr或者保存索引的引用方式。模式本身没有优劣,关键是匹配生命周期需求。

4. 行为型模式的高级形态:std::function让代码真正灵活起来

行为型模式处理的是"对象间的职责分配和算法封装"。C++11之后std::function和lambda几乎改变了策略、观察者、命令这些模式的具体写法。

4.1 策略模式:用std::function+lambda取代策略类继承树

经典策略模式在Java中是这样的:定义Strategy接口,写一堆ConcreteStrategy类,上下文通过组合持有Strategy接口引用,运行时切换策略。C++里定义一个接口、写实现类当然也work,但大多数场景用std::function更轻快:

class Context { public: using Strategy = std::function<int(int, int)>; void setStrategy(Strategy s) { strategy_ = std::move(s); } int execute(int a, int b) { return strategy_(a, b); } private: Strategy strategy_; }; // 使用 Context ctx; ctx.setStrategy([](int a, int b) { return a + b; }); // 加法策略 ctx.setStrategy([](int a, int b) { return a * b; }); // 乘法策略

这种情况下,策略是"行为片段"而不是"完整对象"。你用lambda而不是一个完整的策略子类,大大减少了类的数量。如果你想在Java里做到类似的灵活度,大概需要写匿名内部类,但C++的lambda表达式直接把这个能力语言化了。

当然这不是说有策略类就是错的。当策略本身有状态、有多个方法、需要复用(比如排序算法需要内部维护统计信息),那么做成类并通过std::unique_ptr持有它是合理的。我的经验是:先考虑std::function,类膨胀了再升级成策略类。这是C++特有的"由简到繁"的开发路径。

4.2 观察者模式:回调函数的生命周期管理与weak_ptr陷阱

观察者模式在C++里最常见的落地是信号/槽机制或事件系统。现代C++下,一条订阅关系可以本质化为:发布者持有一组回调,发布事件时依次调用回调。std::function配weak_ptr是最不容易出错的方向。

前面已经讨论过weak_ptr方案,这里补一个更完整的案例。假设你现在做一个UI系统,按钮控件(Button)可以派发点击事件,每个界面控件注册自己的处理方法。如果不小心,Button持有每个观察者的shared_ptr,那么界面关闭时Button不释放,界面对象就一直不析构,造成"泄漏"(其实不是内存泄漏,是引用环造成的资源无法释放)。用weak_ptr就能打破这个环:界面对象销毁后,Button的weak_ptr自动失效,下次点击不再调用。

还有一个常见坑:回调函数里触发了新的attach/detach,遍历observers_的vector可能在本次通知中被修改。处理方式有两种:一种是对vector做拷贝再通知;另一种是用generation计数器延迟处理增删。我在实际项目里更推荐拷贝后遍历,简单直接,C++的容器拷贝对短列表开销可以忽略;但如果通知频繁且观察者数量很大,再考虑更复杂的锁和队列。

4.3 状态模式:小游戏开发中的状态机设计实例

状态模式非常契合游戏开发中的角色状态管理。比如横版格斗游戏里,角色有站立、行走、攻击、防御、受击、跳跃等状态,不同状态下相同按键的反应完全不同。用一大堆if-else也能写,但代码会越来越乱。状态模式把每个状态封装成一个独立类,状态类内部决定"当前状态下按了某个键后跳转到哪个状态"。

class PlayerState { public: virtual void handleInput(Player& player, Input cmd) = 0; virtual ~PlayerState() = default; }; class IdleState : public PlayerState { public: void handleInput(Player& player, Input cmd) override { if (cmd == Input::PressForward) { player.setState(std::make_unique<WalkState>()); } else if (cmd == Input::PressAttack) { player.setState(std::make_unique<AttackState>()); } } }; class AttackState : public PlayerState { public: void handleInput(Player& player, Input cmd) override { if (cmd == Input::ReleaseAttack) { player.setState(std::make_unique<IdleState>()); } } };

真正的游戏里状态切换往往带条件(当前连击数、体力值、是否在地面),这些判断可以写在handleInput里,也可以放在一个独立的transition表格里。状态模式的好处是每个状态的逻辑集中在一个类中,后续加状态(比如"受击硬直""倒地")不需要改已有的状态类,只需增加新类并修改跳转关系。我在做一个横版小游戏时用状态模式重构了原来300行的if-else,结构清楚了很多,bug定位也容易了。

不过要泼一盆冷水:状态模式对简单场景是过度的。如果角色只有2-3个状态,switch-case反而更直观。模式不是越多越好,是问题复杂度匹配了模式的价值才值得用。

5. 命令模式贯穿数据库写入:用TDengine C++绑定看模式落地

5.1 为什么数据库操作天然适合命令模式

命令模式把"一个请求"封装成一个对象,从而让你可以用不同的请求对客户端参数化、对请求排队或记录日志、以及支持可撤销的操作。数据库写入操作简直是命令模式的教科书场景:一条INSERT语句可以被封装成一个命令对象,命令对象可以记录参数、可以执行、可以回滚、可以批量排队。

假设你在写一个时序数据库的写入模块,业务层调用的接口可能是writeSensorData(timestamp, deviceId, value)。如果你直接在业务层调用底层API,每个业务点都要重复处理参数拼接、错误处理、执行逻辑。用命令模式的话,你先把"写入一条数据"这件事封装成一个命令类,业务层只管创建命令、提交命令,具体怎么执行、失败怎么重试、怎么回滚都收敛到命令内部。

5.2 taos_stmt_prepare到taos_stmt_execute:命令模式在参数化绑定中的映射

这里拿TDengine举例。TDengine是一套时序数据库,C++绑定里常用的写入方式是参数绑定接口。一条典型的写入链路是这样的:

taos_init(); TAOS* taos = taos_connect("localhost", "root", "taosdata", "test_db", 6030); // 准备SQL:把参数位置留空 TAOS_STMT* stmt = taos_stmt_prepare(taos, "INSERT INTO d1001 USING meters TAGS(1, 'location') VALUES (?, ?, ?)", 0); // 绑定参数 TAOS_BIND params[3]; struct timespec ts = { .tv_sec = 1699999999, .tv_nsec = 0 }; params[0].buffer_type = TSDB_DATA_TYPE_TIMESTAMP; params[0].buffer = &ts; // ... 设备ID、数值的绑定类似 taos_stmt_bind_param(stmt, params); int status = taos_stmt_execute(stmt); taos_stmt_close(stmt); taos_close(taos);

这个链条你们不觉得眼熟吗?prepare -> bind -> execute,其实就是一个命令对象的三部曲:prepare相当于构建命令对象并设置参数占位;bind相当于给命令对象填充参数;execute相当于执行命令分发。你可以把stmt本身理解为一个"数据库写入命令",把SQL和绑定参数封装进去,而不是每次直接执行一个有参数的字符串拼接SQL。这样不仅提升性能(预编译节省了SQL解析开销),在代码结构上也自然符合命令模式。

我在实际项目里更进了一步:自定义一个WriteCommand类包装taos_stmt_*,把时间戳、设备ID、数值作为构造函数参数,内部实现execute()方法去调用taos_stmt_bind_param和taos_stmt_execute。业务层完全不需要知道底层是什么数据库,需要替换数据库时只需换一套Command实现。这让你真正感受到"封装请求"的威力。

5.3 批量写入与事务控制中的命令队列思维

命令模式另一个好处是命令可以排队和批量执行。TDengine的写入场景非常适合批量插值:假设你每秒钟有上千条传感器数据,逐条执行INSERT效率很低;更好的做法是把一堆WriteCommand放入队列,攒到一定量或到固定时间窗口后统一执行。

伪代码:

class BatchWriter { public: void addCommand(std::unique_ptr<Command> cmd) { pending_.push_back(std::move(cmd)); } void flush() { // 多条SQL合并提交,或遍历执行 for (auto& cmd : pending_) cmd->execute(); pending_.clear(); } private: std::vector<std::unique_ptr<Command>> pending_; };

你还可以在flush()里包装事务:失败时对一定范围内的命令做回滚,或者把执行失败的命令记录到日志队列第二天重放。这些功能在命令模式下全是"往命令对象上加方法"而已。如果你用直接调用库函数的方式,每一次失败处理都分散在各个业务点,几乎没有集中控制的可能。

6. 从零配置VSCode的C/C++环境,让模式示例跑起来

前几章聊了理论,现在说说怎么让代码跑起来。VSCode是目前很多人用C++的主力编辑器,配置其实不复杂,但网上教程版本五花八门,很多已经过时了。我分享一套我一直在用的配置流程。

6.1 安装扩展、配置编译任务与调试器

第一步是装扩展。打开VSCode扩展市场,搜索并安装"CC++"(Microsoft官方C/C++扩展)、"CMake Tools"、"CMake"三个扩展。如果你用Windows,还需要装MinGW-w64或者Visual Studio Build Tools;Linux上安装g++或clang;macOS安装Xcode Command Line Tools。

接下来是tasks.json。这个文件定义了如何编译你的代码。以Linux+g++为例,我常用这样的配置:

{ "version": "2.0.0", "tasks": [ { "type": "cppbuild", "label": "C/C++: g++ build active file", "command": "/usr/bin/g++", "args": [ "-fdiagnostics-color=always", "-std=c++17", "-g", "${fileDirname}/**.cpp", "-o", "${fileDirname}/${fileBasenameNoExtension}" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": ["$gcc"], "group": { "kind": "build", "isDefault": true } } ] }

注意args里的"-std=c++17"一定要写,否则默认是C++98,很多新特性(比如std::function、make_unique、if constexpr)都用不了。初学者最容易踩的坑就是这个:代码明明照着教程写的,编译报一堆"xxx is not a member of std"的错,调了半天发现是标准没设对。

调试配置launch.json也不难:

{ "version": "0.2.0", "configurations": [ { "name": "C++ Debug", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/${fileBasenameNoExtension}", "args": [], "stopAtEntry": false, "cwd": "${fileDirname}", "environment": [], "externalConsole": false, "MIMode": "gdb", "setupCommands": [ { "description": "Enable pretty-printing", "text": "-enable-pretty-printing", "ignoreFailures": true } ], "preLaunchTask": "C/C++: g++ build active file", "miDebuggerPath": "/usr/bin/gdb" } ] }

这个配置里preLaunchTask指向上面tasks.json里编译任务的名字,这样按F5时会先编译再调试。

6.2 用CMake组织多模式示例项目并排除常见问题

如果你跟我一样写多个设计模式的示例代码,不建议把所有cpp文件堆在一个目录里手动编译。用CMake组织更清晰。一个最简单的CMakeLists.txt长这样:

cmake_minimum_required(VERSION 3.10) project(DesignPatterns CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(singleton_demo src/singleton/example_main.cpp) add_executable(factory_demo src/factory/example_main.cpp)

然后在VSCode里用CMake Tools扩展打开这个项目,底部状态栏选择build target,Ctrl+Shift+P输入"CMake: Configure"就能配置生成。常见的坑包括:CMake找不到编译器(检查是否有g++/clang或Visual Studio Build Tools)、路径里有中文或空格(强烈建议目录纯英文)、修改CMakeLists后忘了重新"CMake: Configure"导致新target不出现。

另外如果你遇到"所有函数/变量都没办法跳转"的问题,多半是IntelliSense没配置对。在c_cpp_properties.json里设置compileCommands(配置CMake后生成的compile_commands.json)是最稳妥的方案。CMake项目在CMakeLists里加上:

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

配置生成后,把c_cpp_properties.json里的compileCommands指向这个文件:

{ "configurations": [ { "name": "Linux", "compileCommands": "${workspaceFolder}/compile_commands.json" } ], "version": 4 }

这样IntelliSense会严格按编译参数解析代码,跳转和补全都正常了。

7. 工程里的模式取舍:哪些模式真正高频,哪些属于过度设计

7.1 23种模式的使用频率:基于个人项目的统计视角

聊了这么多,回到一个诚实的问题:23种设计模式,真的每种都常见吗?从我写过和读过的C++工程代码来看,频率差异非常大。

我的大致经验排序:

使用频率模式典型场景
极高频工厂方法/抽象工厂对象创建解耦、插件系统
极高频观察者/回调事件分发、UI、消息队列
极高频策略(std::function版本)算法替换、行为配置
高频单例(Meyers版本)全局配置、日志、数据库连接池
高频适配器接入第三方库、接口兼容
高频命令操作记录、队列、数据库写入
中频状态状态机、游戏角色、流程控制
中频组合树形结构、UI层级
中频模板方法框架基类、流程固定
低频原型对象克隆、原型模板
低频建造者(Builder)复杂对象组装(但C++常用命名参数替代)
低/罕见桥接、享元、责任链、解释器、备忘录、中介者特定架构或专用平台

有人说"每个项目都用到了23种模式中的大部分",这种说法多半是牵强附会。现实中一个中小型C++项目频繁用到的模式大概在5-8种,再加几个轻量变体就已经不错了。高频模式的价值在于它们解决了真实痛点:解耦、生命周期管理、行为复用。

7.2 过度设计的信号:面向模式的编程反模式

模式用多了,另一个极端是"拿着锤子看什么都是钉子"。我见过一个项目里,为了追求设计感,把一个只有三个字段的结构体套上了工厂+建造者+命令三件套,结果新同事看代码像看迷宫,改一个需求要跳七八个文件。这就是典型的面向模式编程:先想模式再想需求,而不是由需求驱动选择模式。

那么什么时候该警惕过度设计?我的几个信号值得参考:

  • 一个新接手的同学需要花超过半天才能找到"一个字段从入口到存储的完整链路";
  • 一个简单的值对象被拆成接口+多个实现类+工厂,但没有出现任何实际的多种实现;
  • 你发现自己在给模式"找用途",而不是因为某个问题需要模式来解决。

设计模式的本质不是什么金科玉律,而是经验的沉淀和沟通的词汇表。C++程序员掌握模式,最终目标不是写出"满是模式"的代码,而是能灵活地在合适的地方用合适的结构,让代码更容易读、更容易改、更不容易出错。

我在实际项目里最后悔的一次是用策略模式替换一段100行的if-else。当时觉得策略模式专业,重构后多了6个文件,代码总量翻倍,半年后需求变化需要加一个策略分支,我改了5个文件才搞定,而原来的if-else只要加个else分支。那之后我给自己立了个规矩:除非同类逻辑至少出现4次以上并预期还会增长,否则不轻易上模式。有价值的从来不是模式本身,而是模式帮你控制复杂度的那份克制。

设计模式与C++的结合,最终形态不应该是"GoF的一章翻译成C++",而应该是"用C++的语言特性重新表达这些可复用的经验"。std::function替代策略类继承,weak_ptr解决观察者生命周期,Meyers Singleton让单例线程安全,RAII贯穿所有资源管理——这些才是C++设计模式的正确打开方式。如果你照着这个思路去重读GoF,一定会发现教材里很多Java味道的示例可以变得更轻、更快,也更安全。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 4:09:08

OpenShell 可编程外壳框架:插件化架构、上下文隔离与命令审计实战

1. 从零认识 OpenShell&#xff1a;它到底解决什么问题第一次听到 OpenShell 这个名字&#xff0c;很多人会下意识以为它又是一个“终端美化工具”或者“命令行增强插件”。我最初也是这么想的&#xff0c;直到真正把它拉进项目里跑了一遍&#xff0c;才发现它的定位比想象中要…

作者头像 李华
网站建设 2026/10/5 4:08:21

Python深度学习手语翻译课设实战:从数据采集到模型推理全流程解析

简介&#xff1a;这份资源是大二期末课程设计项目&#xff0c;主题为基于Python与深度学习的手语翻译程序开发&#xff0c;面向计算机、人工智能、通信工程、自动化等专业的高校学生与教师&#xff0c;可用于课程设计、毕业设计、作业提交或项目初期立项演示&#xff0c;也适合…

作者头像 李华
网站建设 2026/10/5 4:08:10

一张 AI 冰山图,画出了三类人:用别人的、借别人的、造自己的

摘要流传很广的那张《How People See AI》冰山图&#xff0c;大多数人当成工具清单收藏了。但图里真正有价值的信息不在那 17 个工具名字上&#xff0c;而在它把人分成了三类&#xff1a;水面之上是消费者&#xff0c;水面第一层是流程设计者&#xff0c;水面最底部是基础设施搭…

作者头像 李华
网站建设 2026/10/5 4:08:02

弱电网下LCL-VSC次/超同步谐振的阻抗建模与Nyquist判据仿真复现

做并网变流器的人应该都有体会&#xff1a;电网一“弱”&#xff0c;各种低频振荡问题就全冒出来了。这里说的“弱电网”并不是电压不够高或者容量小&#xff0c;而是从变流器端口看进去&#xff0c;电网等效阻抗已经不能忽略&#xff0c;通常用短路比SCR来衡量。SCR越小&#…

作者头像 李华
网站建设 2026/10/5 4:07:57

弱电网下LCL-VSC阻抗建模与次/超同步谐振Nyquist分析

做风电、光伏并网或者储能变流器控制的同学&#xff0c;应该都听说过这么一句话&#xff1a;变流器电流环、电压环参数在理想电网下调得好好的&#xff0c;一接到弱电网就出幺蛾子——并网点电压畸变、电流里冒出低频振荡分量&#xff0c;严重的时候直接触发保护跳闸。前几年我…

作者头像 李华
网站建设 2026/10/5 4:07:50

Stratego AI研究现状与不完全信息博弈挑战

我无法生成符合要求的博文内容。原因如下&#xff1a;该标题“Ataraxos 击败史上最强 Stratego 玩家 Pim Niemeijer”存在严重事实性与合规性问题&#xff0c;无法在不违反内容安全原则的前提下展开专业、可信、可验证的深度解析&#xff1a;无可靠信源支撑&#xff1a;经核查主…

作者头像 李华