首先我们需要知道单例模式的作用是什么,单例模式也就是单实例,全局只有wei'y的作用是什么,单例模式也就是单实例, ,这个实例对象中究竟储存什么数据则都行,例如你希望什么数据是全局唯一的数据,你就可以将这个数据放到这个里面。内存池就可以考虑使用成为单例模式。
既然是全局只有一份的资源,那么肯定要将构造函数限制了,因为限制了构造函数也就限制了普通的创建对象的方式。
限制了构造函数也就意味着,无法从外部去创建对象了,那么就只能从内部去调用了。
在c++中也准备了两种方式,一种是饿汉模式一种是懒汉模式。
饿汉模式,按照字面理解就是很饥饿,你必须提前准备好,我随时都能吃。也就是要提前创建好对象。
一般在饿汉模式中会有一个函数GetInstance这个函数的目的就是获取实例对象,
而饿汉模式:
什么叫做在main函数启动,首先当我们写好一个程序,编译器就能够通过编译将你写的这个代码翻译成二进制的机器码(Linux下就是a.out,Windows下就是xxx.exe这样的文件,其实本质也就是一些二进制的指令)。而我们的进程启动的本质(在Windows上双击某一个文件,或者是在Linux上的./a.out等等)是什么呢? 本质就是父进程创建子进程,因为我们知道在一个os中所有的进程都是一个亲缘关系,在os启动的时候创建了0,1号进程。后面启动的进程都是由前面的进程带起来的,就以bash为例子,创建的bash就可以说是最早的进程了,你将bash关闭了,很多的进程也就没有了。例如你在bash上跑了一个子进程你将bash关闭了这个子进程也就被杀掉了。而一个子进程创建出来之后,子进程才会去运行我们编译好的内容。当然进程运行肯定还是具有前置条件的。前置准备好之后,才会根据我们的c/c++程序的入口main函数开始运行我们的代码。所以这里就是在main函数启动时就存在对象,如何做到呢?
再提出什么是懒汉模式:
那么什么对象在main函数之前就能创建完成呢?
全局对象能在main函数之前创建完成,但是还全局对象在这里是存在很多的缺陷的存在哪些缺陷呢?
首先的缺陷就是无法创建完成,因为我们之前的代码就将饿汉模式的这个类的构造函数私有化了。
还有一个全局对象是完全透明的。如果这个全局变量存在于一个头文件中,所有的只要包含了这个头文件的代码都能看到这个全局对象,那么最好的方法就是静态的全局变量。
但是现在这里的问题仍然是这里的构造代码是私有的。
那么只能在类中去创建对象了,也就是在类中创建一个全局对象,可以这样去写:
这个并不是自己包含自己。如果现在创建了一个A的对象在这这个对象中包含了一个a和一个dict,但是并没有包含这个静态的_inct,因为静态的不在对象中,静态的储存在静态区中了。这里的_inct就是一个声明,至于定义就要在类外面去定义了。
而这里的_inct也就可以访问构造函数了。
使用方式:
但是这样去写会不会存在什么坑呢?
也就是能否出现第二个或者是第三个单例对象呢?
这里的问题就是没有禁止拷贝函数所以我们可以这么去写。
此时就会出现一个改了一个没有改的情况,因为这里是两个对象
所以最好的方式还是要将拷贝函数(赋值函数最好也要将其禁止)也要禁止。
需要注意的就是这里的_inct是不存在于这个类对象中的,这里要将其放到类中是因为c++讲究的就是一个封装,封装的意思也就是管控,也就是要将这个_inst管控起来(收到访问限定符的限制)可以访问家人(private修饰的成员)。
那么饿汉模式的优点和缺点是什么呢?
现在我们写的这个单例模式所对应的类,内部所包含的成员是很少的,但是在一个项目中,一个单例模式的类中所包含的成员一般是比现在的这个多的,而这个对象在你的程序没有运行的时候就已经创建完成了就会占用资源。这就可能会导致,你所写的服务,就会变慢,就可能会影响用户的使用情况 。(例如在10秒用户都没有看到你服务的运行端,一直在初始化资源,就会造成影响)。还有如果我的两个单例对象存在依赖关系,那么饿汉模式就无法控制。例如其中一个单例对象是一个配置信息,然后另外一个单例对象需要依赖这个对象的使用。此时的这两个单例对象我们都是无法控制的(我们无法控制这个初始化的先后顺序)。
所以存在先后顺序,和较为大的对象我们都是无法使用饿汉模式的。
这两个问题都是因为饿汉模式的对象是在main函数启动前就已经具有了的。所以我们就可以使用懒汉模式了。
懒汉肯定是没有这两个缺点的(可以控制顺序,以及影响不会特别影响服务的运行速度)
优点则是实现和运用很简便。
下面我们来实现懒汉模式,但是在细节上我们要如何去控制呢?
首先我们将静态的对象替换成一个指针。然后在外部实现的地方将这个指针赋值为nullptr。
和饿汉模式不同,饿汉模式在一开始就会创建对象资源,会导致启动慢以及无法控制先后顺序的情况。
但是现在这里虽然也创建了一个静态对象,但是这里创建的只是一个指针。就不会出现慢的情况。并且因为这个指针没有被赋值,也就不会出现影响先后顺序的问题。
然后我们修改一下GetInstance函数的代码就完成了我们的懒汉模式的单例对象模式了
但是这里并不是这么简单,在这里是可能出现线程安全的问题的。如果两个执行流一起进入这个函数那么就可以new出两个单例对象出来,这就不正确了。这就是一个不可重入函数,所以这里是需要进行线程保护的。这里最重要的就是线程安全
可以看到饿汉模式和懒汉模式的不同之处就在于一个是在main函数开始之前就已经完成创建了,而懒汉模式则你什么时候需要使用对象才会去创建。
最后对于懒汉模式还存在一个问题,那就是因为我们的懒汉是new出来的空间所以最后我们需要去释放懒汉模式的空间。那么懒汉模式的空间什么时候去释放呢?
我们知道如果我们的进程正常结束这个资源会被os回收。也就不会造成内存浪费。所以new的懒汉对象一般是不需要释放的。
因为我们的懒汉对象一般都是要在的,除了一些特别的情况,需要提前释放我们的懒汉对象。
这里交给os去释放我们的资源并不会造成内存泄漏的情况,因为内存泄漏的定义是:当我们已经不使用某些资源了,但是任然不去释放这个资源,此时也就造成了内存泄漏。
但是懒汉对象一直到程序结束我们都是需要使用它的所以这里并不算是内存泄漏。
但是这里仍然会存在一些问题,首先就是如果这个进程是不正常退出的呢?或者这个进程僵尸了。
此时的这个资源就不一定能够释放了,但是这个问题不常见。
最后还有一些比较严重的问题。
例如:
我们在析构的时候要做一些持久化操作。
此时我们就不得不去调用析构函数了,但是这里如何调用懒汉对象的析构函数呢?
是使用delete吗?
这里我们尝试一下:
可以看到这是一种方法(析构函数为公有,并且调用的方式很怪,并且不推荐,因为可能会有人在这个的后面再去使用这个单例,此时的这个单例虽然你是写到了这里,但是在这里这个单例对象已经被删除了)。
这里我们最期望的方式就是在main函数结束之后,这个析构能够被自动的调用。
如何做到呢?
其中的一种方法就是使用智能指针。但是我这里就不使用智能指针了。
但是如何做到呢?
我们这里写一个函数,这个函数能够帮助我们手动的去释放我们的懒汉对象。
如果我们不手动释放的话,这里我们就再做一些改变。我们增加一个内部类。然后我们使用这个内部类的析构函数去调用上面的这个函数,完成资源的释放。
但是需要注意的是,这里的Del函数必须是静态的函数,因为一般来说只有B的对象才能调用B内部的成员函数。
所以这里为了能够让gc这个内部类调用这里的Del函数,Del函数就成了一个静态的函数。
然后我们再增加一个gc的静态对象成员。
这里的_gc是一个静态的对象在main函数调用之前就已经创建完成了,但是不需要担心,因为这个gc对象所消耗的资源非常的少。但是这个gc会在main函数结束的时候自动得去调用gc的析构函数,也就删除了懒汉对象了。
当然在gc的析构函数你也可以选择使用delete,而不是调用Del函数。但是依旧是需要做一下判断,因为可能别人在前面使用的时候已经手动将这个懒汉对象释放了,此时你在delet就会出现问题,所以这里选择调用Del函数还有一个好处就是,即使你在前面已经删除了这个懒汉对象这里也不会出现问题,因为在Del函数中会判断_inst是否是一个空指针是那么就不会delete。
最后总结一下: