1. 项目概述:为什么C++开发者绕不开设计模式?
干了十几年C++,从桌面客户端到游戏引擎,再到高性能服务器,我越来越觉得,设计模式这东西,真不是面试时才需要背的八股文。它更像是一套内功心法,是你写出既健壮又灵活、既高效又好维护的代码的底层逻辑。很多新手,甚至一些工作了几年的朋友,一提到设计模式就头疼,觉得是“过度设计”,是“为了用而用”。其实恰恰相反,当你被十万行代码里错综复杂的依赖关系搞得焦头烂额,当你发现加一个小功能就要动七八个文件时,设计模式提供的恰恰是“简化设计”的钥匙。
“C++开发者必备:23种设计模式全面解析”这个标题,听起来像是一本教科书目录,但它的内核是实战。它解决的是C++开发中几个最核心的痛点:如何管理对象的生命周期,避免内存泄漏和野指针(创建型模式);如何组织庞大的类结构,让系统易于扩展而非变成一坨“意大利面条”(结构型模式);如何让多个对象优雅地协作,而不是通过一堆
if-else
和全局变量硬耦合在一起(行为型模式)。这23种模式,就是前辈们在无数项目踩坑后,总结出的23种针对特定场景的最佳实践“套路”。掌握它们,你就能在架构设计时,心里有谱,下笔有神。
2. 设计模式核心思想与C++特性结合
2.1 理解设计模式的本质:封装变化
设计模式不是银弹,它的首要原则是“识别变化,并封装它”。比如,如果你的代码里散落着各种
new ConcreteProductA()
、
new ConcreteProductB()
,那么当需要增加一个
ProductC
时,你就得把所有
new
的地方都改一遍。这就是“变化点”没有封装。工厂方法模式做的就是把这个“创建对象”的变化封装到一个虚函数里,以后新增产品,只需要增加一个新的工厂子类,原有代码几乎不动。在C++中,这通常意味着利用多态(虚函数)和面向接口编程。C++没有Java或C#的
interface
关键字,但我们用只包含纯虚函数的抽象类来达到同样目的,这就是一种“约定优于配置”的体现。
2.2 C++语言特性带来的独特实现考量
用C++实现设计模式,有几个特性你必须时刻放在心上,它们既是利器,也可能是陷阱:
-
资源管理(RAII)
:这是C++的灵魂。任何设计模式中,如果涉及动态资源(内存、文件句柄、锁等),都必须考虑所有权和生命周期。例如,单例模式中,全局实例谁来释放?用静态局部变量(C++11后线程安全)还是智能指针(
std::unique_ptr)?原型模式中,深拷贝如何实现?这些都需要结合std::unique_ptr、std::shared_ptr、std::move语义来精细设计,避免模式用对了,却留下了内存泄漏的坑。 - 值语义与引用语义 :C++默认是值语义(拷贝),而很多模式(如组合、装饰器)更适用于引用/指针语义。你需要明确在模式中,对象是应该被拷贝、被引用,还是被独占。错误的选择会导致性能问题或逻辑错误。比如,在装饰器模式中,我们通常用指针或引用持有被装饰对象,以保持动态行为。
- 编译时多态与运行时多态 :除了经典的运行时多态(虚函数),C++的模板提供了强大的编译时多态能力。这催生了一些“模板化”的设计模式实现,如策略模式、访问者模式,可以在编译期绑定算法,完全消除运行时开销,这对性能敏感的系统至关重要。但代价是代码膨胀和编译错误信息晦涩。
- 头文件与编译依赖 :不恰当的模式实现会导致头文件包含关系混乱,一点修改就引发全项目重新编译。桥接模式、观察者模式等,其核心价值之一就是解耦,降低编译依赖。在实现时,要善用前置声明、Pimpl惯用法(一种特殊的外观/桥接)来隔离接口与实现。
注意 :不要试图在每一个场景都生搬硬套23种模式。我见过最糟糕的代码,就是一个简单的数据转换,被套上了抽象工厂、建造者、访问者三层模式,结果代码量膨胀了十倍,可读性为零。模式是工具,判断何时使用、何时不用的能力,比记住所有模式的定义更重要。
3. 创建型模式:精准控制对象诞生
创建型模式处理对象创建逻辑,目标是让系统独立于如何创建、组合和表示对象。在C++中,这直接关系到资源获取、内存安全和初始化顺序。
3.1 单例模式(Singleton):唯一的掌控者
单例保证一个类只有一个实例,并提供全局访问点。在C++里,实现一个正确、线程安全的单例需要点技巧。
经典线程安全实现(Meyers‘ Singleton) :
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11保证局部静态变量初始化线程安全
return instance;
}
// 删除拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default; // 私有构造函数
~Singleton() = default;
};
这是目前最推荐的方式,简洁且安全。但在需要跨编译单元确定初始化顺序(如依赖其他单例)时,它存在“静态初始化顺序灾难”的风险。此时,可以改用“双重检查锁定”(Double-Checked Locking),但必须使用
std::atomic
和
std::call_once
来确保正确性,实现复杂,非必要不推荐。
应用场景与陷阱 :
- 场景 :配置管理器、日志记录器、线程池、连接池。这些对象在逻辑上全局唯一。
-
陷阱
:
- 隐藏的耦合 :单例变成了全局变量,使单元测试变得困难。解决方法是为单例类定义接口,在测试时注入模拟对象。
- 多线程安全 :如上所述,不正确的懒汉式初始化会导致创建多个实例。
-
生命周期
:单例何时销毁?Meyers‘ Singleton在程序结束时由系统销毁,顺序不可控。如果单例依赖其他静态对象,可能在析构时访问已销毁对象。对于有明确关闭逻辑的资源(如网络连接),应提供显式的
release()或shutdown()方法。
3.2 工厂方法(Factory Method)与抽象工厂(Abstract Factory):解耦具体类型
工厂方法定义了一个创建对象的接口,但让子类决定实例化哪个类。抽象工厂则提供一个创建一系列相关或依赖对象的接口,而无需指定它们的具体类。
C++实现示例(工厂方法) :
// 产品接口
class Document {
public:
virtual void open() = 0;
virtual void save() = 0;
virtual ~Document() = default;
};
// 具体产品
class PdfDocument : public Document { /* ... */ };
class WordDocument : public Document { /* ... */ };
// 创建者(Creator)基类
class Application {
public:
// 工厂方法
virtual std::unique_ptr<Document> createDocument() = 0;
void newDocument() {
auto doc = createDocument(); // 调用工厂方法
doc->open();
// ... 将doc加入文档列表
}
};
// 具体创建者
class PdfApplication : public Application {
public:
std::unique_ptr<Document> createDocument() override {
return std::make_unique<PdfDocument>();
}
};
这里,
Application
的
newDocument
业务逻辑依赖于抽象的
Document
,而具体的
PdfDocument
创建被延迟到了子类
PdfApplication
。新增一个
ExcelDocument
,你只需要新增产品类和对应的具体创建者类,
Application
的核心逻辑完全不用动。
抽象工厂
则更进一步,它用于创建
产品族
。比如一个GUI库,有
Button
和
Checkbox
,每个都有Windows和Mac风格。抽象工厂会定义
createButton()
和
createCheckbox()
两个接口,然后由
WinFactory
和
MacFactory
分别实现,确保创建出来的
Button
和
Checkbox
风格一致。
实操心得 :
- 如果只是隔离单个产品的创建,用 工厂方法 。
- 如果需要约束一系列相关产品的创建,确保它们能协同工作,用 抽象工厂 。
- 在C++中,可以考虑使用 模板 来实现编译时的工厂,避免虚函数开销,但会损失一些运行时灵活性。
3.3 建造者模式(Builder):分步构建复杂对象
当一个对象构造过程非常复杂,有很多可选参数或步骤,且需要生成不同表示形式时,构造函数或工厂方法就会变得笨重(比如一个“ telescoping constructor” —— 多个重载的构造函数)。建造者模式通过将构造步骤分离到独立的
Builder
类中来解决这个问题。
典型场景
:构造一个
HttpRequest
对象,它有URL、方法、头部、体、超时设置等数十个字段,大部分有默认值,且构造顺序灵活。
class HttpRequest {
// ... 复杂的数据成员
class Builder; // 声明内部建造者类
public:
static Builder create(const std::string& url);
};
auto request = HttpRequest::create("https://api.example.com")
.method("POST")
.header("Content-Type", "application/json")
.body("{...}")
.timeout(5000)
.build(); // 最终返回构造好的HttpRequest对象
这种“流式接口”(Fluent Interface)的建造者,代码可读性极高。在C++中实现时,建造者类通常作为目标类的内部类,以便访问其私有成员。
build()
方法内部调用目标类的私有构造函数,并转移建造者中存储的参数。
3.4 原型模式(Prototype):复制而非新建
通过拷贝一个现有对象(原型)来创建新对象,而不是调用构造函数。这在以下情况很有用:
- 对象创建成本高(如从数据库加载了大量数据)。
- 系统需要独立于其构成和创建方式。
- 需要保存对象状态并在之后恢复。
C++实现关键
:实现一个
clone()
虚函数,通常返回
std::unique_ptr<Base>
。需要深拷贝时,要正确实现拷贝构造函数。
class Graphic {
public:
virtual std::unique_ptr<Graphic> clone() const = 0;
virtual void draw() const = 0;
virtual ~Graphic() = default;
};
class Circle : public Graphic {
Point center;
int radius;
public:
std::unique_ptr<Graphic> clone() const override {
return std::make_unique<Circle>(*this); // 调用Circle的拷贝构造
}
// ... 其他方法
};
注意 :原型模式在C++中需要谨慎处理深拷贝和继承关系。如果类层次复杂,每个派生类都必须正确实现
clone,这可能会很繁琐。可以考虑使用序列化/反序列化作为另一种“克隆”手段。
4. 结构型模式:构建灵活可扩展的类结构
结构型模式关注如何组合类和对象以形成更大的结构。它们通过继承和组合两种机制来达成目标,在C++中,组合通常优于继承。
4.1 适配器模式(Adapter):让不兼容的接口协同工作
适配器就像电源转接头。当你有一个现成的类(Adaptee),其接口不符合客户端的期望(Target)时,创建一个适配器类包装它,将其接口转换成客户端需要的接口。
C++中的两种实现 :
-
类适配器(通过多重继承)
:适配器同时继承Target和Adaptee。这要求Adaptee是一个类,且Target是接口(抽象类)。C++支持多重继承,但需警惕菱形继承等问题。
class Target { public: virtual void request() = 0; }; class Adaptee { public: void specificRequest() { /* ... */ } }; class Adapter : public Target, private Adaptee { // 私有继承Adaptee public: void request() override { specificRequest(); } // 转换调用 }; -
对象适配器(通过组合)
:更常用、更灵活。适配器持有Adaptee的一个实例(指针或引用)。
class Adapter : public Target { private: std::unique_ptr<Adaptee> adaptee_; public: Adapter(std::unique_ptr<Adaptee> adaptee) : adaptee_(std::move(adaptee)) {} void request() override { adaptee_->specificRequest(); } };
应用场景 :集成第三方库、复用遗留代码、统一不同子系统接口。
4.2 装饰器模式(Decorator):动态添加职责
装饰器通过将对象放入包含行为的特殊封装对象中来为原对象添加新功能。它提供了比继承更灵活的扩展功能的方式。
C++实现要点 :
-
装饰器和被装饰对象实现相同的接口(
Component)。 -
装饰器内部持有一个
Component的引用(通常用指针或智能指针)。 - 在覆盖的方法中,可以在调用被装饰对象的方法前后添加自己的行为。
class Stream { // Component
public:
virtual void write(const std::string& data) = 0;
virtual ~Stream() = default;
};
class FileStream : public Stream { /* ... */ };
class Decorator : public Stream { // 所有装饰器的基类
protected:
std::unique_ptr<Stream> stream_;
public:
Decorator(std::unique_ptr<Stream> stream) : stream_(std::move(stream)) {}
};
class CompressedStream : public Decorator {
public:
using Decorator::Decorator;
void write(const std::string& data) override {
auto compressed = compress(data);
stream_->write(compressed); // 委托给被装饰对象
}
};
class EncryptedStream : public Decorator { /* 类似,先加密再委托 */ };
// 使用:可以动态组合装饰器
auto stream = std::make_unique<EncryptedStream>(
std::make_unique<CompressedStream>(
std::make_unique<FileStream>("test.txt")));
stream->write("Hello World"); // 数据会被先压缩,再加密,最后写入文件
优势
:你可以在运行时动态地、透明地、按任意顺序添加功能,避免了使用继承导致的“类爆炸”(比如
CompressedFileStream
,
EncryptedFileStream
,
CompressedEncryptedFileStream
...)。
4.3 桥接模式(Bridge):分离抽象与实现
桥接模式将抽象部分(功能定义)与它的实现部分(具体实现)分离,使它们都可以独立地变化。它解决了多层继承带来的复杂性。
核心结构 :有两个独立的继承层次。一个是“抽象”(Abstraction),定义高层控制逻辑;另一个是“实现”(Implementor),定义底层平台相关的操作。抽象层持有实现层的一个引用。
// 实现部分接口
class Device {
public:
virtual void enable() = 0;
virtual void setVolume(int percent) = 0;
virtual ~Device() = default;
};
class Tv : public Device { /* ... */ };
class Radio : public Device { /* ... */ };
// 抽象部分
class RemoteControl {
protected:
std::unique_ptr<Device> device_; // 桥接的关键:持有实现
public:
RemoteControl(std::unique_ptr<Device> device) : device_(std::move(device)) {}
virtual void togglePower() {
// ... 一些通用逻辑
device_->enable();
}
virtual void volumeUp() {
device_->setVolume(/* 新音量 */);
}
};
class AdvancedRemoteControl : public RemoteControl {
public:
using RemoteControl::RemoteControl;
void mute() {
device_->setVolume(0);
}
};
现在,
RemoteControl
(抽象)和
Device
(实现)可以独立扩展。你可以新增一个
GameConsole
设备,或者新增一个
VoiceRemoteControl
,它们可以任意组合,而不是为每一种组合创建一个新类。
与适配器的区别 :适配器是事后补救,用于连接两个已有的、不兼容的接口。桥接是事前设计,用于将可能变化的不同维度(如遥控器类型和设备类型)解耦。
4.4 组合模式(Composite):统一处理树形结构
组合模式让你可以用一致的方式处理单个对象和对象组合(树形结构)。它定义了包含基本对象和容器对象的类层次结构。
经典例子:图形编辑器
。所有图形元素(
Graphic
)都有
draw()
方法。一个
Line
、
Circle
是基本对象,而
Picture
是一个容器,它可以包含多个
Graphic
(包括其他
Picture
)。
class Graphic {
public:
virtual void draw() const = 0;
virtual void add(std::unique_ptr<Graphic>) { /* 默认实现,叶子节点抛出异常或忽略 */ }
virtual void remove(Graphic*) { /* 同上 */ }
virtual ~Graphic() = default;
};
class Circle : public Graphic { /* 实现draw, add/remove保持默认 */ };
class Picture : public Graphic {
private:
std::vector<std::unique_ptr<Graphic>> children_;
public:
void draw() const override {
for (const auto& child : children_) {
child->draw(); // 递归绘制所有子元素
}
}
void add(std::unique_ptr<Graphic> graphic) override {
children_.push_back(std::move(graphic));
}
// ... 实现remove等其他容器操作
};
客户端代码可以统一调用
graphic->draw()
,无需关心它是单个图形还是一组图形。这在处理文件系统、UI组件树、表达式解析树时非常有用。
注意事项
:在C++中,组合模式需要仔细设计对象的所有权(通常容器拥有子对象)和生命周期管理。使用
std::unique_ptr
可以清晰地表达所有权关系。
5. 行为型模式:高效管理对象间的协作
行为型模式关注对象之间的职责分配和通信方式。它们让算法和对象间的交互更灵活、更松耦合。
5.1 观察者模式(Observer):事件驱动的通信基石
观察者模式定义了对象间的一对多依赖关系,当一个对象(Subject,主题)状态改变时,所有依赖它的对象(Observer,观察者)都会得到通知并自动更新。这是MVC架构、事件处理系统的核心。
C++现代实现(避免裸指针和手动管理) :
#include <functional>
#include <vector>
class Subject {
private:
std::vector<std::function<void(int)>> observers_; // 使用std::function存储回调
int state_;
public:
void attach(const std::function<void(int)>& observer) {
observers_.push_back(observer);
}
void setState(int newState) {
state_ = newState;
notify();
}
private:
void notify() {
for (const auto& obs : observers_) {
obs(state_); // 通知所有观察者
}
}
};
// 使用
Subject sensor;
sensor.attach([](int value) { std::cout << "Observer A: " << value << std::endl; });
sensor.attach([](int value) { std::cout << "Observer B: " << value << std::endl; });
sensor.setState(10);
进阶技巧 :
-
使用
std::weak_ptr解决生命周期问题 :如果观察者对象可能先于主题被销毁,主题持有std::weak_ptr<Observer>,在通知前尝试lock()获取强引用,失败则移除该观察者。 -
定义更丰富的事件对象
:通知时传递一个
Event结构体,包含事件类型、来源、数据等,而不仅仅是状态值。 -
线程安全
:在多线程环境中,对观察者列表的增删改查需要加锁(如
std::mutex)。
5.2 策略模式(Strategy):自由切换算法
策略模式定义了一系列算法,并将每个算法封装起来,使它们可以互相替换。它让算法的变化独立于使用算法的客户端。
C++实现(经典多态 vs 模板策略) :
// 经典多态实现
class SortStrategy {
public:
virtual void sort(std::vector<int>& data) const = 0;
virtual ~SortStrategy() = default;
};
class QuickSort : public SortStrategy { /* ... */ };
class BubbleSort : public SortStrategy { /* ... */ };
class Context {
std::unique_ptr<SortStrategy> strategy_;
public:
void setStrategy(std::unique_ptr<SortStrategy> strategy) {
strategy_ = std::move(strategy);
}
void executeSort(std::vector<int>& data) {
if (strategy_) strategy_->sort(data);
}
};
// 模板策略(编译时绑定,零开销)
template <typename SortStrategy>
class ContextT {
SortStrategy strategy_;
public:
void executeSort(std::vector<int>& data) {
strategy_.sort(data);
}
};
// 使用
ContextT<QuickSort> ctx; // 算法在编译时确定
如何选择 :如果算法需要在运行时动态切换(如根据数据量选择排序算法),用多态版本。如果算法类型在编译期已知且对性能要求极高,用模板版本。
5.3 状态模式(State):用类表示状态
状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来好像修改了它的类。它通过将不同状态的行为分割到不同的状态类中,消除了庞大的条件语句(
switch-case
或
if-else
链)。
场景
:一个网络连接对象有
Closed
、
Listening
、
Established
等状态,每个状态下
open()
、
close()
、
send()
等操作的行为不同。
class TCPConnection; // 前向声明
class TCPState {
public:
virtual void open(TCPConnection* conn) { /* 默认实现,可能抛异常或忽略 */ }
virtual void close(TCPConnection* conn) { /* ... */ }
virtual void send(TCPConnection* conn, const void* data, size_t len) { /* ... */ }
virtual ~TCPState() = default;
};
class TCPClosed : public TCPState {
public:
void open(TCPConnection* conn) override;
// close() 在Closed状态下可能无效
};
class TCPEstablished : public TCPState {
public:
void send(TCPConnection* conn, const void* data, size_t len) override;
void close(TCPConnection* conn) override;
};
class TCPConnection {
private:
std::unique_ptr<TCPState> state_;
public:
TCPConnection() : state_(std::make_unique<TCPClosed>()) {}
void open() { state_->open(this); }
void close() { state_->close(this); }
void send(const void* data, size_t len) { state_->send(this, data, len); }
void changeState(std::unique_ptr<TCPState> newState) {
state_ = std::move(newState);
}
};
每个状态类都知道在什么条件下应该切换到什么状态。例如,
TCPClosed::open()
的实现会执行打开连接的实际操作,然后调用
conn->changeState(std::make_unique<TCPEstablished>())
。这样,所有与特定状态相关的逻辑都集中在一个类里,清晰且易于维护。
5.4 命令模式(Command):将请求封装为对象
命令模式将请求(操作)封装成一个对象,从而使你可以用不同的请求对客户进行参数化,支持请求的排队、记录、撤销/重做等。
核心组件 :
- Command(命令) :声明执行操作的接口。
-
ConcreteCommand(具体命令)
:将一个接收者对象绑定于一个动作,实现
execute()方法。 - Invoker(调用者) :要求命令执行请求。
- Receiver(接收者) :知道如何实施与执行一个请求相关的操作。
C++示例(支持撤销) :
class Document { // Receiver
public:
void insertText(size_t pos, const std::string& text) { /* ... */ }
void deleteText(size_t pos, size_t len) { /* ... */ }
std::string getText(size_t pos, size_t len) const { /* ... */ }
};
class Command { // Command
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class InsertCommand : public Command {
Document& doc_;
size_t pos_;
std::string text_;
public:
InsertCommand(Document& doc, size_t pos, const std::string& text)
: doc_(doc), pos_(pos), text_(text) {}
void execute() override { doc_.insertText(pos_, text_); }
void undo() override { doc_.deleteText(pos_, text_.size()); }
};
class CommandHistory { // Invoker & 历史记录
std::vector<std::unique_ptr<Command>> history_;
size_t current_ = 0;
public:
void execute(std::unique_ptr<Command> cmd) {
cmd->execute();
// 简化处理:执行新命令后,丢弃后面的重做历史
history_.resize(current_);
history_.push_back(std::move(cmd));
++current_;
}
void undo() {
if (current_ > 0) {
--current_;
history_[current_]->undo();
}
}
void redo() {
if (current_ < history_.size()) {
history_[current_]->execute();
++current_;
}
}
};
应用场景 :GUI按钮和菜单操作、任务队列、事务操作、宏录制。命令对象将操作的所有信息(接收者、参数)打包,使得操作可以被存储、传递和延迟执行。
5.5 模板方法模式(Template Method):定义算法骨架
模板方法在超类中定义了一个算法的框架,允许子类在不改变算法结构的情况下重写算法的特定步骤。这是“好莱坞原则”(“别调用我们,我们会调用你”)的体现。
C++实现 :
class DataProcessor { // 抽象基类
public:
virtual ~DataProcessor() = default;
// 模板方法,定义了算法骨架
void process() final { // final 防止子类重写算法结构
loadData();
transformData(); // 这是子类可以重写的步骤
saveResult();
}
protected:
virtual void loadData() { /* 通用实现,子类可重写 */ }
virtual void transformData() = 0; // 纯虚函数,强制子类实现
virtual void saveResult() { /* 通用实现 */ }
};
class CSVProcessor : public DataProcessor {
protected:
void transformData() override {
// CSV特定的转换逻辑
}
};
关键点
:
process()
方法被声明为
final
(C++11),确保算法骨架不被子类破坏。子类只关心如何实现特定的步骤(如
transformData
)。这是一种典型的代码复用方式,避免了重复的算法流程代码。
6. 模式应用误区与性能考量
6.1 常见应用误区与反模式
- 模式滥用(Golden Hammer) :手里有把锤子,看什么都像钉子。在简单的、不会变化的场景使用复杂模式,徒增系统复杂度。例如,为一个只会创建一种对象的类套上抽象工厂。
- 过度设计(Over-engineering) :在项目初期,为所有“可能”的变化点都设计模式,导致代码难以理解。正确的做法是“三次法则”(Rule of Three):当类似代码出现第三次时,再考虑抽象和模式。
-
误解模式意图
:例如,把单例模式当作全局变量管理器来用,到处
Singleton::getInstance().doSomething(),导致高度耦合。单例应谨慎用于真正的全局唯一资源。 -
忽视C++特性
:在C++中,有时简单的函数指针、
std::function、lambda表达式或模板,比实现一个完整的策略模式或命令模式更轻量、更合适。模式是指导,不是教条。
6.2 C++中的性能影响与优化
设计模式引入的抽象层(虚函数、动态分配、额外对象)会带来一定的开销。在性能关键路径(Hot Path)上需要仔细权衡:
-
虚函数开销
:虚函数调用比普通函数调用多一次间接寻址(通过虚表)。在紧密循环中调用数百万次,这可能成为瓶颈。考虑使用
模板和策略模式(编译时多态)
或
CRTP(奇异递归模板模式)
来消除运行时多态开销。
// CRTP 示例:静态多态 template <typename Derived> class Base { public: void interface() { static_cast<Derived*>(this)->implementation(); // 编译时绑定 } }; class Derived : public Base<Derived> { public: void implementation() { /* ... */ } }; -
动态内存分配
:工厂模式、原型模式、组合模式等经常涉及
new/delete。频繁分配小对象可能导致堆碎片和性能下降。可以考虑使用 对象池 、 自定义内存分配器 或 栈上分配结合移动语义 来优化。 -
对象拷贝
:在装饰器、组合模式中,如果对象较大,深拷贝成本高。应优先使用指针或引用,并配合智能指针管理生命周期。使用移动语义(
std::move)来转移资源所有权,避免不必要的拷贝。 -
内联优化
:编译器很难内联通过虚函数或函数指针调用的函数。如果某个策略或命令非常简单(如一个比较函数),将其实现为模板参数或
std::function,并在性能分析后,如果确实需要,可以考虑使用宏或手动内联(但会牺牲可读性)。
基本原则 : 先让代码正确、清晰,然后进行性能分析(Profiling) 。在真正测出瓶颈之前,不要为了想象中的性能损失而放弃良好的设计。大多数情况下,设计模式带来的清晰架构的收益,远大于其微小的运行时开销。只有在确实验证为性能热点后,才针对性地进行优化。
7. 实战:将模式融入日常开发思维
学习设计模式的最终目的,不是记住23个模式的UML图,而是培养一种“模式思维”。当你面对一个设计问题时,能下意识地联想到:“哦,这个问题可以用X模式来优雅地解决。”
如何培养这种思维?
-
重构识别
:在阅读或维护现有代码时,识别哪些地方出现了“代码坏味道”(Code Smells),并思考可以用哪种模式重构。例如:
- 冗长的条件语句 (检查对象状态):考虑 状态模式 。
- 散落各处的相似代码 :考虑 模板方法 或 策略模式 提取算法。
- 类与类之间直接调用,耦合过紧 :考虑引入 中介者 或 观察者 来解耦。
- 构造函数参数过多、逻辑复杂 :考虑 建造者模式 。
- 设计先行 :在动手写一个新模块前,花几分钟画个简单的类图,思考一下模块中哪些部分可能变化。针对这些变化点,预先考虑使用模式进行封装。例如,如果未来可能需要支持多种数据存储方式(文件、数据库、网络),那么一开始就使用 抽象工厂 或 策略模式 来隔离数据访问层。
-
结合C++最佳实践
:将设计模式与C++的现代特性结合。例如:
-
用
std::function和lambda实现轻量级的 命令模式 或 策略模式 。 -
用
std::variant和std::visit实现 访问者模式 的另一种风格(std::visit访问者模式)。 - 用RAII管理模式中创建的资源,确保异常安全。
-
用
- 从小处着手 :不要试图在一个项目里用上所有模式。从一个具体的、可理解的问题开始。比如,为你的日志系统实现一个 装饰器 ,让它既能输出到控制台,又能同时输出到文件。或者,用 工厂方法 来创建不同的网络协议解析器。
最后,记住GoF在《设计模式》开篇说的话:“设计模式不应该随意地使用……只有当它确实是最简单的解决方案时,才使用它。” 模式是优秀设计的结果,而不是原因。当你写出清晰、灵活、易维护的C++代码时,你可能已经在不经意间使用了某种模式的思想。持续地编码、反思和重构,这23种设计模式终将内化为你的编程直觉,让你在面对复杂系统设计时,更加从容自信。


被折叠的 条评论
为什么被折叠?



