这两天项目终于结束了,有了些空闲时间来看些书并修改修改自己编写的垃圾代码。想到自己为实现全局唯一实例而写的一个类,仔细的看了看,并对照C++设计新思维上对于Singleton的分析,发现自己的设计果然够有缺陷,呵呵!这个缺陷曾经在调试时出现过,当时还不能理解,今天读到大师的佳作,豁然开朗!
Singleton单件模式可以说是GoF书中最简单的最容易理解的模式了,但是却又是最难以实现的模式!对单件模式的定义大概是这样的:保证全局范围只有唯一一个此类的实例,同时向用户提供一个全局的访问点。设计模式书中给出了一个大体的实现,我把它完善以后如下:
class Singleton
{
public:
static Singleton* Instance()
{
if(!m_Instance)
m_Instance = new Singleton();
return m_Instance;
}
protected:
Singleton();
virtual ~Singleton();
Singleton(Singleton& rh);
Singleton& operator= (Singleton& rh);
private:
static Singleton* m_Instance;
};
但是,这个实现有几个问题没有解决。
一、Singleton所含的资源由谁来释放?由用户来释放吗?如果要由用户来释放必须要提供相关的接口,其次就算提供了这样的接口,如果用户一不小心忘记调用怎么办?所以,由用户来处理资源释放的问题,不太妥!最好,能够由Singleton类自己释放资源,看起来还是有一些困难的,让我们联想一下,使用指针时我们一般用什么好的方法防止内存泄露或野指针的情况,很简单的问题,使用auto_ptr!所以,很明显需要一个auto_ptr来包装m_Instance;另外,这个auto_ptr的作用域是什么样的呢?Singleton的普通成员对象?明显不行;全局对象?嗯,倒是说得通,当程序退出时C++运行库自动清理全局或静态对象,但是,每使用一个Singleton客户都需要定义一个全局auto_ptr对象,多麻烦,还有可能污染名字空间,明显也不妥(其实还有一个原因,下面会说明)!那么,这个auto_ptr是Singleton的静态成员对象呢?看上去还是不错的,明显不存在作为全局对象可能引发的弊端。
下面已auto_ptr作为Singleton的静态成员的方式,尝试一下。
//Singleton.h
class Singleton
{
public:
static Singleton* Instance()
{
if(!m_Instance)
{
m_Instance = new Singleton();
m_apSingleton = m_Instance;
}
return m_Instance;
}
//...
private:
static Singleton* m_Instance;
static std::auto_ptr<Singleton> m_apSingleton;
};
当然了,还需要为两个静态成员赋值:
//Singleton.cpp
Singleton* Singleton::m_Instance = 0;
std::auto_ptr<Singleton> Singleton::m_apSingleton = 0;
然后我们写个测试程序:
//main.cpp
static Singleton* pStaticSingleton = Singleton::Instance():
int main()
{
pStaticSingleton = 0;
return 0;
}
你会发现,整个程序可以编译通过,但是执行时却总是报错,错误大概是非法访问了0x00000000。确实是这样的,我们都知道进程空间的前xxx个字节是不能访问的。我觉得这是auto_ptr的一个问题,也是c++语言本身缺陷导致的,具体的原因我将在auto_ptr部分分析一下。另外,在调试过程中你会发现静态成员m_Instance的初始化居然在调用Singleton::Instance之后,这一点其实已经有些不符合我们的愿望了,原因就在于全局变量(对象)的初始化的顺序是不确定的,主要体现在不同编译模块中定义的全局变量或对象,你不能准确的判断它们的初始化顺序,当然了同一编译模块中定义的全局变量或对象的初始化顺序还是确定的。全局对象的初始化顺序不确定性也给单件模式的设计带来了问题,这个问题后面再讲述。
针对这个无法自动释放的问题,有人提出了另外一种方法,不用静态数据成员来保存指向分配的对象了,改用从类的静态函数中定义一个静态的Singleton对象,如下:
static Singleton& Singleton::Instance()
{
static Singleton _instance;
return _instance;
}
这个静态的成员函数的设计可以实现全局唯一的标准,并且不需要认为的去释放内存空间,因为返回的对象可以在程序退出的时候自动调用析构函数来析构自己,所以也就不存在内存泄露的隐患了。很好!
下面我们要考虑另外一个问题:如果该单件实例被多个线程使用怎么办?如果不采取措施,很有可能产生多个单件实例(原因是很简单的)。方法很简单,需要在产生单件实例的地方加上一把锁就可以了,这把锁可以是Critical_Section,也可以是Mutex互斥对象。比如:
static Singleton& Singleton::Instance()
{
static Critical_Section cs;
EnterCritialSection(&cs);
static Singleton _instance;
LeaveCritialSection(&cs);
return _instance;
}
或者使用Mutex互斥对象也是可以的,C++设计新思维上提出了一种称为“双检测模式”,也可以解决这个问题,还可以保证效率,但是好像又有些隐蔽的问题,具体的我也没有仔细的考虑过,不过使用互斥对象肯定可以避免因多线程访问导致的问题。
上面的这个单件可能已经满足我们的要求了,但是还是我们还是碰到了一些疑问,我们辛辛苦苦折腾了半天单件的设计,但却忽视了一个问题,那就是这个单件能否被复用呢?难道每次我们要实现一个单件,“都要重新造一次车”,如果是那样太令人伤心了,不幸的是就目前来看,要想实现从单件Sigleton继承并产生我们自己的单件,似乎很有问题!问题在哪里呢?
试问那个产生静态Singleton的静态成员函数能否被继承而改写?不可以,因为它不是普通成员函数,那可怎么办,总不能每次派生一个类都要自己在添加一个产生单件的静态成员函数吧?就算你使用new Singleton();也解决不了这个问题啊,每次都只构造父类,那子类怎么办?
哈哈哈,真是非常的苦涩,这个最简单的设计模式设计起来这么困难。我觉得,在项目中遇到这个情况没有必要去追求完美,只要能实现单件的功能要求就可以了,有设计可复用的单件模式,对我这种新手来说还不如多花时间来干其他事情了,因为要实现可复用的单件真的很困难。刚才说到,Singleton无法被继承而最为可复用的代码,其实还是有其他的方法可以解决这个问题的,比如泛型编程中使用的模板,什么是模板,我的理解就是可以被复用的代码,要不然就不叫模板了。具体怎么实现的,以后再讲。