C++设计模式创建型之单例模式

一、概述

单例模式也称单态模式,是一种创建型模式,用于创建只能产生一个对象实例的类。例如,项目中只存在一个声音管理系统、一个配置系统、一个文件管理系统、一个日志系统等,甚至如果吧整个Windows操作系统看成一个项目,那么其中只存在一个任务管理器窗口等。引入单例模式的实现意图:保证一个类仅有一个实例存在,同时提供能对该实例访问的全局方法。

二、单例模式分类

1、懒汉模式

1)代码示例

class CSingletonImpl

{

public:

static CSingletonImpl* GetInstance()

{

if (m_pInstance == nullptr)

{

m_pInstance = new CSingletonImpl;

}

return m_pInstance;

}

private:

CSingletonImpl(){};

~CSingletonImpl(){};

CSingletonImpl(const CSingletonImpl& the);

CSingletonImpl& operator=(const CSingletonImpl& other);

private:

static CSingletonImpl* m_pInstance;

};

CSingletonImpl*CSingletonImpl::m_pInstance = nullptr;

2)说明

单例模式为了防止多对象问题,将构造函数,析构函数,拷贝构造函数,赋值运算符函数设置为私有,同时设置公有唯一接口方法来创建对象,同时定义类静态指针。这是通用方法,那么会有什么问题呢?如果在单一线程中使用则没什么问题,但是在多线程中使用则可能导致问题,如果多个线程可能会因为操作系统时间片调度问题切换造成多对象产生,那么解决这个问题的方案就是对GetInstance()成员函数枷锁。

示例代码:

加入私有成员变量:static std::mutex m_mutex;

static CSingletonImpl* GetInstance()

{

m_mutex.lock();

if (m_pInstance == nullptr)

{

m_pInstance = new CSingletonImpl;

}

m_mutex.unlock();

return m_pInstance;

}

加入以上代码没有问题了吗?呵呵,还不行,虽然对接口函数加锁,从代码逻辑上没有问题,实现了线程安全,但是从执行效率上来说,是有大问题的。当程序运行中GetInstance()可能会被多个线程频繁调用,每次调用都会经历加锁解锁的过程,这样的话会严重影响程序执行效率,而且加锁机制仅仅对第一次创建对象有意义,对象一旦创建则变成只读对象,在多线程中,对只读对象的访问加锁不仅代价大,而且无意义。那么如何解决这个问题呢?那就是双重锁定机制,基于这种机制函数实现代码:

static CSingletonImpl* GetInstance()

{

if (m_pInstance == nullptr)

{

std::lock_guard<std::mutex> siguard(si_mutex);

if (m_pInstance == nullptr)

{

m_pInstance = new CSingletonImpl;

}

}

return m_pInstance;

}

上述双重锁定机制看起来比较完美,但实际上存在潜在的问题,内存访问重新排序导致双重锁定失效的问题,比较推荐的方法时C++11新标准的一些特性,示例代码如下:

#include <mutex>

#include <atomic>

//通过原子变量解决双重锁定底层问题(load,store)

class CSingletonImpl

{

public:

static CSingletonImpl* GetInstance()

{

CSingletonImpl* task = m_taskQ.load(std::memory_order_relaxed);

std::atomic_thread_fence(std::memory_order_acquire);

if (task == nullptr)

{

std::lock_guard<std::m_mutex> lock(m_mutex);

task = m_taskQ.load(std::memory_order_relaxed);

if (task == nullptr)

{

task = new CSingletonImpl;

std::atomic_thread_fence(std::memory_order_release);

m_taskQ.store(task, std::memory_order_relaxed);

}

}

return task;

}

private:

CSingletonImpl(){};

~CSingletonImpl(){};

CSingletonImpl(const CSingletonImpl& the);

CSingletonImpl& operator=(const CSingletonImpl& other);

private:

static std::mutex m_mutex;

static std::atomic<CSingletonImpl*> m_taskQ;

};

std::mutex CSingletonImpl::m_mutex;

std::atomic<CSingletonImpl*> CSingletonImpl::m_taskQ;

2、饿汉模式

1)示例代码

class CSingletonImpl

{

public:

static CSingletonImpl* GetInstance()

{

return m_pInstance;

}

private:

CSingletonImpl(){};

~CSingletonImpl(){};

CSingletonImpl(const CSingletonImpl& the);

CSingletonImpl& operator=(const CSingletonImpl& other);

private:

static CSingletonImpl* m_pInstance;

};

CSingletonImpl*CSingletonImpl::m_pInstance = new CSingletonImpl();

2)说明

此类模式可称为饿汉式--------程序一执行不管是否调用了GetInstance()成员函数,这个单例类对象就已经被创建了。在饿汉式单例类代码的实现必须要注意,如果一个项目中有多个.cpp源文件,而且这些源文件中包含对全局变量的初始化代码,例如某个.cpp中可能存在如下代码:

int g_test = CSingletonImpl::GetInstance()->m_i; //m_i是int类型变量

那么这样的代码是不安全的,因为多个源文件中全局变量的初始化顺序是不确定的,很可能造成GetInstance()函数返回是nullptr,此时去访问m_i成员变量肯定会导致程序执行异常。所以,对饿汉式单例类对象的使用,应该在程序入口函数开始执行后,例如main函数后。

注意:函数第一次执行时被初始化的静态变量与通过编译器常量进行初始化的基本类型静态变量这两种情况,不要再单例类的析构函数中引用其他单例类对象。

相关推荐
键盘会跳舞16 分钟前
C++:std::tuple 源码级深度拆解——变参模板、SFINAE与模板元编程核心技巧
开发语言·c++·sfinae·变参模板
bkspiderx29 分钟前
从零开始:VS Code搭建C/C++开发环境全指南(Windows/macOS/Linux)
c语言·c++·windows·vs code·c/c++开发环境
东华万里31 分钟前
第40篇C++核心基础与工程实践:从底层逻辑到避坑指南
开发语言·c++·面试·大学生专区
luj_176844 分钟前
元设计的诱惑与现实
c语言·开发语言·c++·经验分享·算法
小星星闪亮登场1 小时前
ST表--倍增思想
开发语言·数据结构·c++·算法·思维
东华万里1 小时前
第40篇 C++核心基础与工程实践:从底层逻辑到避坑指南
开发语言·c++·大学生专区
AC赳赳老秦1 小时前
OpenClaw 多源采集公开行业数据:从原始信息到研究报告初稿的自动化实践
java·c语言·c++·python·php·deepseek·openclaw
玩三国杀玩的1 小时前
Pytorch-c++-CUDA
c++·pytorch·python·深度学习
captain3762 小时前
多线程单例模式
java·单例模式·java-ee
wangjialelele3 小时前
动态规划DP经典题型总结(Java、C++):路径、子数组、子序列、背包问题详解
java·c++·算法·面试·动态规划·代理模式