简介:《设计模式精解——GoF 23种设计模式解析附C++实现源码》是一份系统讲解经典设计模式的PDF电子书,面向C++开发者和希望提升软件架构能力的中级程序员。全书按创建型、结构型、行为型三大类别组织,涵盖工厂、抽象工厂、单例、建造者、原型、桥接、适配器、装饰、组合等23种模式,每个模式均给出定义、用途、C++实现代码以及在实际项目中的适用场景。此外还讨论了模式间的相互关系与选择思路,帮助读者建立完整的设计模式知识体系。资源为单个PDF文件,体积仅2.52MB,内容精简便于离线阅读和反复研读,目录层级清楚,可按需跳转。目前已有1706人学习浏览,是系统学习设计模式的高性价比资料,有助于开发者设计出高质量、易维护、易扩展的软件系统。 最近后台收到好几条私信,都是同一个主题:设计模式(C++)。有人是期末复习,对着23种模式的名字背了忘、忘了背;有人是面试前刷“C++八股文”,想知道设计模式到底会被怎么问;还有人手里就攥着一份《23种设计模式(C++).pdf》,看是看完了,但遇到实际需求还是不知道怎么下手。
这份PDF我太熟了,几乎每个学C++的人手头都有过这么一份。很多人的问题不是资料不够好,而是打开方式不对——把它当成小说从头读到尾,读完脑子里只剩几个名词。这篇博文我就从这份PDF出发,用我实际学习和使用的经验,把整个设计模式在C++这条路上的关键点捋一遍。哪些模式值得背代码、哪些模式只需要理解思想、哪些模式在C++里有专属的写法,都会讲到,顺便把面试和作业里容易踩的坑也一并说掉。
1. 23种设计模式在C++语境下的真正价值
1.1 为什么C++学设计模式比Java更“难受”也更有用
Design Patterns这本书(也就是“GoF书”)最初用C++和Smalltalk做示例,但在国内技术圈,设计模式的资料大部分是Java系的,类图、概念、例子全都是Java那一套。C++学习者看的时候会有一种很明显的割裂感。
C++有值语义(value semantics)、栈上对象、RAII、模板等多范式特性,导致同样的模式在两种语言里落地方式差异极大。比如单例模式,Java里经典的懒汉式“双重检查锁”在C++里用不到那么复杂,因为C++11的静态局部变量初始化天然就是线程安全的。如果按照Java资料里的写法去套C++,往往写出又长又容易出错的代码。
但反过来,正因为C++没有垃圾回收,内存管理的责任全在程序员自己身上,设计模式里那些对象创建、组合、生命周期控制的思想反而体会得更深。Java里new一个对象不用管释放,而C++里每次new都要想清楚“谁负责delete”。理解了这一点,再去看工厂模式、建造者模式、原型模式,你看到的就不是“如何创建对象”,而是“谁拥有对象、何时销毁对象”。这份PDF如果只当字典翻,就太可惜了。
1.2 从“背名字”到“建体系”:正确拆解23种的思路
先看分类,23种设计模式分为三类:创建型(Creational)、结构型(Structural)、行为型(Behavioral)。这三类名称本身就是在回答不同的问题:
- 创建型:怎么new对象才能不写死,让代码更灵活?
- 结构型:怎么组合类和对象,让大架构更清晰、复用性更好?
- 行为型:怎么分配职责、怎么交互,让对象之间的通信更优雅?
这个分类背下来不难,但需要在心里形成一种“解决问题的工具箱思维”。遇到一个需求,先问自己:是对象创建太僵硬?还是类和类之间的关系太乱?还是对象之间的交互逻辑太绕?对应去翻类别,再去挑选具体模式。
在学习顺序上,我的建议是分梯队掌握,而不是从第1个读到第23个:
- 第一梯队(高频、必考必用):单例、工厂方法、抽象工厂、策略、观察者、模板方法、装饰器、适配器。这8个模式是最常出现在代码和面试里的,务必能做到不看资料手写关键结构。
- 第二梯队(理解思想、能画类图):建造者、原型、组合、外观、代理、迭代器、状态、命令、责任链。
- 第三梯队(低频,但开阔视野):享元、桥接、中介者、备忘录、解释器、访问者。
建好体系之后,再回头看PDF,就会觉得它不是23个孤立知识点,而是一张可以随时查的“处方”。
2. 几个C++特有难点的模式拆解与实操
2.1 单例模式:C++11之后有最简写法
单例模式网上争议最大,因为“被滥用”得太厉害。但作为学习、面试、考试题,它又是绕不开的。传统思路是要解决几个问题:全局唯一实例、线程安全、避免拷贝、内存释放。
经典写法是“Meyers' Singleton”,也就是利用C++11规定的“函数内静态局部变量的初始化是线程安全的”这一特性:
class Singleton { public: static Singleton& getInstance() { static Singleton instance; return instance; } // 删除拷贝 Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; private: Singleton() = default; ~Singleton() = default; };这段代码在C++11及以后版本可以直接用,不用加锁、不用双重检查,最省心。注意C++11之前的老编译器不支持这个保证,还需要用pthread_once或std::call_once来实现。
实际项目中我做过一次重构:把某个全局配置文件类改成单例,最初写的是饿汉式——直接new一个static对象。后来迁移到移动平台,发现启动时间多了几十毫秒,一看是因为程序启动就要构造配置类,里面调用了不少文件IO。改回局部静态变量的懒汉式之后,只有真正第一次访问getInstance时才触发构造,启动时间降下来了。这就是“创建时机”的一个真实案例。
2.2 工厂系列:C++里要重点解决“谁来释放”
工厂方法模式(虚函数创建产品)和抽象工厂模式(创建一族产品)在面试里几乎是必问。很多资料给的是Java代码,new完直接返回,C++学习者照着写就会遇到内存管理问题。
一个比较安全的写法是返回智能指针,比如用std::unique_ptr或std::shared_ptr作为工厂方法的返回值:
class Product { public: virtual void use() = 0; virtual ~Product() = default; }; class ProductA : public Product { public: void use() override { /* ... */ } }; class Factory { public: virtual std::unique_ptr<Product> createProduct() = 0; }; class FactoryA : public Factory { public: std::unique_ptr<Product> createProduct() override { return std::make_unique<ProductA>(); } };这样写的好处很直接:调用方拿到unique_ptr之后不用担心delete,也不用重写拷贝逻辑,生命周期在离开作用域时自动结束。我在实际维护老代码时经常见到裸指针满天飞的工厂,析构函数里要手动判断、手动delete,代码又碎又容易漏。换成智能指针之后,工厂类的析构函数几乎不用写。
还有一个容易被忽略的点:工厂方法模式中的抽象基类析构函数必须声明为virtual。因为基类指针指向派生类对象时,如果析构函数不是虚的,delete时只会调用基类析构,派生类资源就不会被释放,这就是未定义行为。面试里如果提到工厂模式,十有八九会被追问“析构函数为什么要是虚的”,这个点需要提前准备。
2.3 观察者模式:管理好观察者的生命周期
观察者模式在C++里比在Java里更考验内存安全功底。核心场景是:一个主题对象维护一个观察者列表,状态变化时通知所有观察者。
Java里的Observer接口和observable类是JDK自带的,但在C++里完全没有标准库支持,就需要自己实现。最容易踩的坑是观察者生命周期已结束,但主题对象还持有它的指针,一旦触发通知就变成悬空指针,程序直接崩溃。
解决方案有几种。推荐是用std::weak_ptr来保存观察者列表:
class Observer { public: virtual ~Observer() = default; virtual void onNotify() = 0; }; class Subject { public: void attach(const std::shared_ptr<Observer>& obs) { observers_.push_back(obs); } void notify() { for (auto& weak : observers_) { if (auto sp = weak.lock()) { sp->onNotify(); } else { // 观察者已经销毁,删除失效的weak_ptr // 实际项目中可以先收集失效项,再统一清理 } } } private: std::vector<std::weak_ptr<Observer>> observers_; };如果要求更轻量,也可以用裸指针加“手动解绑”的约定,类似信号槽机制里要求connect和disconnect成对出现。但在一个多人维护、迭代很久的项目里,手动解绑很容易漏,一旦漏掉就出线上崩溃。我经历过一次就是因为观察者模式指针悬空导致的偶发闪退,排查了整整两天,最后定位到某个界面销毁时忘记从全局事件总线里removeObserver。从那以后,新代码里只要用观察者模式,默认就是weak_ptr方案。
顺带一提,观察者模式在面试中还喜欢让你“手写一个简单版事件系统”。如果你在代码里顺手用std::function替代Observer接口,直接存回调,代码会简洁很多:
class EventBus { public: using Handler = std::function<void(int)>; void subscribe(Handler h) { handlers_.push_back(std::move(h)); } void post(int value) { for (auto& h : handlers_) h(value); } private: std::vector<Handler> handlers_; };这种写法更贴近现代C++风格,也符合实战中“轻量观察者”的需求。
2.4 策略模式和模板方法:C++里还要想想“要不要用虚函数”
策略模式的经典意图是把可变的算法逻辑封装到独立类里,运行时可以替换。传统写法是定义一个策略接口(纯虚基类),上下文类持有一个策略基类指针。
在C++里有一个替代思路:直接用std::function传递行为,省掉一套虚函数和派生类。尤其是策略数量不多、每个策略内部状态很简单时,用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; });但这里有个取舍需要意识到:std::function会有一次间接调用开销,而且捕获lambda时可能会触发堆分配。性能极其敏感的场景,比如高频交易系统里的撮合策略,可能还是虚函数方案更可控。普通的业务代码里,std::function写法效率足够,代码更短。面试中如果能把这两种写法的权衡讲出来,是一个明显的加分项。
模板方法模式则很有意思,它和策略模式正好对应“继承vs组合”两种思路。模板方法把算法骨架放在基类里,把可变步骤设计成虚函数,让子类重写。使用时要特别注意:如果骨架里有“步骤A后必须步骤B”这类顺序要求,应该写成非虚的模板方法,把顺序控制权收在基类里,而不是暴露给子类自由调用。这个如果用错了,子类重写不当,算法顺序就被破坏了,这种bug非常隐蔽。C++的final关键字可以用来防止子类重写模板方法本身,是很好的防御性编程习惯。
3. 学习路径:从读PDF到纸上代码到面试对答
3.1 单张类图驱动的“代码复现”练习法
很多人看PDF的模式,每个都看懂了,但关上文件就写不出来,原因在于“看懂”是浅层理解,手写才是深层理解。
我的方法是:每个模式只看它的UML类图,然后把类图挡住,用自己的话把类图“翻译”成C++代码。比如装饰器模式,类图核心就是:装饰器和被装饰对象继承同一个接口,装饰器内部持有该接口的引用/指针,同时增强其行为。理解到这一步,自己就可以写出来:
class Stream { public: virtual void write(const std::string& data) = 0; virtual ~Stream() = default; }; class FileStream : public Stream { public: void write(const std::string& data) override { // 写文件 } }; class EncryptedStream : public Stream { public: explicit EncryptedStream(std::unique_ptr<Stream> s) : stream_(std::move(s)) {} void write(const std::string& data) override { // 加密 stream_->write(encrypt(data)); } private: std::unique_ptr<Stream> stream_; };用这种方式,把第一梯队8个模式都自己实现一遍,花不了太多时间,但收获比翻十遍PDF都大。
还有一点,代码里一定要给每个类注释它是“Component”、“ConcreteComponent”、“Decorator”还是“DecoratorA”,这样能加强模式结构在脑中的锚定。考试大作业和面试手写时,命名能直接反映出你对模式结构的理解,老师或面试官看到清晰的映射关系会非常认可。
3.2 “八股文”式面试:设计模式常考的4个方向
结合最近大家在热搜里关心的“C++八股文”“C++面试题”,我总结一下设计模式在面试中常见的考法,希望对准备换工作的朋友有帮助:
- 第一个方向是“讲一个你项目中用到的设计模式”。这题不是白给的,面试官想听的是真实使用场景。此前遇到过候选人背了一堆模式,但问他“你们项目里观察者模式用在哪”,他想了半天说“我们好像没用到”。这是扣分很严重的。正确的准备方式是把简历上某一个具体的模块和某个模式对应起来,能说清楚:当时为什么不用if-else直接写?用了模式之后解决了什么痛点?
- 第二个方向是“对比两个相关模式”。比如策略vs状态、装饰器vs继承、工厂方法vs抽象工厂。这些对比题考察的是你对模式适用边界的理解,背定义没有用,要能举出具体的反例。
- 第三个方向是“手写代码时追问边界条件”。比如单例模式写完,问“线程安全吗”“能拷贝吗”“能不能延迟加载”;观察者模式写完,问“观察者死了怎么办”;工厂模式写完,问“谁来释放对象”。这些追问就是上面代码块里提到的那些细节,面试前务必逐一过一遍。
- 第四个方向是“识别坏味道并改造成模式”。面试官会给一段满是if-else的代码,问你会怎么重构。这种题没有标准答案,但在C++语境下,用策略模式重写switch-case、用工厂模式收敛对象创建、用观察者模式解耦消息分发,都是非常高频的解题路径。
3.3 应对设计模式大作业和期末:怎么在短时间内交付高质量成果
不少读者留言提到“设计模式大作业”、“设计模式期末”,这里我专门给一个操作建议。如果时间紧张,不要追求把23种模式全用在一个项目里,那样项目会异常臃肿,答辩时反而说不清楚。
更聪明的策略是:做一个中等规模的小系统(比如学生选课系统、图书馆管理系统、简单游戏引擎),在其中自然、合理地应用6-8个模式,并且每个模式都加上“如果不用这个模式会怎样”的分析。打分老师最看重的是:你是否真的理解了每个模式的存在意义。这比把所有模式硬塞进一个项目里高好几个档次。
我曾经帮一个学弟改过他的设计模式大作业,他把抽象工厂、建造者、单例、观察者、命令、状态6个模式揉进了一个“智能家居控制系统”,码量不大,但每一处模式使用都贴合实际需求。答辩时老师问“为什么状态机状态用状态模式而不用if-else”,他直接现场画了类图说明状态变更逻辑。最后拿了很高分。这个思路应该也能直接用在你的大作业上。
4. 常见问题与避坑技巧实录
4.1 我在实际项目中踩过的设计模式相关的坑
第一个坑:把策略模式和状态模式搞混。刚开始工作时接了一个播放器模块,状态有播放、暂停、停止、缓冲。我一开始用策略模式设计,把每个行为封装成一个策略。结果发现状态之间有转移关系(播放->暂停、暂停->停止),而策略模式完全不关心策略之间怎么切换。后来改成状态模式,把每个状态作为对象,状态对象持有对上下文对象的引用,才能在状态内部触发状态转移。这两种模式类图结构很像,但意图完全不同:策略是“算法可替换”,状态是“行为随内部状态变化”。
第二个坑:单例模式被滥用导致代码难以测试。早期写代码时喜欢把所有“工具类”都写成Singleton,日志、配置、缓存、工具函数全是getInstance()。后来发现单元测试没法做,因为单例对象在测试之间共享状态,测试用例相互污染。现在我的准则是:只有真正需要“全局唯一且必须有状态”的对象才用单例模式,比如配置中心、日志器;而只用static方法就能完成的功能,定义为普通类加静态函数就行,不需要单例。
第三个坑:装饰器模式重写了基类的所有方法,但容易漏掉一部分。比如包装一个网络流,我在装饰器里只重写了Read和Write,忘了还有Close和Flush要透传。结果上层调用Close时直接调到了被包装对象的Close,装饰器里的缓存没有刷新,丢了一部分数据。这个问题在面试里可以主动提出来,算是展示“我会考虑边界情况”的加分点。
4.2 C++版本和编译环境常见问题
如果你正在vscode里配C/C++环境练代码,可能遇到过“单独编译没问题,但多文件编译链接报错”的情况,尤其是模板类和单例模式这类静态成员相关代码。建议:
- 使用C++11或更高的标准(在vscode的tasks.json里给g++加
-std=c++11参数),这样Meyers单例才是线程安全的; - 多文件项目记得把所有.cpp文件加入到编译命令中,只编译一个文件会导致模板实例化失败或未定义引用;
- 如果用了抽象基类和纯虚函数,base类析构函数务必声明为
virtual,否则delete basePtr时会出现未定义行为。
如果编译时看到类似“undefined reference to vtable”的报错,一般都是某个类有虚函数,但虚函数没有被定义,或者对应的.cpp文件没参与链接。这类问题在实际开发里也不少见,特别是写模式相关代码时类特别多,很容易漏掉某个函数的定义。我的排查习惯是先把所有纯虚函数和虚函数列表打出来,和.cpp里的定义逐个对照。
4.3 C++设计模式学习的“性价比”排序
同样花了时间,学习不同模式的回报是不一样的。根据我自己的经验和面试复盘,如果是准备面试或应对期末,参考这个顺序:
第一优先:单例、工厂方法、抽象工厂、策略、观察者、模板方法、装饰器、适配器。这些是出现频率最高的。第二优先:建造者、原型、组合、外观、代理、状态、命令、迭代器、责任链。这些也常在项目中出现,问起来能说个大概。第三优先:桥接、享元、中介者、备忘录、解释器、访问者。这些模式要理解适用场景,面试很少要求手写,但偶然会考察概念辨识。
需要特别说明一下,这个排序不是贬低某些模式的价值,而是从“初学者投资回报率”角度给的顺序。像解释器模式在语言解析领域非常经典,访问者模式在编译器实现里也是一等一的好用,但如果你只是做一个课设级别的项目,确实不容易用到,为它们投入过多时间反而不划算。
4.4 关于“要不要背代码”的一点坦白话
关于设计模式的学习,市面上有一种声音说“千万别背代码”,但我个人的经验是:关键模式的骨架代码,背下来并无坏处。因为背下来的不是死板的实现,而是模式的结构模板。就像作文需要背名言警句一样,脑子里有足够的“结构样本”,遇到问题时才能自然联想到“这里好像挺适合用观察者”。
但同时必须明确:背是为了“用的时候能想起来”,而不是为了“将来照抄”。真正上手一个新项目时,每个模式都要根据具体场景调整,甚至可以不拘泥于GoF的经典类图。比如现在C++里有了std::function,很多行为型模式可以用更轻量的方式实现;而有了智能指针,创建型模式也完全不用像老代码那样裸指针满天飞。
5. 还想再多说两句
这份《23种设计模式(C++)》PDF,我是在大三的时候第一次完整啃下来的。当时边读边把每一个模式都手写了一遍demo,写完感觉“C++功力大涨”;但真正理解这些模式,反而是在工作之后的第三个项目里,我被满屏if-else折磨到不行,顺手抽出PDF翻到策略模式那一页,才忽然明白了为什么当年要学它。
如果你正对着这份PDF发愁,我的建议很朴素:别只读,要动手写代码,写不出来就去对照类图;别只写Demo,要把模式代入到具体业务场景里思考“它会解决什么痛点”;也别贪多,把第一梯队掌握扎实,就已经比大多数停留在“认识名字”状态的人强太多了。等手头的项目里因为一次重构而真正用到某个模式时,你就知道这玩意儿值不值得学了。
最后分享一个小技巧:可以在VSCode里把这份PDF的目录做成Markdown笔记,每学完一个模式就更新笔记,类图画一遍、关键代码贴一份、适用场景和坑位各写一行。后面复习或面试前,翻这份笔记的效率比翻原PDF高很多。这也是我现在带新人时一定会让他们做的事情。
本文还有配套的精品资源,点击获取