🔧 C++ 设计模式系列:单例模式完全指南
📅 更新时间:2025年12月14日
🏷️ 标签:C++ | 设计模式 | 单例模式 | 线程安全 | 音视频开发
文章目录
- [📖 什么是单例模式](#📖 什么是单例模式)
-
- [1. 生活中的例子](#1. 生活中的例子)
- [2. 程序中的定义](#2. 程序中的定义)
- [3. 模式结构图](#3. 模式结构图)
- [🎯 为什么需要这个模式](#🎯 为什么需要这个模式)
-
- 官方定义
- 问题场景
-
- 生活中的例子:打印机
- [❌ 没有单例模式](#❌ 没有单例模式)
- [✅ 使用单例模式](#✅ 使用单例模式)
- 适用场景
- [🔧 实现方式详解](#🔧 实现方式详解)
- [💻 C++ 完整实现](#💻 C++ 完整实现)
- [⚠️ 常见问题](#⚠️ 常见问题)
- [📋 总结](#📋 总结)
📖 什么是单例模式
1. 生活中的例子
想象一个公司的CEO:
整个公司 → [ 唯一的CEO ] → 所有员工
(单例) 都向同一个CEO汇报
关键点:
- 一个公司只有一个CEO
- 所有人访问的都是同一个CEO
- CEO在公司成立时就确定,不会随意更换
2. 程序中的定义
单例模式(Singleton Pattern) 是一种创建型设计模式:
全局访问点 → [ 唯一实例 ] → 所有调用者
getInstance() 单例对象 共享同一个对象
核心要素:
- 私有构造函数:防止外部创建实例
- 私有静态实例:保存唯一的对象
- 公有静态方法:提供全局访问点
- 禁止拷贝和赋值:确保唯一性
3. 模式结构图
getInstance getInstance getInstance Singleton 调用者1 调用者2 调用者3
🎯 为什么需要这个模式
官方定义
根据 GoF(Gang of Four)《设计模式》原书的定义:
Intent : "Ensure a class has only one instance, and provide a global point of access to it."
(确保一个类只有一个实例,并提供一个全局访问点)
核心动机:
"The most common reason for this is to control access to some shared resource --- for example, a database or a file."
(最常见的原因是控制对共享资源的访问------例如数据库或文件)
问题场景
生活中的例子:打印机
办公室场景:
┌─────────────────────────────────────────────────────┐
│ 一台打印机 │
│ 🖨️ │
│ │
│ 员工A ──打印──┐ │
│ 员工B ──打印──┼──→ 【打印队列】 ──→ 按顺序输出 │
│ 员工C ──打印──┘ │
└─────────────────────────────────────────────────────┘
如果没有统一管理:
员工A 直接打印 ──→ ╲
员工B 直接打印 ──→ ╲ 打印内容混乱、卡纸、冲突!
员工C 直接打印 ──→ ╱
❌ 没有单例模式
cpp
// 多个打印管理器实例,导致打印任务混乱
class PrinterSpooler {
public:
void addJob(const std::string& document) {
jobQueue_.push(document);
}
void processJobs() {
while (!jobQueue_.empty()) {
std::cout << "打印: " << jobQueue_.front() << std::endl;
jobQueue_.pop();
}
}
private:
std::queue<std::string> jobQueue_;
};
int main() {
PrinterSpooler spooler1; // 队列1
PrinterSpooler spooler2; // 队列2 - 又一个队列!
PrinterSpooler spooler3; // 队列3 - 再来一个!
spooler1.addJob("文档A");
spooler2.addJob("文档B"); // 进入了不同的队列!
spooler3.addJob("文档C"); // 打印顺序无法统一管理
// 三个队列各自为政,打印机无法协调
// 可能导致打印冲突、顺序混乱
}
问题:
- ❌ 资源冲突:多个实例同时操作同一台打印机
- ❌ 顺序混乱:无法保证打印任务的先后顺序
- ❌ 状态不一致:每个实例维护自己的队列,无法统一调度
✅ 使用单例模式
cpp
class PrinterSpooler {
public:
static PrinterSpooler& getInstance() {
static PrinterSpooler instance;
return instance;
}
void addJob(const std::string& document) {
std::lock_guard<std::mutex> lock(mutex_);
jobQueue_.push(document);
std::cout << "添加打印任务: " << document << std::endl;
}
void processJobs() {
std::lock_guard<std::mutex> lock(mutex_);
while (!jobQueue_.empty()) {
std::cout << "正在打印: " << jobQueue_.front() << std::endl;
jobQueue_.pop();
}
}
private:
PrinterSpooler() = default;
std::queue<std::string> jobQueue_;
std::mutex mutex_;
};
int main() {
// 所有打印任务都通过唯一的队列管理
PrinterSpooler::getInstance().addJob("文档A"); // 进入统一队列
PrinterSpooler::getInstance().addJob("文档B"); // 同一个队列
PrinterSpooler::getInstance().addJob("文档C"); // 同一个队列
PrinterSpooler::getInstance().processJobs(); // 按顺序打印
// 输出:文档A → 文档B → 文档C(有序)
}
优点:
- ✅ 资源统一:全局只有一个打印队列,避免冲突
- ✅ 顺序可控:所有任务按先进先出顺序处理
- ✅ 状态一致:所有调用者共享同一个队列状态
- ✅ 便于管理:可以统一监控、暂停、取消打印任务
适用场景
单例模式的核心价值是控制对共享资源的访问,以下是典型场景:
| 场景 | 单例对象 | 为什么需要单例 |
|---|---|---|
| 打印队列 | PrinterSpooler | 多实例会导致打印冲突和顺序混乱 |
| 数据库连接池 | ConnectionPool | 多实例会创建过多连接,耗尽数据库资源 |
| 线程池 | ThreadPool | 多实例会导致线程数失控,系统过载 |
| 配置管理 | ConfigManager | 多实例可能读取到不一致的配置 |
| 设备管理 | DeviceManager | 硬件设备天然唯一,必须统一管理 |
| 音视频播放器 | MediaPlayer | 播放状态和资源需要统一协调 |
🔧 实现方式详解
1. 饿汉式
特点
- 程序启动时就创建实例
- 简单直接,天然线程安全
- 可能造成启动时资源浪费
cpp
class Singleton {
public:
static Singleton& getInstance() {
return instance;
}
void doSomething() {
std::cout << "Singleton working!" << std::endl;
}
private:
Singleton() {
std::cout << "Singleton created!" << std::endl;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton instance; // 静态成员,程序启动时初始化
};
// 必须在类外(.cpp)定义静态成员
Singleton Singleton::instance;
执行流程
程序启动 静态成员 instance 初始化 调用构造函数创建对象 单例已就绪 调用者1: getInstance 调用者2: getInstance 调用者3: getInstance 返回同一个 instance
2. 懒汉式
特点
- 首次使用时才创建实例
- 节省启动资源
- 需要注意线程安全
❌ 非线程安全版本
cpp
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr) { // 线程A执行到这里
instance = new Singleton(); // 线程B也执行到这里
} // 可能创建两个实例!
return instance;
}
private:
Singleton() = default;
static Singleton* instance;
};
Singleton* Singleton::instance = nullptr;
问题:多线程环境下可能创建多个实例!
✅ 线程安全版本(双重检查锁)
cpp
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
if (instance == nullptr) { // 第一次检查(无锁)
std::lock_guard<std::mutex> lock(mutex); // 加锁
if (instance == nullptr) { // 第二次检查(有锁)
instance = new Singleton();
}
}
return *instance;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance;
static std::mutex mutex;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
双重检查锁执行流程
否 是 否 是 调用 getInstance instance == nullptr?
第一次检查 返回 instance 获取互斥锁 instance == nullptr?
第二次检查 释放锁 创建实例
instance = new Singleton 释放锁 返回单例
3. C++11 静态局部变量(推荐)
特点
- C++11标准保证线程安全
- 代码简洁优雅
- 现代C++的最佳实践
cpp
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11保证线程安全
return instance;
}
void doSomething() {
std::cout << "Singleton working!" << std::endl;
}
private:
Singleton() {
std::cout << "Singleton created!" << std::endl;
}
~Singleton() {
std::cout << "Singleton destroyed!" << std::endl;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
为什么C++11静态局部变量是线程安全的?
C++11标准规定:
如果多个线程同时首次进入声明静态局部变量的语句,
只有一个线程会执行初始化,其他线程会等待初始化完成。
这是编译器层面的保证,无需手动加锁!
4. 各种实现方式对比
| 实现方式 | 线程安全 | 延迟加载 | 代码复杂度 | 推荐指数 |
|---|---|---|---|---|
| 饿汉式 | ✅ 是 | ❌ 否 | ⭐ 简单 | ⭐⭐⭐ |
| 懒汉式(无锁) | ❌ 否 | ✅ 是 | ⭐ 简单 | ⭐ |
| 双重检查锁 | ✅ 是 | ✅ 是 | ⭐⭐⭐ 复杂 | ⭐⭐⭐ |
| C++11静态局部变量 | ✅ 是 | ✅ 是 | ⭐ 简单 | ⭐⭐⭐⭐⭐ |
💻 C++ 完整实现
cpp
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
// ========== 推荐的单例实现(C++11) ==========
class Singleton {
public:
// 获取唯一实例
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
// 示例方法
void showMessage(const std::string& msg) {
std::lock_guard<std::mutex> lock(mutex_);
std::cout <<msg << std::endl;
}
int getCount() const {
return count_;
}
void increment() {
std::lock_guard<std::mutex> lock(mutex_);
++count_;
}
private:
// 私有构造函数
Singleton() : count_(0) {
std::cout << "[单例] 构造函数被调用,实例已创建!" << std::endl;
}
// 私有析构函数
~Singleton() {
std::cout << "[单例] 析构函数被调用,实例已销毁!" << std::endl;
}
// 禁止拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 禁止移动
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
int count_;
mutable std::mutex mutex_;
};
// ========== 测试代码 ==========
void threadFunc(int threadId) {
// 每个线程都访问同一个单例
Singleton& singleton = Singleton::getInstance();
singleton.increment();
singleton.showMessage("线程 " + std::to_string(threadId) + " 访问单例");
}
int main() {
std::cout << "========== 单例模式测试 ==========" << std::endl;
std::cout << std::endl;
// 创建多个线程同时访问单例
std::vector<std::thread> threads;
std::cout << "[主线程] 创建5个线程..." << std::endl;
for (int i = 1; i <= 5; ++i) {
threads.emplace_back(threadFunc, i);
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
std::cout << std::endl;
std::cout << "[主线程] 所有线程完成" << std::endl;
std::cout << "[主线程] 单例被访问次数: "
<< Singleton::getInstance().getCount() << std::endl;
std::cout << std::endl;
std::cout << "========== 程序结束 ==========" << std::endl;
return 0;
}
运行结果示例
========== 单例模式测试 ==========
[主线程] 创建5个线程...
[单例] 构造函数被调用,实例已创建!
线程 5 访问单例
线程 2 访问单例
线程 3 访问单例
线程 1 访问单例
线程 4 访问单例
[主线程] 所有线程完成
[主线程] 单例被访问次数: 5
========== 程序结束 ==========
[单例] 析构函数被调用,实例已销毁!
⚠️ 常见问题
问题1:内存泄漏
使用指针的懒汉式
cpp
// ❌ 存在内存泄漏风险
class Singleton {
public:
static Singleton* getInstance() {
if (!instance) {
instance = new Singleton(); // new 了但没有 delete
}
return instance;
}
private:
static Singleton* instance;
};
解决方案:
cpp
// ✅ 方案1:使用静态局部变量(推荐)
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
// ✅ 方案2:使用智能指针
static std::shared_ptr<Singleton> getInstance() {
static std::shared_ptr<Singleton> instance(new Singleton());
return instance;
}
问题2:多线程竞争
数据竞争场景
cpp
class Counter {
public:
static Counter& getInstance() {
static Counter instance;
return instance;
}
// ❌ 非线程安全的操作
void increment() {
count_++; // 多线程同时调用会出问题
}
private:
int count_ = 0;
};
解决方案:
cpp
class Counter {
public:
static Counter& getInstance() {
static Counter instance;
return instance;
}
// ✅ 使用互斥锁保护
void increment() {
std::lock_guard<std::mutex> lock(mutex_);
count_++;
}
// ✅ 或者使用原子变量
void incrementAtomic() {
atomicCount_++;
}
private:
int count_ = 0;
std::atomic<int> atomicCount_{0};
std::mutex mutex_;
};
问题3:单例的继承问题
cpp
// ❌ 单例类难以被继承
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
protected:
Singleton() = default;
};
class DerivedSingleton : public Singleton {
// 问题:无法创建 DerivedSingleton 的单例
};
解决方案:使用模板
cpp
// ✅ 单例模板(CRTP模式)
template<typename T>
class Singleton {
public:
static T& getInstance() {
static T instance;
return instance;
}
protected:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
// 使用方式
class Logger : public Singleton<Logger> {
friend class Singleton<Logger>; // 允许基类访问私有构造
private:
Logger() = default;
public:
void log(const std::string& msg) {
std::cout << msg << std::endl;
}
};
// 调用
Logger::getInstance().log("Hello Singleton!");
📋 总结
核心要点
1️⃣ 单例模式 = 私有构造 + 静态实例 + 公有访问点
2️⃣ C++11静态局部变量是最佳实践,天然线程安全
3️⃣ 禁止拷贝和赋值,使用 = delete
4️⃣ 单例内部操作仍需考虑线程安全
5️⃣ 避免滥用单例,它本质上是一个全局变量
使用场景总结
核心原则 :当需要控制对共享资源的访问时,考虑使用单例模式。
| 场景类型 | 是否适用 | 说明 |
|---|---|---|
| 打印队列/任务队列 | ✅ 非常适合 | 物理资源天然唯一,必须统一调度 |
| 数据库连接池 | ✅ 非常适合 | 防止连接过多导致资源耗尽 |
| 线程池 | ✅ 非常适合 | 防止线程数失控导致系统崩溃 |
| 配置管理器 | ✅ 适合 | 保证配置一致性 |
| 播放器核心 | ✅ 适合 | 状态和资源统一管理 |
| 日志管理器 | ⚠️ 可用但非必需 | 更多是便利性而非必要性 |
| 频繁创建的对象 | ❌ 不适用 | 应使用对象池 |
| 需要多实例的场景 | ❌ 不适用 | 与单例矛盾 |
| 需要依赖注入 | ⚠️ 慎用 | 影响可测试性 |
最佳实践建议
cpp
// ✅ 推荐的单例写法
class MySingleton {
public:
static MySingleton& getInstance() {
static MySingleton instance;
return instance;
}
// 业务方法...
private:
MySingleton() = default;
~MySingleton() = default;
MySingleton(const MySingleton&) = delete; //禁止拷贝
MySingleton& operator=(const MySingleton&) = delete; //禁止赋值
MySingleton(MySingleton&&) = delete; //禁止移动构造
MySingleton& operator=(MySingleton&&) = delete; //禁止移动赋值
};
如果您觉得这篇文章对您有帮助,不妨点赞 + 收藏 + 关注,更多设计模式系列教程将持续更新 🔥!