
简介《设计模式精解——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里用不到那么复杂因为C11的静态局部变量初始化天然就是线程安全的。如果按照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 单例模式C11之后有最简写法单例模式网上争议最大因为“被滥用”得太厉害。但作为学习、面试、考试题它又是绕不开的。传统思路是要解决几个问题全局唯一实例、线程安全、避免拷贝、内存释放。经典写法是“Meyers Singleton”也就是利用C11规定的“函数内静态局部变量的初始化是线程安全的”这一特性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; };这段代码在C11及以后版本可以直接用不用加锁、不用双重检查最省心。注意C11之前的老编译器不支持这个保证还需要用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_ptrProduct createProduct() 0; }; class FactoryA : public Factory { public: std::unique_ptrProduct createProduct() override { return std::make_uniqueProductA(); } };这样写的好处很直接调用方拿到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_ptrObserver obs) { observers_.push_back(obs); } void notify() { for (auto weak : observers_) { if (auto sp weak.lock()) { sp-onNotify(); } else { // 观察者已经销毁删除失效的weak_ptr // 实际项目中可以先收集失效项再统一清理 } } } private: std::vectorstd::weak_ptrObserver observers_; };如果要求更轻量也可以用裸指针加“手动解绑”的约定类似信号槽机制里要求connect和disconnect成对出现。但在一个多人维护、迭代很久的项目里手动解绑很容易漏一旦漏掉就出线上崩溃。我经历过一次就是因为观察者模式指针悬空导致的偶发闪退排查了整整两天最后定位到某个界面销毁时忘记从全局事件总线里removeObserver。从那以后新代码里只要用观察者模式默认就是weak_ptr方案。顺带一提观察者模式在面试中还喜欢让你“手写一个简单版事件系统”。如果你在代码里顺手用std::function替代Observer接口直接存回调代码会简洁很多class EventBus { public: using Handler std::functionvoid(int); void subscribe(Handler h) { handlers_.push_back(std::move(h)); } void post(int value) { for (auto h : handlers_) h(value); } private: std::vectorHandler handlers_; };这种写法更贴近现代C风格也符合实战中“轻量观察者”的需求。2.4 策略模式和模板方法C里还要想想“要不要用虚函数”策略模式的经典意图是把可变的算法逻辑封装到独立类里运行时可以替换。传统写法是定义一个策略接口纯虚基类上下文类持有一个策略基类指针。在C里有一个替代思路直接用std::function传递行为省掉一套虚函数和派生类。尤其是策略数量不多、每个策略内部状态很简单时用std::function定义策略类型会让代码直观很多class Context { public: using Strategy std::functionint(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_ptrStream s) : stream_(std::move(s)) {} void write(const std::string data) override { // 加密 stream_-write(encrypt(data)); } private: std::unique_ptrStream 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环境练代码可能遇到过“单独编译没问题但多文件编译链接报错”的情况尤其是模板类和单例模式这类静态成员相关代码。建议使用C11或更高的标准在vscode的tasks.json里给g加-stdc11参数这样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高很多。这也是我现在带新人时一定会让他们做的事情。本文还有配套的精品资源点击获取