设计模式与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 编译期多态 性能敏感、代码复用 模板参数传递派生类
设计模式与C++核心关系图 C++设计模式 单例模式 工厂模式 观察者模式 CRTP 线程安全 全局唯一 对象创建 解耦 发布-订阅 事件通知 编译期多态 零开销抽象 四种模式各有适用场景,实际项目中经常组合使用

我的经验之谈:设计模式不是越多越好。我见过有人为了用设计模式而用设计模式,结果代码变得又臭又长。记住:模式是工具,不是目的。能用简单代码解决的问题,就别硬套模式。

最后说一句,CRTP是C++独有的「黑魔法」,其他语言学不来。如果你能把CRTP用好,说明你对C++模板的理解已经到一定深度了。但别急着炫技——先把单例、工厂、观察者这三个经典模式用熟练,再考虑CRTP。

公众号:蓝海资料掘金营,微信deep3321