什么是单例模式?
单例模式是设计模式中的一种,它隶属于创建型设计模式,作用是让使用者能保证该类只会存在一个实例,同时对外提供一个访问该实例的全局节点。
单例解决了两个问题:
保证一个类只有一个实例
它的运作机制是这样的:如果创建了一个对象,同时过一会决定再创建一个对象则使用者会获得之前创建的对象而不是一个新的对象,换言之——也就是保证了一个类只会存在一个实例
为什么会有人想要只有一个实例?最常见的原因是为了控制某些共享资源(例如数据库、任务队列或者文件)的访问权限。
同时还要注意,普通构造函数一定无法完成这个任务,因为构造函数的设定使得它每次使用必须返回一个新对象,因此需要一些特殊处理,暂时按下不表,后文详细介绍。
为该实例提供了一个全局访问节点
从前面的描述看起来,单例与全局变量似乎起到了一个作用,但是全局变量的使用具用极强的安全隐患,因为任何代码都有可能覆盖掉那个全局变量的内容,它只是保证了名字不变,但内容不一定始终如一,同时全局变量也无法保证这个类只有一个实例(请牢记:不要把安全隐患暴露给使用者,而应尽可能从源头遏制相关问题)
单例虽然也允许程序在任何地方访问特定对象,但它可以保护该实例不被其他代码覆盖。
另外:没有一个代码编写者希望解决同一个问题的代码分散在程序各处的,把它们放到同一个类中是一个特别好的方法,尤其是如果还有其他代码已经依赖于这个类的时候。
编写单例模式的解决方案
所有单例的实现一定不会跳脱出以下两个相同的步骤:
new
运算符编写单例模式的具体步骤
使用C++实现单例模式
上文有提到,单例模式应禁止掉涉及一个类会产生新对象的构造函数,在C++中,会涉及到一个类多对象操作的函数有如下几个:
构造函数:
创建一个新的对象拷贝构造函数:
根据已有对象拷贝出一个新的对象拷贝赋值操作符重载函数:
对于=
的重载,用于两个对象之间的赋值操作
为了把一个类的实例化多个对象的路封死,可以进行如下处理:
构造函数私有化,使得只有类内部才能调用,并通过一定方式保证只会调用一次
- 考虑到使用者在类外部不能使用构造函数,所以在类内部创建的这个唯一的对象必须是静态的,这样就可以仅通过类名来进行访问,同时为了不破坏封装,一般会选择将这个静态对象的访问权限设置为私有
- C++中类的静态成员变量只有其静态成员函数才可以访问,因此给这个单例类提供一个公有的静态函数来访问得到这个静态的单例对象
拷贝构造函数私有化(private\protect
)或者禁用(使用=delete后缀
)
拷贝赋值操作符重载函数私有化(private\protect
)或者禁用(使用=delete后缀
),从单例的语义上讲这个函数已经毫无意义,因此类中不再提供这样一个函数,所以也将它一并处理掉
单例模式的类的模版——雏形
根据上述信息,我们给出单例模式的第一版代码:
-
将相关函数禁用的写法:
// 定义一个单例模式的类 class Singleton { public: // = delete 代表函数禁用, 也可以将其访问权限设置为私有 Singleton(const Singleton &obj) = delete; Singleton &operator=(const Singleton &obj) = delete; static Singleton *getInstance(); private: Singleton() = default; static Singleton *m_obj; };
-
将相关函数设置为私有化的写法:
// 定义一个单例模式的类 class Singleton { public: static Singleton *getInstance(); private: // = default 代表使用默认的实现 Singleton() = default; Singleton(const Singleton &obj) = default; Singleton &operator=(const Singleton &obj) = default; static Singleton *m_obj; };
以上就是一个单例模式的C++实现的基本雏形,首先,它有两种方案,一个是将构造函数禁用,一个是将构造函数私有化,这里要注意一点如果选用禁用构造函数的方案的话不能把所有构造函数都禁用,因为我们还是需要一个实例。
接下来,我们要注意一个细节,无论哪种方案目前有一个函数都还没有去实现,那就是getInstance()
方法,通过这个方法可以获得这个类的唯一实例,同时因为要操作静态成员变量,因此这个函数也是静态成员函数,可以直接通过类名进行访问。
具体到getInstance()
的实现,这里需要先引入两个概念,懒汉模式
与饿汉模式
:
- 懒汉模式:懒汉比较懒,不考虑场景,在类加载的时候就立即进行实例化,这样就得到了一个唯一的可用对象
- 饿汉模式:饿汉则比较乖巧,在类加载的时候(不饿)不去创建这个唯一的实例,而是在需要使用的时候(饿了)再进行实例化
从这个两个概念我们很容易看出来,懒汉模式比较简单直接,只要程序开始运行就会有这个唯一的类,而饿汉只有只有用到的时候才去创建,因此懒汉模式更适合全局要使用的对象(比如服务器的数据库连接池),而饿汉更适合局部使用的对象,同时仅在需要的时候才去创建,会更节省相应资源,这也是饿汉模式的一个优点。接下来我们看一下懒汉模式以及饿汉模式应如何编写相关代码。
单例模式的类的模版——饿汉模式
饿汉模式实现代码如下:
// 懒汉模式
// 定义一个单例模式的类
class Singleton {
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
Singleton(const Singleton &obj) = delete;
Singleton &operator=(const Singleton &obj) = delete;
static Singleton *getInstance() {
return m_obj;
}
private:
Singleton() = default;
static Singleton *m_obj;
};
Singleton *Singleton::m_obj = new Singleton;
可以看到,饿汉模式的代码非常简单,只需要直接返回私有成员变量m_obj
即可,这是因为在第20行m_obj
成员变量已经被定义好了,因此每次返回的都是这个唯一的成员变量。
这里注意一下,C++中类的静态成员变量在使用之前必须在类的外部进行初始化才可以使用
单例模式的类的模版——懒汉模式
有了上面的经验,我们很容易给出饿汉模式的第一版代码:
// 饿汉模式
// 定义一个单例模式的类
class Singleton {
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
Singleton(const Singleton &obj) = delete;
Singleton &operator=(const Singleton &obj) = delete;
static Singleton *getInstance() {
if (m_obj == nullptr) {
m_obj = new Singleton;
}
return m_obj;
}
private:
Singleton() = default;
static Singleton *m_obj;
};
Singleton *Singleton::m_obj = nullptr;
也就是在getInstance()
方法中对私有成员变量进行一个判断,如果是空指针那么就说明还没有使用过它,需要创建,而如果不是空指针则说明已经使用过,直接返回私有成员变量即可。
这在单线程下自然是没有问题,接下来我们进一步思考,这个实现是多线程安全的实现方式吗,很显然不是,如果有多个线程同时执行这个函数,则每一个线程都会判断m_obj
是空指针,也就每个线程都会创建一个新的实例对象,换言之,在第一次使用这个单例对象时,有多少个线程同时调用这个方法就会生成多少个实例对象。这明显与预期不符。
同时我们考虑一下之前的懒汉模式有没有线程安全问题,很显然没有,因为每个线程在使用之前就已经生成了唯一的实例对象,也就不会产生新的对象。接下来我们看一下要如何修改饿汉模式的代码使它成为线程安全的实现。
双重检查锁定
想要像饿汉模式一样没有线程安全问题,就需要保证对象只有一个,解决它最常用的办法是互斥锁,通过将创建单例对象的代码使用互斥锁锁住就可以保证线程安全,代码如下:
// 饿汉模式
// 定义一个单例模式的类
class Singleton {
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
Singleton(const Singleton &obj) = delete;
Singleton &operator=(const Singleton &obj) = delete;
void test_print() {
cout