在C++的代码中是要尽量避免使用全局变量的,全局变量可能在程序的任一地方被修改,提高代码的定位难度,还会导致代码耦合性变高,难以模块化测试。但有时候一个类对象想要全局使用,且全局只能初始化一次,这时就可以引入单例模式的思想。这里提到的只是单例模式应用的一个场景,实际上全局变量和单例没太大的相关性,单例和静态类的区别在这里不再赘述,网上可以查找到相关的对比,比如下面这个是C#上的说明:
一、单例模式
在《Head First 设计模式》一书中,单例模式的定义是“确保一个类只有一个实例,并提供一个全局访问点”,类图如下:
通过类图可以看到单例类有两个明显的特点:① 构造函数是私有的,这样在类外就不能随意进行实例化;② 有一个静态的函数,用来获取类内实例化的对象,类的静态函数可以在代码的任何地方进行调用。根据单例类的这两个特点,可以设计出两种经典的单例模式:懒汉式和饿汉式,还有基于懒汉式的线程安全的双检测锁模式,在下面这篇文章中有很详细的说明:
二、双检测锁模式
为了在多线程下使用单例模式,基于线程安全问题仅出现在第一次初始化(new)过程中,引入了双检锁模式。第一次检测不涉及到对象的创建,因此是不加锁的检测,不需要锁的时间消耗,只有检测到对象暂未创建的时候,才会进行对象的加锁操作,然后再进行数据的第二次检测,避免在加锁期间,对象已经创建成功,只有二次检测到对象尚未创建才会创建唯一的实例。
为了在大规模的代码中,方便多个类都定义为单例类,且在写类的代码时不需要太大变动且模式统一,因此本文定义了一个模板类,通过可变模板参数和友元函数的概念,实现调用的统一,具体实现代码如下:
template <typename T>
class Singleton {
public:
template<typename... Args>
static inline std::shared_ptr<T> instance(Args&& ... args) {
// 双检测锁模式
if (!instance_.get()) {
std::unique_lock<std::mutex> lock(instanceMutex_);
if (!instance_.get()) {
instance_.reset(new T(std::forward<Args>(args)...));
}
}
return instance_;
}
private:
Singleton() = default;
virtual ~Singleton() = default;
Singleton(const Singleton&) = default;
Singleton& operator = (const Singleton&) = delete;
// 实例
static std::shared_ptr<T> instance_;
static std::mutex instanceMutex_;
};
template <typename T>
std::shared_ptr<T> Singleton<T>::instance_;
template <typename T>
std::mutex Singleton<T>::instanceMutex_;
#define SINGLETON_DECL(type) \
friend class std::shared_ptr< type >; \
friend class Singleton< type >;
双检锁模式可以在多线程下保证线程的安全,但并不是绝对的。某些内存模型、编译器的优化或者运行时优化等情况下,会先分配完内存再进行数据的构造,造成另一个线程如果调用getInstance()获取到一个不完全初始化的对象,从而出现崩溃的情况。这种情况一个是通过内存屏障(memory barrier)的方式解决,一个是在主函数或者是比较早期的时候就完成单例的创建,还有一个方式是atomic实现,可以参考第一节中《C++ 单例模式》链接里面讲解的很清晰,这里不再赘述,只提供一种单例模板类的实现思路。
三、单例模板类的调用
将单例模板类定义在头文件Singleton.h中,其它头文件中想要定义为单例模式的类,只需要对其进行引用(#include Singleton.h)即可进行使用,假设我们想定义一个单例类A,则可以通过以下代码实现:
class A{
private:
A() {}
public:
~A() {}
int funA() {}
private:
SINGLETON_DECL(A);
};
// 调用单例类A的公共成员函数
Singleton<A>::instance()->funA();
// 在调用的时候会进行实例是否已创建的检查
可以看到调用是比较简单且统一的,不需要针对每个类再单独写双检测锁的部分。单例模式的实现方式有很多,可以根据项目实际需求选择具体方式,不管是懒汉式、饿汉式还是优化实现,都可以通过可变参数模板进行全局的统一管理。