C++单例模式的几种实现

单例模式(Singleton Pattern)的概念

模式定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

饿汉单例和懒汉单例

常见的单例模式有两个分支,饿汉单例懒汉单例

  • 饿汉单例是指在程序初始化时就把单例对象创建出来。其优点是当要使用对象时可以直接获取,缺点是无论对象有没有被调用,都会被创建出来占据内存。
  • 懒汉单例是指当单例对象第一次被调用时才创建对应对象。其优点是不被调用的对象将不被创建,缺点是创建对象时会花费较多的时间,导致第一次调用的特殊性,且对象创建时是线程不安全的。

使用建议
应当采用饿汉单例,将单例对象的初始化直接放在主线程的开头位置执行
这样做有很多好处,首先保证创建对象时不会产生竞争条件(race condition),使得代码编写起来简单、清晰(如果不在主线程创建好,则要考虑多个线程同时创建对象,此时需要通过互斥量等手段加以限制,虽然C++11的static关键字可以确保只创建一个对象,但依然不推荐这么使用);其次可以保证获取对象的时间是固定的,不会像懒汉模式那样第一次调用会花费额外的时间。

单例模式的实现

1、最基本的饿汉单例模式的实现

#include <iostream>

class SingletonClass {
public:
	//单例对象的全局访问点
	static SingletonClass* getInstance() {
		if (m_pInstance == nullptr) {
			m_pInstance = new SingletonClass();
			return m_pInstance;
		}
		std::cout << "对象的地址为 " << m_pInstance << std::endl;
		return m_pInstance;
	}

	//销毁单例对象
	static void destoryInstance() {
		delete m_pInstance;  //即使单例对象没有被真正创建,但在定义时为其赋值nullptr了,因此直接delete是不会存在问题的
		m_pInstance = nullptr;
		return;
	}

private:
	//构造函数
	SingletonClass() {
		std::cout << "SingletonClass的单例对象被创建" << std::endl;
	}

	//析构函数
	~SingletonClass() {
		std::cout << "SingletonClass的单例对象被销毁" << std::endl;
	}

	//禁止拷贝
	SingletonClass(const SingletonClass&) = delete;
	SingletonClass operator=(const SingletonClass&) = delete;

	static SingletonClass* m_pInstance;  //单例对象的声明
};

SingletonClass* SingletonClass::m_pInstance = nullptr;  //单例对象的定义

int main() {
	//在主线程最开始的地方就创建出单例对象,相当于饿汉单例
	SingletonClass::getInstance();

	//...

	//多次调用,获取的都是同一个对象
	SingletonClass* obj1 = SingletonClass::getInstance();
	SingletonClass* obj2 = SingletonClass::getInstance();
	SingletonClass* obj3 = SingletonClass::getInstance();

	//...

	//最后要手动销毁单例对象
	SingletonClass::destoryInstance();

	return 0;
}

输出:
SingletonClass的单例对象被创建
对象的地址为 00000172D3499600
对象的地址为 00000172D3499600
对象的地址为 00000172D3499600
SingletonClass的单例对象被销毁

2、提供额外接口,让饿汉单例体现在接口设计上

在1中,虽然我们希望实现的是饿汉单例,但因为没有提供特定的接口,因此需要程序员保证在主函数中先调用一次SingletonClass::getInstance();来确保逻辑上的正确性,这显然是不合适的,因此可以通过增加一个接口,用于专门创建饿汉单例对象,在没有创建出单例对象前尝试获取单例对象会抛出异常,以此提醒程序员这是饿汉单例,应当在主函数中进行创建。

#include <iostream>
#include <exception>

class SingletonClass {
public:
	//单例对象的全局访问点
	static SingletonClass* getInstance() {
		if (m_pInstance == nullptr) {
			throw std::logic_error("this instance is not construct, please construct the instance in main() function first");
		}
		std::cout << "对象的地址为 " << m_pInstance << std::endl;
		return m_pInstance;
	}

	//创建单例对象
	static void constructInstance() {
		if (m_pInstance == nullptr) {
			m_pInstance = new SingletonClass();
		}
		return;
	}

	//销毁单例对象
	static void destoryInstance() {
		delete m_pInstance;  //即使单例对象没有被真正创建,但在定义时为其赋值nullptr了,因此直接delete是不会存在问题的
		m_pInstance = nullptr;
		return;
	}

private:
	//构造函数
	SingletonClass() {
		std::cout << "SingletonClass的单例对象被创建" << std::endl;
	}

	//析构函数
	~SingletonClass() {
		std::cout << "SingletonClass的单例对象被销毁" << std::endl;
	}

	//禁止拷贝
	SingletonClass(const SingletonClass&) = delete;
	SingletonClass operator=(const SingletonClass&) = delete;

	static SingletonClass* m_pInstance;  //单例对象的声明
};

SingletonClass* SingletonClass::m_pInstance = nullptr;  //单例对象的定义

int main() {
	//若没有创建单例对象,就尝试获取它,会抛出异常
	try {
		SingletonClass* obj1 = SingletonClass::getInstance();
	}
	catch (const std::exception& e) {
		std::cout << e.what() << std::endl;
	}

	//创建单例对象
	SingletonClass::constructInstance();

	//...

	//多次调用,获取的都是同一个对象
	SingletonClass* obj1 = SingletonClass::getInstance();
	SingletonClass* obj2 = SingletonClass::getInstance();
	SingletonClass* obj3 = SingletonClass::getInstance();

	//...

	//最后要手动销毁单例对象
	SingletonClass::destoryInstance();

	return 0;
}

输出:
this instance is not construct, please construct the instance in main() function first
SingletonClass的单例对象被创建
对象的地址为 000001F768EE9600
对象的地址为 000001F768EE9600
对象的地址为 000001F768EE9600
SingletonClass的单例对象被销毁

3、利用RAII机制,实现自动释放单例对象

在1、2中,需要程序员手动来回收单例对象,实际上,在特定情况下,可以利用RAII机制,实现自动释放单例对象。因为单例对象实际是类的静态成员,所以需要使用到特殊的技巧,通过一个类内类的对象的生命周期来管理单例对象的生命。
注意static对象的生命周期是和程序生命周期一致的,因此只要在主函数开始的时候初始化好对象,之后程序运行时都可以正常的获得到单例对象。
这种方式的弊端是没法自己决定销毁单例对象的时机了。

#include <iostream>
#include <exception>

class SingletonClass {
public:
	//单例对象的全局访问点
	static SingletonClass* getInstance() {
		if (m_pInstance == nullptr) {
			throw std::logic_error("this instance is not construct, please construct the instance in main() function first");
		}
		std::cout << "对象的地址为 " << m_pInstance << std::endl;
		return m_pInstance;
	}

	//初始化单例对象
	static void constructInstance() {
		if (m_pInstance == nullptr) {
			m_pInstance = new SingletonClass();
			static SingletonClass_Helper helper;  //利用此对象的生命周期来自动销毁单例对象
		}
		return;
	}

private:
	//构造函数
	SingletonClass() {
		std::cout << "SingletonClass的单例对象被创建" << std::endl;
	}

	//析构函数
	~SingletonClass() {
		std::cout << "SingletonClass的单例对象被销毁" << std::endl;
	}

	//禁止拷贝
	SingletonClass(const SingletonClass&) = delete;
	SingletonClass operator=(const SingletonClass&) = delete;

	static SingletonClass* m_pInstance;  //单例对象的声明

	//内部类,用于辅助销毁单例对象
	class SingletonClass_Helper {
	public:
		~SingletonClass_Helper() {
			delete SingletonClass::m_pInstance;  //即使单例对象没有被真正创建,但在定义时为其赋值nullptr了,因此直接delete是不会存在问题的
			m_pInstance = nullptr;
		}
	};
};

SingletonClass* SingletonClass::m_pInstance = nullptr;  //单例对象的定义

int main() {
	std::cout << "main() start" << std::endl;

	{
		std::cout << "Scope start" << std::endl;

		//创建单例对象
		SingletonClass::constructInstance();

		//...

		//多次调用,获取的都是同一个对象
		SingletonClass* obj1 = SingletonClass::getInstance();
		SingletonClass* obj2 = SingletonClass::getInstance();
		SingletonClass* obj3 = SingletonClass::getInstance();

		//...

		std::cout << "Scope end" << std::endl;
	}  //出作用域,static SingletonClass_Helper helper;也不会被销毁,因此单例也不会被销毁

	//依然可以获得单例对象
	SingletonClass* obj1 = SingletonClass::getInstance();
	SingletonClass* obj2 = SingletonClass::getInstance();
	SingletonClass* obj3 = SingletonClass::getInstance();

	std::cout << "main() end" << std::endl;

	return 0;
}

输出:
main() start
Scope start
SingletonClass的单例对象被创建
对象的地址为 00000278654695F0
对象的地址为 00000278654695F0
对象的地址为 00000278654695F0
Scope end
对象的地址为 00000278654695F0
对象的地址为 00000278654695F0
对象的地址为 00000278654695F0
main() end
SingletonClass的单例对象被销毁

4、C++11利用静态局部对象实现的懒汉单例模式

对于懒汉单例而言,必须要考虑多线程下同时调用全局访问点,而该单例对象并没有被创建的情况(也就是第一次被调用的情况),这种情况下,单例对象是有可能被创建两次的,其逻辑如下:
在这里插入图片描述

  • 存在线程安全问题的懒汉单例代码:
#include <iostream>
#include <thread>

class SingletonClass {
public:
	//这样的接口实际是一个懒汉单例
	static SingletonClass* getInstance() {
		if (m_pInstance == nullptr) {
			m_pInstance = new SingletonClass();
		}
		std::cout << "单例对象的地址为 " << m_pInstance << std::endl;
		return m_pInstance;
	}

	//销毁单例对象
	static void destoryInstance() {
		delete m_pInstance;  //即使单例对象没有被真正创建,但在定义时为其赋值nullptr了,因此直接delete是不会存在问题的
		m_pInstance = nullptr;
		return;
	}
private:
	//构造函数
	SingletonClass() {
		std::cout << "SingletonClass的单例对象被创建" << std::endl;
	}

	//析构函数
	~SingletonClass() {
		std::cout << "SingletonClass的单例对象被销毁" << std::endl;
	}

	//禁止拷贝
	SingletonClass(const SingletonClass&) = delete;
	SingletonClass operator=(const SingletonClass&) = delete;

	static SingletonClass* m_pInstance;  //static data member,其本质是global object
};

SingletonClass* SingletonClass::m_pInstance = nullptr;

//线程初始化函数
void func() {
	SingletonClass::getInstance();
	return;
}

int main() {
	//多个线程同时去尝试创建单例对象
	std::thread th1(func);
	std::thread th2(func);

	th1.join();
	th2.join();

	//最后要手动销毁单例对象
	SingletonClass::destoryInstance();

	return 0;
}

可能的输出:
SingletonClass的单例对象被创建
单例对象的地址为 000001750F380410
SingletonClass的单例对象被创建
单例对象的地址为 000001750F3802B0
SingletonClass的单例对象被销毁

在C++11前,一般会通过加互斥量来解决这个问题,当然这会使得代码编写较为麻烦,此时的逻辑如下:
在这里插入图片描述

  • 利用互斥量保证线程安全的懒汉单例代码(这里利用了双重锁定的技巧):
#include <iostream>
#include <thread>
#include <mutex>

class SingletonClass {
public:
	//这样的接口实际是一个懒汉单例
	static SingletonClass* getInstance() {
		if (m_pInstance == nullptr) {  //双重锁定 A
			std::unique_lock<std::mutex> my_unique_lock(m_mutex);   //自动上锁解锁
			if (m_pInstance == nullptr) {  //双重锁定 B
				m_pInstance = new SingletonClass();
			}
		}
		std::cout << "单例对象的地址为 " << m_pInstance << std::endl;
		return m_pInstance;
	}

	//销毁单例对象
	static void destoryInstance() {
		delete m_pInstance;  //即使单例对象没有被真正创建,但在定义时为其赋值nullptr了,因此直接delete是不会存在问题的
		m_pInstance = nullptr;
		return;
	}
private:
	//构造函数
	SingletonClass() {
		std::cout << "SingletonClass的单例对象被创建" << std::endl;
	}

	//析构函数
	~SingletonClass() {
		std::cout << "SingletonClass的单例对象被销毁" << std::endl;
	}

	//禁止拷贝
	SingletonClass(const SingletonClass&) = delete;
	SingletonClass operator=(const SingletonClass&) = delete;

	static SingletonClass* m_pInstance;  //static data member,其本质是global object
	static std::mutex m_mutex;  //互斥量
};

SingletonClass* SingletonClass::m_pInstance = nullptr;
std::mutex SingletonClass::m_mutex;

//线程初始化函数
void func() {
	SingletonClass::getInstance();
	return;
}

int main() {
	//多个线程同时去尝试创建单例对象
	std::thread th1(func);
	std::thread th2(func);

	th1.join();
	th2.join();

	//最后要手动销毁单例对象
	SingletonClass::destoryInstance();

	return 0;
}

输出:
SingletonClass的单例对象被创建
单例对象的地址为 0000027A7A2502C0
单例对象的地址为 0000027A7A2502C0
SingletonClass的单例对象被销毁

C++11标准保证,如果多个线程试图同时初始化同一静态局部对象,则初始化严格只发生一次。参考
需要注意这里保证的是静态局部对象,但并不是静态成员变量,所以1、2、3中实现的单例模式在创建对象时是线程不安全的(当然因为1、2、3的设计都是饿汉单例,所以不存在这个问题),因为其单例对象是static SingletonClass* m_pInstance;

  • 利用C++11静态局部对象实现的懒汉单例代码:
#include <iostream>
#include <thread>

class SingletonClass {
public:
	//这样的接口实际是一个懒汉单例
	static SingletonClass& getInstance() {
		static SingletonClass instance;  //function-local static object
		return instance;
	}

private:
	//构造函数
	SingletonClass() {
		std::cout << "SingletonClass的单例对象被创建" << std::endl;
	}

	//析构函数
	~SingletonClass() {
		std::cout << "SingletonClass的单例对象被销毁" << std::endl;
	}

	//禁止拷贝
	SingletonClass(const SingletonClass&) = delete;
	SingletonClass operator=(const SingletonClass&) = delete;
};

//线程初始化函数
void func() {
	SingletonClass::getInstance();
	return;
}

int main() {
	//多个线程同时去尝试创建单例对象
	std::thread th1(func);
	std::thread th2(func);

	th1.join();
	th2.join();

	return 0;
}

输出:
SingletonClass的单例对象被创建
SingletonClass的单例对象被销毁

想要把懒汉单例改为饿汉单例也非常简单,只要确保在主函数中先获取一次单例对象即可。

对1、2、3、4实现的总结

在C++11标准后,4的实现方式较为流行,一般按照4这种方式使用即可。
但2的实现方式也很优秀,接口设计清晰,并且饿汉模式是较为推荐的使用方式。

5、利用模板实现一个较为通用的饿汉单例模式

在C++11前,模板的参数是固定的,因此常见的做法是只适配6个参数以内的构造函数。
代码如下:

#include <iostream>

template <typename T>  //模板参数应当是一个类类型
class Singleton {
public:
	//单例对象的全局访问点
	//因为不需要参数了,所以可以设计的很简单
	static T* getInstance() {
		if (m_pInstance == nullptr) {  //单例对象未创建,说明代码逻辑存在问题
			throw std::logic_error("this instance is not construct, please construct the instance in main() function first");
		}
		std::cout << "单例对象的地址为 " << m_pInstance << std::endl;
		return m_pInstance;
	}

	//创建单例对象 支持0个参数
	static void constructInstance() {
		if (m_pInstance == nullptr) {
			m_pInstance = new T();
		}
		return;
	}
	
	//创建单例对象 支持1个参数
	template <typename T0>
	static void constructInstance(T0 arg0) {
		if (m_pInstance == nullptr) {
			m_pInstance = new T(arg0);
		}
		return;
	}

	//创建单例对象 支持2个参数
	template <typename T0, typename T1>
	static void constructInstance(T0 arg0, T1 arg1) {
		if (m_pInstance == nullptr) {
			m_pInstance = new T(arg0, arg1);
		}
		return;
	}

	//创建单例对象 支持3个参数
	template <typename T0, typename T1, typename T2>
	static void constructInstance(T0 arg0, T1 arg1, T2 arg2) {
		if (m_pInstance == nullptr) {
			m_pInstance = new T(arg0, arg1, arg2);
		}
		return;
	}

	//创建单例对象 支持4个参数
	template <typename T0, typename T1, typename T2, typename T3>
	static void constructInstance(T0 arg0, T1 arg1, T2 arg2, T3 arg3) {
		if (m_pInstance == nullptr) {
			m_pInstance = new T(arg0, arg1, arg2, arg3);
		}
		return;
	}

	//创建单例对象 支持5个参数
	template <typename T0, typename T1, typename T2, typename T3, typename T4>
	static void constructInstance(T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4) {
		if (m_pInstance == nullptr) {
			m_pInstance = new T(arg0, arg1, arg2, arg3, arg4);
		}
		return;
	}

	//创建单例对象 支持6个参数
	template <typename T0, typename T1, typename T2, typename T3, typename T4, typename T5>
	static void constructInstance(T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) {
		if (m_pInstance == nullptr) {
			m_pInstance = new T(arg0, arg1, arg2, arg3, arg4, arg5);
		}
		return;
	}

	//销毁单例对象
	static void destoryInstance() {
		delete m_pInstance;  //即使单例对象没有被真正创建,但在定义时为其赋值nullptr了,因此直接delete是不会存在问题的
		m_pInstance = nullptr;
		return;
	}

private:
	Singleton() = default;
	~Singleton() = default;
	//禁止拷贝
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
	static T* m_pInstance;
};

template <typename T>
T* Singleton<T>::m_pInstance = nullptr;

//测试部分
//类A,构造函数无参
struct A {
	A() {
		std::cout << "A类对象被创建" << std::endl;
	}

	~A() {
		std::cout << "A类对象被销毁" << std::endl;
	}
};

//类B,构造函数有参数
struct B {
	B(int x, int y, int z) {
		std::cout << "B类对象被创建" << std::endl;
	}

	~B() {
		std::cout << "B类对象被销毁" << std::endl;
	}
};

int main() {
	//创建单例对象
	Singleton<A>::constructInstance();  //创建A类对象单例
	Singleton<B>::constructInstance(1, 2, 3);  //创建B类对象单例

	//获取单例对象
	auto obj_a1 = Singleton<A>::getInstance();
	auto obj_a2 = Singleton<A>::getInstance();

	auto obj_b1 = Singleton<B>::getInstance();
	auto obj_b2 = Singleton<B>::getInstance();

	//销毁单例对象
	Singleton<A>::destoryInstance();
	Singleton<B>::destoryInstance();

	return 0;
}

输出:
A类对象被创建
B类对象被创建
单例对象的地址为 000001BA054D02E0
单例对象的地址为 000001BA054D02E0
单例对象的地址为 000001BA054D0400
单例对象的地址为 000001BA054D0400
A类对象被销毁
B类对象被销毁

在C++11后,引入了可变长模板、万能引用、完美转发,利用这些技术,可以编写出非常通用的模板。
代码如下:

#include <iostream>

template <typename T>  //模板参数应当是一个类类型
class Singleton {
public:
	//单例对象的全局访问点
	//因为不需要参数了,所以可以设计的很简单
	static T* getInstance() {
		if (m_pInstance == nullptr) {  //单例对象未创建,说明代码逻辑存在问题
			throw std::logic_error("this instance is not construct, please construct the instance in main() function first");
		}
		std::cout << "单例对象的地址为 " << m_pInstance << std::endl;
		return m_pInstance;
	}

	//创建单例对象 可以传入任意参数
	template <typename... Args>
	static void constructInstance(Args&&... args) {  //Args&&... args是万能引用
		if (m_pInstance == nullptr) {
			m_pInstance = new T(std::forward<Args>(args)...);  //完美转发
		}
		return;
	}

	//销毁单例对象
	static void destoryInstance() {
		delete m_pInstance;  //即使单例对象没有被真正创建,但在定义时为其赋值nullptr了,因此直接delete是不会存在问题的
		m_pInstance = nullptr;
		return;
	}

private:
	Singleton() = default;
	~Singleton() = default;
	//禁止拷贝
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
	static T* m_pInstance;
};

template <typename T>
T* Singleton<T>::m_pInstance = nullptr;

//测试部分
//类A,构造函数无参
struct A {
	A() {
		std::cout << "A类对象被创建" << std::endl;
	}

	~A() {
		std::cout << "A类对象被销毁" << std::endl;
	}
};

//类B,构造函数有参数
struct B {
	B(int x, int y, int z) {
		std::cout << "B类对象被创建" << std::endl;
	}

	~B() {
		std::cout << "B类对象被销毁" << std::endl;
	}
};

//类C,有带左值和带右值的构造函数
struct C {
	C(const std::string&) {
		std::cout << "C类对象被创建,参数为左值" << std::endl;
	}

	C(std::string&&) {
		std::cout << "C类对象被创建,参数为右值" << std::endl;
	}

	~C() {
		std::cout << "C类对象被销毁" << std::endl;
	}
};

int main() {
	//创建单例对象
	Singleton<A>::constructInstance();  //创建A类对象单例
	Singleton<B>::constructInstance(1, 2, 3);  //创建B类对象单例
	std::string c_arg = "hello";
	Singleton<C>::constructInstance(std::move(c_arg));  //创建C类对象单例

	//获取单例对象
	auto obj_a1 = Singleton<A>::getInstance();
	auto obj_a2 = Singleton<A>::getInstance();

	auto obj_b1 = Singleton<B>::getInstance();
	auto obj_b2 = Singleton<B>::getInstance();

	auto obj_c1 = Singleton<C>::getInstance();
	auto obj_c2 = Singleton<C>::getInstance();

	//销毁单例对象
	Singleton<A>::destoryInstance();
	Singleton<B>::destoryInstance();
	Singleton<C>::destoryInstance();

	return 0;
}

输出:
A类对象被创建
B类对象被创建
C类对象被创建,参数为右值
单例对象的地址为 0000019BE71E0340
单例对象的地址为 0000019BE71E0340
单例对象的地址为 0000019BE71E03B0
单例对象的地址为 0000019BE71E03B0
单例对象的地址为 0000019BE71E0380
单例对象的地址为 0000019BE71E0380
A类对象被销毁
B类对象被销毁
C类对象被销毁