设计模式与C++:单例模式、工厂模式、观察者模式在C++中的实现;CRTP(奇异递归模板模式)
设计模式这东西,说白了就是前辈们踩过无数坑之后总结出来的「套路」。我刚开始学C++那会儿,觉得设计模式就是花架子,直到自己写的代码被改得面目全非……嗯,后来我老实了。
今天咱们聊四个在C++里特别实用的模式:单例、工厂、观察者,再加一个C++独有的CRTP。前三个是GoF经典模式,最后一个算是C++模板编程的「黑魔法」。
单例模式:全局唯一,但别滥用
单例模式保证一个类只有一个实例,并提供一个全局访问点。我在项目中遇到过最典型的场景就是日志系统——你肯定不希望每个模块都new一个日志对象吧?
核心要点:构造函数私有化、静态实例、线程安全
C++11之后,实现线程安全的单例变得非常简单。我个人习惯用局部静态变量,因为C++11保证了局部静态变量的初始化是线程安全的。
class Logger {
public:
static Logger& getInstance() {
static Logger instance; // C++11 线程安全
return instance;
}
void log(const std::string& msg) {
std::cout << "[LOG] " << msg << std::endl;
}
// 禁止拷贝和赋值
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
Logger() = default; // 私有构造函数
~Logger() = default;
};
我曾经踩过的坑:单例的析构顺序问题。如果两个单例互相依赖,程序退出时可能会崩溃。解决办法是尽量让单例之间没有依赖关系,或者用智能指针管理生命周期。
工厂模式:把对象的创建交给专业的人
工厂模式的核心思想是「不要自己new,让工厂帮你造」。你想想看,如果你的代码里到处都是new XXX(),哪天要换一个实现类,岂不是要改到吐?
我常用的工厂模式有两种:简单工厂和抽象工厂。简单工厂适合产品种类不多的场景,抽象工厂则适合产品族的概念。
// 简单工厂示例
class Shape {
public:
virtual void draw() = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() override { std::cout << "画圆" << std::endl; }
};
class Square : public Shape {
public:
void draw() override { std::cout << "画方" << std::endl; }
};
class ShapeFactory {
public:
static std::unique_ptr<Shape> create(const std::string& type) {
if (type == "circle") return std::make_unique<Circle>();
if (type == "square") return std::make_unique<Square>();
return nullptr;
}
};
我的建议:工厂模式配合std::unique_ptr使用,可以避免内存泄漏。另外,如果产品类型很多,可以考虑用map注册的方式,避免长长的if-else链。
观察者模式:一对多的依赖关系
观察者模式定义了一种一对多的依赖关系,当一个对象状态发生变化时,所有依赖它的对象都会得到通知。说白了就是「发布-订阅」机制。
我记得在做一个实时监控系统时,用观察者模式来处理各种告警事件,代码结构清晰了很多。
class Observer {
public:
virtual void update(const std::string& message) = 0;
virtual ~Observer() = default;
};
class Subject {
std::vector<Observer*> observers;
public:
void attach(Observer* obs) { observers.push_back(obs); }
void detach(Observer* obs) { /* 移除逻辑 */ }
void notify(const std::string& msg) {
for (auto* obs : observers) {
obs->update(msg);
}
}
};
// 具体观察者
class ConsoleObserver : public Observer {
public:
void update(const std::string& msg) override {
std::cout << "控制台收到: " << msg << std::endl;
}
};
注意:观察者模式容易造成内存泄漏,因为Subject持有Observer的原始指针。我建议用std::weak_ptr来管理观察者,或者确保观察者的生命周期比Subject长。
CRTP:C++独有的模板技巧
CRTP(Curiously Recurring Template Pattern)中文叫奇异递归模板模式。名字听着吓人,其实原理很简单:派生类把自己作为模板参数传给基类。
为什么要这么干?因为可以实现静态多态。你想想看,虚函数有运行时开销,而CRTP把多态决策提前到编译期,性能更好。
template <typename Derived>
class Base {
public:
void interface() {
// 调用派生类的实现
static_cast<Derived*>(this)->implementation();
}
void implementation() {
std::cout << "基类默认实现" << std::endl;
}
};
class Derived : public Base<Derived> {
public:
void implementation() {
std::cout << "派生类实现" << std::endl;
}
};
// 使用
Derived d;
d.interface(); // 输出: 派生类实现
CRTP的典型应用场景:
- 实现静态多态(替代虚函数)
- 实现对象计数(跟踪有多少个实例)
- 实现单例模式(模板化单例)
- 实现策略模式(编译期策略绑定)
我在项目中用CRTP实现过一个对象池,性能比虚函数版本提升了30%左右。不过CRTP也有缺点——代码可读性差,调试困难。所以我的建议是:性能敏感的地方用CRTP,其他地方用虚函数就够了。
四种模式对比
| 模式 | 核心思想 | 适用场景 | C++特有技巧 |
|---|---|---|---|
| 单例模式 | 全局唯一实例 | 日志、配置、线程池 | 局部静态变量、delete拷贝构造 |
| 工厂模式 | 封装对象创建 | 产品类型多变、解耦 | unique_ptr、map注册 |
| 观察者模式 | 一对多通知 | 事件系统、消息推送 | weak_ptr避免泄漏 |
| CRTP | 编译期多态 | 性能敏感、代码复用 | 模板参数传递派生类 |
我的经验之谈:设计模式不是越多越好。我见过有人为了用设计模式而用设计模式,结果代码变得又臭又长。记住:模式是工具,不是目的。能用简单代码解决的问题,就别硬套模式。
最后说一句,CRTP是C++独有的「黑魔法」,其他语言学不来。如果你能把CRTP用好,说明你对C++模板的理解已经到一定深度了。但别急着炫技——先把单例、工厂、观察者这三个经典模式用熟练,再考虑CRTP。