资讯详情

C++工厂模式详解:三种形态、现代C++实践与面试要点

📅 2026/9/10 4:10:41 | 华诺云谱 👁 阅读
C++工厂模式详解:三种形态、现代C++实践与面试要点
做C开发这些年我最深的体会是代码腐烂往往不是从算法写错开始的而是从对象创建到处new、逻辑分支像蜘蛛网一样蔓延开始的。工厂模式Factory Pattern就是C里专门对付这类问题的经典设计模式它把“创建对象”这件事从业务代码里剥离出来让调用方只关心“我要什么”不关心“怎么来”。这篇文章会从实际项目代码出发聊透简单工厂、工厂方法、抽象工厂三种形态再带上C17/20的新玩法、VSCodeCMake工程实战以及面试官真正想听的点。不管你是刚学C不久还是准备跳槽刷八股这篇都能给你点能直接用的东西。1. 先从一段“烂代码”说起为什么我们要谈工厂模式1.1 没有工厂模式时代码是怎么烂掉的假设你正在写一个消息分发模块客户端会发来不同类型的请求你需要根据消息类型创建对应的处理器对象。很多人第一版代码是这么写的// 初始版本if-else 铺满整个函数 Message* CreateMessage(int type) { if (type 1) { return new LoginMessage(); } else if (type 2) { return new MoveMessage(); } else if (type 3) { return new ChatMessage(); } else if (type 4) { return new HeartBeatMessage(); } // 每次加协议这里就要多一个 else if return nullptr; }这段代码在只有三四个类型时看起来还挺清爽。但项目迭代半年之后你的消息类型可能膨胀到几十个这个函数就会变成一个几百行的“大泥球”。每次新加一种消息你都要动这个函数这在设计原则上叫“对修改开放”——说白了就是改一个地方可能把其他正常逻辑带崩。更麻烦的是这种写法让调用方和具体类产生了强耦合。你测试的时候想用一个Mock对象替换真实处理器却发现改造起来特别费劲因为类型选择逻辑散落在各个角落没有统一入口可替换。1.2 工厂模式到底是来解决什么问题的工厂模式的核心思想如果用人话说就是“把创建对象这件事从业务代码里摘出来单独交给一个叫做工厂的东西负责”。它强调的就是两个字分离。你点餐的时候只和服务员说“来一份宫保鸡丁”不用关心后厨是先切鸡丁还是先炸花生米。工厂就是那个服务员你只需要递需求给它它负责把对象端到你面前。从设计模式分类看工厂模式属于创建型模式Gang of FourGoF里定义的创建型模式重点就是“系统与对象的创建方式解耦”。工厂模式具体能带来的好处调用方不依赖具体类依赖的是抽象接口符合依赖倒置原则。新增加产品时尽量做到对已有代码的最小改动贴近开闭原则。对象的创建逻辑集中管理便于统一设置初始参数、做日志、做内存管理。1.3 到底什么时候才值得用工厂我见过不少同学学完工厂模式后写着写着就“万物皆工厂”一个玩具项目恨不得造出八个类结果代码量翻倍维护更累。这里讲几个真正值得上工厂的信号对象类型的确定需要依据运行时信息比如网络包里的type字段、配置文件里的字符串。创建对象时有复杂的初始化逻辑比如要读取配置、做依赖注入、设置默认参数。你希望换一组对象时调用方代码完全不用改只需要换工厂实现。如果你的项目里对象类型是编译期就完全确定的、创建逻辑就一行new那硬套工厂反而画蛇添足。这个“什么时候不用”的判断其实比怎么实现更重要。2. 三种工厂形态逐一拆解简单工厂、工厂方法、抽象工厂2.1 简单工厂入门首选但别让它背太多锅简单工厂严格来说不算GoF 23种设计模式之一它更像是一种编程习惯。它的结构非常直白一个工厂类内部通过分支语句根据参数决定创建哪一种产品对象。// product.h class IProduct { public: virtual ~IProduct() default; virtual void Use() 0; }; class ConcreteProductA : public IProduct { public: void Use() override { /* ... */ } }; class ConcreteProductB : public IProduct { public: void Use() override { /* ... */ } }; // factory.h class SimpleFactory { public: enum class Type { A, B }; static std::unique_ptrIProduct Create(Type type) { switch (type) { case Type::A: return std::make_uniqueConcreteProductA(); case Type::B: return std::make_uniqueConcreteProductB(); } return nullptr; } };简单工厂的优点是真简单一眼就能看懂适合产品类型比较少、不太会频繁变化的场景。但我实际项目里的体会是它的天然缺陷也很明显每新增一个产品类型都要去改工厂的switch或者if-else这违背了开闭原则。产品少时无所谓产品一旦超过十几个这个工厂内部又是一个新的“泥球”。2.2 工厂方法把创建逻辑下沉到子类工厂方法Factory Method是GoF正式成员。它的思路是把“工厂”本身也抽象化定义抽象工厂基类声明一个创建产品的纯虚函数然后每个具体产品对应一个具体工厂。这样一来客户端和具体工厂解耦新增产品时不需要改已有工厂只需要继承出新的工厂类。class IImageDecoder { public: virtual ~IImageDecoder() default; virtual void Decode(const std::string filePath) 0; }; class JpegDecoder : public IImageDecoder { public: void Decode(const std::string filePath) override { /* JPEG 解码逻辑 */ } }; class PngDecoder : public IImageDecoder { public: void Decode(const std::string filePath) override { /* PNG 解码逻辑 */ } }; // 工厂基类 class ImageDecoderFactory { public: virtual ~ImageDecoderFactory() default; virtual std::unique_ptrIImageDecoder CreateDecoder() 0; }; class JpegDecoderFactory : public ImageDecoderFactory { public: std::unique_ptrIImageDecoder CreateDecoder() override { return std::make_uniqueJpegDecoder(); } }; class PngDecoderFactory : public ImageDecoderFactory { public: std::unique_ptrIImageDecoder CreateDecoder() override { return std::make_uniquePngDecoder(); } };工厂方法的好处是符合开闭原则添加新图片格式时你不需要改已有类只要新增Decoder类和对应的Factory类。坏处也明显每加一个产品就要额外增加一个工厂类类的数量翻倍。我自己的经验是当产品类型增加不频繁、且每个产品的创建逻辑确实比较复杂时工厂方法是划算的如果产品类型动辄几十上百个类的爆炸会让你怀疑人生。2.3 抽象工厂解决产品族的创建问题抽象工厂Abstract Factory讲的不是单个产品而是“产品族”。举个例子你的应用需要支持Windows和Linux两套UI控件每套都包含Button、TextEdit、CheckBox。如果你用工厂方法每个控件都要配一个工厂控制很分散而抽象工厂的思路是定义一个“UI控件工厂”抽象接口里面声明创建整套控件的方法Windows下有一套实现Linux下有另一套实现。class IButton { public: virtual ~IButton() default; virtual void Render() 0; }; class WinButton : public IButton { public: void Render() override { /* 绘制Windows风格按钮 */ } }; class LinuxButton : public IButton { public: void Render() override { /* 绘制Linux风格按钮 */ } }; class IUIFactory { public: virtual ~IUIFactory() default; virtual std::unique_ptrIButton CreateButton() 0; // 假设还有 CreateTextEdit、CreateCheckBox 等纯虚函数 }; class WinUIFactory : public IUIFactory { public: std::unique_ptrIButton CreateButton() override { return std::make_uniqueWinButton(); } }; class LinuxUIFactory : public IUIFactory { public: std::unique_ptrIButton CreateButton() override { return std::make_uniqueLinuxButton(); } };客户端在运行时选择一个UIFactory之后创建的整套UI控件风格天然一致不会出现“按钮是Windows风格、输入框却是Linux风格”的混搭。抽象工厂和工厂方法的核心区别可以记成一句话工厂方法解决的是“一个产品”的创建抽象工厂解决的是“一族产品”的创建并且保证族内产品风格统一。三者的选择我个人的判断表如下维度简单工厂工厂方法抽象工厂产品对象数量较少且变化不频繁单个产品支持扩展一组产品产品族是否符合开闭原则否是是类数量膨胀程度低高较高典型应用场景快速原型、配置较少插件化、解码器注册跨平台UI、多套主题皮肤3. 实战一用工厂模式重构一个消息分发系统3.1 需求场景一个最简单的网络消息分发模块先交代一下背景。假设我们在做一个模拟网络服务的控制台程序客户端会上报不同的消息登录、移动、聊天、心跳。服务端收到消息后根据消息类型创建一个对应的消息处理器处理器负责解析、校验、执行业务动作。这个场景在游戏服务器、物联网平台里非常常见特别适合练手工厂模式。最初代码确实是那段经典的if-else。问题发生在第五种消息“玩家退出”加进来之后新增逻辑改了旧函数导致其他消息分支出现了潜在的回归。那一刻我下定决心重构把对象创建剥出来。3.2 环境准备VSCode CMake 搭一个可复现工程在开始写工厂代码之前先把工程环境配好。我用的是VSCode CMake这套组合在C开发里很常见热词里也有大量关于VSCode配置C/C环境的内容这里给一份精简但能直接跑的配置。首先确保本机装了编译器Windows上推荐MinGW-w64或MSVCLinux/macOS直接用g或clang即可。CMake3.16以上版本后续要用到一些较新的target属性。VSCode扩展C/C扩展Microsoft官方、CMake Tools扩展。然后创建一个最小工程目录factory-demo/ ├── CMakeLists.txt ├── message.h ├── message.cpp ├── simple_factory.h ├── main.cppCMakeLists.txt是核心直接写cmake_minimum_required(VERSION 3.16) project(FactoryDemo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) add_executable(factory_demo main.cpp message.cpp )这里把C标准设成17因为后面现代C工厂要用到std::variant、std::optional、if constexpr这些特性。在VSCode里按F1打开命令面板输入CMake: Configure选择编译套件再按F7构建就完成了最基本的验证。如果你的环境是C11或C14也别担心文章前面那三种传统工厂形态只用到智能指针和虚函数C11完全能跑。现代C部分我会单独标注出需要C17。3.3 用简单工厂重构从if-else到统一入口重构第一步先给处理器定义统一的抽象基类// message.h #pragma once #include memory #include string class MessageProcessor { public: virtual ~MessageProcessor() default; virtual void Process(const std::string rawData) 0; virtual std::string Name() const 0; }; class LoginProcessor : public MessageProcessor { public: void Process(const std::string rawData) override; std::string Name() const override { return Login; } }; class MoveProcessor : public MessageProcessor { public: void Process(const std::string rawData) override; std::string Name() const override { return Move; } }; class ChatProcessor : public MessageProcessor { public: void Process(const std::string rawData) override; std::string Name() const override { return Chat; } };在message.cpp里实现每个Processor的具体逻辑。接着写一个工厂类负责根据MessageType创建Processor// message_factory.h #pragma once #include memory #include string #include message.h enum class MessageType { Login, Move, Chat }; class MessageFactory { public: static std::unique_ptrMessageProcessor Create(MessageType type) { switch (type) { case MessageType::Login: return std::make_uniqueLoginProcessor(); case MessageType::Move: return std::make_uniqueMoveProcessor(); case MessageType::Chat: return std::make_uniqueChatProcessor(); default: return nullptr; } } };main.cpp里原先那几百行if-else现在压缩成这么一段// main.cpp #include iostream #include message_factory.h int main() { MessageType types[] { MessageType::Login, MessageType::Move, MessageType::Chat }; for (auto type : types) { std::unique_ptrMessageProcessor processor MessageFactory::Create(type); if (processor) { std::cout Handling: processor-Name() std::endl; processor-Process(raw data); } } return 0; }编译运行后控制台会依次输出Login、Move、Chat对应的处理结果。这个重构的好处是业务端只和MessageProcessor抽象基类打交道新加一种消息时只需要新增一个Processor类然后去MessageFactory里补一个case。当然这里补case仍然是要改工厂的所以它是简单工厂不是完全符合开闭原则。等消息类型再膨胀我建议直接跳到第4章的注册表工厂方案。3.4 重构过程中踩过的三个坑这个项目麻雀虽小五脏俱全。我实际编码时踩了几个坑逐一分享下第一个坑是容易忘记delete堆上对象。用裸指针new返回对象客户端一旦忘记delete就内存泄漏。这是老生常谈但几乎每个C项目都会遇到。我的解法很直接工厂返回值一律用std::unique_ptr让对象的生命周期随作用域自动收尾。第二个坑是switch分支返回后编译器警告control reaches end of non-void function。当MessageType里加上新枚举值而switch没有对应的case时有的编译器会提示警告。解决方法是加上default分支返回nullptr或者干脆用std::unordered_map替代switch这样新增消息类型时甚至可以做到“不改工厂代码”。第三个坑比较隐蔽处理器对象创建时可能需要读取配置文件。如果这个逻辑塞在工厂里工厂就会越来越臃肿。后来我把“创建”和“初始化”分开了工厂只负责构造对象初始化逻辑放在Processor自身的Init方法里。这算是对单一职责原则的一个应用。4. 实战二现代C下用注册表实现零改动扩展4.1 使用std::function与unordered_map替代if-else传统简单工厂最大的痛点是每加一种产品就要去改工厂类。有没有办法让“新增产品类型”时完全不碰工厂代码答案是用注册表工厂把“创建函数”和“类型名”的对应关系存到一个std::unordered_map里新类型只需在自身源文件里注册一个创建器工厂本体永远不用动。我实际项目里常用这种写法// registry_factory.h #pragma once #include functional #include memory #include string #include unordered_map class MessageProcessor { public: virtual ~MessageProcessor() default; virtual void Process(const std::string rawData) 0; virtual std::string Name() const 0; }; using ProcessorCreator std::functionstd::unique_ptrMessageProcessor(); class ProcessorRegistry { public: static ProcessorRegistry Instance() { static ProcessorRegistry instance; return instance; } void Register(const std::string name, ProcessorCreator creator) { creators_[name] std::move(creator); } std::unique_ptrMessageProcessor Create(const std::string name) { auto it creators_.find(name); if (it creators_.end()) { return nullptr; } return it-second(); } private: ProcessorRegistry() default; std::unordered_mapstd::string, ProcessorCreator creators_; }; // 注册辅助类利用静态成员变量的初始化时机执行注册 class ProcessorRegistrar { public: ProcessorRegistrar(const std::string name, ProcessorCreator creator) { ProcessorRegistry::Instance().Register(name, std::move(creator)); } };注册方式有两种我推荐第二种因为它在文件底部一行搞定不需要额外调用初始化函数。第一种是在main函数里手动注册int main() { auto registry ProcessorRegistry::Instance(); registry.Register(Login, [] { return std::make_uniqueLoginProcessor(); }); registry.Register(Move, [] { return std::make_uniqueMoveProcessor(); }); // ... }第二种是使用静态注册器在各自的.cpp文件下面写// login_processor.cpp #include registry_factory.h class LoginProcessor : public MessageProcessor { public: void Process(const std::string rawData) override { /* ... */ } std::string Name() const override { return Login; } }; static ProcessorRegistrar loginRegistrar(Login, [] { return std::make_uniqueLoginProcessor(); });这样当login_processor.cpp参与编译时静态对象loginRegistrar的构造函数就会在程序启动时自动执行注册。使用方只需要根据消息名从注册表取处理器std::string name Login; auto processor ProcessorRegistry::Instance().Create(name); if (processor) { processor-Process(data); }新加消息类型时你只需要新写一个xxx_processor.cpp里面定义Processor类并放一个静态注册器连main函数都不需要动。这才是真正意义上的开闭原则。4.2 静态注册器可能遇到的两个问题问题一链接器把静态对象优化掉了。这是静态注册方式最常踩的坑。如果你把各处理器的.cpp编成静态库而main程序里没有直接引用这些目标文件链接器可能认为这些符号是“未引用的”直接不把它们链接进最终可执行文件。结果就是注册器根本没运行map是空的。解决方式有几种在CMake里把整个目录的源文件直接加入add_executable不要编成静态库。编成静态库时在main里显式引用一下每个源文件提供的某个符号比如加一个全局函数bool LoginProcessorInit();main里调用它。用链接器选项来强制保留所有目标文件比如-Wl,--whole-archive但一般不推荐。从维护成本来看我自己的经验是如果项目不大直接把所有源文件加进add_executable最省心如果项目大到需要拆库再考虑显式初始化。问题二静态对象初始化顺序不确定。如果注册器A依赖注册器B而B还没注册A就会拿到空。不同编译单元的静态初始化顺序在C标准里是不确定的如果一个注册器依赖另一个注册器就属于“静态初始化顺序fiasco”。实际工程里注册器之间必须做到没有依赖注册本身只做“往map里放一个创建函数”不要在这个阶段做任何复杂的业务逻辑就能避免这个问题。4.3 用std::variant和if constexpr做编译期工厂有些场景里产品类型在编译期是可以枚举的比如有限状态机里的状态对象、表达式求值里的AST节点。这时候再用运行时注册表反而绕远了更优雅的是用std::variant加if constexpr在编译期就完成类型分发。举个例子假设我们有三种命令加法、减法和乘法。传统做法是写一个虚函数接口然后每种命令一个类。但如果我们只需要“根据命令类型执行对应逻辑”其实可以用variant存参数用std::visit做编译期分派#include variant #include iostream struct AddCommand { int a; int b; }; struct SubCommand { int a; int b; }; struct MulCommand { int a; int b; }; using Command std::variantAddCommand, SubCommand, MulCommand; int Execute(const Command cmd) { return std::visit([](const auto c) { using T std::decay_tdecltype(c); if constexpr (std::is_same_vT, AddCommand) { return c.a c.b; } else if constexpr (std::is_same_vT, SubCommand) { return c.a - c.b; } else if constexpr (std::is_same_vT, MulCommand) { return c.a * c.b; } }, cmd); } int main() { Command cmd AddCommand{3, 4}; std::cout Execute(cmd) std::endl; // 7 return 0; }这段代码要C17才能编译因为需要std::variant和if constexpr。它把“创建一个类型”转换成了“在编译期选择lambda里的分支”没有虚函数、没有动态内存、没有运行时查找性能上是最优解。它的局限也很明显产品集合必须编译期可枚举不能运行时动态扩展。真实项目中我通常是这样判断的如果产品类型在编译期就能确定优先考虑variant如果需要根据用户配置、消息字段等runtime信息来创建对象才用注册表工厂。5. 面试题里的工厂模式除了背概念还要会讲故事5.1 面试官为什么总爱问工厂模式C面试对设计模式的考察工厂模式是出镜率特别高的一个。因为它在“简单类设计”和“复杂软件架构”之间架了座桥能考察一个人的抽象思维又不至于像大结构设计模式那样空泛。热词里的“C八股文”“C面试题”反映出大家确实在卷这类问题。常见的说法“简单工厂、工厂方法、抽象工厂的区别”“工厂模式解决了什么问题违反了哪些设计原则”“为什么不用直接new非要包一层工厂”“你的项目里怎么用工厂模式的”如果只背定义面试官一听就腻。谈关键点是理解它背后的设计原则面向接口编程、把变的部分和不变的部分分离。5.2 有说服力的回答结构我的建议是回答时按“场景 → 问题 → 方案 → 代价”四步走。直接上例子“我在做一个消息分发模块时前期用if-else根据消息类型创建处理器类型一旦变多函数膨胀得很严重而且每加一种消息都要修改公共代码回归风险高。后来我用简单工厂把创建逻辑收敛到一个类里调用方只依赖MessageProcessor抽象类。再往后消息类型更多了简单工厂自身也膨胀就把switch改成了注册表用unordered_map存创建器新消息类型只需要新增一个源文件在文件里用静态注册器注册旧代码一行都不用改。代价是多了几个类还有静态注册器要考虑链接期被优化掉和静态初始化顺序的问题。”这段回答既覆盖了三种工厂形态又展示了真实工程思考强过单纯背“优点缺点”。5.3 容易被追问的两个坑面试官很喜欢追问的一个点是“工厂模式一定符合开闭原则吗”。答案是否定的。简单工厂添加产品时要修改工厂方法本身违反开闭原则工厂方法/抽象工厂通过继承扩展更符合开闭原则但代价是类数量膨胀。完整答案应该包含这个“代价”视角。第二个高频追问是“工厂模式和单例模式能一起用吗”。我的观点是能。常见的做法是把工厂本身设计成单例比如前面的ProcessorRegistry就用了静态局部变量实现单例。这里要小心如果工厂持有没有线程安全的map多线程并发注册或查询时会出问题。C11起局部静态变量的初始化是线程安全的但map的读写本身不是需要在注册和查询时加锁或者用std::call_once。6. 性能开销、内存管理与架构权衡6.1 工厂模式真的会拖慢程序吗这是被问得最多的问题之一。我的判断是绝大多数业务系统里工厂模式的性能开销可以忽略不计。一次虚函数调用在现代CPU上也就是几个纳秒一个unordered_map查找也就是几十纳秒相比网络IO、数据库访问、日志写入完全不在一个数量级。真正要注意性能的是高频创建短生命周期对象的场景比如每秒钟创建和销毁几百万个小对象这时候工厂加虚函数的成本就不可忽略了。应对高频场景常见做法是对象池工厂创建出来的对象不立即销毁而是放回池中复用。这种思路在游戏服务器、数据库连接池、线程池里非常常见。如果连虚函数调用都想省就可以用std::variant做编译期分派运行时开销几乎为零。6.2 内存管理千万别在工厂里返回裸指针工厂模式的返回值强烈建议使用std::unique_ptr除非你的对象生命周期需要跨模块共享那才考虑shared_ptr。原因很简单工厂是创建方客户端是使用方两者之间没有明确的“谁负责销毁”的约定时裸指针就是内存泄漏的温床。我在第3章的实际代码里已经用了std::unique_ptr。这里补充一个细节如果你的工厂要返回基类指针而具体产品类的析构函数不是虚函数那delete掉基类指针是未定义行为。所以基类析构函数必须声明为virtual这是C多态的最低要求。我见过太多人把基类析构函数漏掉结果程序跑着跑着随机崩溃排查半天才发现是这个原因。6.3 架构层面的最终建议工厂模式不是银弹它解决的是“对象创建”这个环节的问题但解决不了系统的整体模块划分、依赖关系设计问题。我这些年做项目的体会是小项目、产品类型少先用简单工厂甚至直接用if-else别过度设计。产品类型多、可能扩展用注册表工厂让新增类型不碰老代码。产品类型编译期可枚举优先考虑std::variant省掉虚函数开销。产品族伴随平台差异抽象工厂才是正确选项。如果你正在写的代码里出现了一个方法接了五个bool参数然后创建六种对象或者一个函数里有三层if-else只为了选类型那这就是工厂模式该出场的信号。但如果你只有两个产品类型且不会变老老实实new也没毛病过度封装反而会让阅读代码的人抓狂。另外再分享一个我平时特别喜欢的小技巧用工厂做单元测试的替身注入。你的业务代码依赖抽象接口在测试代码里注册一个Mock创建器到工厂生产代码注册真实创建器。切换环境只需要改注册阶段服务逻辑完全不用动。这个做法在依赖注入框架里叫“控制反转”用工厂模式手动实现时代码结构清晰、直观特别适合不想引入大型框架的中小型项目。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。