
在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::vectorObserver* observers_; // 裸指针谁负责释放 };第一版跑起来没问题但你很快就会遇到观察者对象已经析构了但Subject里还留着它的裸指针下次notify()直接访问已释放内存程序崩溃而且崩得毫无规律。Java里你根本不需要想这件事C里你却要自己定规矩。这就是为什么看教材时觉得懂了一上手就废了——教材没教你C模式下生命周期到底归谁管。正确的做法之一是用std::weak_ptr保存观察者每次notify时先尝试lockclass Subject { public: void attach(const std::shared_ptrObserver obs) { observers_.push_back(obs); } void notify() { for (auto weak : observers_) { if (auto obs weak.lock()) obs-update(); } } private: std::vectorstd::weak_ptrObserver observers_; };1.2 值语义、RAII与模板模式底层的三大C差异化基础理解C设计模式之前你首先要接受一个事实C的语言哲学和Java完全不同。Java是引用语义变量本身是对象的句柄复制一个变量只是复制句柄C则默认是值语义复制就是整个对象的拷贝。这直接影响模式的实现方式。比如原型模式PrototypeJava版直接clone()返回一个ObjectGC管着它。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了其他还在使用单例的模块就崩了。C11之后业界普遍接受的写法是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; };C11标准保证局部静态变量的初始化是线程安全的编译器会生成相应的同步代码。返回引用而不是指针也从源头上回避了谁delete的问题。这个例子特别能说明C模式的风格借助语言本身的能力简化实现而不是在语言之上再搭一层框架。不过还是要说一句Meyers Singleton也不是万能药。如果两个单例之间存在相互依赖单例A的构造函数访问单例B单例B的构造函数又访问单例A就会在静态初始化阶段构造出未定义行为。这种情况最好的解决办法是重新设计依赖关系不要让单例之间互相引用。2.2 工厂模式用unique_ptr封装产品构造替代原始指针返回工厂模式的核心动机是把对象的创建逻辑集中起来不让调用方到处new。Java版通常返回抽象产品接口的引用C版推荐的做法是返回std::unique_ptrclass 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_ptrProduct create() 0; }; class FactoryA : public Factory { public: std::unique_ptrProduct create() override { return std::make_uniqueConcreteProductA(); } };为什么不用裸指针因为unique_ptr的语义表达得很清楚调用方拿到这个指针它就是唯一所有者离开作用域自动析构不会泄漏。这个语义是类型系统的一部分而不是靠注释约定。如果产品本身需要共享访问再改成shared_ptr。工厂模式配合智能指针使用几乎可以消灭new了忘delete这类问题。抽象工厂Abstract Factory则是生产一族相关产品。C里实现抽象工厂时模板是一个强大的武器。假设你有不同风格的主题组件按钮、输入框、菜单可以用模板 策略来处理工厂之间的公共逻辑而不是为每个具体工厂复制粘贴创建代码。我自己试过用CRTP奇异递归模板模式来提取公共工厂逻辑对减少样板代码效果显著但要注意模板代码的可读性和编译错误信息别为了省代码把维护难度拉满。2.3 原型模式虚克隆与深拷贝的取舍原型模式是通过复制现有实例来创建新对象。C实现时最关键的是clone()怎么写。因为需要多态克隆通常定义一个虚函数clone()返回类型使用协变返回类型或unique_ptrclass Prototype { public: virtual std::unique_ptrPrototype clone() const 0; virtual ~Prototype() default; }; class ConcretePrototype : public Prototype { public: std::unique_ptrPrototype clone() const override { return std::make_uniqueConcretePrototype(*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::dequeint dq; dq.push_back(1); dq.push_front(2); // deque可以直接操作两端stack不能 std::stackint 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_uniqueImage(path_); // 真正读取磁盘 } } std::string path_; std::unique_ptrImage 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_ptrComponent child) { children_.push_back(std::move(child)); } void display() override { for (auto child : children_) child-display(); } private: std::vectorstd::unique_ptrComponent children_; };这里用unique_ptr存储子节点父节点销毁时自动销毁所有子节点彻底避免了资源泄漏。组合模式在C中不会增加太多心智负担因为树的递归结构在STL容器支持下非常自然。但要注意如果这个树会被多方共享比如多个视图同时读取同一棵资源树unique_ptr就不行了需要换成shared_ptr或者保存索引的引用方式。模式本身没有优劣关键是匹配生命周期需求。4. 行为型模式的高级形态std::function让代码真正灵活起来行为型模式处理的是对象间的职责分配和算法封装。C11之后std::function和lambda几乎改变了策略、观察者、命令这些模式的具体写法。4.1 策略模式用std::functionlambda取代策略类继承树经典策略模式在Java中是这样的定义Strategy接口写一堆ConcreteStrategy类上下文通过组合持有Strategy接口引用运行时切换策略。C里定义一个接口、写实现类当然也work但大多数场景用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; }); // 乘法策略这种情况下策略是行为片段而不是完整对象。你用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_uniqueWalkState()); } else if (cmd Input::PressAttack) { player.setState(std::make_uniqueAttackState()); } } }; class AttackState : public PlayerState { public: void handleInput(Player player, Input cmd) override { if (cmd Input::ReleaseAttack) { player.setState(std::make_uniqueIdleState()); } } };真正的游戏里状态切换往往带条件当前连击数、体力值、是否在地面这些判断可以写在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_ptrCommand cmd) { pending_.push_back(std::move(cmd)); } void flush() { // 多条SQL合并提交或遍历执行 for (auto cmd : pending_) cmd-execute(); pending_.clear(); } private: std::vectorstd::unique_ptrCommand pending_; };你还可以在flush()里包装事务失败时对一定范围内的命令做回滚或者把执行失败的命令记录到日志队列第二天重放。这些功能在命令模式下全是往命令对象上加方法而已。如果你用直接调用库函数的方式每一次失败处理都分散在各个业务点几乎没有集中控制的可能。6. 从零配置VSCode的C/C环境让模式示例跑起来前几章聊了理论现在说说怎么让代码跑起来。VSCode是目前很多人用C的主力编辑器配置其实不复杂但网上教程版本五花八门很多已经过时了。我分享一套我一直在用的配置流程。6.1 安装扩展、配置编译任务与调试器第一步是装扩展。打开VSCode扩展市场搜索并安装CCMicrosoft官方C/C扩展、CMake Tools、CMake三个扩展。如果你用Windows还需要装MinGW-w64或者Visual Studio Build ToolsLinux上安装g或clangmacOS安装Xcode Command Line Tools。接下来是tasks.json。这个文件定义了如何编译你的代码。以Linuxg为例我常用这样的配置{ version: 2.0.0, tasks: [ { type: cppbuild, label: C/C: g build active file, command: /usr/bin/g, args: [ -fdiagnostics-coloralways, -stdc17, -g, ${fileDirname}/**.cpp, -o, ${fileDirname}/${fileBasenameNoExtension} ], options: { cwd: ${fileDirname} }, problemMatcher: [$gcc], group: { kind: build, isDefault: true } } ] }注意args里的-stdc17一定要写否则默认是C98很多新特性比如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 targetCtrlShiftP输入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味道的示例可以变得更轻、更快也更安全。