单例是个什么鬼
写在前面
常常听到有人说起单例,那么单例到底是什么呢?又该怎么用呢?或者说,它的应用场景有哪些呢?为了搞清楚这些问题,决定自己亲自实践一下,加深感悟。文中用到的一些单例的实现方式可能是从网上参考的,感谢大家的分享和讲解,这里就不一一引用啦。
单例是什么
单例,顾名思义,就是单个实例,也就是说,某个类如果实现了单例模式,那这个类就只能生成一个实例。单例模式是设计模式的一种,关于设计模式,我大概了解过有工厂模式,抽象工程模式,观察者模式,原型模式等,具体使用哪种设计模式,要视具体应用场景而定。
何时使用单例
在一些应用中,可能会有一些工具类,这些类为其他类服务,本身没有太多数据要保存。如果使用这样的类的时候,每次都用new创建一个对象的话,会增加系统的开销。在实际设计时,这种只需要一个实例对象的类就应该设计为单例模式。
特点
根据单例的实现要求,可以看出单例模式有三个特点:
1、按照单例模式设计的类只能有一个实例
2、这个类的唯一的实例必须是类自身创建的,其他类不能创建
3、这个类创建了唯一的实例后,需要向其他类提供获取唯一实例的方法
如何实现单例
查看相关资料可以发现,单例的实现方式有很多。从单例模式的设计要求可以看出,我们在实现单例的是,要把构造函数设置为私有的。然后需要设计一个共有的返回唯一实例的静态方法。另外,考虑到内存释放的问题,还应该实现析构函数。类的UML如下图:
实现代码如下:
目前这个版本是没有考虑多线程环境的
class Singleton
{
private:
Singleton()
{
test = 7;
}
static Singleton *instance;//该类的唯一实例,声明为静态的,属于类所有
int test;//类的数据成员
public:
static Singleton *getInstance()//获取类实例的函数
{
if ( instance == NULL)//每次检查是否已经创建
{
instance = new Singleton();
}
return instance;
}
static void DestoryInstance()//析构函数
{
if (instance != NULL )
{
delete instance;
instance = NULL ;
}
}
int getTest()
{
return test;
}
};
在上述代码中,构造函数设置为私有,然后用共有的getInstance()方法去调用构造函数
在main函数中生成类的实例,代码如下:
int main()
{
Singleton *p1 = Singleton::getInstance();
cout<<p1->getTest()<<endl;
p1->DestoryInstance();
cout<<"p1 = "<<p1<<endl;
system("pause");
return 0;
}
问题:链接错误
原因:静态数据成员未初始化
如果按照上述代码创建实例,在编译时会报如下错误:
一开始看到这个错误的时候,我并没有发现错误出在哪里。好吧,我承认之前看的static关键字的特性都喂狗去了=。=
仔细查看这个错误,发现这个错误发生在链接的时候。在网上搜索这个连接诶错误,看到的大多是模版函数声明和定义的问题,大意是说,如果函数模版的声明放.h文件里面,而函数模版的定义放在.cpp文件里面,在main所在的文件里面使用定义的模版函数,会出现这样的链接错误,原因涉及到编译器的编译原理。。(感觉有点跑偏了,还是不继续解释了。。)反过来看我的代码中出现的错误,从错误提示中推测是某个静态数据成员或者静态成员函数出错了。这时候看看类的声明,里面有一个静态的成员变量,我似乎想起了什么——静态的数据成员一定要初始化啊喂,如果是普通的整型静态变量,没有显示初始化时,编译器会把它初始化为0,但是对于静态数据成员,编译器并不知道要怎么初始化,因此需要我们自己初始化。好的,搞清楚这个问题之后,我们就可以用以下语句初始化了。
//静态数据成员必须在类声明外进行初始化,
//初始化语法为:<数据类型><类名>::<静态数据成员名>=<值>
Singleton* Singleton::instance = new Singleton();//
在初始化的过程中,碰到了一个很有意思的问题,就是在哪里写这句初始化语句。因为看到别人是写在main函数之前的,由于是做样例讲解,类的相关代码和main函数的代码写在了同一个文件里面。这时候我突然觉得很神奇:instance和Singleton()都是Singleton类私有的,为何在类的外面,main函数的前面可以访问?当然,在main函数中是不能访问的,因为私有的数据不能在类外部访问。为了更好的搞清楚这个问题,又创建了一个test.cpp,然后在test.cpp中包含了Singleton.h,再把这句初始化语句剪切到test.cpp中,程序也能通过编译。
test.cpp中的内容如下:
#include"Singleton.h"
Singleton* Singleton::instance = new Singleton();
也就是说,类类型的静态数据成员的初始化可以放在任何一个包含了该类头文件的地方,语法上是没有任何问题的,但在实际使用的时候,为了不破坏封装,我们应该把类的静态数据成员的初始化放在类的时间文件中,或者放在类声明文件的末尾。(类的结束大括号的分号后面)。
好的,下面我们可以创建类的多个实例指针,然后查看这些指针的指向
Singleton *p1 = Singleton::getInstance();
Singleton *p2 = Singleton::getInstance();
cout<<"p1 = "<<p1<<endl;
cout<<"p2 = "<<p2<<endl;
cout<<endl;
p1->DestoryInstance();
p2->DestoryInstance();
输出结果如下:
这个时候无论我们创建多少指针,最后指向的都是同一个对象
总结
经过大半天的探索,发现单例其实也没那么神秘。我们平时写代码的时候也会编写工具类,只是没有使用单例的习惯,在实现工具类的时候,类内部的方法都设计为静态的,这样这些方法就是为整个类服务的,而不是某个对象。有了单例之后,可以通过单例来访问这些方法,思想其实是相通的。
现在这个版本的单例实现在多线程情况下也会出错,原因是getInstance()方法中的判断语句可能同时被两个线程调用,如果对instance做初始化时不是用构造函数,而是直接用Singleton* Singleton::instance = NULL;初始化,那么两个线程对instance的判断都为空,这时候就会创建两个实例,这不是我们希望的。解决方案是可以引入锁的机制。这些内容就不在本文中记录了。
最后,经过这次的探索,我发现对static数据成员以及单例的认识深刻了很多,感觉要理解C++(当然其他语言也是)的一些机制,还是应该实际操作,然后查看结果,把理论和实践结合起来,才能不断进步。