设计模式入门:1. 单例模式详解 C++实现

设计模式入门:1. 单例模式详解 C++实现

前言:什么是设计模式?

在软件开发的世界里,我们经常会遇到一些重复出现的问题。设计模式(Design Pattern)就是这些问题的经过验证的、通用的解决方案 。它们不是具体的代码,而是一套解决特定问题的最佳实践和思想

设计模式最早由"四人帮"(GoF,Gang of Four)在1994年的《设计模式:可复用面向对象软件的基础》一书中系统地提出,共包含23种经典模式,分为三大类:

  • 创建型模式:关注对象的创建过程,如单例、工厂、建造者等
  • 结构型模式:关注类和对象的组合结构,如适配器、装饰器、代理等
  • 行为型模式:关注对象间的交互和职责分配,如观察者、策略、迭代器等

学习设计模式的好处不言而喻:

  • 提高代码的可复用性和可维护性
  • 提供了一套通用的"设计语言",让开发者之间的沟通更高效
  • 帮助我们写出更优雅、更健壮的代码

今天,我们就从最简单也最常用的单例模式开始我们的设计模式之旅。


单例模式概述

什么是单例模式?

单例模式(Singleton Pattern)是一种创建型设计模式,它的核心思想非常简单:确保一个类只有一个实例,并提供一个全局访问点来获取这个实例

换句话说,单例模式保证了在整个程序生命周期内,某个类只能被实例化一次,所有地方访问到的都是同一个对象。

单例模式的应用场景

单例模式适用于以下场景:

  • 资源管理器:如日志管理器、配置管理器、数据库连接池
  • 全局状态管理:如游戏中的游戏管理器、应用程序的主控制器
  • 硬件访问:如打印机驱动、传感器控制器
  • 工具类:如数学工具类、字符串工具类(不过现代C++更推荐使用命名空间)

单例模式的优缺点

优点:

  • 严格控制对实例的访问
  • 避免频繁创建和销毁对象,提高性能
  • 节省系统资源
  • 提供全局统一的访问点

缺点:

  • 违反了单一职责原则(一个类既要负责自己的业务逻辑,又要负责实例的创建)
  • 扩展困难,因为单例类没有抽象层
  • 对测试不友好,难以模拟单例对象
  • 在多线程环境下需要特别注意线程安全问题

C++实现单例模式的几种方式

在C++中,实现单例模式有多种方式,每种方式都有其优缺点和适用场景。我们将从最简单的版本开始,逐步演进到最推荐的版本。

1. 饿汉式单例(Eager Initialization)

饿汉式单例是最简单的实现方式,它在程序启动时就创建实例,不管你用不用。

cpp 复制代码
// SingletonEager.h
class SingletonEager {
public:
    // 禁止拷贝和移动
    SingletonEager(const SingletonEager&) = delete;
    SingletonEager& operator=(const SingletonEager&) = delete;
    SingletonEager(SingletonEager&&) = delete;
    SingletonEager& operator=(SingletonEager&&) = delete;

    // 全局访问点
    static SingletonEager& getInstance() {
        return instance;
    }

    // 示例业务方法
    void doSomething() {
        // 业务逻辑
    }

private:
    // 私有构造函数,防止外部实例化
    SingletonEager() {
        // 初始化代码
    }

    // 静态成员变量,程序启动时就创建
    static SingletonEager instance;
};

// SingletonEager.cpp
// 在类外初始化静态成员变量
SingletonEager SingletonEager::instance;

优点:

  • 实现简单
  • 天生线程安全(因为静态变量在程序启动时就初始化了)

缺点:

  • 程序启动时就创建实例,即使从未使用过,也会占用内存
  • 如果有多个单例类,且它们之间有依赖关系,可能会出现初始化顺序问题

2. 懒汉式单例(Lazy Initialization)- 基础版

懒汉式单例是延迟初始化 的,只有在第一次调用getInstance()时才创建实例。

cpp 复制代码
// SingletonLazyBasic.h
class SingletonLazyBasic {
public:
    // 禁止拷贝和移动
    SingletonLazyBasic(const SingletonLazyBasic&) = delete;
    SingletonLazyBasic& operator=(const SingletonLazyBasic&) = delete;
    SingletonLazyBasic(SingletonLazyBasic&&) = delete;
    SingletonLazyBasic& operator=(SingletonLazyBasic&&) = delete;

    // 全局访问点
    static SingletonLazyBasic* getInstance() {
        if (instance == nullptr) {
            instance = new SingletonLazyBasic();
        }
        return instance;
    }

    // 示例业务方法
    void doSomething() {
        // 业务逻辑
    }

private:
    // 私有构造函数
    SingletonLazyBasic() {
        // 初始化代码
    }

    // 静态指针,初始化为nullptr
    static SingletonLazyBasic* instance;
};

// SingletonLazyBasic.cpp
SingletonLazyBasic* SingletonLazyBasic::instance = nullptr;

优点:

  • 延迟初始化,只有在需要时才创建实例,节省内存
  • 避免了饿汉式的初始化顺序问题

缺点:

  • 线程不安全 !在多线程环境下,如果多个线程同时进入if (instance == nullptr)判断,可能会创建多个实例

3. 线程安全的懒汉式单例 - 双重检查锁(DCL)

为了解决基础版懒汉式的线程安全问题,我们可以使用双重检查锁(Double-Checked Locking)。

cpp 复制代码
// SingletonDCL.h
#include <mutex>

class SingletonDCL {
public:
    // 禁止拷贝和移动
    SingletonDCL(const SingletonDCL&) = delete;
    SingletonDCL& operator=(const SingletonDCL&) = delete;
    SingletonDCL(SingletonDCL&&) = delete;
    SingletonDCL& operator=(SingletonDCL&&) = delete;

    // 全局访问点
    static SingletonDCL* getInstance() {
        // 第一次检查:如果实例已经存在,直接返回,避免每次都加锁
        if (instance == nullptr) {
            // 加锁,保证只有一个线程进入下面的代码块
            std::lock_guard<std::mutex> lock(mutex);
            // 第二次检查:防止多个线程同时通过第一次检查后,重复创建实例
            if (instance == nullptr) {
                instance = new SingletonDCL();
            }
        }
        return instance;
    }

    // 示例业务方法
    void doSomething() {
        // 业务逻辑
    }

private:
    // 私有构造函数
    SingletonDCL() {
        // 初始化代码
    }

    // 静态指针和互斥量
    static SingletonDCL* instance;
    static std::mutex mutex;
};

// SingletonDCL.cpp
SingletonDCL* SingletonDCL::instance = nullptr;
std::mutex SingletonDCL::mutex;

优点:

  • 线程安全
  • 延迟初始化
  • 性能较好,只有第一次创建实例时才需要加锁

注意:

在C++11之前,由于编译器的指令重排问题,双重检查锁可能会失效。但在C++11及以后的标准中,new操作符的语义得到了明确,双重检查锁是安全的。

4. 局部静态变量单例(Meyers' Singleton)- 最推荐的方式

这是由Scott Meyers提出的一种非常优雅的单例实现方式,也是现代C++中最推荐的单例实现方式

cpp 复制代码
// SingletonMeyers.h
class SingletonMeyers {
public:
    // 禁止拷贝和移动
    SingletonMeyers(const SingletonMeyers&) = delete;
    SingletonMeyers& operator=(const SingletonMeyers&) = delete;
    SingletonMeyers(SingletonMeyers&&) = delete;
    SingletonMeyers& operator=(SingletonMeyers&&) = delete;

    // 全局访问点
    static SingletonMeyers& getInstance() {
        // 局部静态变量,第一次调用时初始化
        static SingletonMeyers instance;
        return instance;
    }

    // 示例业务方法
    void doSomething() {
        // 业务逻辑
    }

private:
    // 私有构造函数
    SingletonMeyers() {
        // 初始化代码
    }
};

为什么这是最推荐的方式?

  • 实现最简单:代码量最少,最容易理解和维护
  • 线程安全:在C++11及以后的标准中,局部静态变量的初始化是线程安全的
  • 延迟初始化 :只有在第一次调用getInstance()时才创建实例
  • 没有内存泄漏问题:静态变量在程序结束时会自动销毁

5. 模板单例

如果我们需要多个单例类,为每个类都写一遍单例代码会很繁琐。这时我们可以使用模板来实现一个通用的单例基类。

cpp 复制代码
// SingletonTemplate.h
template <typename T>
class SingletonTemplate {
public:
    // 禁止拷贝和移动
    SingletonTemplate(const SingletonTemplate&) = delete;
    SingletonTemplate& operator=(const SingletonTemplate&) = delete;
    SingletonTemplate(SingletonTemplate&&) = delete;
    SingletonTemplate& operator=(SingletonTemplate&&) = delete;

    // 全局访问点
    static T& getInstance() {
        static T instance;
        return instance;
    }

protected:
    // 保护构造函数,允许子类继承
    SingletonTemplate() = default;
    virtual ~SingletonTemplate() = default;
};

// 使用示例
class MyClass : public SingletonTemplate<MyClass> {
    // 让基类可以访问私有构造函数
    friend class SingletonTemplate<MyClass>;

public:
    void doSomething() {
        // 业务逻辑
    }

private:
    MyClass() {
        // 初始化代码
    }
};

// 调用方式
// MyClass::getInstance().doSomething();

优点:

  • 代码复用,避免重复编写单例逻辑
  • 所有单例类的实现方式统一

缺点:

  • 子类需要将基类声明为友元,稍微有点麻烦

单例模式的注意事项和常见陷阱

1. 内存泄漏问题

在使用指针实现的懒汉式单例中,new出来的对象在程序结束时不会自动销毁,可能会导致内存泄漏。虽然现代操作系统会在程序结束时回收所有内存,但这仍然是一个不好的编程习惯。

解决方法:

  • 使用Meyers' Singleton(局部静态变量),它会自动销毁
  • 使用智能指针(std::unique_ptr)来管理实例
  • 提供一个destroyInstance()方法,在程序结束时手动调用

2. 多线程安全问题

这是单例模式中最容易出错的地方。在多线程环境下,一定要确保单例的创建是线程安全的。

永远不要使用基础版的懒汉式单例,除非你能保证它只会在单线程环境下使用。

3. 序列化和反序列化问题

如果单例类需要支持序列化和反序列化,那么反序列化时可能会创建新的实例,破坏单例的特性。

解决方法:

  • 重写反序列化方法,让它返回已有的实例
  • 避免让单例类支持序列化

4. 反射问题

在支持反射的语言(如Java、C#)中,可以通过反射调用私有构造函数来创建新的实例。不过C++没有原生的反射机制,所以这个问题在C++中不常见。


总结

单例模式是最简单也最常用的设计模式之一,它确保一个类只有一个实例,并提供全局访问点。

在C++中,**Meyers' Singleton(局部静态变量)**是最推荐的实现方式,它简单、线程安全、延迟初始化且没有内存泄漏问题。

实现方式 线程安全 延迟初始化 实现复杂度 推荐指数
饿汉式 ⭐⭐⭐
基础懒汉式
双重检查锁 ⭐⭐⭐⭐
Meyers' Singleton 极低 ⭐⭐⭐⭐⭐
模板单例 ⭐⭐⭐⭐

最后需要提醒的是,单例模式虽然好用,但不要滥用。只有当你确实需要确保一个类只有一个实例时,才应该使用单例模式。过度使用单例会导致代码耦合度增加,难以测试和维护。

相关推荐
小马爱打代码43 分钟前
Spring源码中的设计模式实战:从理论到源码的深度解析
java·spring·设计模式
Brilliantwxx1 小时前
【C++】 红黑树封装 STL set/map 超详细解析
开发语言·c++
程序大视界1 小时前
【C++ 从基础到项目实战】C++(八):运算符重载——让你的类用起来像内置类型
开发语言·c++·cpp
z200509301 小时前
今日算法(回溯全排列)
c++·算法·leetcode
不会C语言的男孩1 小时前
C++ Primer 第6章:函数
开发语言·c++
码上有光1 小时前
c++:多态
java·jvm·c++·多态·多态原理
Lumbrologist1 小时前
【C++】零基础入门 · 第 18 节:互斥锁与线程同步
java·开发语言·c++
tangchao340勤奋的老年?1 小时前
C++ OpenGL显示地图
c++·opengl
I Promise342 小时前
C++ 多线程编程:从入门到实战
开发语言·c++