ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C++ 设计模式实战指南:一文搞懂 10 大核心模式(附代码)

C++ 设计模式实战指南:一文搞懂 10 大核心模式(附代码) 一、创建型模式1. 单例模式 Singleton全局唯一别乱用一句话一个类在全局只有一个实例大家共用它。生活类比公司只有一个前台。谁来都找她但她只有一个。核心角色Singleton自己负责创建唯一实例并提供全局访问点。C11 推荐写法Meyers Singleton#include string #include unordered_map class ConfigManager { public: static ConfigManager instance() { static ConfigManager inst; // C11 起静态局部变量初始化线程安全 return inst; } void set(const std::string key, const std::string value) { data_[key] value; } std::string get(const std::string key) const { auto it data_.find(key); return it ! data_.end() ? it-second : ; } ConfigManager(const ConfigManager) delete; ConfigManager operator(const ConfigManager) delete; private: ConfigManager() default; std::unordered_mapstd::string, std::string data_; };用法ConfigManager::instance().set(host, 127.0.0.1); auto host ConfigManager::instance().get(host);为什么推荐 Meyers Singleton什么时候用懒加载第一次调用才创建。线程安全C11 保证静态局部变量初始化是线程安全的。自动析构程序结束自动释放。不用手动写双检锁。全局配置日志器连接池线程池全局缓存工程坑全局状态难测试。单元测试之间互相影响。依赖隐藏。看函数签名不知道它依赖了单例。多线程析构顺序。程序退出时单例之间互相引用可能出问题。能不用就不用。优先依赖注入把对象传进去。饿汉 vs 懒汉饿汉程序启动时就创建好实例不管用不用。优点是简单、天然线程安全缺点是启动变慢、浪费资源。懒汉第一次调用时才创建实例。优点是按需加载、省资源缺点是并发下要处理线程安全。双检锁Double-Checked Locking是什么双检锁是一种用于「懒汉式单例」的线程安全优化技巧。核心思路是先不加锁检查一次实例是否已存在如果不存在才加锁加锁后再检查一次确认确实没有才创建实例。这样既能保证线程安全又能避免每次调用都加锁带来的性能开销。典型写法如下class Singleton { public: static Singleton* instance() { if (inst_ nullptr) { // 第一次检查不加锁快速返回 std::lock_guardstd::mutex lock(mutex_); if (inst_ nullptr) { // 第二次检查加锁后确认 inst_ new Singleton(); } } return inst_; } private: Singleton() default; static Singleton* inst_; static std::mutex mutex_; };为什么要检查两次因为第一次检查可以过滤掉「实例已经创建」的绝大多数情况避免每次都抢锁第二次检查则保证在并发下只有一个线程真正执行创建。为什么要volatile双检锁里volatile是为了防止指令重排。如果不加volatile编译器或 CPU 可能先把「分配内存」和「赋值指针」重排到「调用构造函数」之前导致另一个线程拿到一个未初始化完成的对象。C11 之后推荐直接用 Meyers Singleton静态局部变量的初始化由标准保证线程安全就不用再手写双检锁了。单例能不能被继承怎么防止反射/克隆单例类可以被继承但子类无法再创建第二个实例因为构造函数是私有的。C 里没有反射机制所以不存在反射创建的问题要防止克隆直接把拷贝构造函数和赋值运算符声明为delete即可这也是 Meyers Singleton 示例里的做法。2. 工厂模式 Factory要对象找工厂工厂其实有三种常见形态形态一句话特点简单工厂一个工厂造所有产品简单但违反开闭工厂方法每个产品一个工厂扩展好类变多抽象工厂造一族产品跨平台、换整套2.1 简单工厂#include memory #include string class Logger { public: virtual ~Logger() default; virtual void log(const std::string msg) 0; }; class ConsoleLogger : public Logger { public: void log(const std::string msg) override { // std::cout [Console] msg std::endl; } }; class FileLogger : public Logger { public: void log(const std::string msg) override { // 写文件 } }; enum class LoggerType { Console, File }; class LoggerFactory { public: static std::unique_ptrLogger create(LoggerType type) { switch (type) { case LoggerType::Console: return std::make_uniqueConsoleLogger(); case LoggerType::File: return std::make_uniqueFileLogger(); } return nullptr; } };用法auto logger LoggerFactory::create(LoggerType::Console); logger-log(hello);简单工厂不是 GoF 23 里的标准模式但工程里最常用。缺点是新增日志类型要改switch。2.2 工厂方法class LoggerFactory { public: virtual ~LoggerFactory() default; virtual std::unique_ptrLogger create() 0; }; class ConsoleLoggerFactory : public LoggerFactory { public: std::unique_ptrLogger create() override { return std::make_uniqueConsoleLogger(); } }; class FileLoggerFactory : public LoggerFactory { public: std::unique_ptrLogger create() override { return std::make_uniqueFileLogger(); } };用法std::unique_ptrLoggerFactory factory std::make_uniqueConsoleLoggerFactory(); auto logger factory-create(); logger-log(hello);新增产品时加一个工厂类不改老代码。2.3 抽象工厂抽象工厂造的是「产品族」。比如Windows 按钮 Windows 文本框Mac 按钮 Mac 文本框class Button { public: virtual ~Button() default; virtual void paint() 0; }; class TextBox { public: virtual ~TextBox() default; virtual void paint() 0; }; class GUIFactory { public: virtual ~GUIFactory() default; virtual std::unique_ptrButton createButton() 0; virtual std::unique_ptrTextBox createTextBox() 0; }; class WindowsFactory : public GUIFactory { public: std::unique_ptrButton createButton() override { /* WindowsButton */ } std::unique_ptrTextBox createTextBox() override { /* WindowsTextBox */ } }; class MacFactory : public GUIFactory { public: std::unique_ptrButton createButton() override { /* MacButton */ } std::unique_ptrTextBox createTextBox() override { /* MacTextBox */ } };什么时候用工厂创建逻辑复杂客户端不关心具体类需要统一管理对象创建需要替换产品族工程坑类爆炸。每个产品一个工厂。简单场景别硬上工厂直接make_unique更清晰。工厂可以配合注册表实现可扩展。面试点工厂方法 vs 抽象工厂简单工厂为什么不满足开闭原则工厂和策略的区别工厂管创建策略管行为。3. 建造者模式 Builder复杂对象一步步组装一句话参数太多别写一长串构造函数用建造者一步步拼。生活类比点奶茶选杯型、温度、糖度、加料最后下单。核心角色Builder抽象建造者ConcreteBuilder具体建造者Director导演负责组装流程Product最终产品C 链式建造者示例#include string struct HttpRequest { std::string url; std::string method; std::string body; int timeout 30; }; class HttpRequestBuilder { public: HttpRequestBuilder url(std::string v) { url_ std::move(v); return *this; } HttpRequestBuilder method(std::string v) { method_ std::move(v); return *this; } HttpRequestBuilder body(std::string v) { body_ std::move(v); return *this; } HttpRequestBuilder timeout(int v) { timeout_ v; return *this; } HttpRequest build() const { return {url_, method_, body_, timeout_}; } private: std::string url_; std::string method_ GET; std::string body_; int timeout_ 30; };用法HttpRequest req HttpRequestBuilder() .url(https://example.com) .method(POST) .body({\name\:\cpp\}) .timeout(10) .build();什么时候用对象有很多可选参数构造顺序有要求需要生成不同表示不希望暴露内部构造细节工程坑链式调用返回引用注意对象生命周期。build()后最好让产品不可变避免被改坏。如果参数没那么多直接构造函数或聚合初始化更好。面试点建造者 vs 工厂工厂关注「创建哪个」建造者关注「怎么一步步创建」。建造者 vs 构造函数可读性、可选参数、分步构建。二、结构型模式4. 适配器模式 Adapter老接口不兼容加个转接头一句话把一个类的接口转换成客户端想要的另一个接口。生活类比国标插头插不进美标插座加个转换头。两种写法类适配器多继承不推荐C 容易菱形继承。对象适配器组合推荐。对象适配器代码#include string class OldPrinter { public: void printOld(const std::string text) { // 老接口 } }; class NewPrinter { public: virtual ~NewPrinter() default; virtual void print(const std::string text) 0; }; class PrinterAdapter : public NewPrinter { public: void print(const std::string text) override { old_.printOld(text); } private: OldPrinter old_; };什么时候用旧系统改造第三方库接口不匹配统一多个不同接口工程坑不要为了适配而适配。能改原接口就改。适配器只做接口转换不要塞业务逻辑。注意适配器生命周期和线程安全。面试点适配器 vs 装饰器适配器改接口装饰器加功能。适配器 vs 代理代理接口不变适配器接口改变。5. 装饰器模式 Decorator奶茶加珍珠、加椰果一句话动态给对象加功能一层包一层。生活类比一杯咖啡可以加奶、加糖、加奶油核心还是咖啡。核心角色Component抽象组件ConcreteComponent具体组件Decorator装饰器基类持有组件ConcreteDecorator具体装饰C 代码#include memory class Coffee { public: virtual ~Coffee() default; virtual double cost() const 0; }; class Espresso : public Coffee { public: double cost() const override { return 15.0; } }; class CoffeeDecorator : public Coffee { public: explicit CoffeeDecorator(std::unique_ptrCoffee inner) : inner_(std::move(inner)) {} protected: std::unique_ptrCoffee inner_; }; class Milk : public CoffeeDecorator { public: using CoffeeDecorator::CoffeeDecorator; double cost() const override { return inner_-cost() 3.0; } }; class Sugar : public CoffeeDecorator { public: using CoffeeDecorator::CoffeeDecorator; double cost() const override { return inner_-cost() 1.0; } };用法std::unique_ptrCoffee coffee std::make_uniqueEspresso(); coffee std::make_uniqueMilk(std::move(coffee)); coffee std::make_uniqueSugar(std::move(coffee)); // coffee-cost() 19C 里的典型应用IO 流std::ifstream、std::stringstream、缓冲区。网络协议TCP 上叠 TLS、压缩、日志。权限包装基础服务上叠鉴权、限流、缓存。什么时候用需要动态、可撤销地加功能。继承会导致子类爆炸。功能可以自由组合。工程坑小对象变多调试时调用栈深。装饰器和被装饰对象接口必须一致。用unique_ptr管理生命周期注意std::move。面试点装饰器 vs 继承继承静态装饰器动态。装饰器 vs 代理装饰器增强功能代理控制访问。6. 代理模式 Proxy经纪人先拦一道一句话为其他对象提供代理控制对它的访问。生活类比明星的经纪人。你想找明星先找经纪人。常见代理虚拟代理懒加载保护代理权限控制远程代理本地代表远程对象智能指针C 里最典型的代理虚拟代理代码懒加载大图#include memory #include string class Image { public: virtual ~Image() default; virtual void draw() 0; }; class RealImage : public Image { public: explicit RealImage(std::string file) : file_(std::move(file)) { // 模拟加载大图 } void draw() override { // 绘制 } private: std::string file_; }; class ImageProxy : public Image { public: explicit ImageProxy(std::string file) : file_(std::move(file)) {} void draw() override { if (!real_) { real_ std::make_uniqueRealImage(file_); } real_-draw(); } private: std::string file_; std::unique_ptrRealImage real_; };什么时候用对象创建开销大延迟创建。需要权限校验。需要远程调用本地代表。需要引用计数、写时复制。工程坑代理和真实对象接口要一致。代理不要变成「万能中间层」否则逻辑臃肿。智能指针是代理但别乱用shared_ptr造成循环引用。面试点代理 vs 装饰器代理偏控制访问装饰器偏增强功能。代理 vs 适配器代理接口不变适配器改接口。三、行为型模式7. 观察者模式 Observer公众号一发订阅者全收到一句话一对多依赖一个对象状态改变所有依赖者自动更新。生活类比你关注了公众号公众号发文章你收到通知。核心角色Subject主题维护观察者列表。Observer观察者定义更新接口。ConcreteSubject具体主题。ConcreteObserver具体观察者。传统接口写法#include vector #include algorithm class Observer { public: virtual ~Observer() default; virtual void update(int value) 0; }; class Subject { public: void attach(Observer* o) { observers_.push_back(o); } void detach(Observer* o) { observers_.erase( std::remove(observers_.begin(), observers_.end(), o), observers_.end() ); } void notify(int value) { for (auto* o : observers_) { o-update(value); } } private: std::vectorObserver* observers_; };现代 C 简化std::function#include functional #include vector class Subject { public: using Observer std::functionvoid(int); void subscribe(Observer obs) { observers_.push_back(std::move(obs)); } void notify(int value) { for (auto obs : observers_) { obs(value); } } private: std::vectorObserver observers_; };用法Subject subject; subject.subscribe([](int v) { // 收到通知 }); subject.notify(42);什么时候用事件系统UI 刷新消息订阅信号槽模型-视图解耦工程坑生命周期观察者被销毁后主题还拿着指针通知时崩溃。循环引用观察者也持有主题shared_ptr互相引用。通知中删除观察者遍历时删除会导致迭代器失效。多线程通知和订阅可能并发需要加锁。解决生命周期用std::weak_ptr或信号槽库。面试点观察者 vs 发布订阅观察者通常直接通知发布订阅有 Broker。如何避免通知时对象已销毁weak_ptr、自动反注册。8. 策略模式 Strategy算法可替换一句话定义一系列算法封装起来让它们可以互相替换。生活类比去机场可以打车、地铁、自驾。目的地一样方式可换。核心角色Strategy抽象策略ConcreteStrategy具体策略Context持有策略调用策略传统接口写法#include memory #include vector class SortStrategy { public: virtual ~SortStrategy() default; virtual void sort(std::vectorint data) 0; }; class BubbleSort : public SortStrategy { public: void sort(std::vectorint data) override { // 冒泡排序 } }; class QuickSort : public SortStrategy { public: void sort(std::vectorint data) override { // 快速排序 } }; class Sorter { public: explicit Sorter(std::unique_ptrSortStrategy strategy) : strategy_(std::move(strategy)) {} void setStrategy(std::unique_ptrSortStrategy strategy) { strategy_ std::move(strategy); } void run(std::vectorint data) { strategy_-sort(data); } private: std::unique_ptrSortStrategy strategy_; };现代 C 简化std::function#include functional #include vector class Sorter { public: using Strategy std::functionvoid(std::vectorint); explicit Sorter(Strategy strategy) : strategy_(std::move(strategy)) {} void run(std::vectorint data) { strategy_(data); } private: Strategy strategy_; };用法Sorter sorter([](std::vectorint data) { // 快速排序 });什么时候用支付方式微信、支付宝、银行卡。排序算法冒泡、快排、归并。压缩算法zip、rar、7z。折扣计算满减、打折、原价。工程坑策略选择逻辑放哪里可以配合工厂。策略太多时用注册表管理。策略和状态容易混策略由客户端选状态自动切换。面试点策略 vs 工厂工厂创建对象策略替换算法。策略 vs 状态策略外部选择状态内部驱动。9. 命令模式 Command请求对象化支持撤销一句话把请求封装成对象可以排队、记录、撤销。生活类比遥控器按钮。按一下开灯再按一下撤销。核心角色Command命令接口ConcreteCommand具体命令Invoker调用者比如遥控器Receiver接收者比如灯Client组装命令C 代码支持撤销的遥控器#include memory #include vector class Command { public: virtual ~Command() default; virtual void execute() 0; virtual void undo() 0; }; class Light { public: void on() { /* 开灯 */ } void off() { /* 关灯 */ } }; class LightOnCommand : public Command { public: explicit LightOnCommand(Light light) : light_(light) {} void execute() override { light_.on(); } void undo() override { light_.off(); } private: Light light_; }; class RemoteControl { public: void press(std::unique_ptrCommand cmd) { cmd-execute(); history_.push_back(std::move(cmd)); } void undo() { if (!history_.empty()) { history_.back()-undo(); history_.pop_back(); } } private: std::vectorstd::unique_ptrCommand history_; };什么时候用撤销/重做任务队列宏命令日志记录远程调用工程坑命令类可能爆炸。可以用std::function简化。撤销要保存状态注意内存。命令和策略容易混命令强调「请求对象化」策略强调「算法替换」。面试点命令 vs 策略命令可撤销、可排队策略是算法替换。如何实现撤销保存命令历史反向执行。10. 模板方法 Template Method父类定骨架子类填步骤一句话父类定义算法骨架某些步骤延迟到子类实现。生活类比泡面包装写好步骤烧水、放面、放调料、等三分钟。你只需要决定加不加蛋。核心角色AbstractClass定义模板方法包含算法骨架。ConcreteClass实现具体步骤。C 代码class DataParser { public: virtual ~DataParser() default; void parse() { // 模板方法通常不要 virtual open(); parseContent(); close(); } protected: virtual void open() 0; virtual void parseContent() 0; virtual void close() 0; }; class CsvParser : public DataParser { protected: void open() override { /* 打开 CSV */ } void parseContent() override { /* 解析 CSV */ } void close() override { /* 关闭 CSV */ } }; class JsonParser : public DataParser { protected: void open() override { /* 打开 JSON */ } void parseContent() override { /* 解析 JSON */ } void close() override { /* 关闭 JSON */ } };现代 C 注意NVI 模式Non-Virtual Interfaceclass Base { public: void run() { // 非虚接口 before(); doRun(); after(); } virtual ~Base() default; private: virtual void doRun() 0; void before() { /* 固定逻辑 */ } void after() { /* 固定逻辑 */ } };好处父类控制流程子类只填步骤。什么时候用框架钩子数据处理流程测试基类编译流程工程坑继承耦合高。能用组合 策略就别硬继承。模板方法一般声明为final或非虚防止子类改流程。钩子函数给默认空实现子类可选覆盖。面试点模板方法 vs 策略模板方法用继承固定骨架策略用组合替换算法。好莱坞原则「别调用我们我们会调用你」。四、常用模式怎么选一张表说清你想解决什么优先考虑全局只要一个实例单例但慎用创建逻辑复杂、不想暴露具体类工厂对象参数多、分步构建建造者老接口不兼容适配器动态加功能、可组合装饰器控制访问、懒加载、权限代理一对多通知观察者算法可替换策略请求要排队、撤销命令流程固定、步骤可变模板方法记忆口诀创建找工厂复杂用建造接口不适配装饰加功能代理控访问观察发通知策略换算法命令可撤销流程定骨架模板方法收。五、最后别为了模式而模式设计模式是「沟通词汇」不是「代码 KPI」。C 里尤其记住组合优于继承。RAII 管资源智能指针管生命周期。能用std::function简化策略、观察者、命令就别写一堆类。单例慎用依赖注入更可测。观察者注意生命周期否则通知时对象已经没了。先写简单代码等变化点出现再重构出模式。把上面这 10 个用熟C 日常开发、面试、工程实践基本够用了。
返回列表