设计模式——单例模式(懒汉模式,饿汉模式)

声明: 本博客参考C语言中文网和优秀博客总结得出:
(1)C语言中文网链接
(2)优秀博客链接

单例模式的定义: 指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、打印机的后台处理服务、应用程序中的对话框、系统中的缓存等常常被设计成单例。
单例模式在现实生活中的应用也非常广泛,例如公司 CEO、部门经理等都属于单例模型。

单例模式特点:
(1)单例类只有一个实例对象;
(2)该单例对象必须由单例类自行创建;
(3)单例类对外提供一个访问该单例的全局访问点。
为了满足以上三个特点,我们通常对类进行以下设计:
(1)将构造函数设置为私有。(这样外界就不可以随便实例化对象了)
(2)有一个指向实例的静态指针。
(3)用一个静态成员方法(或者全局友元函数,只不过我们通常都是用静态成员方法)来实例化对象,并且对实例化进行控制,只实例化一次,让静态指针指向实例,第一次进行实例操作,后面都直接返回已经指向实例的静态指针变量本身。

单例模式的优点和缺点:
单例模式的优点:
(1)单例模式可以保证内存里只有一个实例,减少了内存的开销。
(2)可以避免对资源的多重占用。
(3)单例模式设置全局访问点,可以优化和共享资源的访问。
单例模式的缺点:
(1)单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
(2)在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
(3)单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

一、懒汉模式

根据代码看问题:

//单例模式-懒汉模式
class singleton_idler
{
public:
	static singleton_idler* Get_objectptr()//静态成员方法
	{
		if (object_ptr == nullptr)
		{
			mtx.lock();
			if (object_ptr == nullptr)
			{
				object_ptr = new singleton_idler;
			}
			mtx.unlock();
		}
		return object_ptr;
	}
	int& Get_val()
	{
		return m_val;
	}
	
private:
	singleton_idler() {}
	static singleton_idler* object_ptr;
	static int m_val;
	static mutex mtx;
};

mutex singleton_idler::mtx;
int singleton_idler::m_val = 10;
singleton_idler* singleton_idler::object_ptr = nullptr;

int main()
{
	singleton_idler* ptr1 = singleton_idler::Get_objectptr();
	cout << "ptr1指向的实例地址为:" << ptr1 << "\t" << "m_val地址为:"
		<< &(ptr1->Get_val()) << "\t" << "m_val值为:" << ptr1->Get_val() << endl;

	singleton_idler* ptr2 = singleton_idler::Get_objectptr();
	cout << "ptr1指向的实例地址为:" << ptr2 << "\t" << "m_val地址为:"
		<< &(ptr2->Get_val()) << "\t" << "m_val值为:" << ptr2->Get_val() << endl;

	return 0;
}

运行结果:
在这里插入图片描述
可以通过两个指针指向的实例的地址看出来,两个指针指向的是同一个实例,整个过程就只实例化出来一个对象。
这就是懒汉模式,你需要的时候才给你创建一个实例,而且后面你用的就只有这一个实例,创建一个实例后,就不创建了。
构造函数被设置成私有,就是为了让创建对像的权力只交给下面这个静态函数:

static singleton_idler* Get_objectptr()//静态成员方法
	{
		if (object_ptr == nullptr)
		{
			mtx.lock();
			if (object_ptr == nullptr)
			{
				object_ptr = new singleton_idler;
			}
			mtx.unlock();
		}
		return object_ptr;
	}

这个静态方法是整个懒汉模式的核心,而这个静态方法的核心就是创建对象,对象的创建被层层保护起来,就是为了确保只实例化一次。
如果object_ptr不为nullptr,说明已经创建过对象了(注意,object_ptr是静态指针变量),那么就直接返回指针本身就好了。如果object_ptr没有创建过对象,那么object_ptr就是nullptr(注意代码中,静态指针成员变量object_ptr,我们在类外初始化为nullptr了),那么就会进到第一个if语句,而进到第一个if语句加锁是因为,在多线程的情况下,在没有实例化对象的情况下,可能两个地方同时调用这个静态方法,那么此时两个地方用的是同一个object_ptr都是nullptr,那么都会进到第一层for循环,所以第一层for循环里面加锁了,此时只能一个线程拿到锁,进到里面。那么为什么锁里面又有个if语句呢? 注意,此时一个线程拿到锁了,进去了,还一个线程没有拿到锁,就阻塞在mtx.lock()这个地方了,当进去的线程实例化完成后,释放锁,在mtx.lock()被阻塞的线程就会拿到锁,如果此时没有里面那一层if语句,这个线程就会重新new一个对象,将object_ptr重新指向这个对象,这只是两个线程可能出现的问题,如果更多个线程进到第一个if语句,就会new很多次对象了。当然,如果已经object_ptr已经不为nullptr了,那么没有进到第一个if语句的线程,第一个if语句就进不去了。所以最安全的办法就是锁外锁内都有一个if语句。
当然,在博客上还可以看到一种写法:

static singleton_idler* Get_objectptr()//静态成员方法
	{
		mtx.lock();
		if (object_ptr == nullptr)
		{
			object_ptr = new singleton_idler;
		}
		mtx.unlock();
		
		return object_ptr;
	}

将锁外面的if语句去掉了,这样也是线程安全的,因为锁只能被一个线程拿到,当第一个实例化对象之后,其他线程拿到锁进去之后,if语句判断就会发现object_ptr不为nullptr,就不会再去实例化了。
但是这段代码和上面两个if语句的代码有点区别,就是当已经实例化过对象之后,多个线程同时进到这个静态方法里面之后,这个时候就会只有一个线程拿到锁,去判断if语句,而其他线程就会都阻塞到mtx.lock这里,然后前面的线程拿到锁判断完if语句之后,解锁,被阻塞的这些线程会抢锁,有一个线程抢到,进入到里面,进行if语句判断,其他线程阻塞到mtx.lock()这里…
而两个if语句的代码,当已经实例化过后,就不存在阻塞到mtx.lock()这里的情况了,因为第一个if语句判断完不为nullptr之后就直接return object_ptr了。

二、饿汉模式

饿汉模式和单例模式的区别在于,懒汉模式在你需要的时候才去创建,饿汉模式利用静态指针变量存在数据区的特点,在一开始就直接创建一个实例(在线程创建之前),所以饿汉模式的线程肯定是安全的。
代码示例:

//单例模式-懒汉模式

class singleton_hungry
{
public:
	static singleton_hungry* Get_objectptr()
	{
		return object_ptr;
	}
private:
	singleton_hungry() { cout << "创建一个实例" << endl; }
	~singleton_hungry() { cout << "析构一个实例" << endl; }
	//为了让实例整个程序运行的时候都存在,所以不允许实例自己调动析构函数
	//因为对于饿汉模式来说,实例的创建只能在编译时创建一次,如果析构了,后面不可能再创建了
	//除非重新编译运行
	static singleton_hungry* object_ptr;
};

singleton_hungry* singleton_hungry::object_ptr = new singleton_hungry;//直接实例化

int main()
{
	singleton_hungry* ptr1 = singleton_hungry::Get_objectptr();
	singleton_hungry* ptr2 = singleton_hungry::Get_objectptr();

	return 0;
}

运行结果:
设计模式——单例模式(懒汉模式,饿汉模式)_第1张图片
由于实例在一开始创建,而且在这之后不再可能创建,除非重新编译运行,为了不让实例自己析构,我们就将析构函数写成私有。
将析构函数写成私有之后,那么实例就只能等程序运行结束,自动回收实例占有的堆空间了。然后如果想要在程序运行结束之前析构实例,就成了难题,(其实我在想,我们将析构函数写成私有就是为了让实例在整个程序运行的时候都存在,然后将析构函数写成私有之后,我们却又在想怎么析构实例,那么我们为了让实例在整个程序运行阶段都存在而把析构函数写成私有的意义呢?,但是这些解决办法都了解了解挺好的),有以下几点解决办法:
(1)在类中写一个释放资源的方法

//单例模式-懒汉模式

class singleton_hungry
{
public:
	static singleton_hungry* Get_objectptr()
	{
		return object_ptr;
	}
	/***************************添加释放资源的方法*********************************/
	static void Deleteobject()//释放资源的方法
	{
		delete object_ptr;
	}
	/*******************************************************************************/
private:
	singleton_hungry() { cout << "创建一个实例" << endl; }
	~singleton_hungry() { cout << "析构一个实例" << endl; }
	//为了让实例整个程序运行的时候都存在,所以不允许实例自己调动析构函数
	//因为对于饿汉模式来说,实例的创建只能在编译时创建一次,如果析构了,后面不可能再创建了
	//除非重新编译运行
	static singleton_hungry* object_ptr;
};

singleton_hungry* singleton_hungry::object_ptr = new singleton_hungry;//直接实例化

int main()
{
	singleton_hungry* ptr1 = singleton_hungry::Get_objectptr();
	singleton_hungry* ptr2 = singleton_hungry::Get_objectptr();
	
	singleton_hungry::Deleteobject();//释放资源
	return 0;
}

运行结果:
在这里插入图片描述
(2)定义一个内部类
内部类的对象生存期到了之后,会调用内部类的析构函数,我们在内部类的析构函数中对singleton_hungry的实例进行析构。

//单例模式-懒汉模式

class singleton_hungry
{
public:
	static singleton_hungry* Get_objectptr()
	{
		return object_ptr;
	}
private:
	singleton_hungry() { cout << "创建一个实例" << endl; }
	~singleton_hungry() { cout << "析构一个实例" << endl; }
	//为了让实例整个程序运行的时候都存在,所以不允许实例自己调动析构函数
	//因为对于饿汉模式来说,实例的创建只能在编译时创建一次,如果析构了,后面不可能再创建了
	//除非重新编译运行
	static singleton_hungry* object_ptr;
private://内部类
	class Inside 
	{
	public:
		Inside() {}
		~Inside() //利用内部类的析构函数,对object_ptr指向的资源进行释放,即调用single_hungry的析构函数释放实例
		{
			if (object_ptr != NULL) //题外话,据本人测试,对指向NULL的指针进行delete不会出错
			{
				delete object_ptr;
				object_ptr = NULL;
			}
		}
	};
	static Inside ins_object;
};


singleton_hungry* singleton_hungry::object_ptr = new singleton_hungry;//直接实例化
singleton_hungry::Inside singleton_hungry::ins_object;
//程序结束前,生存期到,会调用析构函数,析构函数中对singleton_hungry的实例进行析构
//前半的singleton_hungry::Inside是类型,后半的singleton_hungry是作用域

int main()
{
	singleton_hungry* ptr1 = singleton_hungry::Get_objectptr();
	singleton_hungry* ptr2 = singleton_hungry::Get_objectptr();

	return 0;
}

运行结果:
在这里插入图片描述
(3)利用智能指针
对于这个方法,我实践了,但是没有证据证明调用了实例的析构函数进行对象析构了(哪怕在析构函数里做上标记)。

//单例模式-懒汉模式
class singleton_hungry
{
public:
	static shared_ptr<singleton_hungry> Get_objectptr()
	{
		return object_ptr;
	}
private:
	singleton_hungry() { cout << "创建一个实例" << endl; }
	~singleton_hungry() { cout << "析构一个实例" << endl; }
	//为了让实例整个程序运行的时候都存在,所以不允许实例自己调动析构函数
	//因为对于饿汉模式来说,实例的创建只能在编译时创建一次,如果析构了,后面不可能再创建了
	//除非重新编译运行
	static shared_ptr<singleton_hungry> object_ptr;
	static void Destroy(singleton_hungry*)
	{
		object_ptr.~shared_ptr();
		object_ptr = nullptr;
	}
};
shared_ptr<singleton_hungry> singleton_hungry::object_ptr(new singleton_hungry, Destroy);//直接实例化

int main()
{
	shared_ptr<singleton_hungry> ptr1 = singleton_hungry::Get_objectptr();
	shared_ptr<singleton_hungry> ptr2 = singleton_hungry::Get_objectptr();

	return 0;
}

总的来说,对于饿汉模式,我们将析构函数设置成私有,是为了程序运行的整个过程,实例都存在,程序结束,系统会自动回收堆空间,如果说不想要等程序结束自动回收,那就别把析构函数设置成私有了,省的还大费周章的考虑用其他的方式来析构。
一般我们都会将析构函数设置成私有,等程序结束,让系统自动回收。
博客参考链接:优秀博客链接

你可能感兴趣的:(设计模式,单例模式,设计模式,c++)