由“单件模式”引发的思考及解决方案

       很久没有更新我的博客了,很抱歉,原因有两个:
       1、如我在曾经在公告栏所描述的,最近我“异常烦恼”,个中原因当前还不好明说,待事件结束后,或许可以给朋友们一点信息。
       2、要写自己的文章其实不容易。虽然这个结论不能定位于所有人,但总会有一些人的,至少我就是。
 
       废话太多了,言归正传。      
       在曾经的公告中,我向朋友们推荐过一个讨论过设计模式的博客: [url]http://www.cppblog.com/converse/archive/2006/08/11/11139.html[/url] 里面有常用的20多种设计模式的UML机构图和精练的模式测试代码,这是一个优秀的读书笔记类原创,大家可以对照 《设计模式――可复用面向对象软件的基础》一书认真研究。
       我也仅仅算是一个设计模式的初学者,在拜读《设计模式》一书时颇为迷惑,因为书中的例子虽然完整,但代码却很不完整,而且例子有点复杂,不很精简。在此非常感谢笔记的作者,萃取了精要部分,并给出了简练的代码。按照笔记作者的思路,我用stl/boost库的智能指针(或者有其他)重新实现了源码,受益匪浅。
 
       先更正作者的一个小失误:(Factory.cpp中void Creator::AnOperation()
局部指针变量没有释放资源)
void Creator::AnOperation()    
{    
    Product* p = FactoryMethod();    
    
    std::cout << "an operation of product\n";    
    delete p; //释放资源(redwolf added)    
}    

     
       在实现Singleton模式过程中,引发了一些编程细节上的问题,特别整理出来与朋友们一起分享。
 
       起因是我本想用boos::shared_ptr<typename T>替换裸指针Singleton*,编译不过,找错误的过程中,把智能指针换回去,在vs2005中侦测到了内存泄露。
 
       问题1:如何在vc/vs中检测程序的内存泄露?
       步骤:
       1、在c/cpp文件中包含如下宏及头文件:
#define CRTDBG_MAP_ALLOC    
#include <stdlib.h>    
#include <crtdbg.h>
        2、在需检测的代码首尾分别加上
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
        和
_CrtDumpMemoryLeaks();
       那么,系统将会检测里面的代码是否有内存泄露并给出详细信息。
       注:在vc6中会提示出问题的代码行,但在vs2005中没有提示(不知何故)。详细步骤请网上搜索相关文章,个人觉得写代码,没有这个功能是不可能的(否则心里确实没底 ,底子薄啊)。
 
       用例大致如下:
#include "stdafx.h"
#include "CreateMode\Factory.h"
#include "CreateMode\AbstractFactory.h"
#include "CreateMode\Builder.h"
#include "CreateMode\Prototype.h"
#include "CreateMode\Singleton.h"


#define CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

int _tmain( int argc, _TCHAR* argv[])
{
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
    
//FactoryTest::test();
//AbstractFactoryTest::test();
//BuilderTest::test();
//PrototypeTest::test();
SingletonTest::test();
//boost::shared_ptr<int> _p=boost::shared_ptr<int>(new int(56));

_CrtDumpMemoryLeaks();

system( "pause");
return 0;
}

        注:这也是本代码的main()函数,后面不在重贴。 
       
        接着,我们看下面的代码,与笔记作者所给的几乎相同(我只是在头文件中编写了大部分的函数体,方便一点。)
        注意:静态成员变量必须初始化,并且不能在类声明中经行,故应该在cpp文件中初始化,请注意这里没有贴出cpp中的初始化代码。
#ifndef _SINGLETON_H_
#define _SINGLETON_H_

#include <boost\shared_ptr.hpp>
#include <iostream>

//template<typename T>
class Singleton
{
public:
virtual ~Singleton(){}

static Singleton* GetInstancePtr() {
     if(_p==0)
     _p= new Singleton();
     return _p;
}

void test(){
    std::cout<< "singleton instance runing!"<<std::endl;
}

protected:    
Singleton(){} //保护的型构造函数,不能独立实例化该类
private:
static Singleton* _p;

};


class SingletonTest
{
public:
static void test(){
    Singleton::GetInstancePtr()->test();
}
protected:
private:
};

#endif

        运行之,vs提示存在内存泄露。

        问题2:哪里出现了内存泄露呢?析构函数?
       如果没有经常编写过类似的程序的话,含指向自己的静态指针成员的类,习惯性的第一反应是析构函数出现问题,因为析构函数在对象消失时由系统自动调用,通常就是用来清除资源的。
       再看看代码中的析构函数,虚析构函数,函数体为空,问题在这里?于是我们添加上函数体:
virtual ~Singleton(){
     if(_p!=0)
    {
     delete _p;
     _p=0;
    }
}
       编译运行,还是有内存泄露,怎么回事呢?析构函数没有被调用?在析构函数中添加断点,天啊!果然,析构函数没有被调用。
 
       问题3:析构函数为什么没有被调用呢?
       找啊找,考虑void test() 的调用方式:
Singleton::GetInstancePtr()->test();
       分开来写应该是这样:
Singleton* _pSgt=Singleton::GetInstancePtr();
_pSgt->test();
       于是,问题明显了,指针_pSgt没有被释放,增加代码:
delete _pSgt;
       这样修改之后,应该没有了内存泄露吧?编译运行,你可能发现系统抛出异常或者在析构函数中断点处一直执行。
 
       问题4:为什么成为了死循环?
       这个问题估计大家很快就明白过来了。delete的是指向类Singleton的指针,delete函数会调用类的析构函数,删除指向自身类的指针会循环调用自身类的析构函数,从而陷入死循环。
       看来释放资源的位置不应该在析构函数。
 
       问题5:怎么会这样呢?
       如果你碰巧删除析构函数的函数体代码(或者说把析构函数恢复到空函数),运行就会发现内存泄露没有了,而且系统不会出现任何问题。
       怎么会这样呢?
       问题在于:类中的静态成员等价于(或者说几乎等价于)全局变量。全局变量存储在内存的静态区域(static),在编译时分配空间,程序结束时系统自动释放空间。也就是说,这里的Singleton类中的_p成员在编译期已经分配完毕,位于cpp文件中的初始化:
Singleton* Singleton::_p=(Singleton*)0;
       _p相当于4字节的整型值。而_p将一直到main()函数结束以后才由系统自动释放4字节的资源,但由于_p指向的是一个内存区域(堆资源),这个内存块是由程序运行时new出来的,系统是不会自动释放掉的,所以产生了内存泄露。
       所以我们需要显示的调用delete函数。这里可以这么说:
       像Singleton模式这样的类,静态成员指针_p指向类本身,那么在考虑类的析构函数时,不应该在该类的析构函数中释放_p,而应该把这个释放资源的工作交给使用_p的代码处理,或者用我们后面将要提到的方法处理。但对于其他的成员,特别是指针成员,无论是静态的还是非静态的,都(或者说最好)要该类的析构函数处理资源释放工作。
       下面是我们考虑的一种复杂一点的Singleton类:
       Singleton包含以下成员:
       1、指向本类的静态成员指针_p;
       2、静态的int指针_sm;普通int指针_m;
       3、静态的类型为A的成员指针_spa;
       4、普通的A类型指针成员_pa。
       5、GetInstancePtr()静态函数对_sm和_pa及_p赋值,并且访问_pa的静态指针_sn所指的对象,调用_pa的test()普通成员函数;
       6、test()普通成员函数简单输出字符,代表实际应用中的某个操作,并且访问_pa的test函数和静态指针成员_sn所指的内容。
       类A包含以下成员:
       1、静态的int指针_sn,
       2、普通的int指针_n;
       3、test()普通成员函数输出字符,代表某一个操作;
       4、Getsn()静态成员函数对_sn赋值并返回该指针。
       两个类中所有的普通指针成员均在构造函数中初始化(赋有效值)。

       我先给出部分实现,请大家根据我表达的意思(如果描述清楚了并且大家明白了)实现一下两个类的析构函数。
#ifndef _SINGLETON_H_
#define _SINGLETON_H_

#include <boost\shared_ptr.hpp>
#include <iostream>

class A
{
public:
A():_n( new int(103)){}
void test(){
    std::cout<< "A.test();"<<std::endl;
}
virtual ~A(){
    ?????
}

static int* Getsn(){
     if(_sn==0)
     _sn= new int(200);
     return _sn;
}
    
private:
static int* _sn;
int* _n;
};

class Singleton
{
public:
virtual ~Singleton(){
    ???????
}

static Singleton* GetInstancePtr() {
     if(_sm==0)
     _sm= new int(101);
     if(_spa==0)
    {
     _spa= new A();
     int t=*_spa->Getsn();
     _spa->test();
    }
     if(_p==0)
     _p= new Singleton();
     return _p;
}

void test(){
     if(_pa!=0)
    {
     int i=*_pa->Getsn();
     _pa->test();
    }
    std::cout<< "singleton instance runing!"<<std::endl;
}

protected:    
Singleton():_m( new int(100)),_pa( new A){} //保护的型构造函数,不能独立实例化该类
private:
static Singleton* _p;
static int* _sm;
static A* _spa;
A* _pa;
int* _m;
};


class SingletonTest
{
public:
static void test(){
    Singleton *_st;
    _st=Singleton::GetInstancePtr();
    _st->test();
    delete _st;
}
protected:
private:
};

#endif

 
……
……
……
……

       现在我给出关于两个类的析构函数的实现,供大家参考:
virtual ~A(){
     if(_n!=0)
    {
     delete _n;
     _n=0;
    }
     if(_sn!=0)
    {
     delete _sn;
     _sn=0;
    }
}

virtual ~Singleton(){
     if(_m!=0)
    {
     delete _m;
     _m=0;
    }
     if(_sm!=0)
    {
     delete _sm;
     _sm=0;
    }
     if(_spa!=0)
    {
     delete _spa;
     _spa=0;
    }
     if(_pa!=0)
    {
     delete _pa;
     _pa=0;
    }
}

      
       注:以下的代码应该定义在cpp文件中(相似的代码更改请自行处理)。
int* A::_sn=( int*)0;
Singleton* Singleton::_p=(Singleton*)0;
int* Singleton::_sm=( int*)0;
A* Singleton::_spa=(A*)0;
 
       注意:这里一定要delete后对变量置空(0),大家可以把_sn的置空操作去掉,运行看看,一定出现异常。
       另外:关于Singleton析构函数中,有些网友建议直接把_p=0;添加进去就可以避免循环析构的问题,这是不行的,程序会抛出异常或者泄露内存,现在应该很容易明白是什么原因了吧。

       问题6:不使用静态指针_p,使用静态实例:Singleton instance;行不行?
       关于这个问题,这里我不写测试代码了。我认为不行,因为实例的话不能支持多态,这里我们用的Singleton类极其简单,试想,如果Singleton是基类,或者虚基类甚至是纯虚基类,我们将使用的是Singleton的派生类,这种方案就完全不可用了。当然对于我们这里的非继承的独立类Singleton(用最简单的那个形式),会不会出问题,有兴趣的朋友可以测试一下(记得给个话哦)。
 
       为了简化我们后面的讨论,我们把更改后的复杂的类再换回去,使用最初那个最简单的类。
       前面我们给出了一个结论,对于如同Singleton中的_p,应该把释放资源的问题交给使用_p的代码处理,难道真的没有其他方法了吗?
       答案是否定的。我这里提供两种解决方案。
 
       问题7:自动释放_p的解决方案1。
       我们回到问题的焦点上,我们之所以不能在Singleton的析构中释放_p,是因为这会导致析构函数的调用死循环。
       但通常类的析构函数是最好的资源释放场所,那么我们是否可以构造一个东西X,它安全存在并被系统自动销毁,销毁X时,可以调用delete释放_p,即:X能够访问静态成员_p。设想,如果我们存在一个类Deleter,它能够访问Singleton类的成员,我们在Singleton中定一个Deleter类型的变量,那么,我们就可以在X的析构中去释放_p了。
       我们当然可以产生友元类Deleter,但更可取的是我们生成一个嵌套于Singleton的类PrivateDeleter,使得PrivateDeleter对外不可见,这个类的唯一和全部的责任就是访问_p,并在自己的析构中释放_p。为了能够使deleter
PrivateDeleter deleter;
能够访问静态成员_p, 需要把deleter定义成静态成员
       下面是应用这个思路实现的带自动释放器Singleton代码。
        对于复杂结构的Singleton,可能会对Singleton析构函数提出一些要求,编程时请仔细考虑,大家可以试试我们上面给出的那个复杂Singleton结构(问题不大)。
#ifndef _SINGLETON_H_    
#define _SINGLETON_H_    
    
#include <boost\shared_ptr.hpp>    
#include <iostream>    
    
class Singleton    
{    
public:    
virtual ~Singleton(){}    
    
static Singleton* GetInstancePtr() {    
         if(_p==0)    
         _p= new Singleton();    
         return _p;    
}    
    
void test(){    
        std::cout<< "singleton instance runing!"<<std::endl;    
}    
    
protected:        
Singleton(){} //保护的型构造函数,不能独立实例化该类    
private:    
class PrivateDeleter    
{    
public:    
        ~PrivateDeleter(){    
         if (Singleton::_p){    
                delete Singleton::_p;    
                Singleton::_p=0;    
         }    
        }    
};    
    
static PrivateDeleter deleter;    
static Singleton* _p;    
};    
    
    
class SingletonTest    
{    
public:    
static void test(){    
        Singleton *_st;    
        _st=Singleton::GetInstancePtr();    
        _st->test();    
}    
protected:    
private:    
};    
    
    
    
#endif    
关于这个方案有以下的说明:
       1、请大家跟踪程序,看看deleter是在什么时候释放的?他在main()函数结束后释放的,系统自动销毁释放静态成员变量deleter时调用了PrivateDeleter的析构函数,这时才自动析构了_p。这就是为什么上面代码的在vs2005中提示依然出现内存泄露的原因(其实没有内存泄露)。
       2、这个方案的缺点是额外增加了内嵌类PrivateDeleter,多少在效率和资源上有点点浪费(不过非常小,一种好方法)。记得网上很早就有网友提出过这个方法,曾经参考过,但不记得具体哪位了,感谢!
       3、大家考虑,能不能把deleter定义成静态成员指针_deleter?不行的,系统会自动释放指针本身,但不会自动释放指针指向的资源,这就是问题的本质。
 
       问题8:自动释放_p的解决方案2。
       这个方案很简单,把裸指针换成智能指针,比较好的如boost::shared_ptr等,即:把释放资源、异常安全、线程安全等问题全部交给库去处理,省了大心思啊。这个方案我很喜欢,但必须取得boost库的支持,需要引入外库。     
       如果程序是基于stl和boost的话,这无疑是非常合适的。
 
       下面是用智能指针boost::shared_ptr<typename T>实现简单Singleton和那个复杂Singleton的代码。请大家参考,如果有关于stl和boost::shared_ptr相关的问题,可以跟我留言,我们一起讨论,这里不在详述stl或者boost::shared_ptr的具体用法了。
       说明:这时不能对vs中的内存泄露提示进行责备,因为,我们看不到智能指针的释放资源的过程,它要在main()返回后才进行。
 
       1、简单的Singleton类测试:
#ifndef _SINGLETON_H_
#define _SINGLETON_H_

#include <boost\shared_ptr.hpp>
#include <iostream>

class Singleton
{
public:
virtual ~Singleton(){}

static boost::shared_ptr<Singleton> GetInstancePtr() {
     if(_p==0)
    {
     _p=boost::shared_ptr<Singleton>( new Singleton);
    }
     return _p;
}

static Singleton&    GetInstanceRef() {
     if(_p==0)
    {
     _p=boost::shared_ptr<Singleton>( new Singleton);
    }
     return *_p;
}

void test(){
    std::cout<< "singleton instance runing!"<<std::endl;
}
    
protected:    
Singleton(){} //保护的型构造函数,不能独立实例化该类
private:
static boost::shared_ptr<Singleton> _p;
};

class SingletonTest
{
public:
static void test(){
     //Singleton::GetInstancePtr()->test();
    Singleton::GetInstanceRef().test();
}
protected:
private:
};

#endif

       说明:为了完整性,这里还提供了一个函数以获取实例的引用:
static Singleton&    GetInstanceRef();
 
       2、复杂的Singleton类测试:
#ifndef _SINGLETON_H_
#define _SINGLETON_H_

#include <boost\shared_ptr.hpp>
#include <iostream>

class A
{
public:
A():_n(boost::shared_ptr< int>( new int(103))){}
void test(){
    std::cout<< "A.test();"<<std::endl;
}
virtual ~A(){}

static boost::shared_ptr< int> Getsn(){
     if(_sn==0)
     _sn=boost::shared_ptr< int>( new int(200));
     return _sn;
}
    
private:
static boost::shared_ptr< int> _sn;
boost::shared_ptr< int> _n;
};

class Singleton
{
public:
virtual ~Singleton(){}

static boost::shared_ptr<Singleton> GetInstancePtr() {
     if(_sm==0)
     _sm=boost::shared_ptr< int>( new int(101));
     if(_spa==0)
    {
     _spa=boost::shared_ptr<A>( new A());
     int t=*_spa->Getsn();
     _spa->test();
    }
     if(_p==0)
    {
     _p=boost::shared_ptr<Singleton>( new Singleton);
    }
     return _p;
}

void test(){
     if(_pa!=0)
    {
     int i=*_pa->Getsn();
     _pa->test();
    }
    std::cout<< "singleton instance runing!"<<std::endl;
}

    

protected:
Singleton():_m(boost::shared_ptr< int>( new int(100))),_pa(boost::shared_ptr<A>( new A)){}    
private:
static boost::shared_ptr<Singleton> _p;
static boost::shared_ptr< int> _sm;
static boost::shared_ptr<A> _spa;
boost::shared_ptr<A> _pa;
boost::shared_ptr< int> _m;
};

class SingletonTest
{
public:
static void test(){
    Singleton::GetInstancePtr()->test();
}
protected:
private:
};
#endif

      
       cpp中的初始化代码:
#include "stdafx.h"
#include "Singleton.h"

boost::shared_ptr< int> A::_sn=boost::shared_ptr< int>(( int*)0);
boost::shared_ptr< int> Singleton::_sm=boost::shared_ptr< int>(( int*)0);
boost::shared_ptr<A> Singleton::_spa=boost::shared_ptr<A>((A*)0);
boost::shared_ptr<Singleton> Singleton::_p=boost::shared_ptr<Singleton>((Singleton*)0);

 
       提示:
       经过了方案1或者方案2的处理后,就可以直接使用最初的调用形式了:
Singleton::GetInstancePtr()->test();
      并且方案1处理后,不能对源码中的_st经行delete处理。
 
       问题9:如何实现模板化的Singleton?
       如同笔记作者中所述:
“一般的,如果一个项目中需要使用到Singleton模式比较多的话,那么一般会实现一个Singleton的模板类。”

       下面给出模板化的代码,仅作参考,这里也不打算对模板进行详细描述,有兴趣但不是很清楚的朋友可以给我留言或者参考讲述模板的相关资料。
#ifndef _SINGLETON_H_
#define _SINGLETON_H_

#include <boost\shared_ptr.hpp>
#include <iostream>

class A
{
public:
A():_n(boost::shared_ptr< int>( new int(103))){}
void test(){
    std::cout<< "A.test();"<<std::endl;
}
virtual ~A(){}

static boost::shared_ptr< int> Getsn(){
     if(_sn==0)
     _sn=boost::shared_ptr< int>( new int(200));
     return _sn;
}
    
private:
static boost::shared_ptr< int> _sn;
boost::shared_ptr< int> _n;
};

template<typename T>
class Singleton
{
public:
virtual ~Singleton(){}

static boost::shared_ptr<T> GetInstancePtr() {
     if(_p==0)
    {
     _p=boost::shared_ptr<T>( new T());
    }
     return _p;
}

static T&    GetInstanceRef() {
     if(_p==0)
    {
     _p=boost::shared_ptr<T>( new T());
    }
     return *_p;
}
    
protected:    
Singleton(){} //保护的型构造函数,不能独立实例化该类
private:
static boost::shared_ptr<T> _p;
};

class SingletonTest
{
public:
static void test(){
     //Singleton<A>::GetInstancePtr()->test();
    Singleton<A>::GetInstanceRef().test();
}
protected:
private:
};

#endif

       说明:
      1、这个模板表明:给定一个类型T,我们可以用Singleton<typename T>唯一实例化一个T的实例,并调用T的某个操作接口,如A的test()成员函数。
      2、类T适合模板的条件是都要实现同一调用接口,如这里是test();
      3、这个模板仅作参考,实际应用中需要考虑得更详细,比如,我们实例化T时调用的默认构造函数,这通常不合要求。因为我们的T很可能需要一个或者多个参数,才能构造出满足需求的实例。这需要其他设计模式的支持,这里不做讨论。
 
       结语:
       本文由闲话开始,并从一片优秀的读书笔记中的示例代码出发,讨论了Singleton模式中可能出现的内存泄露,详细解释了泄露原因,并给出了两种解决方案,最后,作为更通用的表示,我们借助C++泛型手法,给出了模板化的Singleton模式参考代码。

       再次感谢笔记作者给予我们的支持,以及其他网友提供的精妙的思路。
       同时,我也感谢朋友们的阅读,并请斧正失误或者错误之处。
                                                             redwolf      2008.9.26
 

你可能感兴趣的:(Singleton,职场,休闲)