C++ 单例模式学习

C++ 单例模式学习笔记

单例模式(Singleton Pattern)是 C++ 中最常用的设计模式之一,它保证一个类在程序运行期间只有一个实例,并提供全局访问点。无论是日志管理、配置中心还是设备驱动,单例模式都能有效避免资源竞争和重复初始化问题。本文将从基础原理到进阶实现,全面讲解 C++ 单例类的设计与优化。

一、单例模式的核心需求

在实际开发中,我们经常需要一个类全局唯一,例如:

  • 日志器:避免多实例导致日志文件混乱
  • 配置管理器:确保配置数据一致性
  • 数据库连接池:控制连接数量,避免资源浪费
  • 设备管理器:硬件资源只能被独占访问

单例模式需满足三个核心条件:

  1. 唯一实例:类只能创建一个对象
  2. 全局访问:提供便捷的全局访问方式
  3. 自主管理:实例的生命周期由类自身控制

二、单例模式的基础实现:饿汉式与懒汉式

C++ 单例模式有两种经典实现思路,分别对应不同的初始化时机。

1. 饿汉式单例(Eager Initialization)

原理:程序启动时(main 函数之前)就创建实例,确保线程安全,但可能提前占用资源。

cpp 复制代码
// 饿汉式单例
class Singleton {
private:
    // 1. 私有构造函数:禁止外部创建实例
    Singleton() {
        // 初始化操作(如加载配置、打开文件)
    }

    // 2. 私有拷贝构造和赋值运算符:禁止复制
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 3. 私有静态实例:程序启动时初始化
    static Singleton instance;

public:
    // 4. 公有静态方法:提供全局访问点
    static Singleton& GetInstance() {
        return instance;
    }

    // 其他成员函数
    void DoSomething() {
        // ...
    }
};

// 类外初始化静态成员(关键步骤)
Singleton Singleton::instance;

优点

  • 实现简单,无需考虑线程安全问题
  • 访问速度快,实例已提前创建

缺点

  • 初始化时机早,可能浪费资源(如果程序全程未使用该单例)
  • 无法处理依赖关系(如初始化需要其他动态数据)

2. 懒汉式单例(Lazy Initialization)

原理:首次使用时才创建实例,避免资源浪费,但需要处理线程安全问题。

cpp 复制代码
// 基础懒汉式单例(非线程安全)
class Singleton {
private:
    // 私有构造函数
    Singleton() {}

    // 禁止复制
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // 私有静态指针:延迟初始化
    static Singleton* instance;

public:
    // 公有访问方法:首次调用时创建实例
    static Singleton& GetInstance() {
        if (instance == nullptr) {  // 第一次检查
            instance = new Singleton();  // 线程不安全点
        }
        return *instance;
    }

    // 可选:手动释放资源(单例通常不需要,进程结束会自动释放)
    static void DestroyInstance() {
        if (instance != nullptr) {
            delete instance;
            instance = nullptr;
        }
    }
};

// 初始化静态指针为nullptr
Singleton* Singleton::instance = nullptr;

优点

  • 延迟初始化,节省资源
  • 支持动态依赖关系

缺点

  • 基础版本在多线程环境下不安全(可能创建多个实例)
  • 需要手动管理内存(或依赖智能指针)

三、线程安全的懒汉式单例:从加锁到优化

基础懒汉式在多线程环境下存在风险:当多个线程同时通过 if (instance == nullptr) 检查时,会创建多个实例。解决线程安全问题是单例模式的核心难点。

1. 加锁的懒汉式(线程安全但效率低)

cpp 复制代码
#include <mutex>

// 加锁的懒汉式单例(线程安全)
class Singleton {
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* instance;
    static std::mutex mtx;  // 互斥锁

public:
    static Singleton& GetInstance() {
        std::lock_guard<std::mutex> lock(mtx);  // 加锁(每次访问都加锁)
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return *instance;
    }
};

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

优点 :线程安全
缺点:每次访问都需要加锁,性能开销大(尤其高频访问场景)

2. 双重检查锁定(Double-Checked Locking)

原理:通过两次检查避免不必要的加锁,兼顾线程安全和效率。

cpp 复制代码
#include <mutex>

// 双重检查锁定的懒汉式单例
class Singleton {
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* instance;
    static std::mutex mtx;

public:
    static Singleton& GetInstance() {
        if (instance == nullptr) {  // 第一次检查:无锁,快速判断
            std::lock_guard<std::mutex> lock(mtx);  // 加锁
            if (instance == nullptr) {  // 第二次检查:确保只创建一次
                instance = new Singleton();
            }
        }
        return *instance;
    }
};

Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

为什么需要两次检查?

  • 第一次检查:避免已创建实例后每次访问都加锁,提高效率
  • 第二次检查:防止多个线程同时通过第一次检查后,重复创建实例

注意:在 C++11 之前,由于编译器优化和指令重排,双重检查锁定可能仍存在风险。C++11 及以后的标准已修复此问题,确保该模式安全。

3. 局部静态变量(C++11 后的最优解)

C++11 标准规定:局部静态变量的初始化在多线程环境下是线程安全的。这为单例模式提供了更简洁的实现方式。

cpp 复制代码
// C++11 局部静态变量单例(推荐)
class Singleton {
private:
    // 私有构造函数
    Singleton() {}

    // 禁止复制
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    // 局部静态变量:首次调用时初始化,线程安全
    static Singleton& GetInstance() {
        static Singleton instance;  // C++11 保证线程安全初始化
        return instance;
    }
};

优点

  • 实现极简,一行核心代码
  • 天然线程安全(C++11 及以上)
  • 自动释放内存,无需手动管理
  • 延迟初始化,节省资源

缺点

  • 依赖 C++11 及以上标准(现代编译器均支持)
  • 无法控制析构顺序(如果多个单例存在依赖关系)

这是目前最推荐的单例实现方式,兼顾简洁性、线程安全性和资源效率。

四、单例模式的进阶问题与解决方案

1. 单例的析构顺序问题

当程序中存在多个单例时,它们的析构顺序与初始化顺序相反,但无法显式控制。如果单例 A 依赖单例 B,而 A 先析构,可能导致 B 在析构时访问已释放的资源。

解决方案:使用智能指针管理依赖关系,或通过手动释放函数控制顺序。

2. 单例的继承与扩展

单例类通常设计为不可继承(构造函数私有),但特殊场景下可能需要扩展。可通过模板基类实现通用单例:

cpp 复制代码
// 单例模板基类
template <typename T>
class SingletonBase {
public:
    // 禁用拷贝
    SingletonBase(const SingletonBase&) = delete;
    SingletonBase& operator=(const SingletonBase&) = delete;

    // 全局访问点
    static T& GetInstance() {
        static T instance;  // 派生类实例
        return instance;
    }

protected:
    // 保护构造函数:允许派生类构造
    SingletonBase() = default;
    virtual ~SingletonBase() = default;  // 虚析构函数
};

// 派生单例类
class MySingleton : public SingletonBase<MySingleton> {
    // 友元声明:允许基类访问私有构造函数
    friend class SingletonBase<MySingleton>;

private:
    // 私有构造函数
    MySingleton() {
        // 初始化逻辑
    }

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

3. 单例的测试问题

单例模式会导致代码耦合度高,难以单元测试(全局状态难以隔离)。

解决方案

  • 测试环境中使用 mock 单例替代真实实现
  • 通过接口抽象单例功能,便于替换测试对象

五、单例模式的应用场景与禁忌

适合使用单例的场景

  • 全局资源管理(日志、配置、连接池)
  • 设备访问控制(打印机、传感器)
  • 工具类(全局唯一的工具实例)

不适合使用单例的场景

  • 需要多实例的类(如数据库连接,应使用连接池)
  • 频繁创建销毁的对象(单例生命周期过长)
  • 存在多线程写入竞争的场景(需额外加锁,影响性能)

六、总结:如何选择单例实现方式?

实现方式 线程安全 资源效率 实现复杂度 推荐场景
饿汉式 简单 初始化快、资源占用少的单例
局部静态变量 是(C++11+) 极简 大多数场景(推荐)
双重检查锁定 较复杂 需兼容旧标准或手动管理内存

最终推荐 :在 C++11 及以上环境中,优先使用局部静态变量实现单例,兼顾简洁性、线程安全性和资源效率。

单例模式看似简单,却涉及初始化时机、线程安全、资源管理等多方面问题。理解各种实现的优缺点,根据实际场景选择合适的方案,才能写出健壮的单例类。希望本文能帮助你掌握 C++ 单例模式的核心要点!