设计模式在C++中的应用:单例、工厂、观察者与策略
设计模式这东西,说白了就是前人踩坑踩出来的「最佳实践」。我早年写C++时,总觉得模式是花架子,直到接手一个多线程日志系统——那崩溃日志打印得比业务日志还勤快。嗯,从那以后我老实了。
今天咱们聊四个最常用的模式:单例、工厂、观察者、策略。每个我都会结合C++的特性,讲讲线程安全、内存管理这些实战中绕不开的坑。
1. 单例模式:线程安全的正确姿势
单例模式,保证一个类只有一个实例。听起来简单,但多线程环境下,稍不注意就给你new出两个对象来。
经典懒汉式(别这么写)
class Singleton {
public:
static Singleton* getInstance() {
if (instance_ == nullptr) {
instance_ = new Singleton();
}
return instance_;
}
private:
static Singleton* instance_;
Singleton() {}
};
这段代码,单线程没问题。多线程下,两个线程同时进入if判断,就会创建两个实例。我当年第一次写服务器时就这么干的,结果线上日志文件被两个实例抢着写,乱成一锅粥。
双检锁 + 内存屏障(C++11起靠谱)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
C++11保证了局部静态变量的初始化是线程安全的。编译器会帮你加锁,你只管用。我个人习惯用这种方式,简单、高效、不出错。
volatile。C++11之前的内存模型不支持,写了也是白写。老老实实用局部静态变量。
2. 工厂模式:把对象的创建封装起来
工厂模式的核心思想:不直接new对象,而是通过一个工厂方法来创建。这样,当需要新增产品时,不用改调用方的代码。
简单工厂
enum class ProductType { A, B };
class Product {
public:
virtual void use() = 0;
virtual ~Product() = default;
};
class ProductA : public Product {
public:
void use() override { std::cout << "ProductA\n"; }
};
class ProductB : public Product {
public:
void use() override { std::cout << "ProductB\n"; }
};
Product* createProduct(ProductType type) {
switch (type) {
case ProductType::A: return new ProductA();
case ProductType::B: return new ProductB();
default: return nullptr;
}
}
简单工厂的缺点很明显:每加一种产品,就要改switch。违反了开闭原则。我在项目中更常用工厂方法模式——把工厂也抽象成接口。
工厂方法模式
class Factory {
public:
virtual Product* create() = 0;
virtual ~Factory() = default;
};
class FactoryA : public Factory {
public:
Product* create() override { return new ProductA(); }
};
class FactoryB : public Factory {
public:
Product* create() override { return new ProductB(); }
};
这样,新增产品时,只需要新增一个工厂子类,不用改现有代码。你想想看,如果产品有几十种,改switch得多痛苦。
std::unique_ptr使用,避免手动delete。工厂返回std::unique_ptr<Product>,调用方不用操心内存。
3. 观察者模式:一对多的通知机制
观察者模式,也叫发布-订阅。一个对象状态变化,通知所有依赖它的对象。GUI事件、消息队列、股票行情推送,都是这个套路。
基本实现
class Observer {
public:
virtual void update(const std::string& message) = 0;
virtual ~Observer() = default;
};
class Subject {
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);
}
}
private:
std::vector<Observer*> observers_;
};
这里有个坑:如果观察者在update中把自己从列表中移除了,迭代器就失效了。我曾经在金融交易系统中遇到过,一个观察者收到行情后取消订阅,结果下一个观察者访问了野指针,程序直接崩了。
std::weak_ptr管理观察者,或者在通知前先拷贝一份观察者列表。
现代C++改进
用std::function代替抽象基类,更灵活:
class EventBus {
public:
using Handler = std::function<void(const std::string&)>;
void subscribe(Handler handler) { handlers_.push_back(handler); }
void publish(const std::string& msg) {
for (auto& h : handlers_) {
h(msg);
}
}
private:
std::vector<Handler> handlers_;
};
这样,观察者可以是lambda、函数对象、成员函数,不用继承任何基类。我个人很喜欢这种设计,代码更轻量。
4. 策略模式:把算法抽出来
策略模式,把不同的算法封装成独立的类,让它们可以互相替换。比如排序算法、压缩算法、支付方式。
经典实现
class Strategy {
public:
virtual int execute(int a, int b) = 0;
virtual ~Strategy() = default;
};
class AddStrategy : public Strategy {
public:
int execute(int a, int b) override { return a + b; }
};
class MultiplyStrategy : public Strategy {
public:
int execute(int a, int b) override { return a * b; }
};
class Context {
public:
void setStrategy(Strategy* s) { strategy_ = s; }
int doOperation(int a, int b) { return strategy_->execute(a, b); }
private:
Strategy* strategy_;
};
使用时:
Context ctx;
ctx.setStrategy(new AddStrategy());
std::cout << ctx.doOperation(3, 4); // 7
ctx.setStrategy(new MultiplyStrategy());
std::cout << ctx.doOperation(3, 4); // 12
策略模式的好处是,新增算法不用改Context的代码。坏处是,如果策略很少变化,用if-else反而更简单。别为了用模式而用模式。
std::function作为策略。比如Context里存一个std::function<int(int,int)>,赋值时传lambda。代码量少一半。
知识体系总览
下面这张图,把这四种模式的核心逻辑串起来了。你一看就明白它们各自解决什么问题。
避坑指南
- 单例的析构顺序: 如果单例A依赖单例B,A析构时B可能已经没了。我建议让单例的生命周期尽量独立,或者用
std::shared_ptr管理依赖。 - 工厂的内存泄漏: 工厂返回裸指针,调用方忘了
delete就泄漏了。用std::unique_ptr或std::shared_ptr,让RAII帮你收尸。 - 观察者的循环依赖: A观察B,B观察A,一个更新触发另一个更新,死循环。加一个递归深度限制,或者用事件队列异步处理。
- 策略的线程安全: 如果策略对象有状态,多线程下要加锁。最好让策略对象无状态,或者每次创建新实例。
设计模式不是银弹。我见过有人为了用模式,把简单的逻辑拆成七八个类,最后自己都看不懂。记住:模式是工具,不是目的。用之前先问自己——「这个模式真的能降低复杂度吗?」如果答案是否定的,那就别用。