C++ 单例模式(Singleton Pattern)完整总结
目录
- [1. 什么是单例模式](#1. 什么是单例模式)
- [2. 饿汉模式 vs 懒汉模式](#2. 饿汉模式 vs 懒汉模式)
- [3. 懒汉模式的线程安全问题与解决方案](#3. 懒汉模式的线程安全问题与解决方案)
- [4. 编程练习:Logger 单例](#4. 编程练习:Logger 单例)
1. 什么是单例模式
单例模式(Singleton Pattern) 确保一个类在整个程序生命周期中 只有一个实例 ,并提供一个 全局访问点。
核心要素:
- 私有/保护构造函数 --- 阻止外部
new - 静态成员变量 --- 保存唯一实例
- 静态方法
instance()--- 全局获取入口
2. 饿汉模式 vs 懒汉模式
| 特征 | 饿汉模式(Eager) | 懒汉模式(Lazy) |
|---|---|---|
| 初始化时机 | 程序启动 / 类加载时 | 首次调用 instance() 时 |
| 典型写法 | static T* m_inst = new T(); |
if (!m_inst) m_inst = new T(); |
| 线程安全 | ✅ 天然安全 | ❌ 需加锁或用 call_once |
| 资源占用 | 不管用不用都创建 | 用时才创建,节省资源 |
| 适用场景 | 实例轻量、一定会用 | 实例较重、可能不使用 |
3. 懒汉模式的线程安全问题与解决方案
3.1 问题描述
经典指针式懒汉在多线程下存在竞态条件:
cpp
CHwrelDataBase* CHwrelDataBase::instance() {
if (m_instance == nullptr) { // 线程A 判断为空
m_instance = new CHwrelDataBase(); // 线程B 也判断为空,重复创建!
}
return m_instance;
}
线程A 刚判断完 nullptr、还没赋值,线程B 也进来判断为空 → 创建两个实例,还可能内存泄漏。
3.2 四种解决方案
方案一:加互斥锁(最直接,但性能差)
cpp
std::mutex mtx;
T* instance() {
std::lock_guard<std::mutex> lock(mtx); // 每次调用都加锁
if (!m_instance)
m_instance = new T();
return m_instance;
}
⚠️ 缺点:实例已创建后,每次获取仍加锁,性能浪费。
方案二:双重检查锁(DCLP)
cpp
std::mutex mtx;
std::atomic<T*> m_instance{nullptr};
T* instance() {
T* tmp = m_instance.load(std::memory_order_acquire); // 第一次无锁检查
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx);
tmp = m_instance.load(std::memory_order_relaxed); // 第二次加锁检查
if (!tmp) {
tmp = new T();
m_instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
✅ 只在首次创建时加锁,之后走快速路径。
⚠️ 必须用 atomic + 正确的内存序,否则编译器/CPU 指令重排会导致返回未构造完成的对象。
方案三:std::call_once
cpp
std::once_flag flag;
T* m_instance = nullptr;
T* instance() {
std::call_once(flag, []() {
m_instance = new T();
});
return m_instance;
}
✅ 标准库保证只执行一次,简洁安全。
方案四:Meyer's Singleton --- C++11 最佳实践 ⭐
cpp
T& instance() {
static T obj; // C++11 标准保证:局部静态变量的初始化是线程安全的
return obj;
}
✅ 最推荐。C++11 标准 §6.7 规定:如果多个线程同时进入,只有一个线程执行初始化,其他线程阻塞等待。
3.3 方案对比
| 方案 | 线程安全 | 性能 | 复杂度 | 推荐度 |
|---|---|---|---|---|
| 无保护 | ❌ | ⭐⭐⭐ | 低 | ❌ |
| 全局锁 | ✅ | ⭐ | 低 | ⚠️ |
| 双重检查锁 (DCLP) | ✅ | ⭐⭐⭐ | 高 | ⚠️ 易写错 |
std::call_once |
✅ | ⭐⭐⭐ | 中 | ✅ |
| Meyer's Singleton | ✅ | ⭐⭐⭐ | 最低 | ⭐⭐⭐ |
4. 编程练习:Logger 单例
题目: 设计一个
Logger类,要求:
- 全局唯一实例,记录日志消息到内部缓冲区
- 支持
log(message)写入和dump()打印所有日志- 分别用 饿汉模式 和 懒汉模式(Meyer's Singleton) 实现
- 在
main()中验证两种实现都返回同一个实例
参考答案
cpp
#include <iostream>
#include <vector>
#include <string>
// ============================================================
// 方式一:饿汉模式(Eager Singleton)
// 实例在程序启动时(main 之前)就已创建
// ============================================================
class EagerLogger
{
public:
static EagerLogger& instance()
{
return s_instance; // 直接返回,已在 main() 前构造完毕
}
void log(const std::string& msg)
{
m_logs.push_back("[Eager] " + msg);
}
void dump() const
{
for (const auto& entry : m_logs)
std::cout << entry << "\n";
}
EagerLogger(const EagerLogger&) = delete;
EagerLogger& operator=(const EagerLogger&) = delete;
private:
EagerLogger() { std::cout << ">>> EagerLogger constructed\n"; }
std::vector<std::string> m_logs;
static EagerLogger s_instance; // 声明静态成员
};
// 关键:类外定义 --- 程序启动时即构造(饿汉的核心)
EagerLogger EagerLogger::s_instance;
// ============================================================
// 方式二:懒汉模式 - Meyer's Singleton(Lazy Singleton)
// 实例在首次调用 instance() 时才创建,C++11 线程安全
// ============================================================
class LazyLogger
{
public:
static LazyLogger& instance()
{
static LazyLogger s_instance; // 首次调用时构造,C++11 保证线程安全
return s_instance;
}
void log(const std::string& msg)
{
m_logs.push_back("[Lazy] " + msg);
}
void dump() const
{
for (const auto& entry : m_logs)
std::cout << entry << "\n";
}
LazyLogger(const LazyLogger&) = delete;
LazyLogger& operator=(const LazyLogger&) = delete;
private:
LazyLogger() { std::cout << ">>> LazyLogger constructed\n"; }
std::vector<std::string> m_logs;
};
// ============================================================
// main:验证
// ============================================================
int main()
{
std::cout << "===== main() started =====\n\n";
// --- 饿汉 ---
EagerLogger& e1 = EagerLogger::instance();
EagerLogger& e2 = EagerLogger::instance();
e1.log("Hello from e1");
e2.log("Hello from e2");
std::cout << "EagerLogger same instance? "
<< (&e1 == &e2 ? "YES" : "NO") << "\n";
e1.dump();
std::cout << "\n";
// --- 懒汉 ---
LazyLogger& l1 = LazyLogger::instance();
LazyLogger& l2 = LazyLogger::instance();
l1.log("Hello from l1");
l2.log("Hello from l2");
std::cout << "LazyLogger same instance? "
<< (&l1 == &l2 ? "YES" : "NO") << "\n";
l1.dump();
return 0;
}
预期输出
>>> EagerLogger constructed ← 饿汉:main() 之前就已构造
===== main() started =====
EagerLogger same instance? YES
[Eager] Hello from e1
[Eager] Hello from e2
>>> LazyLogger constructed ← 懒汉:首次调用 instance() 时才构造
LazyLogger same instance? YES
[Lazy] Hello from l1
[Lazy] Hello from l2
要点总结
| 对比项 | 饿汉 EagerLogger |
懒汉 LazyLogger |
|---|---|---|
| 实例位置 | 类的静态成员,类外定义 | 函数局部 static 变量 |
| 构造时机 | main() 之前 |
首次调用 instance() 时 |
| 输出顺序 | 构造信息在 main started 之前 |
构造信息在 main started 之后 |
| 线程安全 | ✅ 天然安全(无竞态) | ✅ C++11 标准保证 |
| 析构顺序 | 可能引发跨翻译单元的 静态初始化顺序问题 | 无此问题 |