单例模式笔记总结

单例模式笔记总结

首先它都叫单例模式了,那它的特点就是全局只有唯一一个实例。所以这个设计模式的核心就是围绕着如何维护全局唯一实例。

如何保证全局只有一个实例

不妨先想想有什么办法可以实例化对象,再将路堵死

  • 首先想到的就是构造函数
    • 不能在类外使用构造函数构造一个实例
  • 其次就是拷贝构造函数
    • 拷贝构造函数就是用一个旧实例初始化一个新实例,所以也不能在类外使用
  • 最后就是拷贝赋值操作符
    • 拷贝赋值操作会将右值浅拷贝再赋值

以上就是它可以造出实例的办法了,那么只需要在类中设为私有或禁用 即可

**注意: 构造函数不能禁用,只能私有化。否则一个实例都构造不出来

  1. 禁用
c++ 复制代码
class TaskQueue{  
public:  
    TaskQueue(const TaskQueue& t) = delete;  
    TaskQueue& operator= (const TaskQueue& t) = delete;  
private:  
    TaskQueue() = default;  
};

2.私有化

c++ 复制代码
class TaskQueue{  
public:  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
};

现在在类外是无法实例化对象了,而我们又必须要一个实例化对象,只能在类内声明了。

  • 那现在考虑一个问题:
    • 用普通的成员变量可以吗?
    • 答案是否定的。因为如果是普通的成员变量,那怎么拿到这唯一的实例呢,通过成员函数吗?
      • 显然是不行的(类外无法构造出实例了,用不了成员函数)。
      • 那有什么函数是不用实例化对象就可以调用的呢?
      • 显然只有静态函数了,那普通成员变量在静态函数中能访问到吗?
      • 显然也是不行的,静态函数里只能访问静态成员变量。

综上我们总结出了:静态成员+静态函数

懒汉模式与饿汉模式

饿汉模式

饿汉模式就是在类加载时就初始化实例,通过静态函数返回实例。

  • 为什么叫饿汉模式呢?因为在类加载时就初始化了,但不知道什么时候会用它,所以它一直保持"饥饿"状态。(我是这么理解的)
c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance(){  
        return m_tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
  
    static TaskQueue* m_tasks;  
};  
TaskQueue* TaskQueue::m_tasks = new TaskQueue();

懒汉模式

懒汉模式就是第一次通过静态函数返回实例时才初始化。

  • 为什么叫懒汉模式呢?因为它将初始化拖到了使用时才进行,很懒哈,临时抱佛脚。
c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance(){  
        if(m_tasks==nullptr){  
            m_tasks = new TaskQueue();  
        }  
        return m_tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
  
    static TaskQueue* m_tasks;  
};  
TaskQueue* TaskQueue::m_tasks = nullptr;

懒汉模式下多线程的问题

**在多线程下懒汉模式有重复构造实例的隐患

  • 如果现在有两个线程同时获取实例,两个线程同时判断m_tasks==nullptr,都进入了构造逻辑,生成了两个实例
    那怎么办呢?加锁
c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance(){  
        {  
            lock_guard<mutex> lg(m_mutex);  
            if (m_tasks == nullptr) {  
                m_tasks = new TaskQueue();  
            }  
        }  
        return m_tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
  
    static TaskQueue* m_tasks;  
    static mutex m_mutex;  
};  
TaskQueue* TaskQueue::m_tasks = nullptr;  
mutex TaskQueue::m_mutex;

如果是按照上面的情况加锁,每次访问实例都要等待资源,效率太慢。其实只有初始化时才需要上锁,所以我们可以判断:**如果已经初始化了那就可以不用等待资源直接返回实例。

c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance() {  
        if(m_tasks==nullptr) {  
            {  
                lock_guard<mutex> lg(m_mutex);  
                if (m_tasks == nullptr) {  
                    m_tasks = new TaskQueue();  
                }  
            }  
        }  
        return m_tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
  
    static TaskQueue* m_tasks;  
    static mutex m_mutex;  
};  
TaskQueue* TaskQueue::m_tasks = nullptr;  
mutex TaskQueue::m_mutex;

**注意:内层还是需要if再判断一次的,如果现在有两个线程同时获取实例,两个线程同时判断m_tasks==nullptr,都进入了构造逻辑,就会生成了两个实例


问题来了,现在就安全了吗?

  • 看起来是天衣无缝,但是m_tasks = new TaskQueue();在底层正常会按顺序执行:
    • 分配内存
    • 初始化对象
    • 赋值操作
  • 但是编译器为了优化会进行**指令重排序
    • 分配内存
    • 赋值操作
    • 初始化对象
      现在有一个场景:两个线程访问实例,其中一个线程走到m_tasks = new TaskQueue();遇到了指令重排,先赋值没有初始化对象,那么另外一个线程发现指针不为nullptr,直接返回了一个未知对象。

解决方法:c++11中的atomic原子变量,它默认固定了指令的顺序(当然也可以通过load修改顺序)

  • 分配内存
  • 初始化对象
  • 赋值操作
c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance() {  
  
        if(m_tasks==nullptr) {  
            {  
                lock_guard<mutex> lg(m_mutex);  
                if (m_tasks == nullptr) {  
                    m_tasks = new TaskQueue();  
                }  
            }  
        }  
        return m_tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
  
    static atomic<TaskQueue*> m_tasks;  
    static mutex m_mutex;  
};  
atomic<TaskQueue*> TaskQueue::m_tasks = nullptr;  
mutex TaskQueue::m_mutex;

局部静态对象实例

最轮椅的来了。没什么好解释的。

缺点:

  • 只能初始化一次
  • 只有等程序退出才会销毁对象内存
    优点:
  • 代码简洁
c++ 复制代码
class TaskQueue{  
public:  
    static TaskQueue* getInstance() {  
        static TaskQueue tasks;  
        return &tasks;  
    }  
  
private:  
    TaskQueue() = default;  
    TaskQueue(const TaskQueue& t) = default;  
    TaskQueue& operator= (const TaskQueue& t) = default;  
};
相关推荐
阿旭超级学得完2 小时前
C++11(初始化)
java·开发语言·数据结构·c++·算法
洛水水2 小时前
设计模式入门:从设计原则到核心模式
c++·设计模式
Languorous.2 小时前
C++数据结构进阶|并查集(Union-Find)详解:从原理到面试实战
数据结构·c++·面试
不知名的老吴2 小时前
C++中emplace函数的不适场景总结(一)
java·开发语言·c++
Languorous.2 小时前
C++数据结构进阶|堆(Heap)详解:从手写实现到面试高频实战
数据结构·c++·面试
sheeta19983 小时前
LeetCode 每日一题笔记 日期:2026.05.12 题目:1665. 完成所有任务的最少初始能量
笔记·算法·leetcode
khalil10203 小时前
代码随想录算法训练营Day-49 图论01 | 图论理论基础、深搜理论基础、98. 所有可达路径、广搜理论基础
c++·算法·leetcode·深度优先·图论
Cinema KI3 小时前
Linux C/C++ 编译构建:GCC/G++ + Makefile 零基础完整教程
linux·c语言·c++
念恒123063 小时前
基础IO(文件缓冲区)
linux·c语言·c++