设计模式之单例模式

目录

概述

构造函数私有化方式

1.声明private

2.=delete

实现方式

1.静态局部变量方式

2.静态成员变量方式

3.双重检测指针和atexit方式

4.双重检测指针和自定义销毁器方式

5.智能指针方式

6.智能指针和自定义销毁器方式

7.Qt的原子指针方式

优点

缺点

使用场景


概述

单例模式是创建型设计模式中的一种,创建型模式它主要是用来管理对象的分配和释放;单例模式又称单件模式,实质是一种经过改进的全局变量;保证一个class只有一个实体(instance),并为它提供一个全局的访问点。有很多地方需要这样的功能模块,如系统的日志输出,,MODEM的联接需要一条且只需要一条电话线,操作系统只能有一个窗口管理器,只有一个系统时钟,一台PC连一个键盘。UML通用类图如下:

对于C++各种单例模式的设计与实现,主要涉及到以下几点:

1.与一般单纯的全局函数相比,C++用静态成员变量和静态成员函数实现了单例对象,静态 变量存放在全局存储区,且是唯一的,供所有对象使用。
2.用于支持单例的一些C++基本手法,如默认构造函数和复制构造函数私有化,阻止了外界用户私自创建对象,阻止了多实例的发生。
3.单例模式对象的创建方式,如懒汉式和饿汉式,懒汉式是程序中需要此对象的才去创建它;饿汉式是对象就像全局变量一样,进入main函数之前,程序就自动创建了它。
4.摧毁对象并检测摧毁之后的访问动作。

一个合理的Singletons至少应该执行"dead-reference检测",为了做到这一点,我们可以增加一个成员变量static bool destroyed_来追踪析构行为,其值一开始为false,Singletons析构函数会将它设为true,具体的代码如下:

cpp 复制代码
class CSingletons
{
private:
    CSingletons() {}
    CSingletons(const CSingletons&){}
    CSingletons& operator=(const CSingletons&){}
public:
    static  CSingletons* getInstance(){
        if (!m_pInstance){
            onDeadReference();
        }else{
            Create();
        }
        return m_pInstance;
    }
private:
    void Create(){
        static CSingletons  theInstance;
        m_pInstance= &theInstance;
    }
    static void  onDeadReference(){
        throw std::runtime_error("Dead reference detacted");
    }
    ~CSingletons(){
        m_pInstance = 0;
        m_destoryed = true;
    }
private:
    static CSingletons* m_pInstance;
    static bool  m_destoryed;
}
static CSingletons* CSingletons::m_pInstance= 0;
static bool   CSingletons::m_destoryed = false;

5.实现单例模式对象的生命周期管理方案,可以使用生命周期的优先级来解决这个问题。
6.多线程相关问题。

构造函数私有化方式

禁止外界用户私自创建class的方式有两种:

1.声明private

如下代码:

cpp 复制代码
class CSingletons
{
private:
    CSingletons();
    ~CSingletons();
    CSingletons(const CSingletons&);
    CSingletons& operator=(const CSingletons&);
public:
    ...
}

2.=delete

在C++11及其后续版本中,我们可以在函数原型中使用=delete关键字,这是一种显式地禁止某个函数的使用的方式。这种技术常常用于禁止类的复制操作,或者阻止特定类型的参数。代码如下:

cpp 复制代码
class CSingletons
{
public:
    CSingletons()=delete;
    ~CSingletons()=delete;
    CSingletons(const CSingletons&)=delete;
    CSingletons& operator=(const CSingletons&)=delete;
public:
    ...
}

实现方式

1.静态局部变量方式

C++11后,规定了局部静态对象在多线程场景下的初始化行为,只有在首次访问时才会创建实例,后续不再创建而是获取。若未创建成功,其他的线程在进行到这步时会自动等待。注意C++11前的版本不是这样的,如下代码:

cpp 复制代码
class CSingletons
{
private:
    CSingletons() {}
    ~CSingletons(){}
    CSingletons(const CSingletons&){}
    CSingletons& operator=(const CSingletons&){}
public:
    static  CSingletons* getInstance(){
        static CSingletons obj;  //1
        return  &obj;
    }
}

1处看似很简单的一行代码,其实编译器背后在此处会产生很多代码,使得初始化之后,执行期相关机制会登记需被析构的变量,这写代码的C++形式的伪码(pseudo code)看起来像下面这样(两个底线起头的变量应该被视为影藏变量,也就是由编译器产生和管理的变量):

cpp 复制代码
CSingletons& CSingletons::instance(){
    extern void __ConstructSingletons(void* memory);
    extern void __DestroySingletons();
    static bool __initialized = false;
    static char __buffer[sizeof(CSingletons)];
    if (!__initialized ){
        __ConstructSingletons(__buffer);
        atexit(__DestroySingletons);
        __initialized = true;
    }
    return *reinterpret_cast<CSingletons*>(__object);
}

其中的核心动作是对atexit()的调用,atexit()由标准C程序库提供,让你得以注册一些在程序结束之际自动被调用的函数,且其被调用次序为后进先出(LIFO,根据定义,C++对象析构已LIFO方式进行,先产生的对象后摧毁)。

2.静态成员变量方式

这种方式和全局变量方式差不多,代码如下:

cpp 复制代码
class CSingletons
{
private:
    CSingletons() {}
    ~CSingletons(){}
    CSingletons(const CSingletons&){}
    CSingletons& operator=(const CSingletons&){}
public:
    static  CSingletons* getInstance(){
        return  &m_obj;
    }

private:
    static CSingletons m_obj;
}

static CSingletons CSingletons::m_obj;

3.双重检测指针和atexit方式

通过双重检测锁,可以确保线程安全。代码如下:

cpp 复制代码
#include <mutex>
using namespace std;

class CSingletons
{
private:
    CSingletons() {}
    ~CSingletons(){}
    CSingletons(const CSingletons&){}
    CSingletons& operator=(const CSingletons&){}
public:
    static  CSingletons* getInstance(){
        if (m_pObj == 0){    //1
            std::unique_lock locker(m_mutex);//加互斥锁
            if (m_pObj == 0){ //2
                m_pObj = new CSingletons();
                atexit(CSingletons::destroyInstance);
            }
        }
        return m_pObj;
    }
private:
    static void destroyInstance(){
        if (m_pObj){
            delete m_pObj;
        }
    }

private:
    static CSingletons* m_pObj;
    static std::mutex   m_mutex;
}

static CSingletons* CSingletons::m_pObj = 0;
static std::mutex CSingletons::m_mutex;

在代码的1处如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞;等锁解除后,被堵塞的线程就会跳过2处了,因为此时实例已经构建完毕。

上面的代码之所以在getInstance函数里面对m_pObj是否为空做了两次判断,因为该方法调用一次就产生了对象,m_pObj== NULL 大部分情况下都为false,如果按照原来的方法,每次获取实例都需要加锁,效率太低。而改进的方法只需要在第一次 调用的时候加锁,可大大提高效率。

4.双重检测指针和自定义销毁器方式

其实在CSingletons内部也可以自定义内部类,利用析构函数删除m_pObj,代码如下:

cpp 复制代码
#include <mutex>
using namespace std;

class CSingletons
{
private:
    CSingletons() {}
    ~CSingletons(){}
    CSingletons(const CSingletons&){}
    CSingletons& operator=(const CSingletons&){}
public:
    static  CSingletons* getInstance(){
        if (m_pObj == 0){    //1
            std::unique_lock locker(m_mutex);//加互斥锁
            if (m_pObj == 0){ //2
                m_pObj = new CSingletons();
            }
        }
        return m_pObj;
    }
private:
    class MyDeleter{
    public:
        ~MyDeleter(){
            if (m_pObj){
                delete m_pObj;
            }
        }
    }

private:
    static CSingletons* m_pObj;
    static std::mutex   m_mutex;
    static MyDeleter    m_deleter;
}

static CSingletons* CSingletons::m_pObj = 0;
static std::mutex CSingletons::m_mutex;
static CSingletons::MyDeleter CSingletons::m_deleter;

5.智能指针方式

智能指针的最大好处就是能够自动释放内存,这里用std::shared_ptr,代码如下:

cpp 复制代码
#include <mutex>
using namespace std;

class CSingletons
{
private:
    CSingletons() {}
    ~CSingletons(){}
    CSingletons(const CSingletons&){}
    CSingletons& operator=(const CSingletons&){}
public:
    static  std::shared_ptr<CSingletons> getInstance(){
        if (!m_pObj){    //1
            std::unique_lock locker(m_mutex);//加互斥锁
            if (!m_pObj){ //2
                m_pObj.reset(new CSingletons());
            }
        }
        return m_pObj;
    }

private:
    static std::shared_ptr<CSingletons> m_pObj;
    static std::mutex   m_mutex;
}

static std::shared_ptr<CSingletons> CSingletons::m_pObj = 0;
static std::mutex CSingletons::m_mutex;

6.智能指针和自定义销毁器方式

智能指针的销毁也可以自定义,如下代码:

cpp 复制代码
#include <mutex>
using namespace std;

class CSingletons
{
private:
    CSingletons() {}
    ~CSingletons(){}
    CSingletons(const CSingletons&){}
    CSingletons& operator=(const CSingletons&){}
public:
    static  std::shared_ptr<CSingletons> getInstance(){
        if (!m_pObj){    //1
            std::unique_lock locker(m_mutex);//加互斥锁
            if (!m_pObj){ //2
                m_pObj.reset(new CSingletons(), [](CSingletons* p){
                            if (p){
                                delete p;
                            }
                        });
            }
        }
        return m_pObj;
    }

private:
    static std::shared_ptr<CSingletons> m_pObj;
    static std::mutex   m_mutex;
}

static std::shared_ptr<CSingletons> CSingletons::m_pObj = 0;
static std::mutex CSingletons::m_mutex;

关于自定义销毁器还有不明白的地方,可以查看我的另外一篇博客C++智能指针的自定义销毁器(销毁策略)_51如何销毁指针-CSDN博客

7.Qt的原子指针方式

代码如下:

cpp 复制代码
#include <mutex>
using namespace std;

class CSingletons
{
private:
    CSingletons() {}
    ~CSingletons(){}
    CSingletons(const CSingletons&){}
    CSingletons& operator=(const CSingletons&){}
public:
    static  CSingletons& getInstance(){
            //使用双重检测。  
			/*! testAndSetOrders操作保证在原子操作前和后的的内存访问
			* 不会被重新排序。
			*/
			if (m_instance.testAndSetOrdered(0, 0))//第一次检测  
			{
				QMutexLocker locker(&mutex);//加互斥锁。  
				m_instance.testAndSetOrdered(0, new SingleTon());//第二次检测。  
			}
		    return *m_instance;
    }

private:
    static	QMutex m_mutex;//实例互斥锁。  
	static	QAtomicPointer<SingleTon> m_instance;/*!<使用原子指针,默认初始化为0。*/
}

static QAtomicPointer<SingleTon> CSingletons::m_instance;
static QMutex  CSingletons::m_mutex;

单例模式与控制反转

显式地将某个组件变为单例的方式具有明显的侵入性,而如果决定在某一时段不再将某个类作为单例,最终又会付出高昂的代价。另一种解决方案是采用一种约定,在这种约定中,负责组件的函数并不直接控制组件的生命周期,而是外包给控制反转(Inversion of Control, IoC)容器。

当使用Boost.DI的依赖注入框架时,定义单例组件的代码如下:

cpp 复制代码
auto injector = di::make_injector(
    di::bind<IFoo>.to<Foo>.in(di::singleton),
    //other configuration steps here
);
//Foo继承IFoo

从上面的代码中,我们使用字母"I"表示接口类型。本质上,di::bind这一行代码的意思是,每当需要具有IFoo类型成员的组件时,我们使用Foo的单例实例来初始化该组件。

许多开发人员认为,在DI容器中使用单例是唯一可以接受的使用单例的方式。至少,如果需要用其他东西替换单例,使用这种方式就是可以在一个中心位置(配置容器的代码处)执行这个操作。另外一个好处是,我们不必自己实现任何单例的逻辑,这可以防止出现潜在的错误。此外,是否提到过Boost.DI是线程安全的?

优点

1)由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决。

2)由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

3)单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

4)单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

缺点

1)单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求"自行实例化",并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。

2)单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。

3)单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把要单例和业务逻辑融合在一个类中。

使用场景

在一个系统中,要求一个类有仅有一个对象,如果出现多个对象就会出现"个良反应",可以采用单例模式,具体的场景如下:

  1. 数据库连接池,在多线程环境下,通过单例模式管理数据库连接池可以避免资源的重复创建和释放,提高性能和效率

2)日志记录器,使用单例模式管理日志记录器可以确保日志的一致性,并提供全局的访问点,方便记录系统中的日志信息

3)在整个项目中需要一个共享访问点或共享数据,例如一个web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的。创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。

4)需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。

原创不易,如果觉得喜欢,可以收藏和关注!!!

相关推荐
思忖小下22 分钟前
梳理你的思路(从OOP到架构设计)_设计模式Observer模式
观察者模式·设计模式·eit
午言若32 分钟前
MYSQL 架构
c++·mysql
一条小小yu2 小时前
单例模式
单例模式
羑悻的小杀马特2 小时前
【AIGC篇】畅谈游戏开发设计中AIGC所发挥的不可或缺的作用
c++·人工智能·aigc·游戏开发
闻缺陷则喜何志丹2 小时前
【C++动态规划】1105. 填充书架|2104
c++·算法·动态规划·力扣·高度·最小·书架
ThetaarSofVenice3 小时前
能省一点是一点 - 享元模式(Flyweight Pattern)
java·设计模式·享元模式
记得多喝水o3 小时前
图解设计模式
设计模式
InSighT__3 小时前
设计模式与游戏完美开发(2)
java·游戏·设计模式
初学者丶一起加油3 小时前
C语言基础:指针(数组指针与指针数组)
linux·c语言·开发语言·数据结构·c++·算法·visual studio
思忖小下4 小时前
梳理你的思路(从OOP到架构设计)_设计模式Android + Composite模式
设计模式·composite模式