c++单例模式:懒汉式、饿汉式、通用式

题目:设计一个类,我们只能生成该类的一个实例。

单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。有很多地方需要这样的功能模块,如系统的日志输出等。
单例模式有许多种实现方法:
a. 懒汉式:使用的时候才创建,多线程访问的时候线程不安全(双检锁)
b. 饿汉式:类文件加载的时候已经创建好了对象,如果对象一直没有使用,则类对象浪费空间
特点与选择:
如果要进行线程同步,访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。
访问量较小时,采用懒汉实现。这是以时间换空间。

1. 普通的单例模式

分析: 由于要求只能生成一个实例,因此我们必须把构造函数设置为私有函数以禁止他人创建实例。我们可以定义一个静态实例,在需要的时候创建该实例。

//懒汉式 (创建的时候采取new实例)以时间换取空间,线程不安全
class CSingleton{ 
public:
	static CSingleton * GetInstance(){
		if (m_pInstance == NULL)
			m_pInstance = new CSingleton();
		return m_pInstance;
	}
	
private:
	CSingleton() {};
	static CSingleton * m_pInstance;
};
CSingleton* CSingleton::m_pInstance = nullptr;

它有如下特征:

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

2. 增加删除操作的单例模式

继续分析: 大多数时候,上面的实现都不会出现问题。有经验的读者可能会问,m_pInstance指向的空间什么时候释放呢?更严重的问题是,该实例的析构函数什么时候执行?

如果在类的析构行为中有必须的操作,比如关闭文件,释放外部资源,那么上面的代码无法实现这个要求。我们需要一种方法,正常的删除该实例。

2.1 使用析构函数?

//懒汉式 (创建的时候采取new实例)以时间换取空间,线程不安全
class CSingleton{
public:
	static CSingleton * GetInstance(){
		if (m_pInstance == NULL)
			m_pInstance = new CSingleton();
		return m_pInstance;
	}

	//不可以这样用,如果调用了析构函数,会造成无限递归
	~CSingleton(){
		if (m_pInstance != NULL){
			delete  m_pInstance;
			m_pInstance = NULL;
		}
		cout << "~CSingleton()" << endl;
	}
	
private:
	CSingleton() {};
	static CSingleton * m_pInstance;
};
CSingleton* CSingleton::m_pInstance = nullptr;

不可以这样,因为:

  • 单例中的 new 的对象需要delete释放。
  • delete释放对象的时候才会调用对象的析构函数。
  • 如果在析构函数里调用delete,那么程序结束时,根本进不去析构函数,怎么会delete。
  • 如果自己调用delete,那么就会造成一个析构的循坏(经实验,会不断在if (m_pInstance != NULL)和delete m_pInstance;之间循环,最终报栈溢出的错误)

2.2 再定义一个静态函数用于释放

如果定义一个静态函数删除指针这样做可以实现功能,但是不仅很丑陋,而且容易出错,可能会忘记调用:

//懒汉式 (创建的时候采取new实例)以时间换取空间,线程不安全
class CSingleton{ 
public:
	static CSingleton * GetInstance(){
		if (m_pInstance == NULL)
			m_pInstance = new CSingleton();
		return m_pInstance;
	}

	static void DelInstanc() {
		delete m_pInstance;
	}
	
private:
	CSingleton() {};
	static CSingleton * m_pInstance;
};
CSingleton* CSingleton::m_pInstance = nullptr;

2.3 定义一个内嵌类用于删除

一个妥善的方法是让这个类自己知道在合适的时候把自己删除,或者说把删除自己的操作挂在操作系统中的某个合适的点上,使其在恰当的时候被自动执行。

我们知道,程序在结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样。利用这个特征,我们可以在单例类中定义一个这样的静态成员变量,而它的唯一工作就是在析构函数中删除单例类的实例。如下面的代码中的Garbage类:

//懒汉式 (创建的时候采取new实例)以时间换取空间,线程不安全
class CSingleton{ 
public:
	static CSingleton * GetInstance(){
		if (m_pInstance == NULL)
			m_pInstance = new CSingleton();
		return m_pInstance;
	}
	
private:
	CSingleton() {};
	static CSingleton * m_pInstance;

	class Garbage{ // 它的唯一工作就是在析构函数中删除 CSingleton的实例  
	public:
		~Garbage(){
			if (CSingleton::m_pInstance){
				delete CSingleton::m_pInstance;
				CSingleton::m_pInstance = NULL;
			}
		}
	};
	static Garbage garbage;
};
CSingleton* CSingleton::m_pInstance = nullptr;
CSingleton::Garbage CSingleton::garbage;

类Garbage被定义为CSingleton的私有内嵌类,以防该类被在其它地方滥用。
在程序运行结束时,系统会调用CSingleton的静态成员Garbage的析构函数,该析构函数会删除单例的唯一实例。
使用这种方法释放C++单例模式对象有以下特征:

  • 在单例类内部定义专有的嵌套类。
  • 在单例类内定义私有的专门用于释放的静态成员。
  • 利用程序在结束时析构全局变量的特性,选择最终的释放时机。
  • 使用C++单例模式的代码不需要任何操作,不必关心对象的释放。

3. 考虑线程安全

继续分析: 上述代码在单线程的时候工作正常,但在多线程的情况下就有问题了。如果两个线程同时运行到判断 m_pInstance 是否为空的 if 语句,并且 m_pInstance 没有创建,那么两个线程都会创建一个实例,就不再满足单例模式的要求了。

//懒汉式,加入锁机制保证线程安全
class CSingleton{ 
public:
	static CSingleton * GetInstance(){
		mtx.lock();
		if (m_pInstance == NULL)
			m_pInstance = new CSingleton();
		mtx.unlock();
		return m_pInstance;
	}
	
private:
	CSingleton() {};
	static CSingleton * m_pInstance;

	class Garbage{ // 它的唯一工作就是在析构函数中删除 CSingleton的实例  
	public:
		~Garbage(){
			if (CSingleton::m_pInstance){
				delete CSingleton::m_pInstance;
				CSingleton::m_pInstance = NULL;
			}
		}
	};
	static Garbage garbage;
	static mutex mtx;
};
CSingleton* CSingleton::m_pInstance = nullptr;
CSingleton::Garbage CSingleton::garbage;
mutex CSingleton::mtx;

继续分析: 现在还不是很完美,我们每次得到 CSingleton 的实例,都会试图加上一个同步锁,而加锁是一个耗时的操作,应尽量避免使用。所以可以在加锁之前判断 m_pInstance 是否为空。

//懒汉式,加入锁机制保证线程安全
class CSingleton{ 
public:
	static CSingleton * GetInstance(){
		if (m_pInstance == nullptr) {
			mtx.lock();
			if (m_pInstance == NULL)
				m_pInstance = new CSingleton();
			mtx.unlock();
		}
		return m_pInstance;
	}
	
private:
	CSingleton() {};
	static CSingleton * m_pInstance;

	class Garbage{ // 它的唯一工作就是在析构函数中删除 CSingleton的实例  
	public:
		~Garbage(){
			if (CSingleton::m_pInstance){
				delete CSingleton::m_pInstance;
				CSingleton::m_pInstance = NULL;
			}
		}
	};
	static Garbage garbage;
	static mutex mtx;
};
CSingleton* CSingleton::m_pInstance = nullptr;
CSingleton::Garbage CSingleton::garbage;
mutex CSingleton::mtx;

4. 饿汉模式

再来看看饿汉的实现方式,即无论是否调用该类的实例,在程序开始时就会产生一个该类的实例,并在以后仅返回此实例。
由静态初始化实例保证其线程安全性,WHY?因为静态实例初始化在程序开始时进入主函数之前就由主线程以单线程方式完成了初始化,不必担心多线程问题。
故在性能需求较高时,应使用这种模式,避免频繁的锁争夺。
饿汉式本身就是线程安全的,不需要加锁。

//饿汉式(在定义实例的时候就去new对象)以空间换时间
class CSingleton{
public:
	static CSingleton * GetInstance() {
		return m_pInstance;
	}

private:
	CSingleton() {};
	static CSingleton * m_pInstance;

	class Garbage{ // 它的唯一工作就是在析构函数中删除 CSingleton的实例  
	public:
		~Garbage(){
			if (CSingleton::m_pInstance){
				delete CSingleton::m_pInstance;
				CSingleton::m_pInstance = NULL;
			}
		}
	};
	static Garbage garbage;
};
CSingleton* CSingleton::m_pInstance = new CSingleton();
CSingleton::Garbage CSingleton::garbage;

懒汉: 故名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化。与之对应的是饿汉式单例。
饿汉: 饿了肯定要饥不择食。所以在单例类定义的时候就进行实例化。

特点与选择:
懒汉: 在访问量较小时,采用懒汉实现。这是以时间换空间。
饿汉: 由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。

5. 最简洁的方式

我们可以用一个局部静态变量来实现

class Singleton {
public:
	// 注意返回的是引用。
	static Singleton& getInstance() {
		static Singleton m_instance;  //局部静态变量
		return m_instance;
	}

private:
	Singleton() {} //私有构造函数,不允许使用者自己生成对象
};

int main(){
	Singleton& a = Singleton::getInstance();
	Singleton& b = Singleton::getInstance();
	cout << &a << endl;
	cout << &b << endl;
}

然后实验发现返回指针也可以,a和b的值是一样的,但是应该会造成内存泄漏?

class Singleton {
public:
	//返回指针
	static Singleton* getInstance() {
		static Singleton* m_instance = new Singleton;
		return m_instance;
	}

private:
	Singleton() {} //私有构造函数,不允许使用者自己生成对象
};

int main(){
	Singleton* a = Singleton::getInstance();
	Singleton* b = Singleton::getInstance();
	cout << a << endl;
	cout << b << endl;
}

6. 通用型单例模式

继续分析:如何创建一个通用型单例模式,也就是可以传入任何类实现单例。我们可以使用模板

//懒汉式通用型单例模式
#include<iostream>
#include<mutex>

using namespace std;

template<typename T>
class CSingleton {
public:
	static T* get_single() {
		if (!m_single) {
			mtx.lock();
			if (!m_single) {
				m_single = new T();
			}
			mtx.unlock();
		}
		return m_single;
	}

	class Garbage {
	public:
		~Garbage() {
			if (m_single) {
				delete m_single;
			}
			m_single = nullptr;
		}
	};

private:
	CSingleton() {};
	static T* m_single;
	static Garbage garb;
	static mutex mtx;
};

template<typename T>
T* CSingleton<T>::m_single = nullptr;
template<typename T>
typename CSingleton<T>::Garbage CSingleton<T>::garb; //这里要加typename,不然会报这样的错误:“Garbage” : 依赖名称不是类型
template<typename T>
mutex CSingleton<T>::mtx;

class A {
public:
	A() {};
};

int main() {
	A* a = CSingleton<A>::get_single();
	A* b = CSingleton<A>::get_single();
	cout << a << endl;
	cout << b << endl;
}

使用局部静态变量的简易版

#include <iostream>

using namespace std;

template<class T>
class Singleton
{
public:
	static T& instance() {
		static T theInstance;
		return theInstance;
	}

private:
	Singleton() {}
	~Singleton() {}
};

class A {
public:
	A() {};
};

int main() {
	A& a = Singleton<A>::instance();
	A& b = Singleton<A>::instance();
	cout << &a << endl;
	cout << &b << endl;
}

参考:https://blog.csdn.net/zhanghuaichao/article/details/79459130

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值