单例模式笔记总结

单例模式笔记总结

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

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

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

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

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

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

  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;  
};
相关推荐
x173880627314 小时前
XXE外部实体注入
xml·笔记
888CC++15 小时前
C语言与C++的区别:从面向过程到面向对象
java·c语言·c++
Darkwanderor17 小时前
Linux系统编程实战项目:模拟实现shell
linux·c++
himobrinehacken17 小时前
揭秘Windows程序启动的神秘之旅
c++·安全
库玛西18 小时前
C++ 运行时多态 :核心总结与原理图解
c语言·c++·笔记
Byron Loong19 小时前
【C++】重定向是什么
开发语言·c++
魔力女仆19 小时前
语言开发笔记3
笔记
hansang_IR19 小时前
【题解】LC:Z 算法(Z Algorithm)
c++·算法·字符串
x173880627319 小时前
文件包含漏洞介绍
笔记
霍霍的袁21 小时前
【C++】模板从入门到进阶(函数模板 + 类模板 + 特化 + 分离编译)
开发语言·c++·visual studio