82.单例模式 in C++
今天讨论的不是c++的语言特性,而是一种设计模式,即单例模式
🍅单例模式是什么
单例是一个类的单一实例,而单例模式是指打算那个类或者结构体只有一个实例
💡💡💡C++中的单例只是一种组织一堆全局变量和静态函数的方法
🍅为什么要关心单例模式(为什么要用单例模式)
在处理数据时
- 一方面,如果你是有一个对象的单一实例,那么只会有单一的数据集,然后可能有一些功能
- 而另一方面,我们有代表数据的类成员变量,我们有成员函数,代表的是对特定的数据集执行操作,或者以其他方式于数据交互
就是这两种情况
💡我们不打算实例化一个特定类的多个实例(即是不需要多个实例),我们只想要它们自己的数据版本,这些操作将在这些数据上执行
🍅什么时候用单例模式
那么单例设计模式在哪里发挥作用呢?
💡💡💡当我们想要拥有应用于某种全局数据集的功能,且我们只是想要重复使用时,单例是非常有用的
有些单例的例子,比如一个随机数生成器类
- 我们只希望能够查询它,得到一个随机数,而不需要实例化它去遍历所有这些东西
- 我们只想实例化它一次(单例),这样它就会生成随机数生成器的种子,建立起它所需要的任何辅助的东西了
另一个例子就是渲染器,渲染器通常是一个非常全局的东西
- 我们通常不会有一个渲染器的多个实例,我们有一个渲染器,我们向它提交所有这些渲染命令,然后它就会为我们渲染
而从根本上来说,单例的行为就像名称空间,单例类可以像名称空间一样工作,并没有要求单例类像普通类一样工作
💡💡💡故这里可以再次重申定义:C++中的单例只是一种组织一堆全局变量和静态函数的方法(本质上是在一个单一的名称空间下)。而这便是单例的意义
🍅💡如何编写单例
静态函数的使用也好像名称空间的使用啊
静态函数的调用如下面的例子一般,为
SingleTon::get()
便可调用,这乍一看不就是在一个名称空间SingleTon
中定义了一个get()
,然后将其调用么故可如此记忆静态函数的使用
首先,单例不能有构造函数。因为如果有公共的构造函数,它就会允许被实例化(相当于是为了安全考虑,从根上就杜绝其实例化),而如何做到取消构造函数呢?
- 💡把构造函数标记为
private
,这意味着该类不能在外部被实例化
下一步,是提供一种静态访问该类的方法,即是在此处的get()
- 因此我们需要某种静态函数,返回这种特定类型的引用或指针(能用引用的地方其实都可以用指针来替代,引用只不过是为了方便而已)
- 传统的方法就是简单的在私有成员例创建这个单例类的静态实例(惊了,在类里创建实例)
- 然而因为这个实例是静态的,所以需要在某处定义它(在类外部,cpp文件内)
💡只要有了这个get()
函数后,就可以写些任意数量的非静态方法,并通过get()
函数访问它们了
#include <iostream>
class SingleTon {
public:
//static在类里表示将该函数标记为静态函数
static SingleTon &get() {
return m_temp;
}
void Function() {} //比如说这里有一些方法可供使用
private:
SingleTon() {}; //将构造函数标记为私有
static SingleTon m_temp; //在私有成员里创建一个静态实例
}
SingleTon SingleTon::m_temp; //像定义任何静态成员一样定义它
然后如果要使用,则搭配类中返回实例引用的get()
函数来使用就可以了
//接着上面的函数接着写👆👇
int main() {
SingleTon::get().Function(); //这般使用便可
}
如此便可静态地访问单例,就像它只有一个实例,因为我可以使用这个get()
函数
其实这里还是有点不安全,因为如果手贱或者犯了些其他错误不小心又实例化了一个对象,则就不是单个实例了。
所以这里要对复制构造函数进行修改(复制构造函数即是在利用一个类构造另一个类(复制)时所用的构造函数,比如SingleTon(const SingleTon&) {}
这般便是最简单的复制构造函数)
-
所以这里通常要做的是,简单的删除复制构造函数
SingleTon(const SingleTon&) = delete;
在此处标记复制构造函数为
delete
让其
= delete
便可,这里应该是对=
进行了重载实现这个功能,反正作为使用者用就完事了
这般操作,就使得该类无法被复制了
SingleTon temp2 = SingleTon::get(); //❌❌❌
//会报错,因为无法复制了
综上,总结方法论
- 将构造函数设为私有,因为单例类不能有第二个实例
- 提供一个静态访问该类的方法
- 设一个私有的静态的实例,并且在类外将其定义!
- 然后用一个静态函数返回其引用or指针,便可正常使用了
- 为了安全,标记复制构造函数为
delete
(删除复制构造函数)
🍅更高端有用点的单例的应用例子
比如些一个函数来生成一个随机的浮点数(假设)
#include <iostream>
class Random {
public:
Random(const Random&) = delete; //复制构造函数标记为删除
static Random &get() {
return m_temp;
}
//假设这个函数是随机生成一个1数,然后返回
void Float() {
return m_RandomNum;
}
private:
Random() {};
float m_RandomNum = 0.5; //不讨论随机数生成,就直接让其为0.5就好了,仅仅作为例子
static Random m_temp;
}
Random Random::m_temp;
int main() {
float num = Random::get.Float(); //由此得到一个随机浮点数
}
🍅💡小技巧(?)
在使用get()
函数时,我们还需要在类成员中有一个静态实例,然后还要在某个翻译单元中来定义它
所以可以直接对get()
动手脚,直接在函数内定义静态实例就好了
static Random& Get() {
static Random m_temp;
return m_temp;
}
其中的单例只会在最初使用时创建一次
而这个单例的生命周期就是你的应用的生命周期
只要有了这个get()
函数后,就可以些任意数量的非静态方法,并通过get()
函数访问它们了
🍅把单例弄成类而不是名称空间,为什么?
为什么不把它弄成一个命名空间呢?同样的功能都可以实现。
答案是:确实可以
这两者在功能上没有区别,但是会少了public,private
这些类的功能,用起来会比较不顺手且不安全(虽然有其他方法可以达成这个功能,但为什么不直接用类呢?)