单例模式是设计模式中创建型模式 的一种核心模式,其核心目标是:保证一个类在整个程序生命周期内只有一个实例,并提供一个全局访问点。
你可以把它理解为:程序里某个类就像 "唯一的工具间",不管谁要用,都只能拿到这一个工具间的钥匙,不能新建第二个,确保全局只有这一个实例在工作。
一、单例模式的核心特点
- 唯一性:类的实例在程序中只有一个,无论调用多少次创建方法,都返回同一个实例。
- 全局访问性 :提供一个公开的静态方法(如
GetInstance()),让程序任何地方都能获取这个唯一实例。 - 可控创建:禁止外部直接创建 / 拷贝 / 销毁实例(通过私有化构造 / 析构、禁用拷贝语义实现)。
- 私有化析构函数只是禁止外部手动调用
delete销毁实例,但并不会阻止程序退出时的自动析构。
二、单例模式的常见实现方式(C++)
根据实例初始化时机,主要分为两类,其中Meyers 单例是最推荐的方式:
| 实现方式 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| 饿汉式(Eager) | 类内定义静态成员变量,程序启动时就初始化实例。 | 线程安全(C++11 前也可用)、实现简单 | 无懒加载,程序启动时占用内存(即使不用) |
| 懒汉式(Lazy) | 1. 经典 Meyers 单例(你之前写的):静态局部变量,第一次调用时初始化;2. 双重检查锁(DCLP):手动加锁控制初始化。 | 懒加载(用的时候才创建)、节省内存 | 1. Meyers 单例依赖 C++11+;2. DCLP 代码复杂 |
1. 饿汉式实现(简单但无懒加载)
class SingletonEager {
public:
// 全局访问点,static 让方法属于类本身(而非对象),可以直接通过 类名::方法名 调用
static SingletonEager* GetInstance() {
return &instance; // 直接返回预初始化的实例
}
private:
// 私有化构造/析构、禁用拷贝
SingletonEager() = default;
~SingletonEager() = default;
SingletonEager(const SingletonEager&) = delete;
SingletonEager& operator=(const SingletonEager&) = delete;
// 显式禁用移动语义:禁止对象移动、移动赋值(推荐添加)
SingletonEager(SingletonEager&&) = delete;
SingletonEager& operator=(SingletonEager&&) = delete;
// 静态成员变量:程序启动时初始化(全局区) static:让变量属于类本身(而非对象),全局唯一,程序启动时初始化
static SingletonEager instance;
};
// 类外初始化静态成员
SingletonEager SingletonEager::instance;
2. 经典 Meyers 单例(推荐)
就是你之前写的优化版,核心是静态局部变量:
class SingletonMeyers {
public:
static SingletonMeyers* GetInstance() const {
static SingletonMeyers instance; // 第一次调用时初始化(懒加载)
return &instance;
}
private:
SingletonMeyers() = default;
~SingletonMeyers() = default;
SingletonMeyers(const SingletonMeyers&) = delete;
SingletonMeyers& operator=(const SingletonMeyers&) = delete;
};
-
懒加载(Lazy Initialization) 静态局部变量
instance只有在第一次调用GetInstance()时才会初始化,避免了程序启动时就创建单例对象(节省内存,尤其适合单例对象创建成本高的场景)。 -
线程安全C++11 标准明确规定:静态局部变量的初始化是线程安全的 ------ 编译器会自动在初始化代码周围加锁,保证只有一个线程完成初始化,其他线程会等待初始化完成后再执行。❗注意:C++11 之前的标准不保证这一点,旧编译器可能存在线程安全问题。
-
防止对象拷贝 / 创建
- 私有构造 / 析构函数:禁止外部通过
new Singleton()、delete创建 / 销毁对象; - 删除拷贝构造、赋值运算符、移动构造、移动赋值:禁止通过
Singleton s = *GetInstance()等方式拷贝单例对象,确保全局只有一个实例。
- 私有构造 / 析构函数:禁止外部通过
-
自动析构 静态局部变量的生命周期与程序一致,程序退出时会自动调用析构函数,无需手动释放(如果有动态资源需要释放,可通过
DestroyInstance()接口处理)。
3. 双重检查锁(DCLP,兼容旧标准)
适合 C++11 前的场景,手动控制线程安全:
#include <mutex>
class SingletonDCLP {
public:
static SingletonDCLP* GetInstance() {
// 第一次检查:避免每次调用都加锁(提升性能)
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
// 第二次检查:防止多个线程同时通过第一次检查后重复创建
if (instance == nullptr) {
instance = new SingletonDCLP();
}
}
return instance;
}
private:
SingletonDCLP() = default;
~SingletonDCLP() = default;
SingletonDCLP(const SingletonDCLP&) = delete;
SingletonDCLP& operator=(const SingletonDCLP&) = delete;
static SingletonDCLP* instance; // 指针形式
static std::mutex mtx; // 互斥锁
};
// 初始化静态成员
SingletonDCLP* SingletonDCLP::instance = nullptr;
std::mutex SingletonDCLP::mtx;
三、单例模式的适用场景
单例模式不是 "万能钥匙",只适合以下场景:
- 资源独占型组件:比如数据库连接池、日志管理器、配置管理器 ------ 全局只需要一个实例,避免重复创建连接 / 打开文件。
- 状态共享型组件:比如全局计数器、缓存管理器 ------ 需要保证所有地方访问的是同一个状态。
- 创建成本高的组件:比如大型对象、硬件设备控制器 ------ 懒加载可以节省内存,避免程序启动时卡顿。
四、单例模式的注意事项
- 线程安全:C++11 前的 Meyers 单例不保证线程安全,需用 DCLP 或饿汉式。
- 析构问题 :私有析构函数导致无法手动
delete,如果有动态资源(如new的对象),需提供DestroyInstance()接口释放。 - 避免滥用:如果类不需要全局唯一实例,不要用单例(会增加耦合性,不利于测试)。
- 异常安全:如果构造函数抛出异常,单例实例会创建失败,需在构造函数中处理异常。
总结
- 核心目标:保证类的实例唯一,提供全局访问点。
- 推荐实现:C++11 + 优先用 Meyers 单例(懒加载、线程安全、代码简洁);C++11 前用饿汉式或 DCLP。
- 适用场景:资源独占、状态共享、创建成本高的全局组件,避免滥用。