- C++ 设计模式详解,含代码实现、适用场景与 SOLID 原则
- 对应面试问答见 03-Resources/interview/C++ 设计模式
- 完整系列:C++ 基础-初级、C++ 基础-中级、C++ 基础-高级、C++ STL、03-Resources/cpp/C++ 并发编程、03-Resources/cpp/C++ 网络编程、03-Resources/cpp/C++ 数据结构、03-Resources/cpp/C++ 设计模式、03-Resources/cpp/C++ 基本算法、03-Resources/cpp/C++ Linux 编程
- 当前覆盖:SOLID、单例、工厂、建造者、原型、适配器、装饰器、代理、外观、桥接、组合、观察者、策略、模板方法、责任链、状态、命令、迭代器、享元
六大设计原则(SOLID)是什么?
设计原则是设计模式的「底层方法论」:模式是原则在特定场景下的具体落地。中文语境常说「六大设计原则」,即 SOLID 五项 + 迪米特法则。
| 缩写 | 原则 | 一句话解释 |
|---|---|---|
| SRP | 单一职责原则 | 一个类只负责一件事,只有一个引起它变化的原因 |
| OCP | 开闭原则 | 对扩展开放,对修改关闭(加功能不加旧代码) |
| LSP | 里氏替换原则 | 子类必须能完全替换父类,不破坏程序正确性 |
| ISP | 接口隔离原则 | 接口要小而专,客户端不依赖它用不到的接口 |
| DIP | 依赖倒置原则 | 高层与低层都依赖抽象,不依赖具体实现 |
| LoD | 迪米特法则 | 最少知识原则:只和直接朋友说话 |
以「图形绘制」为例,展示 SRP + OCP + DIP 的组合:
// 违反 DIP:高层 Drawer 直接依赖具体类 Circle/Rect,加新图形要改 Drawer
class Drawer {
public:
void drawCircle() { /* ... */ }
void drawRect() { /* ... */ }
// 新增三角形 -> 必须修改 Drawer,违反 OCP
};
// 依赖倒置 + 开闭:面向抽象 Shape 编程
class Shape { // 抽象
public:
virtual ~Shape() = default;
virtual void draw() const = 0;
};
class Circle : public Shape {
public:
void draw() const override { /* 画圆 */ }
};
class Rectangle : public Shape {
public:
void draw() const override { /* 画矩形 */ }
};
class Renderer { // 高层只依赖抽象 Shape
std::vector<std::unique_ptr<Shape>> shapes_;
public:
void add(std::unique_ptr<Shape> s) { shapes_.push_back(std::move(s)); }
void renderAll() const {
for (const auto& s : shapes_) s->draw();
}
};
// 新增 Triangle 只需新增类,Renderer 无需改动 => 开闭原则六大原则里 OCP 是「目标」,其余原则是「手段」:SRP/LSP/ISP/DIP 都是为了能方便地做到对扩展开放、对修改关闭。回答时点出这个递进关系比背定义更出彩。
适用场景与优缺点
- 适用:任何面向对象系统的设计评审;重构时判断「这个类是不是干了两件事」。
- 优点:系统更易维护、易扩展、可测试。
- 缺点:过度设计会徒增类数量与间接层,需在复杂度与收益间权衡。
单例模式(懒汉/饿汉/线程安全)如何实现?
单例保证一个类全局只有一个实例,并提供全局访问点。核心是「私有构造函数 + 静态访问方法」。C++11 后推荐 Meyers 单例(函数内静态局部变量),天然线程安全且无需手动 new。
#include <mutex>
// 1) Meyers 懒汉单例(推荐):C++11 起静态局部变量初始化线程安全
class Singleton {
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance; // 首次调用时初始化,线程安全,进程结束自动析构
return instance;
}
void doWork() { /* ... */ }
private:
Singleton() = default; // 构造函数私有
~Singleton() = default;
};
// 2) 饿汉单例:main 之前即创建,无线程安全顾虑,但可能造成「未使用也创建」
class EagerSingleton {
public:
static EagerSingleton& getInstance() { return instance_; }
private:
EagerSingleton() = default;
static EagerSingleton instance_; // 静态成员定义需在 .cpp 中
};
// EagerSingleton EagerSingleton::instance_;
// 3) 经典双检锁(DCLP):配合 atomic + acquire/release,仅演示,生产优先用 Meyers
class DCLSingleton {
public:
static DCLSingleton* getInstance() {
DCLSingleton* tmp = instance_.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx_);
tmp = instance_.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new DCLSingleton();
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
private:
DCLSingleton() = default;
static std::atomic<DCLSingleton*> instance_;
static std::mutex mtx_;
};- 忘记把拷贝构造/赋值运算符
= delete,导致能复制出第二个实例。 - 双检锁在 C++11 前存在内存序问题,必须用
atomic且正确设置 acquire/release;现在直接写 Meyers 即可。 - 单例的析构顺序在多模块间不可控,跨 DLL/共享库要格外小心。
适用场景与优缺点
- 适用:配置管理器、日志器、连接池、线程池、数据库连接等全局唯一资源。
- 优点:全局唯一、延迟初始化(懒汉)、避免重复资源开销。
- 缺点:隐藏依赖、难测试(难以 mock)、生命周期不可控,滥用会变成「变相全局变量」。
简单工厂、工厂方法、抽象工厂的区别?
三者都是「把对象创建与使用解耦」,区别在于抽象层级与扩展方式:
- 简单工厂:一个工厂类 + 一个静态/普通方法,按参数返回不同产品。缺点:加新产品要改工厂(违反 OCP)。
- 工厂方法:每个产品对应一个工厂类(工厂接口 + 具体工厂),加新产品只需加类不改旧代码。
- 抽象工厂:创建一组相关/相互依赖的产品族(如 Windows 家族 vs Mac 家族)。
#include <memory>
// ---------- 简单工厂 ----------
struct Button { virtual void render() = 0; virtual ~Button() = default; };
struct WinButton : Button { void render() override { /* Windows 按钮 */ } };
struct MacButton : Button { void render() override { /* Mac 按钮 */ } };
class SimpleFactory {
public:
static std::unique_ptr<Button> create(const std::string& os) {
if (os == "win") return std::make_unique<WinButton>();
if (os == "mac") return std::make_unique<MacButton>();
return nullptr;
}
};
// ---------- 工厂方法 ----------
struct Factory { // 抽象工厂
virtual std::unique_ptr<Button> createButton() = 0;
virtual ~Factory() = default;
};
struct WinFactory : Factory {
std::unique_ptr<Button> createButton() override { return std::make_unique<WinButton>(); }
};
struct MacFactory : Factory {
std::unique_ptr<Button> createButton() override { return std::make_unique<MacButton>(); }
};
// ---------- 抽象工厂:一组相关产品(按钮 + 复选框)----------
struct CheckBox { virtual void render() = 0; virtual ~CheckBox() = default; };
struct WinCheckBox : CheckBox { void render() override { /* ... */ } };
struct MacCheckBox : CheckBox { void render() override { /* ... */ } };
struct UIFactory { // 抽象工厂:一个「产品族」
virtual std::unique_ptr<Button> createButton() = 0;
virtual std::unique_ptr<CheckBox> createCheckBox() = 0;
virtual ~UIFactory() = default;
};
struct WinUIFactory : UIFactory {
std::unique_ptr<Button> createButton() override { return std::make_unique<WinButton>(); }
std::unique_ptr<CheckBox> createCheckBox() override { return std::make_unique<WinCheckBox>(); }
};用一句话记住三者递进关系:简单工厂 = 一个函数造所有对象;工厂方法 = 一个工厂造一种对象;抽象工厂 = 一个工厂造一族对象。面试常追问「工厂方法比简单工厂好在哪里」——好在新增产品不违反开闭原则。
适用场景与优缺点
- 适用:UI 跨平台组件、日志框架多后端、支付多渠道、对象创建逻辑复杂且希望集中管理。
- 优点:解耦创建与使用;隐藏创建细节;便于扩展。
- 缺点:类数量增多;抽象工厂新增「产品族内新产品」需改所有工厂接口。
建造者模式如何实现?
建造者将复杂对象的构造过程与其表示分离,允许同样的构建步骤产生不同表示,且支持链式调用(fluent interface)。
#include <string>
// 产品:一台电脑
class Computer {
public:
std::string cpu_, gpu_, ram_, disk_;
void show() const {
// 打印配置
}
};
// 抽象建造者
class Builder {
public:
virtual ~Builder() = default;
virtual Builder& buildCPU(const std::string&) = 0;
virtual Builder& buildGPU(const std::string&) = 0;
virtual Builder& buildRAM(const std::string&) = 0;
virtual Computer build() = 0; // 返回最终产品
};
// 具体建造者
class GameComputerBuilder : public Builder {
Computer c_;
public:
Builder& buildCPU(const std::string& cpu) override { c_.cpu_ = cpu; return *this; }
Builder& buildGPU(const std::string& gpu) override { c_.gpu_ = gpu; return *this; }
Builder& buildRAM(const std::string& ram) override { c_.ram_ = ram; return *this; }
Computer build() override { return c_; }
};
// 指挥者:固定组装流程
class Director {
Builder& b_;
public:
explicit Director(Builder& b) : b_(b) {}
Computer construct() {
return b_.buildCPU("i9").buildGPU("RTX 4090").buildRAM("64G").build();
}
};建造者 vs 工厂:工厂关心「造出哪个类的对象」,一步到位;建造者关心「分步骤构造 + 参数可选组合」,适合构造参数多、有默认值、顺序可变的场景(如 HTTP 请求、SQL 语句、ProtoBuf 消息)。链式 .setA().setB() 是建造者的标志性写法。
适用场景与优缺点
- 适用:构造参数很多且有默认值(配置对象、网络请求)、构建步骤复杂但可复用、需要不可变对象。
- 优点:分步构建、可复用构建流程、链式调用可读性好、隔离复杂构造逻辑。
- 缺点:类数量多;若产品差异不大则引入建造者是过度设计。
原型模式如何实现?
原型通过克隆已有对象来创建新对象,而非 new。适用于创建成本高(如初始化耗时、依赖 DB/网络)的对象。
#include <memory>
class Shape {
public:
virtual ~Shape() = default;
virtual std::unique_ptr<Shape> clone() const = 0; // 原型核心方法
virtual void draw() const = 0;
};
class Circle : public Shape {
double radius_;
public:
explicit Circle(double r) : radius_(r) {}
// 拷贝构造 + clone
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Circle>(*this); // 借助拷贝构造实现深拷贝
}
void draw() const override { /* 画圆 */ }
};
// 使用:原型注册表,按 key 克隆
class ShapeRegistry {
std::unordered_map<std::string, std::unique_ptr<Shape>> prototypes_;
public:
void registerShape(const std::string& key, std::unique_ptr<Shape> p) {
prototypes_[key] = std::move(p);
}
std::unique_ptr<Shape> create(const std::string& key) {
return prototypes_.at(key)->clone(); // 克隆而非 new
}
};C++ 里「拷贝构造/拷贝赋值」默认是浅拷贝。含指针成员时必须实现深拷贝(或对象用智能指针/值语义成员),否则 clone 出的对象共享同一块堆内存,会造成双重释放或数据串改。
适用场景与优缺点
- 适用:对象创建成本高、需要「以一个实例为模板批量复制」、希望隐藏具体类的创建。
- 优点:避免昂贵构造、运行时可动态增删原型、简化创建。
- 缺点:深拷贝实现易出错;对循环引用成员需额外处理。
适配器模式如何实现?
适配器把一个类的接口转换成客户端期望的另一个接口,让原本接口不兼容的类能协同工作。C++ 有两种实现:类适配器(多继承) 与 对象适配器(组合),后者更常用。
#include <memory>
// 已存在的旧接口(被适配者):只有 printText,不接受我们的 Render 接口
class LegacyPrinter {
public:
void printText(const std::string& s) { /* 打印文本 */ }
};
// 客户端期望的新接口
class Printer {
public:
virtual ~Printer() = default;
virtual void render(const std::string& content) = 0;
};
// 对象适配器:内部持有一个 LegacyPrinter,做接口转换
class PrinterAdapter : public Printer {
LegacyPrinter legacy_; // 组合
public:
void render(const std::string& content) override {
legacy_.printText(content); // 适配:render -> printText
}
};
// 使用
void clientCode(Printer& p) { p.render("hello"); }
// PrinterAdapter ad; clientCode(ad);- 适配器:接口不同,改造后复用(“插座转换头”)。
- 装饰器:接口相同,动态增强功能。
- 代理:接口相同,控制访问(延迟加载/权限/远程)。 三者代码结构相似(都「包一层」),区别在意图,面试常拿来对比。
适用场景与优缺点
- 适用:对接第三方库/遗留代码、统一多个相似但不兼容的接口、数据格式转换。
- 优点:复用现有类、符合开闭原则、解耦客户端与被适配者。
- 缺点:增加一层间接;类适配器受 C++ 多继承与访问权限限制。
装饰器模式如何实现?
装饰器在不修改原类的前提下,动态地给对象添加额外职责,且可层层嵌套(如「加糖的加奶的咖啡」)。
#include <memory>
#include <string>
// 组件接口
class Coffee {
public:
virtual ~Coffee() = default;
virtual std::string description() const = 0;
virtual double cost() const = 0;
};
// 具体组件
class SimpleCoffee : public Coffee {
public:
std::string description() const override { return "咖啡"; }
double cost() const override { return 10.0; }
};
// 抽象装饰器:既是一个 Coffee,又持有一个 Coffee
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> inner_;
public:
explicit CoffeeDecorator(std::unique_ptr<Coffee> c) : inner_(std::move(c)) {}
};
// 具体装饰器
class MilkDecorator : public CoffeeDecorator {
public:
using CoffeeDecorator::CoffeeDecorator;
std::string description() const override { return inner_->description() + " + 牛奶"; }
double cost() const override { return inner_->cost() + 2.0; }
};
class SugarDecorator : public CoffeeDecorator {
public:
using CoffeeDecorator::CoffeeDecorator;
std::string description() const override { return inner_->description() + " + 糖"; }
double cost() const override { return inner_->cost() + 1.0; }
};
// 使用:层层包裹
// auto coffee = std::make_unique<SugarDecorator>(
// std::make_unique<MilkDecorator>(std::make_unique<SimpleCoffee>()));
装饰器与继承对比:继承在编译期静态扩展、产生类爆炸;装饰器在运行期动态组合、按需叠加。C++ 中「输入/输出流」(std::fstream、std::istringstream)本质就是装饰器的应用。
适用场景与优缺点
- 适用:需要动态、透明地给对象加功能;职责可自由组合(日志、权限、缓存、压缩包装)。
- 优点:比继承更灵活、符合开闭原则、可任意组合。
- 缺点:小类多、调试时嵌套层数多不易读。
代理模式如何实现?
代理为另一个对象提供替身或占位,以控制对它的访问。常见变体:虚拟代理(延迟加载)、远程代理、保护代理(权限)、日志代理。
#include <memory>
class Image {
public:
virtual ~Image() = default;
virtual void display() = 0;
};
// 真实对象:加载昂贵的大图
class RealImage : public Image {
std::string filename_;
void loadFromDisk() { /* 昂贵的 IO */ }
public:
explicit RealImage(const std::string& f) : filename_(f) { loadFromDisk(); }
void display() override { /* 显示图片 */ }
};
// 虚拟代理:延迟加载,首次 display 才真正创建 RealImage
class ImageProxy : public Image {
std::string filename_;
std::unique_ptr<RealImage> real_;
public:
explicit ImageProxy(const std::string& f) : filename_(f) {}
void display() override {
if (!real_) real_ = std::make_unique<RealImage>(filename_);
real_->display();
}
};三者都「包装」了对象:代理控制访问(创建时机、权限、跨进程);装饰器增强功能;适配器改变接口。回答时务必说明「意图差异」。
适用场景与优缺点
- 适用:大对象延迟加载、访问控制与鉴权、远程服务本地存根(RPC)、日志埋点、智能指针(
std::shared_ptr的 deleter 也算一种控制)。 - 优点:职责分离、延迟加载省资源、可加安全控制。
- 缺点:增加一层间接、首次访问可能变慢。
外观模式如何实现?
外观为一组复杂子系统提供一个统一的高层接口,简化客户端调用。
class CPU { public: void freeze() { /* ... */ } void jump(long) { /* ... */ } void execute() { /* ... */ } };
class Memory { public: void load(long, const std::string&) { /* ... */ } };
class Disk { public: std::string read(long) { return {}; } };
// 外观:把「启动电脑」这一串操作封装成单一入口
class ComputerFacade {
CPU cpu_; Memory mem_; Disk disk_;
public:
void start() {
cpu_.freeze();
mem_.load(0, disk_.read(0));
cpu_.jump(0);
cpu_.execute();
}
void shutdown() { /* ... */ }
};
// 客户端:一行搞定,无需了解子系统内部
// ComputerFacade computer; computer.start();外观 vs 中介者:两者都「降低耦合」,但外观是单向的(客户端 -> 子系统,简化入口);中介者是双向的(子系统之间通过中介者互不直接通信)。这个对比常被追问。
适用场景与优缺点
- 适用:为复杂库/框架提供易用入口、分层架构中隔离子系统、遗留系统封装。
- 优点:降低客户端复杂度、隔离子系统、减少依赖。
- 缺点:外观可能变成「上帝对象」;不符合开闭原则时需扩展外观。
桥接模式如何实现?
桥接将抽象部分与实现部分分离,使两者可独立变化。核心是「组合代替继承」,避免「n 种形状 × m 种颜色」的类爆炸。
#include <memory>
// 实现部分(Implementor)
class Color {
public:
virtual ~Color() = default;
virtual std::string fill() const = 0;
};
class Red : public Color { public: std::string fill() const override { return "红色"; } };
class Blue : public Color { public: std::string fill() const override { return "蓝色"; } };
// 抽象部分(Abstraction)
class Shape {
protected:
std::unique_ptr<Color> color_; // 组合:持有实现
public:
explicit Shape(std::unique_ptr<Color> c) : color_(std::move(c)) {}
virtual ~Shape() = default;
virtual void draw() const = 0;
};
class Circle : public Shape {
public:
using Shape::Shape;
void draw() const override {
// 画一个 color_->fill() 的圆
}
};
// 使用:形状与颜色独立扩展,n+m 个类而非 n*m 个
// auto redCircle = std::make_unique<Circle>(std::make_unique<Red>());
// auto blueCircle = std::make_unique<Circle>(std::make_unique<Blue>());桥接 vs 策略:结构几乎一样(都持有一个接口指针),但桥接是「结构性」的——分离抽象与实现让两者独立演化;策略是「行为性」的——在运行时切换算法。意图不同,代码却很像,这正是面试区分点。
适用场景与优缺点
- 适用:一个类有两个独立变化的维度(形状+颜色、设备+操作系统)、希望运行时切换实现、避免继承爆炸。
- 优点:分离抽象与实现、符合开闭原则、组合复用。
- 缺点:增加设计复杂度,需识别「变化的维度」。
组合模式如何实现?
组合将对象组织成树形结构,使客户端能统一处理单个对象与组合对象(如文件系统:文件与文件夹)。
#include <memory>
#include <vector>
// 统一接口
class Component {
public:
virtual ~Component() = default;
virtual void operation() const = 0;
};
// 叶子:文件
class Leaf : public Component {
std::string name_;
public:
explicit Leaf(const std::string& n) : name_(n) {}
void operation() const override { /* 对 name_ 操作 */ }
};
// 组合:文件夹,可包含叶子或子组合
class Composite : public Component {
std::vector<std::unique_ptr<Component>> children_;
public:
void add(std::unique_ptr<Component> c) { children_.push_back(std::move(c)); }
void operation() const override {
for (const auto& c : children_) c->operation(); // 递归调用
}
};组合 vs 装饰器:两者都是「递归/树形结构」,但组合关注整体-部分结构(统一叶子和容器),装饰器关注给单一对象叠加职责。组合让客户端不用区分「是一个还是多个」。
适用场景与优缺点
- 适用:文件系统、UI 控件树、菜单树、组织架构、AST 语法树。
- 优点:统一处理叶子与组合、易递归、符合开闭原则。
- 缺点:过度泛化会难以限制「某些组合不允许某叶子」,类型约束被削弱。
观察者模式如何实现?
观察者定义一对多依赖:当被观察对象(Subject)状态改变时,自动通知所有观察者(Observer)。典型实现是「发布-订阅」。
#include <vector>
#include <algorithm>
class Observer {
public:
virtual ~Observer() = default;
virtual void update(const std::string& msg) = 0; // 收到通知
};
class Subject {
std::vector<Observer*> observers_;
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(const std::string& msg) {
for (auto* o : observers_) o->update(msg); // 广播
}
};
class LogObserver : public Observer {
public:
void update(const std::string& msg) override { /* 写日志 */ }
};
class UIObserver : public Observer {
public:
void update(const std::string& msg) override { /* 刷新界面 */ }
};
观察者模式最常见的坑:通知过程中观察者被删除/移除导致悬空指针,或观察者回调里又触发通知造成死循环/重入。C++ 可用 std::weak_ptr 或「先拷贝快照再通知」规避。
适用场景与优缺点
- 适用:事件驱动系统、GUI 事件、消息订阅、数据变化联动(如库存变化刷新多个视图)。
- 优点:松耦合、符合开闭原则、动态增删订阅。
- 缺点:通知顺序不可控、可能内存泄漏(忘 detach)、重入与悬空指针风险。
策略模式如何实现?
策略定义一系列算法,把它们封装成可互换的对象,让算法独立于使用它的客户端而变化。
#include <memory>
// 策略接口
class SortStrategy {
public:
virtual ~SortStrategy() = default;
virtual void sort(std::vector<int>& data) const = 0;
};
class QuickSort : public SortStrategy {
public:
void sort(std::vector<int>& data) const override { /* 快排 */ }
};
class MergeSort : public SortStrategy {
public:
void sort(std::vector<int>& data) const override { /* 归并 */ }
};
// 上下文:持有策略,运行时可切换
class Sorter {
std::unique_ptr<SortStrategy> strategy_;
public:
void setStrategy(std::unique_ptr<SortStrategy> s) { strategy_ = std::move(s); }
void sort(std::vector<int>& data) const {
if (strategy_) strategy_->sort(data);
}
};
// 使用:sorter.setStrategy(make_unique<QuickSort>()); 运行时可换 MergeSort
策略 vs 状态:两者结构极像(都持有对象、委托调用),但策略是客户端主动选择算法(互不感知、通常不自动切换);状态是对象自身状态驱动行为变化(状态间有转换关系)。C++ 中 std::function / 函数指针常可替代小型策略,减少类爆炸。
适用场景与优缺点
- 适用:多种算法可选(排序、压缩、支付、折扣策略)、消除大量 if-else。
- 优点:符合开闭原则、运行时可切换、消除条件分支。
- 缺点:类数量增多、客户端需了解各策略差异。
模板方法模式如何实现?
模板方法在父类中定义算法骨架,将可变步骤延迟到子类实现,子类只填空、不改整体流程。
class DataProcessor {
public:
virtual ~DataProcessor() = default;
// 模板方法:固定流程骨架,通常声明为非虚/加 final 防止被重写
void process() {
readData();
transformData(); // 可变的钩子
writeData();
}
protected:
void readData() { /* 固定:读数据 */ }
void writeData() { /* 固定:写数据 */ }
virtual void transformData() = 0; // 可变步骤,子类实现
};
class CsvProcessor : public DataProcessor {
protected:
void transformData() override { /* CSV 特有转换 */ }
};
class JsonProcessor : public DataProcessor {
protected:
void transformData() override { /* JSON 特有转换 */ }
};模板方法 vs 策略:模板方法用继承(父类定骨架、子类填步骤,编译期确定);策略用组合(运行期切换整个算法)。模板方法体现了「好莱坞原则:Don’t call us, we’ll call you」——框架调用你,而非你调用框架。
适用场景与优缺点
- 适用:算法骨架固定、部分步骤可变(批处理、编译器前端、测试框架 setUp/tearDown)。
- 优点:复用公共流程、符合开闭原则、父类控制流程。
- 缺点:继承带来的强耦合、子类受父类骨架约束。
责任链模式如何实现?
责任链将多个处理器串成一条链,请求沿链传递,直到某个处理器处理它(或全部都不处理)。典型应用:审批流、日志分级、HTTP 中间件。
#include <memory>
class Handler {
protected:
std::unique_ptr<Handler> next_;
public:
virtual ~Handler() = default;
Handler& setNext(std::unique_ptr<Handler> n) { next_ = std::move(n); return *this; }
virtual void handle(int request) {
if (next_) next_->handle(request); // 自己不处理,传给下一个
}
};
class Level1Handler : public Handler {
public:
void handle(int request) override {
if (request <= 100) { /* 处理小额审批 */ }
else Handler::handle(request); // 超出权限,交给上级
}
};
class Level2Handler : public Handler {
public:
void handle(int request) override {
if (request <= 1000) { /* 处理中额审批 */ }
else Handler::handle(request);
}
};
// 使用:auto h1 = make_unique<Level1Handler>(); h1->setNext(make_unique<Level2Handler>());责任链的两种变体:拦截式(每个处理器都过一遍,如中间件、日志)与终止式(首个能处理的处理并停止,如审批流)。代码结构上差别只在「处理后是否继续传」。
适用场景与优缺点
- 适用:多级审批、异常处理链、请求过滤器、日志级别路由。
- 优点:解耦请求发送者与处理者、动态增删处理器、职责单一。
- 缺点:请求可能没人处理(需兜底)、链条过长性能下降。
状态模式如何实现?
状态模式让对象在内部状态改变时改变其行为,把每个状态封装成独立类,消除大量 if-else/switch。
#include <memory>
class State {
public:
virtual ~State() = default;
virtual void handle() = 0;
};
class Context; // 前置声明
class StateA : public State { public: void handle() override; };
class StateB : public State { public: void handle() override; };
// 上下文持有当前状态
class Context {
std::unique_ptr<State> state_;
public:
explicit Context(std::unique_ptr<State> s) : state_(std::move(s)) {}
void setState(std::unique_ptr<State> s) { state_ = std::move(s); }
void request() { state_->handle(); }
void toB(); // 提供状态切换入口
};状态模式里状态对象之间的切换关系容易写成相互耦合(A 持有 B 的引用)。可用「上下文统一负责切换」或「状态返回下一个状态」来解耦,避免状态类之间产生循环依赖。
适用场景与优缺点
- 适用:订单状态机(待支付→已支付→已发货→已完成)、TCP 连接状态、游戏角色状态、工作流引擎。
- 优点:消除大量条件分支、状态逻辑内聚、新增状态符合开闭原则。
- 缺点:状态类增多、状态转换关系可能复杂化。
命令模式如何实现?
命令模式把「请求」封装成对象,从而支持参数化、排队、日志、撤销/重做。
#include <memory>
#include <vector>
// 命令接口
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0; // 支持撤销
};
// 接收者:真正干活的对象
class Editor {
public:
void insert(const std::string& s) { /* 插入文本 */ }
void remove(const std::string& s) { /* 删除文本 */ }
};
// 具体命令:绑定接收者与操作
class InsertCommand : public Command {
Editor& editor_;
std::string text_;
public:
InsertCommand(Editor& e, std::string t) : editor_(e), text_(std::move(t)) {}
void execute() override { editor_.insert(text_); }
void undo() override { editor_.remove(text_); }
};
// 调用者:执行/记录命令,支持撤销栈
class Invoker {
std::vector<std::unique_ptr<Command>> history_;
public:
void run(std::unique_ptr<Command> c) { c->execute(); history_.push_back(std::move(c)); }
void undoLast() { if (!history_.empty()) { history_.back()->undo(); history_.pop_back(); } }
};
命令模式是回调/函数对象的面向对象化:C++ 里轻量场景可用 std::function 或 lambda 代替 Command 类,但需要撤销/重做、命令队列、宏录制时,命令对象(execute + undo)更合适。
适用场景与优缺点
- 适用:编辑器撤销/重做、任务队列/线程池任务、GUI 按钮动作、事务回滚、宏录制。
- 优点:解耦调用者与执行者、支持撤销/队列/日志、可组合命令。
- 缺点:类数量增多、每个操作都要封装。
迭代器模式如何实现?
迭代器提供顺序访问聚合对象元素的方式,且不暴露内部表示。C++ 中 STL 已内置迭代器,自己实现时核心是「begin()/end() + operator++ + operator*」。
#include <vector>
// 聚合对象:一个自定义集合
template <typename T>
class MyContainer {
std::vector<T> data_;
public:
void add(const T& v) { data_.push_back(v); }
// 嵌套迭代器:实现指针语义
class Iterator {
typename std::vector<T>::iterator it_;
public:
explicit Iterator(typename std::vector<T>::iterator it) : it_(it) {}
Iterator& operator++() { ++it_; return *this; }
bool operator!=(const Iterator& other) const { return it_ != other.it_; }
T& operator*() { return *it_; }
};
Iterator begin() { return Iterator(data_.begin()); }
Iterator end() { return Iterator(data_.end()); }
};
// 使用:范围 for 或手动迭代
// MyContainer<int> c; for (auto& x : c) { ... }
C++ 的迭代器设计是迭代器模式的「最高级形态」:配合 <algorithm> 泛型算法,实现数据容器与算法彻底解耦。面试可提到「STL 迭代器分类:输入/输出/前向/双向/随机访问」。
适用场景与优缺点
- 适用:自定义容器/集合、遍历复杂数据结构(树、图)、统一遍历接口。
- 优点:隐藏内部结构、统一遍历方式、支持多种遍历策略。
- 缺点:简单集合引入迭代器略重;C++ 中注意迭代器失效问题。
享元模式如何实现?
享元通过共享不可变的内在状态来大量减少内存占用,把可变的「外在状态」抽离出去。典型应用:字体/字符对象池、棋盘棋子。
#include <unordered_map>
#include <memory>
// 享元:共享的内在状态(字体、字号)——不可变
class Font {
std::string name_;
int size_;
public:
Font(std::string name, int size) : name_(std::move(name)), size_(size) {}
void render(const std::string& text, int x, int y) const {
// 用 name_/size_ 在 (x,y) 画 text;x,y 是外在状态
}
};
// 享元工厂:按内在状态缓存/复用对象
class FontFactory {
std::unordered_map<std::string, std::shared_ptr<Font>> cache_;
public:
std::shared_ptr<Font> getFont(const std::string& name, int size) {
std::string key = name + ":" + std::to_string(size);
auto it = cache_.find(key);
if (it != cache_.end()) return it->second;
auto f = std::make_shared<Font>(name, size);
cache_[key] = f;
return f;
}
};内在状态(intrinsic):可共享、不变,如字体的名称与字号。外在状态(extrinsic):随场景变化,如字符的位置 (x,y)、颜色,由客户端在调用时传入,不存入享元。
适用场景与优缺点
- 适用:大量相似对象且内存紧张(文字排版、游戏粒子、缓存池)、对象状态可拆分为内外在。
- 优点:大幅减少内存、集中管理共享对象。
- 缺点:把状态拆开增加复杂度、线程安全需额外处理(共享对象只读则安全)。
中介者、备忘录、访问者模式各解决什么问题?
这三个模式「面试出现频率低于前十七个」,常以「各解决什么问题」打包考。分别回答一句话定位 + 最小骨架即可。
中介者模式(Mediator)
解决多对象间网状通信导致的耦合爆炸:所有交互都通过一个中介者对象,变成星形结构(如聊天室、MVC 的 Controller、机场塔台)。
class Mediator {
public:
virtual ~Mediator() = default;
virtual void notify(class Component* sender, const std::string& event) = 0;
};
class Component { // 组件只认识中介者,不认识彼此
protected:
Mediator* mediator_;
public:
explicit Component(Mediator* m) : mediator_(m) {}
};
// 具体中介者维护所有组件,转发消息
class ChatRoom : public Mediator {
std::vector<Component*> members_;
public:
void notify(Component* sender, const std::string& event) override {
for (auto* m : members_) if (m != sender) { /* 转发给其他人 */ }
}
};备忘录模式(Memento)
解决对象状态快照与恢复(撤销):在不破坏封装的前提下捕获对象内部状态,稍后恢复。C++ 常用「拷贝当前状态对象」简化实现。
#include <memory>
class Memento { // 快照,只存状态
int state_;
public:
explicit Memento(int s) : state_(s) {}
int state() const { return state_; }
};
class Originator { // 发起者:创建/恢复备忘录
int state_ = 0;
public:
void setState(int s) { state_ = s; }
std::unique_ptr<Memento> save() { return std::make_unique<Memento>(state_); }
void restore(const Memento& m) { state_ = m.state(); }
};
class Caretaker { // 管理者:保存/管理历史快照,但不读取其内容
std::vector<std::unique_ptr<Memento>> history_;
public:
void push(std::unique_ptr<Memento> m) { history_.push_back(std::move(m)); }
};备忘录 vs 命令:命令模式通过「重放逆操作」实现撤销(省内存但需每个命令可逆);备忘录通过「存快照」实现撤销(省逻辑但占内存)。大型对象状态常用备忘录,操作型任务常用命令。
访问者模式(Visitor)
解决在不修改元素类的前提下,为对象结构添加新操作,尤其适合「元素稳定、操作频繁增加」的场景(如编译器 AST 上新增各种分析)。
class Element;
class Visitor {
public:
virtual ~Visitor() = default;
virtual void visit(class ElementA* e) = 0;
virtual void visit(class ElementB* e) = 0;
};
class Element {
public:
virtual ~Element() = default;
virtual void accept(Visitor& v) = 0; // 双重分派入口
};
class ElementA : public Element {
public:
void accept(Visitor& v) override { v.visit(this); } // 第一次分派:调用自身 accept
};
class ElementB : public Element {
public:
void accept(Visitor& v) override { v.visit(this); } // 第二次分派:visitor 按类型分派
};
class PrintVisitor : public Visitor {
public:
void visit(ElementA* e) override { /* 打印 A */ }
void visit(ElementB* e) override { /* 打印 B */ }
};访问者利用双重分派(double dispatch)绕开 C++ 单分派限制。代价是:新增元素类型时要改所有 Visitor 接口(违反开闭原则),所以元素稳定、操作多变才用访问者。
三者一句话总结
| 模式 | 解决什么 | 一句话 |
|---|---|---|
| 中介者 | 多对象网状耦合 | 交互都走中间人,星形替代网状 |
| 备忘录 | 状态保存与恢复 | 存快照、可回滚 |
| 访问者 | 稳定结构上加新操作 | 双重分派,操作外置 |