【C++】 ——C++单例模式中的饿汉和懒汉模式

一、 单例模式的定义

单例模式是一种常见的软件设计模式。它的核心结构只包含一个被称为单例的特殊类。它的目的是保证一个类仅有一个实例并提供一个访问它的全局访问点,该实例被所有程序模块共享

有很多地方都需要这样的功能模块,如系统的日志输出,操作系统只能有一个窗口管理器,一台PC连接一个键盘等。

单例模式是通过类本身来管理其唯一实例,这种特性提供了解决问题的办法。唯一的实例是类的一个普通对象,但涉及这个类的时候,让它只能创建一个实例并提供对此实例的全局访问。创建实例的操作我们通常把这个成员函数叫做instance(),他的返回值是唯一实例的指针,另外,我们可以提供一个public静态方法来帮助我们获得这个类的唯一的一个实例化对象

二、 单例模式的懒汉模式

咱先记住第一句话:第一次用到类的实例的时候才回去实例化

什么意思呢?像一个懒汉一样,需要用到创建实例了的程序再去创建实例,不需要创建实例程序就不去创建实例,这是一个时间换空间的做法,同时体现了懒汉本性。

实现方法:定义一个单例类,使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例
如以下代码所示:

#include <iostream>
#include <stdlib.h>
using namespace std;

class singleton  //实现单例模式的类
{
private:
	singleton() //私有的构造函数,这样就不能再其他地方创建该实例
	{

	}
	static singleton* instance;  //定义一个唯一指向实例的指针,并且是私有的
	static int b; 
public:
	static singleton* GetInstance()  //定义一个公有函数,可以获取这个唯一实例
	{
		if (instance == NULL)  //判断是不是第一次使用
			instance = new singleton;
		return instance;
	}
	static void show()
	{
		cout << b << endl;
	}
};
int singleton::b = 10; //静态成员变量在类外进行初始化,它是整个类的一部分并不属于某个类
singleton* singleton::instance = NULL;
int main()
{
	singleton* a1 = singleton::GetInstance();
	cout << a1 << endl;
	a1->show();

	singleton* a2 = singleton::GetInstance();
	cout << a2 << endl;
	a2->show();
	system("pause");
	return 0;
}

我们来看看执行结果:
在这里插入图片描述
显而易见,我们看到实例的两个对象的地址都是一样的,也就是说单例模式的实现是成功的。

从以上实例中我们可以看出,懒汉模式的singleton类有以下特点:

  • 他有一个指向唯一实例的静态指针,并且是私有的
  • 它有一个公有的函数,可以获取这个唯一的实例,并且在需要的时候创建该实例
  • 它的构造函数是私有的,这样就不能从别处创建该类的实例

改进

但是这存在一个缺点,也就是说它在单线程下是正确的,但是在多线程情况下,如果两个线程同时首次调用GetInstance()方法,那么就会同时监测到instance为NULL,则两个线程会同时构造一个实例给instance,这样就会发生错误。所以,我们对以上的单例模式进行改进——没错,就是加锁,当instance不为空的时候就不需要进行加锁的操作,代码如下:

class singleton  //实现单例模式的类
{
private:
	singleton() //私有的构造函数,这样就不能再其他地方创建该实例
	{
	}
	static singleton* instance;  //定义一个唯一指向实例的指针,并且是私有的
	static int b; 
public:
	static singleton* GetInstance()
	{
		Lock(); //上锁
		if (instance == NULL)
		{
			instance = new singleton;
		}
		Unlock();  //解锁
		return instance;
	}
};

三、 单例模式的饿汉模式

咱再记住第二句话:单例类定义的时候就进行实例化

这又是什么意思呢?像一个饿汉一样,不管需不需要用到实例都要去创建实例,即在类产生的时候就创建好实例,这是一种空间换时间的做法。作为一个饿汉而言,体现了它的本质——“我全都要”

在饿汉模式中,实例对象储存在全局数据区,所以要用static来修饰,所以对于饿汉模式来说,是线程安全的,因为在线程创建之前实例就已经创建好了。 我们直接来看看代码:

#include <iostream>
#include <stdlib.h>
using namespace std;

class singleton
{
private:
	static singleton* instance; //这是我们的单例对象,它是一个类对象的指针
	singleton()
	{
		cout << "创建一个单例对象" << endl;
	}
	~singleton()
	{
		cout << "析构掉一个单例对象" << endl;
	}
public:
	static singleton* getinstance();
};
//下面这个静态成员变量在类加载的时候就已经初始化好了
singleton* singleton::instance = new singleton();
singleton* singleton::getinstance()
{
	return instance;  //直接返回inatance
}
int main()
{
	cout << "we get the instance" << endl;
	singleton* a1 = singleton::getinstance();
	singleton* a2 = singleton::getinstance();
	singleton* a3 = singleton::getinstance();
	cout << "we destroy the instance" << endl;
	system("pause");
	return 0;
}

看看结果截图:
在这里插入图片描述
第一行使我们执行构造函数的结果,这应该没有问题,后两句是主函数的执行结果,但是没有执行析构函数!发现了吗?会导致内存泄漏,这个大家应该都能想到,什么原因?

经过博主的学习,发现此时全局数据区中,存储的并不是一个实例对象,而是一个实例对象的指针,它是一个地址而已,我们真正占有资源的实例对象是存在堆中,我们需要手动的去调用delete释放申请的资源。

但是也不能去手动调用析构,因为析构我们已经声明为private了,根本调不动。这里给出的一个解决办法是在类中写一个主动释放资源的方法:
在这里插入图片描述
红框的是我加的delete函数,我们再来看看效果截图:
在这里插入图片描述
这下就调用了我们的析构函数,那其实我们自己写的时候要手动的去调用这个函数来释放资源,是很不方便的,于是大佬们又想出另外一种更加优化的方法——直接声明一个内部类:

#include <iostream>
#include <stdlib.h>
using namespace std;

class singleton
{
private:
	singleton()
	{
		cout << "创建一个单例对象" << endl;
	}
	~singleton()
	{
		cout << "析构掉一个单例对象" << endl;
	}
	static singleton* instance; //这是我们的单例对象,它是一个类对象的指针
public:
	static singleton* getinstance();
	//static void deleteInstance();
private:
	class Garbo   //内部类
	{
	public:
		Garbo()
		{}
		~Garbo()
		{
			if (instance != NULL)
			{
				delete instance;
				instance = NULL;
			}
		}
	};
	static Garbo gar; //定义一个内部类的静态对象,当该对象销毁的时候,调用析构函数顺便销毁我们的单例对象
};
//下面这个静态成员变量在类加载的时候就已经初始化好了
singleton* singleton::instance = new singleton();
singleton::Garbo singleton::gar; //初始化gar静态成员变量
singleton* singleton::getinstance()
{
	return instance;  //直接返回inatance
}

int main()
{
	cout << "we get the instance" << endl;
	singleton* a1 = singleton::getinstance();
	singleton* a2 = singleton::getinstance();
	singleton* a3 = singleton::getinstance();
	cout << "we destroy the instance" << endl;
	//singleton::deleteInstance();
	system("pause");
	return 0;
}

截图如下:
在这里插入图片描述
我们并没有手动的去调用delete函数,还销毁了这个对象,有的博主说智能指针还可以解决这个问题,我只能说,这个确实可以,是大佬无疑了。其实思路也很简单,博主直接把人家的文章贴出来,有兴趣盆友自行查看智能指针实现饿汉模式

四、 单例模式的应用场景

优点
(1)在单利模式中,活动的实例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例,这样就防止其他对象自己实例化,确保所有对象都访问一个实例
(2)单例模式中的类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
(3)提供了对唯一实例的受控访问
(4)避免对共享资源的多重使用
(5)由于在系统只存在一个对象,因此可以节约资源,当需要偏饭创建和销毁对象时,单例模式无疑可以提高系统性能
缺点
(1)不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
(2)滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
(3)由於单利模式中没有抽象层,因此单例类的扩展有很大的困难。

应用场景
(1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
(2)控制资源的情况下,方便资源之间的互相通信。如线程池等。

本文吸取了很多优秀博主的文章,但是博主的文章只适合初学者入个门,惭愧…我把引用把链接贴出来,大家可以理解的更透彻C++单利模式中的饿汉模式以及单例模式的优缺点

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章