1. 核心定义与作用(精准版)
定义
单例模式 是一种创建型设计模式,确保一个类有且仅有一个实例 ,并向整个系统提供唯一的全局访问点。
核心作用
- 控制实例数量 :严格保证类在程序生命周期内只有一个对象
- 全局访问:无需传递对象,在代码任意位置获取同一个实例
- 解决实际问题
- 避免多实例竞争资源(日志文件、数据库连接、配置文件、打印机)
- 减少频繁创建 / 销毁对象的性能开销
- 统一数据状态,保证全局数据一致性
2. 实现方式:饿汉式 vs 懒汉式(对比 + 代码)
一、饿汉式(静态初始化)
核心 :类加载时就创建实例,以空间换时间
cpp
// 饿汉式单例(线程安全,无需加锁)
class Singleton {
private:
// 1. 构造函数私有化
Singleton() = default;
// 2. 禁用拷贝、赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 3. 静态实例(类加载时初始化)
static Singleton instance;
public:
// 4. 全局访问点
static Singleton& getInstance() {
return instance;
}
};
// 类外初始化
Singleton Singleton::instance;
优点
- 天然线程安全(由静态初始化保证)
- 无锁竞争,访问速度快
- 实现简单,无坑
缺点
- 程序启动就初始化,即使不用也占内存
- 大对象会延长启动时间
- 无法做到延迟加载
适用场景:实例小、一定会使用、追求高性能
二、懒汉式(延迟初始化)
核心 :第一次使用时才创建实例,以时间换空间
① 基础版(线程不安全)
cpp
static Singleton* getInstance() {
if (instance == nullptr) { // 多线程下会多次进入
instance = new Singleton();
}
return instance;
}
问题:高并发下会创建多个实例,破坏单例。
3. 多线程安全与优化(面试核心)
① 加锁版(线程安全,但效率低)
cpp
static Singleton* getInstance() {
std::lock_guard<std::mutex> lock(mtx); // 每次都加锁
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
缺点 :99% 的情况实例已存在,仍要加锁,并发性能极差。
② 双重检查锁 DCL(Double-Checked Locking)
面试必考:为什么要两次判断?
- 第一次判断:避免每次都加锁
- 第二次判断:防止多线程同时通过第一次判断
cpp
static Singleton* getInstance() {
if (instance == nullptr) { // 第一次检查(无锁)
std::lock_guard<std::mutex> lock(mtx);
if (instance == nullptr) { // 第二次检查(有锁)
instance = new Singleton();
}
}
return instance;
}
致命问题:指令重排 new 分为三步:
- 分配内存
- 调用构造函数
- 指针赋值给 instance
编译器可能优化为:1 → 3 → 2导致其他线程拿到未构造完成的半成品对象,程序崩溃。
C++11 解决方案 使用 std::atomic + 内存屏障禁止重排。
③ Meyers 单例(C++ 最优写法,强烈推荐)
C++11 特性 :静态局部变量初始化是线程安全的!
cpp
class Singleton {
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance() {
static Singleton instance; // 线程安全,延迟加载
return instance;
}
};
优点
- 延迟加载
- 天然线程安全
- 无锁、无指针、无内存泄漏
- 代码极简、无坑
这是 C++ 工程与面试首选写法!
4. C++ 单例必写规范(面试必问)
① 必须私有化构造函数
防止外部 new 创建对象。
② 必须禁用拷贝构造 + 赋值运算符
cpp
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
防止通过拷贝产生新实例。
③ 析构函数一般私有化
单例由类管理生命周期,外部不能 delete。
④ 实例类型选择
- 饿汉:静态对象(栈 / 全局区)
- 懒汉:静态局部对象(推荐)
- 不推荐裸指针(内存泄漏风险)
5. 单例生命周期与内存管理
- 静态实例 :程序结束时自动释放,无需手动管理
- 指针实例 :需要手动写
destroy()释放,否则内存泄漏 - 依赖顺序:多个单例互相依赖时,可能出现析构顺序问题
- 最佳方案:使用 Meyers 单例,完全交给系统管理
6. 单例模式优缺点总结
优点
- 严格控制唯一实例
- 全局访问方便
- 避免重复创建销毁开销
- 避免资源竞争冲突
缺点
- 扩展性差(很难改成多例)
- 隐藏依赖关系
- 多线程下实现复杂(基础版不安全)
- 不利于单元测试(难以 mock)