设计模式 | 单例模式——饿汉模式 懒汉模式
单例模式
文章目录
- 单例模式
- 一、饿汉模式(Eager Initialization)
- 1. 定义
- 2. 特点
- 3. 饿汉单例模式(定义时-类外初始化)
- 4. 实现细节
- 二、懒汉模式(Lazy Initialization)
- 1. 定义
- 2. 特点
- 3. 懒汉单例模式(第一次调用时-初始化)
- 4. 多线程不安全(需加锁)
- 三、对比 & 使用建议
一、饿汉模式(Eager Initialization)
1. 定义
类加载时就创建实例,不管你用不用,先创建再说。
2. 特点
- 线程安全(因为类加载是线程安全的)
- 启动时就分配资源,资源消耗可能较大
3. 饿汉单例模式(定义时-类外初始化)
#include <iostream>class TaskQueue {
public:// 静态方法:获取唯一实例指针static TaskQueue* getInstance() {return m_taskQ; // 返回静态成员变量指针}// 删除拷贝构造函数:防止复制实例(例如 TaskQueue b = a)TaskQueue(const TaskQueue&) = delete;// 删除赋值运算符:防止赋值复制(例如 a = b)TaskQueue& operator=(const TaskQueue&) = delete;private:// 默认构造函数私有化:禁止类外部构造对象// 外部无法通过 new TaskQueue() 或 TaskQueue t; 构造对象TaskQueue() = default;// 静态成员变量声明:用于保存唯一实例的指针static TaskQueue* m_taskQ;
};// ⚠️ 类外定义并初始化静态成员变量:这一行非常关键!
// ✅ 这是 TaskQueue 类的“静态成员变量定义+初始化”
// ✅ new TaskQueue 调用了 private 构造函数,但因为这是类自己的代码(初始化自己的静态成员),所以**允许访问私有成员**
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
// --------------------------------------------
// ⬆️ 虽然这行“写在类外”(语法上),但它是类的一部分(静态成员初始化),它仍然被认为是类自己的代码(类内部行为),所以可以访问私有构造函数。
// C++ 标准允许它访问类的 private 构造函数。
// 所以不会报错,而是合法的。int main() {// 获取单例对象的指针TaskQueue* q1 = TaskQueue::getInstance();TaskQueue* q2 = TaskQueue::getInstance();// 打印地址验证是否为同一实例std::cout << "q1 地址: " << q1 << std::endl;std::cout << "q2 地址: " << q2 << std::endl;// 输出地址肯定一样return 0;
}
注意:
// 静态成员变量定义和初始化(在类外完成)
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
这句代码在程序启动时就执行,立即创建了 TaskQueue
的唯一实例:
- 是静态变量,生命周期贯穿整个程序;
- 实例在任何
getInstance()
调用之前就已创建完成; getInstance()
只是简单地返回这个已创建好的指针。
因此,它就是一个标准的饿汉单例模式实现。
4. 实现细节
- 为什么
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
属于类内访问,可以访问private构造函数?
是因为它是“静态变量”?“私有变量”?还是“初始化”这件事本身?
条件 | 是否是关键 | 解释 |
---|---|---|
✅ 这是类的成员定义 | ✅ 是关键 | 初始化 TaskQueue::m_taskQ 是类的一部分,因此有权限访问类的私有成员 |
是 static 成员 | ❌ 不是核心原因 | 虽然需要类外初始化,但并不是 static 带来了访问权限 |
是 private 变量 | ❌ 更不是原因 | private 表示“只能被类的代码访问”,而这行被视为类的代码 |
不管是 static
还是 private
,关键原因在于:这是类的“成员变量定义”,属于类的内部实现,因此它拥有类的访问权限。
- 延申:若把
new TaskQueue
写在main()
中
❌ 非法代码(main 函数中访问私有构造函数)
int main() {TaskQueue* q = new TaskQueue(); // ❌ 错!构造函数是 private
}
为什么报错?
- main() 是类外部的普通代码。
- 它不是类成员,不被视为类内部实现。
- 因此没有权限访问私有构造函数,编译器会直接报错。
✅ 合法代码(类外定义静态成员时调用私有构造函数)
TaskQueue* TaskQueue::m_taskQ = new TaskQueue; // ✅ 对!
为什么合法?
- 这是类在定义和初始化自己的静态成员变量。
- 虽然代码写在类外,但它被视为类的一部分(属于 TaskQueue 类实现)。
- 所以有权访问 private 构造函数。
- C++ 语法明确允许这种访问。
二、懒汉模式(Lazy Initialization)
1. 定义
在第一次访问时才创建实例,延迟到真正需要的时候再进行初始化。
2. 特点
- 延迟加载:只有在首次调用
getInstance()
时才会创建实例,节省系统资源; - 线程不安全(默认实现),但可以通过加锁、双重检查、
std::call_once
等方式实现线程安全; - 相较于饿汉模式,更灵活、更节省资源,但实现稍复杂。
3. 懒汉单例模式(第一次调用时-初始化)
#include <iostream>class TaskQueue {
public:// ❌ 没有加锁,线程不安全 ******不同点******static TaskQueue* getInstance() {if (m_taskQ == nullptr) { m_taskQ = new TaskQueue(); // ❌不安全,可能多个线程同时执行这里,创建多个实例}return m_taskQ;}TaskQueue(const TaskQueue&) = delete;TaskQueue& operator=(const TaskQueue&) = delete;private:TaskQueue() = default;static TaskQueue* m_taskQ;
};// 初始化静态实例指针 ******不同点******
TaskQueue* TaskQueue::m_taskQ = nullptr;int main() {TaskQueue* q1 = TaskQueue::getInstance();TaskQueue* q2 = TaskQueue::getInstance();std::cout << "q1 地址: " << q1 << std::endl;std::cout << "q2 地址: " << q2 << std::endl;// 输出地址一样(如果线程不冲突)return 0;
}
4. 多线程不安全(需加锁)
线程冲突时,多个线程可能在getInstance()
创建多个对象,需要加锁!!!
三、对比 & 使用建议
对比项 | 饿汉模式(Eager Singleton) | 懒汉模式(Lazy Singleton) |
---|---|---|
实例创建时机 | 程序启动时 / 类加载时立即创建 | 第一次调用 getInstance() 时才创建 |
资源占用 | 无论是否使用都会占用资源 | 仅在需要时才占用资源,更节省内存 |
线程安全 | ✅ 天然线程安全(由 C++ 静态初始化保证) | ❌ 默认线程不安全,需手动加锁处理 |
实现难度 | 实现简单,逻辑清晰 | 实现复杂(涉及锁、双检、或 call_once) |
性能开销 | 启动时略高,占用资源可能浪费 | 每次调用 getInstance() 可能涉及锁(效率略低) |
适用场景 | 实例始终会用到,资源占用可接受 | 实例可能不一定会用到,或实例化代价较高 |
常用实现 | 类外初始化静态成员指针(如:new Singleton; ) | 内部判断是否为 null + 加锁后 new Singleton(); |
示例构造代码 | TaskQueue* m = new TaskQueue; (类外直接构造) | if (!m) m = new TaskQueue; (函数内延迟构造) |
可扩展性 | 不容易扩展为参数化构造 | 初始化时可自定义参数(但需额外设计) |
使用场景 | 推荐模式 |
---|---|
实例一定会被频繁使用 | ✅ 饿汉模式(简单稳定) |
实例创建代价高或可能不用 | ✅ 懒汉模式(延迟创建) |
多线程访问高频 | ✅ 饿汉 或 call_once 懒汉 |
希望按需控制生命周期 | ✅ 懒汉更灵活 |