现代C++设计模式实战:从RAII到智能指针的工程实现

发布时间:2026/10/9 9:05:22

现代C++设计模式实战:从RAII到智能指针的工程实现 设计模式这四个字在C这条技术栈里的位置一直有点微妙。一方面GoF那本《设计模式》的示例代码几乎全是C写的按说C应该是设计模式的主场另一方面你拿C98时代那套类图和写法放进现代C工程里往往事倍功半甚至会被同事批评过度设计。为什么会这样因为C从来不是一门单纯的面向对象语言虚函数、模板、lambda、智能指针这些工具叠加在一起让同一个模式在C里至少有三种完全不同的落地姿势选错了就是灾难。这篇文章不打算把23种设计模式全部过一遍那种写法既像教科书又像八股文题库。我会从实际工程出发挑出几个高频模式用可编译的C代码讲清楚为什么这样写不这样做会踩什么坑顺带把期末大作业、面试手撕代码这些场景需要掌握的核心知识都覆盖到。适合正在做设计模式大作业的学生、准备C面试的开发者以及想用设计模式重构老模块但不知道如何下手的工程人。先把一句话放在前面设计模式在C里的核心不是类图而是搞清楚变化的点在哪里谁负责对象的生命周期。1. 项目概述为什么C里的设计模式值得单独讲1.1 C在设计模式这件事上到底是个什么位置设计模式的概念最早来自建筑领域后来被GoF引入软件工程。那本经典著作《Design Patterns: Elements of Reusable Object-Oriented Software》里的示例代码用的就是C和Smalltalk。所以很多初学者会形成一个印象C是天生的面向对象语言23种设计模式当然应该在C里逐条落地。这个印象一半对一半不对。对的一面是C确实提供了完整的面向对象武器库类、继承、虚函数、抽象基类、运行时多态。工厂模式要抽象产品接口、策略模式要抽象算法接口、观察者模式要发布订阅结构这些在C里用虚函数都能规规矩矩画出来和Java的体验差不多。不对的一面在于C远远不只面向对象这一条路。模板可以让代码在编译期完成派发std::function可以让函数成为一等公民直接传递lambda可以让行为封装不再需要单独定义一个类。同样是策略你在Java里基本只能写接口实现类但在C里至少有四种写法虚函数接口、函数对象、模板参数、lambda注入。跨度之大在其他主流语言里很少见。这就带来一个C特有的问题设计模式在C里没有所谓的标准实现。网上随便搜一个单例模式能搜出饿汉、懒汉、双重检查锁、静态局部变量、模板单例五六种写法每种都有人自称最标准。其实它们各自有不同的适用场景有的图启动快有的图线程安全有的图延迟加载有的试图兼顾所有优点。真正工程里要做的是根据场景选对实现而不是背一个模板套到所有项目里。后面我会把这些写法的取舍一条一条拆开。1.2 哪些人需要这篇内容能解决什么问题我平时会收到不少跟设计模式C相关的提问梳理下来读者基本分成三类诉求差别很大。第一类是学生正在做设计模式期末大作业或者课程设计。这类读者最需要的不是概念而是一个能串起多个模式的综合案例代码能编译能运行答辩时能讲清楚每个模式解决了什么问题。第二类是准备面试的开发者面试官很喜欢问手写单例模式说说观察者模式设计模式八大原则光背概念不够还得扛得住追问你的单例线程安全吗观察者列表里的指针悬垂了怎么办第三类是写C工程的老手代码跑了好几年业务逻辑越来越复杂if-else成堆想用模式来重构但又怕引入过度设计把简单事情搞复杂。这三类读者的需求点不一样我尽量在后面的内容里都覆盖到综合案例放在第4章给第一类高频模式的完整代码和追问点放在第3章给第二类设计思路和取舍原则散落在全篇给第三类。所以这篇内容的组织方式不是23种模式逐一报幕而是围绕C的具体特性把最常用、最容易踩坑的几种模式讲透。2. 核心设计思路现代C实现设计模式的三个底层工具GoF写书的时候C还是C98没有auto没有unique_ptr没有lambda更没有std::function。所以当时实现设计模式基本只能靠类虚函数这一条路。但现代C至少是C11以及之后的版本引入了几个彻底改变实现方式的特性。在进入具体模式之前我先把这三个底层工具讲清楚后面所有代码都会反复用到它们。2.1 RAII资源安全是设计模式能跑起来的前提RAIIResource Acquisition Is Initialization资源获取即初始化是C独有的资源管理思想把资源的生命周期绑定到一个栈对象上构造时获取资源析构时自动释放。这个思想对设计模式的影响被很多人低估了——它直接决定了模式里的对象协作是否安全。举个最典型的例子。策略模式在Java里用接口定义算法C用纯虚基类也能做到一模一样但关键问题是谁来管理算法对象的生命周期很多C新手写的策略模式是这样的class IStrategy { public: virtual void execute() 0; virtual ~IStrategy() default; }; class AStrategy : public IStrategy { public: void execute() override { /* ... */ } }; class Context { IStrategy* strategy_; public: explicit Context(IStrategy* s) : strategy_(s) {} void run() { strategy_-execute(); } };这段代码能编译能运行但藏着资源泄漏隐患。Context持有一个裸指针却没有说明这个指针的所有权到底归谁。调用方new一个AStrategy传进来Context用完不delete会泄漏Context好心delete掉调用方如果继续用这个指针就变成悬垂指针。双方都在猜责任最后就是内存问题。用RAII思想重新设计把裸指针换成unique_ptr语义一下子清晰了这份算法对象的所有权明确归Context所有析构时自动释放谁都不会误解。这就是RAII对设计模式的意义——它帮你把模式结构里最麻烦的对象管理问题交给语言机制解决。2.2 多态的两条路线运行时虚函数与编译期模板GoF模式依赖的核心机制是多态。传统C多态用虚函数实现也就是运行时根据对象的真实类型调用对应实现。这种多态的好处是灵活对象可以在运行时被替换某个组件只要换一个新实现整个系统的行为就跟着变。代价是性能损耗和代码膨胀每个具体实现都要写一个类虚函数调用本身也多了一次跳转。C还提供第二种多态——模板。比如策略模式用模板参数传入算法完全不需要虚函数template typename Strategy class Context { Strategy strategy_; public: explicit Context(Strategy s) : strategy_(std::move(s)) {} void run() { strategy_.execute(); } };编译器在实例化模板时会直接把合适的算法代码嵌进Context没有虚函数调用内联机会也更大性能明显更好。缺点同样明显Context一旦用某个策略类型实例化运行时就再也没法换成其他策略了。所以两条路线的取舍很清晰需要运行时切换策略选虚函数或std::function策略在编译期就能确定优先选模板。好多C开发者只熟悉虚函数这一条路遇到模板就懵其实模板才是C里真正区别于Java的优势所在。下面这个表格可以帮助快速对比两种多态的差异。对比维度虚函数多态模板多态决策时机运行时编译期灵活性可运行时替换类型固定性能有间接调用开销可内联更高可读性类图清晰依赖类型推导适用模式大部分GoF模式策略、观察者回调等2.3 生命周期语义与所有权传递现代C把对象生命周期分成几种明确角色独占所有权的unique_ptr、共享所有权的shared_ptr、不拥有对象但能安全观察的weak_ptr。设计模式里的创建型模式工厂、单例和结构型模式代理、组合都涉及对象在不同对象之间传递所有权语义只要不清bug就跟着来。举个例子工厂模式如果返回裸指针调用方看着返回值心里打鼓我要不要delete如果不delete泄漏如果delete万一工厂内部还缓存着这份指针程序崩给看。把返回值改成unique_ptr之后语义立刻明确产品对象的所有权交给了调用方调用方能安全释放工厂不再负责。观察者模式里也是同理Subject保存Observer指针但Observer可能提前销毁如果存的是shared_ptr就可能造成循环引用改用weak_ptr才能既持有又不影响生命周期。设计一个模式时先问一句这个对象在生命周期的每个阶段由谁负责释放很多设计难题瞬间就解开了。3. 经典模式在C中的落地实现从这一章开始进入具体代码。我选了四个在C工程里用得最多、期末和面试也最容易考到的模式单例、工厂、观察者、策略。每个模式都会先交代经典GoF意图再给出现代C实现最后点出关键坑位。3.1 单例模式线程安全的各种实现细节单例模式是最简单也最容易被问烂的一个模式意图只有一个确保一个类只有一个实例并提供全局访问点。但唯一实例在C里涉及构造控制、线程安全、延迟加载、销毁顺序等一堆细节每个细节都能玩出花样。先看经典的懒汉写法这也是新手最容易写、坑最多的版本class Singleton { private: static Singleton* instance_; public: static Singleton* getInstance() { if (instance_ nullptr) { instance_ new Singleton(); } return instance_; } Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; private: Singleton() default; }; Singleton* Singleton::instance_ nullptr;这个写法在单线程下没问题多线程下就出事两个线程同时判断instance_为空然后各自new内存泄漏加实例不唯一。于是有人加互斥锁static Singleton* getInstance() { std::lock_guardstd::mutex lock(mutex_); if (instance_ nullptr) { instance_ new Singleton(); } return instance_; }线程安全了但每次读实例都要加锁。对于读多写少的场景这种全局锁的性能开销不小于是又有人写出双重检查锁Double-Checked Locking Patternstatic Singleton* getInstance() { if (instance_ nullptr) { std::lock_guardstd::mutex lock(mutex_); if (instance_ nullptr) { instance_ new Singleton(); } } return instance_; }双重检查锁在C里有个著名陷阱new表达式在编译器视角里大致分三步——分配内存、调用构造函数、把地址赋给instance_。C98/03没有严格的内存模型保证处理器和编译器都可能乱序另一个线程可能看到instance_非空但构造函数还没跑完拿到一个半成品对象。C11以后可以用原子变量加内存序修正但普通开发者很容易写错。所以现代C最推荐的写法是Meyers Singleton直接用函数局部静态变量class Singleton { public: static Singleton getInstance() { static Singleton instance; return instance; } Singleton(const Singleton) delete; Singleton operator(const Singleton) delete; private: Singleton() default; };C11标准明确规定函数局部静态变量的初始化是线程安全的编译器自动加保护。这段代码既延迟加载又线程安全还不用手动delete是工程里的首选。它唯一解决不了的是销毁顺序问题如果多个单例互相依赖程序退出时析构顺序不受控制可能崩在奇奇怪怪的地方。实际写代码时尽量让单例之间不互相持有引用把生命周期问题交给依赖注入来管理更稳妥。提示面试时如果被问单例和静态全局变量有什么区别答案核心是两点局部静态变量拥有延迟初始化和线程安全两种优势。能主动说出但无法处理销毁顺序说明你不是在背八股。3.2 工厂模式当简单工厂遇到智能指针工厂模式的本质是把对象的创建逻辑从使用方剥离让调用方不关心具体类型。GoF定义了三种简单工厂不是正式模式但最常见、工厂方法通过子类延迟创建对象、抽象工厂创建一族相关对象。C工程里日常用得最多的是简单工厂和带注册表的工厂先把简单工厂写出来class Product { public: virtual void use() 0; virtual ~Product() default; }; class ProductA : public Product { public: void use() override { std::cout Product A std::endl; } }; class ProductB : public Product { public: void use() override { std::cout Product B std::endl; } }; class Factory { public: static std::unique_ptrProduct create(const std::string type) { if (type A) { return std::make_uniqueProductA(); } else if (type B) { return std::make_uniqueProductB(); } return nullptr; } };返回unique_ptr是重点。它把产品对象的所有权明确交给调用方调用方不需要手动delete即使create中途抛异常也不会泄漏。如果调用方确实需要多处共享一个产品对象再显式把它转换成shared_ptr即可所有权变更清晰可见。但简单工厂有个明显问题每增加一种产品就要改create函数里的if-else链违背开闭原则。改进方案是注册表工厂用map把类型名映射到创建函数class RegistryFactory { public: using Creator std::functionstd::unique_ptrProduct(); static void registerCreator(const std::string type, Creator creator) { registry()[type] std::move(creator); } static std::unique_ptrProduct create(const std::string type) { auto it registry().find(type); if (it registry().end()) { return nullptr; } return it-second(); } private: static std::mapstd::string, Creator registry() { static std::mapstd::string, Creator reg; return reg; } }; // 模块内注册 static bool regA [] { RegistryFactory::registerCreator(A, [] { return std::make_uniqueProductA(); }); return true; }();这种注册表工厂在游戏插件系统、GUI控件注册、日志格式扩展里非常常见。新增产品不需要改已有工厂代码只写新的产品类和注册语句符合开闭原则。期末大作业如果能从简单工厂延伸到注册表工厂再解释一下对扩展开放、对修改封闭的含义会比单纯写一个if-else工厂有说服力得多。3.3 观察者模式从手写接口到回调函数观察者模式也叫发布订阅模式核心是一个对象状态变化时自动通知所有依赖它的对象。GoF经典实现是Subject和Observer两个抽象类现代C里用std::function替代Observer抽象基类代码会轻便很多也贴合C的函数对象特性。经典写法大致是这样Observer是抽象接口ConcreteObserver继承后重写update方法Subject维护Observer指针列表状态改变时遍历列表调update。这个写法最大的问题不是复杂而是两个每个观察者都必须实现整个接口哪怕它只想关心其中一个事件并且如果观察者的生命周期比Subject短Subject里还留着裸指针发布事件时会直接访问已销毁对象。用std::function改造之后观察者不再是一个类而是一个可调用对象订阅者可以用lambda、函数对象、成员函数任意组合class EventBus { public: using Handler std::functionvoid(const std::string); void subscribe(const std::string event, Handler handler) { handlers_[event].push_back(std::move(handler)); } void notify(const std::string event, const std::string data) { auto it handlers_.find(event); if (it handlers_.end()) { return; } for (const auto handler : it-second) { handler(data); } } private: std::unordered_mapstd::string, std::vectorHandler handlers_; };使用时可以这样EventBus bus; bus.subscribe(login, [](const std::string user) { std::cout user login: user std::endl; }); bus.notify(login, alice);这段代码把谁发布、谁订阅彻底解耦发布者不需要知道订阅者是什么类型。唯一的生命周期隐患是如果订阅者对象已析构但lambda回调还留在handlers_里通知时会触发未定义行为。常规做法是订阅时生成一个token退订时用token删除或者让回调持有weak_ptr执行前lock判断对象是否存活。后面第5章会专门展开这个问题。3.4 策略模式与模板方法选择运行期还是编译期接口策略模式定义一族算法让它们可以互相替换属于行为型模式里出现频率最高的一个。C实现策略模式至少有三种形态经典虚函数版、std::function版、模板参数版我把三种放到一起对比能帮助你做选型。第一种是虚函数版也就是GoF类图的标准样子策略接口、具体策略、上下文三个角色。优点是对象可以在运行期切换策略缺点是小算法也得写成一个完整类文件数量蹭蹭上涨。第二种是std::function版上下文直接持有一个函数对象class Context { public: using SortStrategy std::functionstd::vectorint(std::vectorint); explicit Context(SortStrategy strategy) : strategy_(std::move(strategy)) {} void setStrategy(SortStrategy strategy) { strategy_ std::move(strategy); } std::vectorint execute(std::vectorint data) { return strategy_(std::move(data)); } private: SortStrategy strategy_; }; Context ctx([](std::vectorint v) { std::sort(v.begin(), v.end()); return v; }); ctx.setStrategy([](std::vectorint v) { // 另一种排序算法比如逆序 std::sort(v.rbegin(), v.rend()); return v; });这个版本写起来极轻策略就是一行lambda非常适合策略数量不多、逻辑不复杂的场景。第三种是模板参数版它把策略作为编译期类型绑定在上下文里没有虚函数调用性能最好但运行期无法替换策略。三种写法之间没有绝对优劣决策标准很简单策略需要在运行期换来换去选虚函数或std::function策略类型编译期能确定追求极致性能选模板参数。我最常用的判断方式是在性能和灵活度之间划一条线——如果这个模块未来极大概率不会在运行时换策略就上模板只要有一丝运行时替换的可能就选std::function因为它的改动成本最低。模板方法模式跟策略有点像它用基类定义算法骨架把某些步骤延迟到子类实现。C里同样可以变通骨架不变变化的部分用std::function注入这样既有模板方法的稳定性又不必为每个变体都写一个子类。两者联合使用时我通常把固定流程放在普通函数里把可变动作作为函数参数传入简单直接比硬套继承树更容易维护。4. 实操过程用四种设计模式搭建一个C日志系统很多同学做设计模式大作业喜欢做成模式展示大全每个模式单独一段代码互相之间没有关系。这种做法的问题在于答辩时很难讲明白这些模式为什么能协同工作。我看过的优秀作业几乎都是用一个综合案例把多个模式自然地串起来。这里我选择日志系统作为案例理由有两个一是日志系统几乎每个C项目都需要场景接地气二是它能自然地把单例、工厂、观察者、策略四种模式融进同一套代码里逻辑上一点都不生硬。4.1 需求分析与模式选型先明确日志系统的需求。日志器要接收消息消息在输出前可能需要格式化比如加时间戳、加日志级别标签输出目标可能是控制台、文件、网络等全局只需要一个日志入口避免到处new一个Logger后续还要能轻松新增输出目标类型。按这个需求做模式选型日志器本身用单例模式保证全局只有一个入口输出目标用观察者模式日志器发布消息各输出端订阅消息格式化用策略模式运行期可以根据需要切换格式日志器的创建和输出目标创建交给工厂模式把创建逻辑集中管理。四个模式各司其职没有一个是硬凑的。4.2 完整代码实现与关键点讲解下面给出完整代码这一段可以直接复制到支持C17的编译环境里运行#include iostream #include fstream #include memory #include vector #include functional #include string #include map #include ctime enum class LogLevel { Info, Warning, Error }; // 策略模式消息格式化器 class IFormatter { public: virtual std::string format(const std::string msg, LogLevel level) 0; virtual ~IFormatter() default; }; class DefaultFormatter : public IFormatter { public: std::string format(const std::string msg, LogLevel level) override { return [LOG] msg; } }; class DetailedFormatter : public IFormatter { public: std::string format(const std::string msg, LogLevel level) override { std::time_t t std::time(nullptr); char buf[32] {0}; std::strftime(buf, sizeof(buf), %Y-%m-%d %H:%M:%S, std::localtime(t)); std::string levelStr; switch (level) { case LogLevel::Info: levelStr INFO; break; case LogLevel::Warning: levelStr WARN; break; case LogLevel::Error: levelStr ERROR; break; } return std::string(buf) [ levelStr ] msg; } }; // 观察者模式输出目标订阅者接口 class ILogSink { public: virtual void write(const std::string formatted) 0; virtual ~ILogSink() default; }; class ConsoleSink : public ILogSink { public: void write(const std::string formatted) override { std::cout formatted std::endl; } }; class FileSink : public ILogSink { public: explicit FileSink(const std::string filename) { ofs_.open(filename, std::ios::app); } void write(const std::string formatted) override { if (ofs_.is_open()) { ofs_ formatted std::endl; } } private: std::ofstream ofs_; }; // 单例模式日志器 class Logger { public: using SinkPtr std::shared_ptrILogSink; using FormatterPtr std::shared_ptrIFormatter; static Logger instance() { static Logger logger; return logger; } void attach(SinkPtr sink, const std::string name) { sinks_[name] std::move(sink); } void detach(const std::string name) { sinks_.erase(name); } void setFormatter(FormatterPtr formatter) { formatter_ std::move(formatter); } void log(const std::string msg, LogLevel level LogLevel::Info) { std::string formatted formatter_-format(msg, level); for (auto item : sinks_) { item.second-write(formatted); } } private: Logger() : formatter_(std::make_sharedDefaultFormatter()) {} Logger(const Logger) delete; Logger operator(const Logger) delete; std::mapstd::string, SinkPtr sinks_; FormatterPtr formatter_; }; // 工厂模式创建输出目标 class SinkFactory { public: enum class SinkType { Console, File }; static std::shared_ptrILogSink create(SinkType type, const std::string file ) { switch (type) { case SinkType::Console: return std::make_sharedConsoleSink(); case SinkType::File: return std::make_sharedFileSink(file); default: return nullptr; } } }; int main() { Logger logger Logger::instance(); logger.attach(SinkFactory::create(SinkFactory::SinkType::Console), console); logger.attach(SinkFactory::create(SinkFactory::SinkType::File, app.log), file); logger.setFormatter(std::make_sharedDetailedFormatter()); logger.log(application started, LogLevel::Info); logger.log(resource not found, LogLevel::Warning); logger.log(failed to connect, LogLevel::Error); return 0; }这段代码里有几个细节值得展开说。第一Logger的构造函数是private同时删除了拷贝构造和赋值运算符这是单例模式的标准控制手段外部代码不可能偷偷再创建第二个日志器。第二sinks_容器用shared_ptr保存输出目标是因为同一个输出目标可能被多个订阅场景共享引用shared_ptr能保证只要还有使用者目标就不会被提前释放。第三Logger内部持有的是FormatterPtr而不是裸指针通过setFormatter方法可以在运行期自由切换格式化策略这就是策略模式在运行期生效的地方。如果你在vscode里配置好C/C环境新建cpp文件把代码粘贴进去再编译运行应该能在控制台看到带时间戳的日志输出同时app.log文件里也会写入同样的记录。注意Logger::instance()返回的是引用而不是指针这是Meyers Singleton的推荐形态。如果返回指针调用方可能会错误地delete它返回引用在语义上更安全也更难被误用。4.3 如何扩展这个案例以满足期末答辩这个日志系统的扩展点非常多作业总结或者答辩时可以多准备几个方向。比如想支持按级别过滤可以在Logger里加一个levelThreshold成员log函数里先对比当前级别和阈值不满足就直接返回想支持异步写日志可以把sinks_收到的消息先压进队列由另一个工作线程消费这就是一个典型的生产者消费者模型想让输出目标的创建完全符合开闭原则可以把SinkFactory从switch换成第3.2节里的注册表工厂新增大日志类型时不再改动Factory代码。我还想特别提一个思路如果你想做小游戏类的设计模式大作业也不一定要去抄一份跑酷游戏源码。小游戏里同样能串起设计模式——游戏主循环用模板方法定义骨架角色生成用工厂模式玩家事件分发用观察者模式计分规则用策略模式。模式选型跟着职责走不要反过来让模式安排代码结构。日志系统也好、小游戏也好核心都是先找变化点再配模式。5. 常见问题与排查技巧实录写代码这么多年我在设计模式的落地过程中踩过不少坑也帮别人排查过很多类似问题。这一章把最典型的四个问题整理成速查表每个都能直接对应到你以后写代码的场景里。5.1 单例的线程安全与销毁顺序陷阱第一个坑是双重检查锁前面已经详细讲过了这里补充一个实际排查场景。我曾经遇到过一个服务程序退出时概率性崩溃gdb看了很久发现是Logger单例和另一个配置单例的析构顺序问题。程序退出时配置单例先析构另一方面配置单例的某个函数还在给Logger发消息此时Logger已经销毁调用即崩溃。这种问题解决起来很棘手因为析构顺序由全局变量的构造顺序决定而全局变量跨编译单元的构造顺序本身是未定义的。我的经验是两条路一是尽量在main函数结束前显式清理依赖关系让单例之间不要你中有我二是使用依赖注入而不是直接访问单例把Logger指针显式传给需要它的对象生命周期完全由外部控制。虽然依赖注入写起来更啰嗦但它能从根本上消除这类幽灵依赖。5.2 观察者模式中的悬垂指针与循环引用观察者模式有两个方向相反的生命周期问题悬垂和循环引用。悬垂发生在Subject保存Observer指针但Observer先被销毁的情况下。用裸指针必然有风险用shared_ptr又会带来第二个问题——如果Observer的回调里又引用Subject两者互相持有shared_ptr引用计数永远不为零内存就泄漏了这就是典型的循环引用。工程上的标准解法是打破环一端用weak_ptr比如Subject持有weak_ptr观察者列表通知前先lock判断对象是否存活或者Observer不直接持有Subject的shared_ptr而是通过weak_ptr观察。我在第3.3节写的EventBus例子其实留下了一个隐患就是订阅者销毁后lambda仍然留在列表里严谨的做法是在订阅时生成token退订时通过token移除。这里我建议你在期末或者面试时主动提一句weak_ptr对循环引用的作用这会是加分项。5.3 模式滥用过度设计的三个典型信号设计模式不是越多越好这句话说了无数遍但实际code review里还是经常看到过度设计。最常见的是三个信号一是类数量爆炸每个if分支都抽象成一个策略类最后整个项目没人能看懂对象之间怎么协作二是到处用单例每个模块都要全局唯一结果隐式全局状态污染了所有测试三是人为制造接口只有一个实现类也要抽象出接口多态需求根本不存在白白增加一层跳转。我的判断标准其实非常简单如果未来只有一种可能的变化就不要为它引入模式等变化真的来了再抽象也来得及成本往往比预先设计低得多。设计模式的本质是响应变化而不是预防一切可能的变化。用这个标准去检查自己的代码能砍掉一半以上为了用模式而用模式的类。5.4 面试追问如何看待C为什么没有普遍使用GoF设计模式网上有人搜C为什么没有普遍这种问题我猜完整版很可能是C为什么没有普遍使用GoF设计模式或者为什么C社区不像Java那样强调设计模式。我的看法是C社区并不是不用设计模式而是很多模式被语言特性内化了策略模式被std::function和模板吸收观察者模式被信号槽和回调函数吸收迭代器模式直接被标准库的迭代器和范围for循环覆盖。C用更轻量的方式实现了同样的意图所以你不会在工程里频繁看到StrategyImpl.cpp这种文件但模式的思路仍然藏在高阶函数、泛型算法和RAII封装里。面试时如果被问到这个问题可以从语言范式角度回答C支持多范式面向对象只是手段之一设计模式不是C唯一的代码组织思路。这个回答既展示了你对C特性的理解也说明你没有把设计模式当成教条来背。6. 实操总结与个人经验6.1 一次真实重构带来的启发我自己在最开始用设计模式时也走过弯路想给老模块引入工厂模式结果先是把简单工厂写成了switch工厂然后又堆了一堆类重构完代码量翻倍可读性反而下降了。后来有一次真正让我尝到甜头的重构是把一个到处if-else根据配置项选算法的模块改成注册表工厂加策略模式。新算法只需要写一个独立文件并注册主函数从那以后整整三个月没动过。那次我彻底明白了一件事设计模式的价值不在于代码有多少类而在于把变化的点隔离在接口背后。你对变化点判断得越准模式用得就越轻。6.2 给期末大作业和面试备战的两点建议如果你正在准备设计模式期末大作业我的建议是不要死记23种模式的类图。先画一张问题到模式的映射表想全局唯一入口选单例想解耦创建逻辑选工厂想行为可替换选策略想状态变更通知别人选观察者。练熟六七种高频模式比背完23种但一个都不会写强得多。面试也是同样的道理考官问手写单例真正想看的不是你会背Meyers Singleton而是你能不能在追问中讲清楚为什么这样写、双检锁有什么问题、销毁顺序怎么处理。这些细节比模式本身的类图值钱得多。
延伸阅读

更多相关文章

2026/10/9 9:05:22

Claude 记忆管理工具 claude-mem 实战:原理、配置与工作流

1. 这个项目到底解决了什么痛点先说个我自己的经历。用 Claude 写代码、写文档、做方案,最烦的一件事就是它“记性太差”。今天跟它讨论完一个项目的架构选型,明天再打开对话,它一脸茫然,好像我们昨天根本没聊过。重新描述一遍上下…

2026/10/9 9:05:22

pstack-claude实战:用AI辅助进程栈分析与系统排障

1. 从 pstack-claude 这个名字说起:它到底想解决什么问题 第一次看到 pstack-claude 这个项目名,我的直觉是:这大概率是一个把 pstack 和 claude 两件事拼在一起的工具链或者封装层。 pstack 在系统排查领域是个老面孔,用…

2026/10/9 10:06:02

Cocos Creator 3.x 3D拼图开发:核心机制与性能优化

老板把需求丢给我的时候,我正盯着满屏的“羊了个羊”竞品分析发愁。他说得没错,2D拼图市场是真的卷——换皮、联名、剧情化、番外篇,你能想到的姿势同行都试过了。但他下一句话才是重点:“你去做个3D版本的吧。”这句话听着像脑洞…

2026/10/8 10:03:18

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/8 10:03:20

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/8 6:05:44

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

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

2026/10/9 0:04:27

毕业论文初稿完成后首次进行AIGC疑似度自查的摸底与分流策略

毕业论文初稿完成后首次进行AIGC疑似度自查的摸底与分流策略当数万字的学位论文初稿经历开题、实验、问卷与多轮文献梳理最终成形时,绝大多数研究生都会面临一道全新的形式审查关卡:AIGC 疑似度排查。在高校毕业审核流程中,盲审前的文本检测通…

2026/10/9 0:04:27

食堂节能改造源头工厂,商用厨房设备焕新方案广受好评

商用厨房作为餐饮经营、单位供餐的核心后勤阵地,其设备配置、动线规划与运维体系直接决定后厨作业效率、运营成本与合规性。从基础的灶具、制冷存储设备,到油烟净化、水处理等配套系统,每一个环节的合理性都与食品安全、能耗管控、消防安全挂…

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

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

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