C++:单例模式(饿汉模式和懒汉模式)

单例模式(饿汉模式和懒汉模式)

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

单例模式概念

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。单例模式有两种实现方法:饿汉模式和懒汉模式。

饿汉模式

饿汉模式其实就是在编译期间就完成了这个唯一的对象的创建和初始化,后序因为私有了构造函数因此也不能再创建别的对象了。叫饿汉可以形象的理解为是一个非常饥饿的人,一上来就开始吃东西。

优点:简单
缺点:因为对象的创建初始化都是在编译期间完成的,因此可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定

#include 
class Configsingleton
{
public:
	//公有的接口来获取这个对象
	static Configsingleton& GetInstance()
	{
		return _sinst;
	}
	void Print()//打印
	{
		std::cout << _ip << std::endl;
		std::cout << _port << std::endl;
	}
private:
	//构造函数私有
	Configsingleton(const char* ip,int port)
		:_ip(ip)
		, _port(port)
	{
		//进行初始化工作
	}

	//防拷贝
	Configsingleton(const Configsingleton&) = delete;//拷贝构造函数
	Configsingleton& operator=(const Configsingleton&) = delete;//赋值运算符重载

	//成员变量
	const char* _ip;
	int _port;

	//定义一个自己类型的对象(相当于全局的,但是此时受类域的限制)
	static Configsingleton _sinst;
};

//在类外定义这个对象_sinst
Configsingleton Configsingleton::_sinst("127.0.0.1", 80);

int main()
{
	//获取这个对象然后对象调Print函数
	Configsingleton::GetInstance().Print();
	
	system("pause");
	return 0;
}

思路解释:

  1. 首先把构造函数私有,防止在类外创建多个对象
  2. 在这个类的私有部分定义一个自身类型的成员变量,这个对象就是全局唯一的一个对象,为了保证可以初始化它,把它定义为静态的,然后在类外初始化它。
  3. 同时在类里定义一个公有的接口来获取到这个私有的对象,并返回该对象。
  4. 再次因为不能创建多余的对象,所以这个公有的接口要定义为static的,这样在main函数里就不用对象就可以调用了。
  5. 如果我们调用该函数生成了一个对象,那么使用拷贝构造函数又可以拷贝构造出一个,这就不对了,因此我们要将拷贝构造函数以及赋值运算符的重载封杀,如果是C++11可以用delete删除,如果是C++98就定义为私有的并且只声明不实现。

懒汉模式

如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。

懒汉模式就是编译时只创建一个指针,创建一个指针是不需要耗费太多时间的,然后如果我要用这个对象的时候在进行创建。叫懒汉模式可以形象的理解为一个特别懒得人,饿的不行了才去吃饭。

优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制。
缺点:复杂

#include 
#include 
#include 
class ConfigSingleton
{
public:
	static ConfigSingleton* GetInstance()
	{
		//双重检查
		if (_psinst == nullptr)//这个判断是如果对象已经定义出来了,那么就直接返回_psinst,就不用加锁解锁了,提高效率
		{
			_mutex.lock();//加锁
			if (_psinst == nullptr)//第一次调用时,如果是空就创建对象
			{
				_psinst = new ConfigSingleton;
			}
			_mutex.unlock();//解锁
		}
		return _psinst;
	}
	void Print()
	{
		std::cout << _ip << std::endl;
		std::cout << _port << std::endl;
	}
private:
	//构造函数私有
	ConfigSingleton(const char* ip = "127.0.0.1", int port = 80)
		:_ip(ip)
		, _port(port)
	{
		std::cout << "ConfigSingleton(const char* ip, int port)" << std::endl;
	}

	//析构函数
	~ConfigSingleton()
	{
		std::cout << "~ConfigSingleton()" << std::endl;
	}

	//成员变量
	const char* _ip;
	int _port;

	//内部类(垃圾回收站)
	class GC
	{
	public:
		~GC()
		{
			delete _psinst;
		}
	};

	//防拷贝
	ConfigSingleton(const ConfigSingleton&) = delete;
	ConfigSingleton& operator=(const ConfigSingleton&) = delete;

	//创建唯一对象的指针
	static ConfigSingleton* _psinst;
	static std::mutex _mutex;
	static GC _gc;
};

ConfigSingleton* ConfigSingleton::_psinst = nullptr;
std::mutex ConfigSingleton::_mutex;
ConfigSingleton::GC ConfigSingleton::_gc;

void fun(int n)
{
	ConfigSingleton::GetInstance()->Print();
	std::cout << ConfigSingleton::GetInstance() << std::endl;;
}
int main()
{
	std::thread t1(fun, 10);
	std::thread t2(fun, 10);
	t1.join();
	t2.join();

	return 0;
}

思路解释:

  1. 首先构造函数私有,为了防止别人随便创建对象,同时要进行防拷贝的工作,将拷贝构造函数和赋值运算符重载封杀,C++98可以只声明不实现这两个函数,C++11里可以使用delete删除这两个函数。
  2. 在编译期间不能消耗太多资源,因此在编译的时候只创建一个指针,创建指针的消耗并不大
  3. 要有一个公有的接口来获取一个唯一的对象,调这个函数就可以得到该对象
  4. 因此在该接口内部就要用new来创建该对象,因此就是_psinst = new ConfigSingleton;
  5. 但是如果这样的话每次调用这个公有的接口就会产生一个对象,所以此时要加判断条件,如果指针为空则创建对象,如果指针不为空则返回这个对象的指针。
  6. 如上基本完成了单例,但是有个很严重的问题就是如果有多个线程来调用该接口的话,并且多个线程同时来访问,这样还是会创建出多个对象。那么该怎么办呢?这就需要加锁,加锁的话可以让并行的线程变成串行来执行,这样就可以保证这个公有的接口每次只能由一个线程来调度,之后的线程就会被挂起等待,直到前面的线程将锁释放。(具体讲解:第一个线程过来调度该接口,拿到锁,进入函数访问,发现指针为空,于是创建对象,第二个线程会被挡在锁处,第一个线程将锁释放。然后第二个线程拿到锁,进入函数进行访问,发现指针不为空,释放锁,然后直接返回该指针,后面的线程来访问该函数也无法创建对象了,因为指针已经不为空了,所以就算后面的线程拿到锁也只能再释放锁然后返回)
  7. 基于上面的操作我们完成了单例懒汉模式的90%。还有一个小的效率问题,就是如果线程很多的话,每个线程过来都要申请锁释放锁就会非常的影响效率,所以这里就用到了双重检查,再给外层加一条判断语句,保证后序再来的线程发现该指针为空就直接返回,不用再进行加锁解锁的操作了,这样就提高了整个程序的效率。

你可能感兴趣的:(C++)