☰
C++常用的11种设计模式解释及示例代码详解
2026/10/20 14:51:51 网站建设 项目流程

前言


设计模式(design pattern)是对反复出现的对象协作问题的可复用解法。经典著作《设计模式》收录了 23 种,本文挑出日常 C++ 项目里出现频率最高的 11 种:创建型的单例、工厂方法、抽象工厂、建造者、原型;结构型的适配器、装饰器、代理;行为型的观察者、策略、模板方法。这个挑选是经验性的,不是标准规定——不同资料列出的「常用 11 种」并不一致。


需要先纠正一个常见误解:设计模式不是「背下来就能用」的代码模板。同一种模式在不同语言里落地方式差别很大,C++ 里还要考虑值语义、所有权、模板替代方案。例如策略模式在 C++ 里常常用一个std::function或模板参数就够了,不必为每种策略写一个类;单例模式在现代 C++ 里要特别小心销毁顺序与线程安全。生搬硬套类比生搬硬套函数更容易让代码变复杂。


本文每个模式给出意图、适用场景和一段最小示例,代码基准为 C++17,示例只保留能说明结构的部分,省略日志、错误处理等细节。文末给出一张选择对照表。


一、创建型模式(5 种)


1. 单例(Singleton)


意图:保证一个类只有一个实例,并提供全局访问点。C++11 起,函数内局部静态变量的初始化是线程安全的(标准保证「初始化只发生一次」,主流实现用加锁或原子指令实现),因此 Meyers 单例是推荐写法:


// 适用于 C++17
class Config {
public:
static Config& instance() {
static Config inst;
return inst;
}
int port = 8080;
private:
Config() = default;
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
};

删除拷贝构造与拷贝赋值,防止Config c = Config::instance();产生第二个对象。缺点也很明确:它本质是全局状态,难以替换、难以测试,析构顺序在跨翻译单元时容易出问题。能用依赖注入(把对象传进构造函数)就不要用单例。


2. 工厂方法(Factory Method)


意图:把「创建哪个具体类型」推迟到子类。调用方只依赖抽象产品与抽象工厂:


// 适用于 C++17
#include <memory>
#include <string>

struct Product {
virtual ~Product() = default;
virtual std::string name() const = 0;
};

struct ProductA : Product {
std::string name() const override { return "A"; }
};

struct Factory {
virtual ~Factory() = default;
virtual std::unique_ptr<Product> create() const = 0;
};

struct FactoryA : Factory {
std::unique_ptr<Product> create() const override {
return std::make_unique<ProductA>();
}
};

基类析构必须是virtual,否则通过Factory*删除派生对象是未定义行为。返回std::unique_ptr表达所有权转移,比返回裸指针清晰。


3. 抽象工厂(Abstract Factory)


意图:创建一族相互关联的对象,保证它们风格一致。与工厂方法的区别是「一个工厂造多个产品」:


// 适用于 C++17
#include <memory>

struct Button { virtual ~Button() = default; virtual void paint() const = 0; };
struct CheckBox { virtual ~CheckBox() = default; virtual void paint() const = 0; };

struct GUIFactory {
virtual ~GUIFactory() = default;
virtual std::unique_ptr<Button> make_button() const = 0;
virtual std::unique_ptr<CheckBox> make_checkbox() const = 0;
};

新增一整套主题只需新增一个工厂实现;但新增一种产品(例如Slider)要改所有工厂,这是它的固有代价。


4. 建造者(Builder)


意图:把复杂对象的「构造过程」与「表示」分离,用链式调用逐步配置:


// 适用于 C++17
#include <map>
#include <string>
#include <utility>

struct Request {
std::string url;
std::map<std::string, std::string> headers;
int timeout_ms = 1000;
};

class RequestBuilder {
public:
RequestBuilder& url(std::string value) {
request_.url = std::move(value);
return *this;
}
RequestBuilder& header(std::string key, std::string value) {
request_.headers.emplace(std::move(key), std::move(value));
return *this;
}
RequestBuilder& timeout(int ms) {
request_.timeout_ms = ms;
return *this;
}
Request build() const { return request_; }
private:
Request request_;
};

当构造函数参数超过四五个、且多数有默认值时,建造者比一长串位置参数可读得多。若参数很少,直接聚合初始化更简单。


5. 原型(Prototype)


意图:通过克隆已有对象来创建新对象,避免知道具体类型,也避免重复昂贵的初始化:


// 适用于 C++17
#include <memory>

struct Shape {
virtual ~Shape() = default;
virtual std::unique_ptr<Shape> clone() const = 0;
virtual void draw() const = 0;
};

struct Circle : Shape {
double radius = 1.0;
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Circle>(*this);
}
void draw() const override {}
};

注意返回类型必须是std::unique_ptr<Shape>,不能写成std::unique_ptr<Circle>——虚函数的返回类型协变只支持指针和引用,模板实例之间没有协变关系。


二、结构型模式(3 种)


6. 适配器(Adapter)


意图:把一个类的接口转换成调用方期望的另一个接口。下面的适配器把只接受 C 字符串的老接口包装成std::string接口:


// 适用于 C++17
#include <string>

class LegacyPrinter {
public:
void print_old(const char* text) { (void)text; }
};

class Printer {
public:
virtual ~Printer() = default;
virtual void print(const std::string& text) = 0;
};

class PrinterAdapter : public Printer {
public:
explicit PrinterAdapter(LegacyPrinter& legacy) : legacy_(legacy) {}
void print(const std::string& text) override {
legacy_.print_old(text.c_str());
}
private:
LegacyPrinter& legacy_;
};

适配器持有引用还是值,取决于被适配对象的生命周期。引用成员要求被适配对象活得比适配器久,跨作用域传递时容易悬垂。


7. 装饰器(Decorator)


意图:在不修改原类的前提下,动态地给对象叠加职责。关键是装饰器与被装饰对象实现同一接口,并持有一个内部对象:


// 适用于 C++17
#include <memory>
#include <string>

class Notifier {
public:
virtual ~Notifier() = default;
virtual void send(const std::string& message) = 0;
};

class LoggingNotifier : public Notifier {
public:
explicit LoggingNotifier(std::unique_ptr<Notifier> inner)
: inner_(std::move(inner)) {}

void send(const std::string& message) override {
// 这里可以先记录日志
inner_->send(message);
}
private:
std::unique_ptr<Notifier> inner_;
};

装饰器可以层层嵌套,顺序由组合方式决定。与继承相比,它避免了「每个功能组合都写一个子类」的组合爆炸。


8. 代理(Proxy)


意图:为另一个对象提供替身,以控制访问。常见变体有远程代理、保护代理、虚代理(延迟加载)。下面是最简单的虚代理:


// 适用于 C++17
#include <memory>
#include <string>
#include <utility>

class Image {
public:
virtual ~Image() = default;
virtual void display() = 0;
};

class RealImage : public Image {
public:
explicit RealImage(std::string path) : path_(std::move(path)) {}
void display() override { /* 加载并显示 */ }
private:
std::string path_;
};

class ImageProxy : public Image {
public:
explicit ImageProxy(std::string path) : path_(std::move(path)) {}
void display() override {
if (!real_) {
real_ = std::make_unique<RealImage>(path_);
}
real_->display();
}
private:
std::string path_;
std::unique_ptr<RealImage> real_;
};

代理与装饰器的结构几乎一样,区别在意图:装饰器是「增加行为」,代理是「控制访问」。结构相同而意图不同,正是设计模式容易被误用的地方。


三、行为型模式(3 种)


9. 观察者(Observer)


意图:定义一对多依赖,当主题状态变化时通知所有观察者。示例用裸指针保存观察者,因此必须保证观察者在主题之前注销或销毁:


// 适用于 C++17
#include <algorithm>
#include <vector>

class Observer {
public:
virtual ~Observer() = default;
virtual void on_event(int value) = 0;
};

class Subject {
public:
void subscribe(Observer* observer) { observers_.push_back(observer); }

void unsubscribe(Observer* observer) {
observers_.erase(
std::remove(observers_.begin(), observers_.end(), observer),
observers_.end());
}

void notify(int value) {
for (Observer* observer : observers_) {
observer->on_event(value);
}
}
private:
std::vector<Observer*> observers_;
};

通知过程中若观察者调用unsubscribe,正在遍历的vector会被修改,迭代器失效。稳妥做法是先拷贝一份观察者列表再遍历,或者用「标记删除 + 通知后清理」。


10. 策略(Strategy)


意图:把可互换的算法封装成独立对象,让算法的变化独立于使用它的客户端:


// 适用于 C++17
#include <memory>

class Discount {
public:
virtual ~Discount() = default;
virtual double apply(double price) const = 0;
};

class NoDiscount : public Discount {
public:
double apply(double price) const override { return price; }
};

class HalfPrice : public Discount {
public:
double apply(double price) const override { return price * 0.5; }
};

class Order {
public:
explicit Order(std::unique_ptr<Discount> discount)
: discount_(std::move(discount)) {}
double total(double price) const { return discount_->apply(price); }
private:
std::unique_ptr<Discount> discount_;
};

如果策略无状态、逻辑很短,用std::function<double(double)>更轻;如果策略需要在编译期确定并参与内联,用模板参数代替虚函数可以消除间接调用,但会增加实例化数量。


11. 模板方法(Template Method)


意图:在基类固定算法骨架,把可变步骤留给子类。骨架函数通常是非虚的公有函数,调用一组受保护的虚函数:


// 适用于 C++17
class DataExporter {
public:
virtual ~DataExporter() = default;

void run() {
open();
write_header();
write_body();
close();
}

protected:
virtual void open() = 0;
virtual void write_header() = 0;
virtual void write_body() = 0;
virtual void close() = 0;
};

run()不是虚函数,子类无法改变流程顺序,只能填充步骤,这就是「好莱坞原则」:不要调用我们,我们会调用你。缺点是流程一旦固定,运行时无法调整;需要运行时组合时应当用策略。


四、如何选择




模式一句话意图典型信号主要代价



单例全局唯一实例配置、日志器全局状态、难测试

工厂方法子类决定创建类型创建逻辑会扩展类数量增加

抽象工厂创建一族相关对象多套主题/平台新增产品要改所有工厂

建造者分步构造复杂对象构造参数多多一个类

原型克隆现有对象初始化昂贵深拷贝要自己实现

适配器转换接口接入旧代码/第三方库多一层间接

装饰器动态叠加职责功能可组合对象层次变深

代理控制对对象的访问延迟加载、权限多一层间接

观察者状态变化通知多方事件、UI 刷新生命周期与重入问题

策略可互换算法多种计费/排序规则虚调用或模板膨胀

模板方法固定流程、可变步骤导出、解析流水线流程不能运行时改变



选择时先问「变化点在哪里」,再问「这个变化需要运行期决定还是编译期决定」。运行期用虚函数或std::function,编译期用模板。若一个问题用普通函数和if就能表达清楚,不要为了「用上模式」而引入类层次。


常见坑点



  1. ❌ 多态基类析构不写virtual,却通过基类指针delete。


✅ 基类析构声明为virtual;否则是未定义行为,派生部分不会析构。



  1. ❌ 单例用new且从不释放,或依赖跨翻译单元的静态对象初始化顺序。


✅ 用函数内局部静态对象;需要提前销毁时明确管理生命周期,不要假设初始化顺序。



  1. ❌ 原型clone()返回std::unique_ptr<Circle>试图协变覆盖基类的std::unique_ptr<Shape>。


✅ 返回类型必须一致;模板实例之间不存在协变。



  1. ❌ 观察者通知时直接遍历容器,观察者在回调里注销自己。


✅ 先拷贝列表再通知,或延迟删除;遍历中修改vector会使迭代器失效。



  1. ❌ 装饰器和代理混用概念,给代理叠加了业务功能。


✅ 代理控制访问,装饰器增加职责;结构相同但意图要分清。



  1. ❌ 策略对象用裸指针保存,调用方销毁后继续使用。


✅ 用std::unique_ptr表达独占所有权,或明确文档化借用语义。



  1. ❌ 模板方法把run()也设为virtual,子类可以改流程。


✅ 骨架函数保持非虚;只把步骤设为受保护虚函数。



  1. ❌ 为了「符合模式」给只有一种实现的接口加抽象基类。


✅ 模式服务于变化;没有第二种实现时,具体类加普通函数更简单。


总结




分类模式关键机制



创建型单例、工厂方法、抽象工厂、建造者、原型控制对象的创建时机与类型

结构型适配器、装饰器、代理控制对象之间的组合与访问

行为型观察者、策略、模板方法控制算法与协作的分派方式



这 11 种模式的共同点是「把变化隔离到一处」:创建型隔离类型选择,结构型隔离接口与组合,行为型隔离算法与流程。真正决定代码质量的不是记住多少模式名,而是能否判断变化点在哪、以及这个变化是否值得付出额外的类与间接层。现代 C++ 还提供了模板、std::function、lambda、std::variant等工具,很多传统模式可以用更轻的方式实现;先写最直接的代码,等变化出现再重构,通常比预先套用模式更稳妥。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询