设计模式 | 单例模式——饿汉模式 & 懒汉模式

发布于:2025-06-21 ⋅ 阅读:(18) ⋅ 点赞:(0)

单例模式

一、饿汉模式(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 的唯一实例:

  1. 是静态变量,生命周期贯穿整个程序;
  2. 实例在任何 getInstance() 调用之前就已创建完成;
  3. getInstance() 只是简单地返回这个已创建好的指针。

因此,它就是一个标准的饿汉单例模式实现。

4. 实现细节

  1. 为什么TaskQueue* TaskQueue::m_taskQ = new TaskQueue;属于类内访问,可以访问private构造函数?
    是因为它是“静态变量”?“私有变量”?还是“初始化”这件事本身?
条件 是否是关键 解释
这是类的成员定义 ✅ 是关键 初始化 TaskQueue::m_taskQ 是类的一部分,因此有权限访问类的私有成员
static 成员 ❌ 不是核心原因 虽然需要类外初始化,但并不是 static 带来了访问权限
private 变量 ❌ 更不是原因 private 表示“只能被类的代码访问”,而这行被视为类的代码

不管是 static 还是 private,关键原因在于:这是类的“成员变量定义”,属于类的内部实现,因此它拥有类的访问权限。

  1. 延申:若把 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 懒汉
希望按需控制生命周期 ✅ 懒汉更灵活

网站公告

今日签到

点亮在社区的每一天
去签到