程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 由“單件模式”引發的思考及解決方案

由“單件模式”引發的思考及解決方案

編輯:關於C語言

       很久沒有更新我的博客了,很抱歉,原因有兩個:        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  

本文出自 “狼窩” 博客,請務必保留此出處http://redwolf.blog.51cto.com/427621/102348

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved