C++设计模式:让代码从“能跑”到“好维护”

📅 2026/7/3 3:08:19
C++设计模式:让代码从“能跑”到“好维护”
引言很多算法竞赛选手和初学者都有一个共同的困惑我写的代码能跑、能AC但过了一个月自己都看不懂了加一个新功能要改十几个文件修一个bug引出了三个新bug……这不是你的代码写得“不对”而是没有用设计模式来组织代码。设计模式不是某种具体的算法或数据结构而是解决特定问题的经验总结——是无数前辈在长期软件开发中总结出来的“最佳实践”。如果说算法是“解题的招式”那么设计模式就是“盖房子的结构图”——招式决定你能不能打赢结构图决定你的房子能住多久、能不能加层、好不好修漏水。前置知识在学习设计模式之前你需要了解几个面向对象的基本概念封装把数据和操作数据的方法放在一起对外隐藏内部实现。继承子类复用父类的属性和方法。多态同一接口在不同对象上有不同表现虚函数。抽象类/接口定义一组方法签名由子类具体实现。组合优于继承优先使用对象组合而非类继承来复用代码。C中的关键语法virtual关键字定义虚函数支持多态。override关键字显式标明重写基类虚函数。std::unique_ptr/std::shared_ptr智能指针管理对象生命周期。第一章设计模式分类——3种模式的“导航图”设计模式通常分为三大类类型解决的问题代表模式创建型如何创建对象单例、工厂、建造者结构型如何组合类和对象适配器、装饰器、代理行为型如何分配职责和算法观察者、策略、命令本文选取最常用的五种模式详细讲解单例、工厂、观察者、策略、装饰器。第二章单例模式Singleton—— “全局唯一人人共享”2.1 问题场景某些类在整个程序中只能有一个实例配置管理器全局配置只需一份日志记录器所有模块写同一个日志文件线程池全局统一的线程管理如果不用单例模式每个模块都会创建自己的实例导致资源浪费和数据不一致。2.2 经典实现C11线程安全版C11 之后静态局部变量的初始化是线程安全的可以用最简洁的方式实现单例cppclass Logger { public: // 禁止拷贝和赋值 Logger(const Logger) delete; Logger operator(const Logger) delete; // 全局访问点返回唯一实例的引用 static Logger getInstance() { static Logger instance; // C11保证线程安全 return instance; } void log(const std::string msg) { std::cout [LOG] msg std::endl; } private: Logger() {} // 私有构造函数防止外部创建 }; // 使用 int main() { Logger::getInstance().log(程序启动); }2.3 优缺点优点缺点全局唯一节省资源隐藏了依赖关系难以测试延迟初始化第一次使用时才创建多线程下需小心C11后已解决实现简单违反单一职责原则既管理自己又管理唯一性第三章工厂模式Factory—— “把创建过程藏起来”3.1 问题场景假设你有一个Shape基类派生类有Circle、Rectangle、Triangle。客户端代码需要根据输入创建不同的形状cppShape* s; if (type circle) s new Circle(); else if (type rectangle) s new Rectangle(); else if (type triangle) s new Triangle();这种写法的问题每次新增形状都要修改这段代码违反开闭原则。创建逻辑分散在各处难以维护。工厂模式把“创建哪种对象”的决策集中到一个地方。3.2 简单工厂cppclass ShapeFactory { public: static Shape* create(const std::string type) { if (type circle) return new Circle(); if (type rectangle) return new Rectangle(); if (type triangle) return new Triangle(); return nullptr; } }; // 使用 Shape* s ShapeFactory::create(circle);3.3 工厂方法更优雅工厂方法让子类决定创建哪种对象符合开闭原则cpp// 抽象工厂 class Factory { public: virtual Shape* create() 0; }; // 具体工厂每个形状对应一个工厂 class CircleFactory : public Factory { public: Shape* create() override { return new Circle(); } }; class RectangleFactory : public Factory { public: Shape* create() override { return new Rectangle(); } }; // 使用 Factory* f new CircleFactory(); Shape* s f-create();3.4 应用场景游戏中的“怪物生成器”不同关卡生成不同怪物。GUI框架中的“控件创建”根据配置创建按钮、文本框等。数据库连接池根据配置创建不同类型的数据库连接。第四章观察者模式Observer—— “一有动静全员通知”4.1 问题场景当股票价格变化时需要更新所有显示器上的价格。当用户点击按钮时需要触发多个响应播放音效、记录日志、更新界面。当机器人检测到障碍物时需要通知避障模块、紧急制动模块、报警模块。观察者模式定义了对象间的“一对多”依赖关系当一个对象被观察者状态改变时所有依赖它的对象观察者都会自动收到通知。4.2 结构text------------- --------------- | Subject | | Observer | | (被观察者) |◄-----| (观察者接口) | ------------- --------------- | attach() | | update() | | detach() | --------------- | notify() | ▲ ------------- │ ▲ │ │ │ -------------- ------------ | ConcreteSubject | | ConcreteObs | ---------------- -------------4.3 C实现cpp// 观察者接口 class Observer { public: virtual void update(float price) 0; virtual ~Observer() default; }; // 被观察者股票 class Stock { private: std::vectorObserver* observers; float price; public: void attach(Observer* obs) { observers.push_back(obs); } void detach(Observer* obs) { /* 从列表中移除 */ } void setPrice(float newPrice) { price newPrice; notifyAll(); } void notifyAll() { for (auto* obs : observers) { obs-update(price); } } }; // 具体观察者显示器 class Display : public Observer { public: void update(float price) override { std::cout 股价更新: price std::endl; } }; // 使用 int main() { Stock stock; Display d1, d2; stock.attach(d1); stock.attach(d2); stock.setPrice(100.5f); // 两个显示器都会自动更新 }4.4 ROS2中的观察者模式ROS2的订阅-发布机制就是观察者模式的典型应用。当你create_subscription时就是在“注册”一个回调函数作为观察者当话题上有新消息时所有回调函数都会被自动调用。第五章策略模式Strategy—— “算法可以随时换”5.1 问题场景一个类需要支持多种算法或行为而且这些算法需要在运行时动态切换。导航系统可以选择“最短路径”“最快路径”“避开收费”三种策略。排序工具可以根据数据规模选择快排、归并、插入排序。机器人的行为可以切换到“巡逻模式”“攻击模式”“避障模式”。5.2 结构text---------------- --------------- | Context | | Strategy | | (上下文/使用方) |---------| (策略接口) | ---------------- --------------- | -strategy | | execute() | | setStrategy() | --------------- | doSomething() | ▲ ---------------- │ ---------------- │ ConcreteStr │ ----------------- │ execute() │ -----------------5.3 C实现cpp// 策略接口 class SortingStrategy { public: virtual void sort(std::vectorint data) 0; virtual ~SortingStrategy() default; }; // 具体策略快速排序 class QuickSort : public SortingStrategy { public: void sort(std::vectorint data) override { std::sort(data.begin(), data.end()); std::cout 使用快速排序 std::endl; } }; // 具体策略冒泡排序小数据时用 class BubbleSort : public SortingStrategy { public: void sort(std::vectorint data) override { // 冒泡排序实现... std::cout 使用冒泡排序 std::endl; } }; // 上下文 class Sorter { private: std::unique_ptrSortingStrategy strategy; public: void setStrategy(std::unique_ptrSortingStrategy s) { strategy std::move(s); } void sort(std::vectorint data) { if (strategy) strategy-sort(data); } }; // 使用 int main() { Sorter sorter; std::vectorint data {5, 2, 8, 1, 9}; sorter.setStrategy(std::make_uniqueQuickSort()); sorter.sort(data); // 使用快排 sorter.setStrategy(std::make_uniqueBubbleSort()); sorter.sort(data); // 使用冒泡 }5.4 策略模式 vs 状态模式策略模式算法切换由外部决定Context 不知道具体策略。状态模式状态切换由内部决定状态变化会影响 Context 的行为。第六章装饰器模式Decorator—— “给对象穿衣服”6.1 问题场景你想给一个对象动态地添加额外功能而不是通过继承静态地扩展。咖啡店点了一杯拿铁可以加糖、加奶泡、加巧克力、加奶油……每一种添加都是“装饰”。机器人给机器人添加传感器激光雷达、摄像头、IMU来扩展功能而不是为每种组合写一个新类。6.2 结构text------------- --------------- | Component | | Decorator | | (抽象构件) |◄---------| (抽象装饰器) | ------------- --------------- | operation()| | operation() | ------------- --------------- ▲ ▲ │ │ ------------ ------------ | ConcreteComp| | ConcreteDec | ------------- -------------6.3 C实现cpp// 抽象构件 class Coffee { public: virtual std::string getDescription() 0; virtual double getCost() 0; virtual ~Coffee() default; }; // 具体构件拿铁 class Latte : public Coffee { public: std::string getDescription() override { return 拿铁; } double getCost() override { return 20.0; } }; // 抽象装饰器 class CoffeeDecorator : public Coffee { protected: std::unique_ptrCoffee coffee; public: CoffeeDecorator(std::unique_ptrCoffee c) : coffee(std::move(c)) {} }; // 具体装饰器加糖 class Sugar : public CoffeeDecorator { public: Sugar(std::unique_ptrCoffee c) : CoffeeDecorator(std::move(c)) {} std::string getDescription() override { return coffee-getDescription() 糖; } double getCost() override { return coffee-getCost() 2.0; } }; // 具体装饰器加奶泡 class MilkFoam : public CoffeeDecorator { public: MilkFoam(std::unique_ptrCoffee c) : CoffeeDecorator(std::move(c)) {} std::string getDescription() override { return coffee-getDescription() 奶泡; } double getCost() override { return coffee-getCost() 3.0; } }; // 使用 int main() { auto myCoffee std::make_uniqueLatte(); myCoffee std::make_uniqueSugar(std::move(myCoffee)); myCoffee std::make_uniqueMilkFoam(std::move(myCoffee)); std::cout myCoffee-getDescription() std::endl; // 拿铁 糖 奶泡 std::cout 价格: myCoffee-getCost() std::endl; // 25.0 }6.4 优点开闭原则不需要修改原有类就能添加新功能。避免类爆炸如果用继承实现功能组合7种添加会有2^7128个类。第七章设计模式的选择指南场景推荐模式确保某个类只有一个实例单例模式创建对象时逻辑复杂想要集中管理工厂模式一个对象变化需要通知多个其他对象观察者模式需要在运行时切换算法或行为策略模式需要动态地给对象增加功能装饰器模式一个接口不兼容需要适配适配器模式提供统一的接口访问子系统外观模式总结设计模式不是“银弹”——用了不一定好不用不一定错。但掌握它们能让你写出更可维护的代码模块间耦合度低修改一个地方不影响其他地方。写出更可扩展的代码新增功能时不需要重写旧代码。写出更可读的代码模式名称本身就是一种“文档”告诉读者你的设计意图。和团队高效沟通“这里用观察者模式”比解释5分钟更高效。学习设计模式的三个阶段模仿阶段照着例子写理解每个模式的结构。识别阶段看别人的代码时能认出用了什么模式。应用阶段在自己的设计中主动选择合适的模式。