C++單例模式實現(採用C++11智能指針)

原文鏈接:https://blog.csdn.net/cjbct/article/details/79266057

    單例模式,顧名思義,即一個類只有一個實例對象。C++一般的方法是將構造函數、拷貝構造函數以及賦值操作符函數聲明爲private級別,從而阻止用戶實例化一個類。那麼,如何才能獲得該類的對象呢?這時,需要類提供一個public&static的方法,通過該方法獲得這個類唯一的一個實例化對象。這就是單例模式基本的一個思想。

    下面首先討論不考慮線程安全的問題(即:單線程環境),這樣能體現出單例模式的本質思想。常見的單例模式分爲兩種:

    1、餓漢式:即類產生的時候就創建好實例對象,這是一種空間換時間的方式

    2、懶漢式:即在需要的時候,才創建對象,這是一種時間換空間的方式

首先說一下餓漢式:餓漢式的對象在類產生的時候就創建了,一直到程序結束才釋放。即對象的生存週期和程序一樣長,因此 該實例對象需要存儲在內存的全局數據區,故使用static修飾。代碼如下(注:類的定義都放在了一個頭文件CSingleton.h中,爲了節省空間,該類有些實現和定義就放在測試的主文件中,沒有單獨創建一個CSingleton.cpp文件):

頭文件:

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H
 
#include<iostream>
using namespace std;
class CSingleton
{
private:
    CSingleton(){ cout << "單例對象創建!" << endl; };
    CSingleton(const CSingleton &);
    CSingleton& operator=(const CSingleton &);
    ~CSingleton(){ cout << "單例對象銷燬!" << endl; };
 
    static CSingleton myInstance; // 單例對象在這裏!
public:
    static CSingleton* getInstance()
    {        
        return &myInstance;
    }
};
 
#endif

源文件:

// 源文件,用於測試用例的生成
#include<iostream>
#include"CSingleton.h"
 
using namespace std;
CSingleton CSingleton::myInstance;
int main()
{
    CSingleton *ct1 = CSingleton::getInstance();
    CSingleton *ct2 = CSingleton::getInstance();
    CSingleton *ct3 = CSingleton::getInstance();
 
    return 0;
}

對於餓漢式來說,是線程安全的。

運行結果如下所示:

能夠看出,類的實例只有一個,並且正常銷燬。這裏,問題來了!!!

頭文件:

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H
 
#include<iostream>
using namespace std;
class CSingleton
{
private:
    CSingleton(){ cout << "單例對象創建!" << endl; };
    CSingleton(const CSingleton &);
    CSingleton& operator=(const CSingleton &);
    ~CSingleton(){ cout << "單例對象銷燬!" << endl; };
 
    static CSingleton *myInstance; // 這裏改了!
public:
    static CSingleton* getInstance()
    {        
        return myInstance; // 這裏也改了!
    }
};
 
#endif

源文件:

// 源文件,用於測試用例的生成
#include<iostream>
#include"CSingleton.h"
 
using namespace std;
CSingleton * CSingleton::myInstance=new CSingleton();
int main()
{
    CSingleton *ct1 = CSingleton::getInstance();
    CSingleton *ct2 = CSingleton::getInstance();
    CSingleton *ct3 = CSingleton::getInstance();
 
    return 0;
}

運行結果如下所示:

咦!怎麼沒有進入析構函數?這裏就有問題了,如果單例模式的類中申請了其他資源,就無法釋放,導致內存泄漏!

原因:此時全局數據區中,存儲的並不是一個實例對象,而是一個實例對象的指針,即一個地址變量而已!實例對象呢?在堆區,因爲是通過new得來的!雖然這樣能夠減小全局數據區的佔用,把實例對象這一大坨都放到堆區。可是!如何釋放資源呢?

首先能想到的第一個方法:我自己手動釋放呀!我在程序結束的時候delete不就可以了?對!這是可以的,程序如下:

頭文件:

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H
 
#include<iostream>
using namespace std;
class CSingleton
{
private:
    CSingleton(){ cout << "單例對象創建!" << endl; };
    CSingleton(const CSingleton &);
    CSingleton& operator=(const CSingleton &);
    ~CSingleton(){ cout << "單例對象銷燬!" << endl; };
 
    static CSingleton *myInstance; 
public:
    static CSingleton* getInstance()
    {        
        return myInstance;
    }
    static void releaseInstance() // 這裏加了個方法
    {
        delete myInstance;
    }
};
 
#endif

源文件:

// 源文件,用於測試用例的生成
#include<iostream>
#include"CSingleton.h"
 
using namespace std;
CSingleton * CSingleton::myInstance=new CSingleton();
int main()
{
    CSingleton *ct1 = CSingleton::getInstance();
    CSingleton *ct2 = CSingleton::getInstance();
    CSingleton *ct3 = CSingleton::getInstance();
        CSingleton::releaseInstance(); // 手動釋放
    return 0;
}

運行結果如下:

運行結果沒問題!可是,要是我寫着寫着我忘記了,沒有顯式調用釋放的函數怎麼辦?如果有一個自動釋放的方法就好了!天無絕人之路。

方法二:

頭文件:

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H
 
#include<iostream>
using namespace std;
class CSingleton
{
private:
    CSingleton(){ cout << "單例對象創建!" << endl; };
    CSingleton(const CSingleton &);
    CSingleton& operator=(const CSingleton &);
    ~CSingleton(){ cout << "單例對象銷燬!" << endl; };
 
    static CSingleton *myInstance; 
public:
    static CSingleton* getInstance()
    {        
        return myInstance;
    }
 
private:
    // 定義一個內部類
    class CGarbo{
    public:
        CGarbo(){};
        ~CGarbo()
        {
            if (nullptr != myInstance)
            {
                delete myInstance;
                myInstance = nullptr;
            }
        }
    };
    // 定義一個內部類的靜態對象
    // 當該對象銷燬時,順帶就釋放myInstance指向的堆區資源
    static CGarbo m_garbo; 
};
 
#endif

源文件:

// 源文件,用於測試用例的生成
#include<iostream>
#include"CSingleton.h"
 
using namespace std;
CSingleton * CSingleton::myInstance=new CSingleton();
CSingleton::CGarbo CSingleton::m_garbo; // 注意這裏!!!
int main()
{
    CSingleton *ct1 = CSingleton::getInstance();
    CSingleton *ct2 = CSingleton::getInstance();
    CSingleton *ct3 = CSingleton::getInstance();
 
    return 0;
}

運行結果如下:


可見能夠正常釋放堆區申請的資源了!問題解決!這裏又會想到,C++11中把boost庫中的智能指針變成標準庫的東西。智能指針可以在引用計數爲0的時候自動釋放內存,方便使用者管理內存,爲什麼不嘗試用一下智能指針呢?現在修改代碼如下(不可以正常運行):

頭文件:

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H
 
#include<iostream>
#include<memory>
using namespace std;
class CSingleton
{
private:
    CSingleton(){ cout << "單例對象創建!" << endl; };
    CSingleton(const CSingleton &);
    CSingleton& operator=(const CSingleton &);
    ~CSingleton(){ cout << "單例對象銷燬!" << endl; };
 
    static shared_ptr<CSingleton> myInstance; 
public:
    static shared_ptr<CSingleton> getInstance()
    {        
        return myInstance;
    }
 
};
 
#endif

源文件:

// 源文件,用於測試用例的生成
#include<iostream>
#include<memory>
#include"CSingleton.h"
 
using namespace std;
shared_ptr<CSingleton> CSingleton::myInstance(new CSingleton());
int main()
{
    shared_ptr<CSingleton>  ct1 = CSingleton::getInstance();
    shared_ptr<CSingleton>  ct2 = CSingleton::getInstance();
    shared_ptr<CSingleton>  ct3 = CSingleton::getInstance();
    
    return 0;
}

結果編譯都過不了。仔細一看,原來智能指針shared_ptr無法訪問私有化的析構函數。當shared_ptr內部的引用計數爲零時,會自動調用所指對象的析構函數來釋放內存。然而,此時單例模式類的析構函數爲private,故出現編譯錯誤。如何修改呢?當然,最簡單的方法是把析構函數變成public。但是如果某用戶不小心手殘,顯式調用了析構函數,這不就悲劇了。第二種方法,就是不通過析構函數來釋放對象的資源。怎麼辦呢?不要忘了shared_ptr在定義的時候可以指定刪除器(deleter)。可是通過測試,無法直接傳入析構函數。我現在想到的一個方法是,在單例函數內部再定義一個Destory函數,該函數也要爲static的,即通過類名可直接調用。當然,在使用該單例類的時候,也需要使用shared_ptr獲取單一實例對象

代碼如下:

頭文件:

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H
 
#include<iostream>
#include<memory>
using namespace std;
class CSingleton
{
private:
    CSingleton(){ cout << "單例對象創建!" << endl; };
    CSingleton(const CSingleton &);
    CSingleton& operator=(const CSingleton &);
    ~CSingleton(){ cout << "單例對象銷燬!" << endl; };
    static void Destory(CSingleton *){ cout << "在這裏銷燬單例對象!" << endl; };//注意這裏
    static shared_ptr<CSingleton> myInstance; 
public:
    static shared_ptr<CSingleton> getInstance()
    {        
        return myInstance;
    }
 
};
 
#endif

源文件:

// 源文件,用於測試用例的生成
#include<iostream>
#include<memory>
#include"CSingleton.h"
 
using namespace std;
shared_ptr<CSingleton> CSingleton::myInstance(new CSingleton(), CSingleton::Destory);
int main()
{
    shared_ptr<CSingleton>  ct1 = CSingleton::getInstance();
    shared_ptr<CSingleton>  ct2 = CSingleton::getInstance();
    shared_ptr<CSingleton>  ct3 = CSingleton::getInstance();
    
    return 0;
}

運行結果如下:

刪除器聲明時(即static void Destory(CSingleton *)),需要傳入該對象的指針,否則編譯出錯。這裏僅作說明,用不上該指針,故沒有給形參取名字。詳細說明一下,通過去掉形參的聲明,出錯後,就能定位到 頭文件<memory>中釋放對象的位置,代碼如下:

    template<class _Ux>
    void _Resetp(_Ux *_Px)
        {    // release, take ownership of _Px
        _TRY_BEGIN    // allocate control block and reset
        _Resetp0(_Px, new _Ref_count<_Ux>(_Px));
        _CATCH_ALL    // allocation failed, delete resource
        delete _Px;
        _RERAISE;
        _CATCH_END
        }
 
    template<class _Ux,
        class _Dx>
        void _Resetp(_Ux *_Px, _Dx _Dt)
        {    // release, take ownership of _Px, deleter _Dt
        _TRY_BEGIN    // allocate control block and reset
        _Resetp0(_Px, new _Ref_count_del<_Ux, _Dx>(_Px, _Dt));
        _CATCH_ALL    // allocation failed, delete resource
        _Dt(_Px);
        _RERAISE;
        _CATCH_END
        }

可以看出,上面代碼中第一個_Resetp就是沒有指明deleter時調用的函數。第二個_Resetp是指明deleter時調用的函數,此時deleter _Dt需要接收一個參數_Px(即指向shared_ptr內部對象的指針),從而釋放shared_ptr內部對象的內容。

迴歸正題,到這裏,我們可以看到:餓漢式的單例模式原理很簡單,也很好寫,並且線程安全,不需要考慮線程同步!

懶漢式單例模式

懶漢式單例模式,是在第一次調用getInstance()的時候,才創建實例對象。想到這裏,是不是直接把對象定義爲static,然後放在getInstance()中。第一次進入該函數,就創建實例對象,然後一直到程序結束,釋放該對象。動手試一試。

代碼如下:

頭文件:

// 懶漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H
 
#include<iostream>
using namespace std;
class CSingleton
{
private:
    CSingleton(){ cout << "單例對象創建!" << endl; };
    CSingleton(const CSingleton &);
    CSingleton& operator=(const CSingleton &);
    ~CSingleton(){ cout << "單例對象銷燬!" << endl; };
 
 
public:
    static CSingleton * getInstance()
    {    
        static CSingleton myInstance;
        return &myInstance;
    }
 
};
 
#endif

源文件:

//源文件,用於測試用例的生成
#include<iostream>
#include"CSingleton.h"
 
using namespace std;
 
int main()
{
    CSingleton *   ct1 = CSingleton::getInstance();
    CSingleton *   ct2 = CSingleton::getInstance();
    CSingleton *   ct3 = CSingleton::getInstance();
    
    return 0;
}

運行結果如下:


程序正常運行。此時,如果想把對象放在堆區,也可以這麼實現:

頭文件:

// 懶漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H
 
#include<iostream>
#include<memory>
using namespace std;
class CSingleton
{
private:
    CSingleton(){ cout << "單例對象創建!" << endl; };
    CSingleton(const CSingleton &);
    CSingleton& operator=(const CSingleton &);
    ~CSingleton(){ cout << "單例對象銷燬!" << endl; };
 
    static CSingleton *myInstance;
 
 
public:
    static CSingleton * getInstance()
    {    
        if (nullptr == myInstance)
        {
            myInstance = new CSingleton();
        }
        return myInstance;
    }
 
private:
    // 定義一個內部類
    class CGarbo{
    public:
        CGarbo(){};
        ~CGarbo()
        {
            if (nullptr != myInstance)
            {
                delete myInstance;
                myInstance = nullptr;
            }
        }
    };
    // 定義一個內部類的靜態對象
    // 當該對象銷燬時,順帶就釋放myInstance指向的堆區資源
    static CGarbo m_garbo;
};
 
#endif

源文件:

// 源文件,用於測試用例的生成
#include<iostream>
#include<memory>
#include"CSingleton.h"
 
using namespace std;
CSingleton * CSingleton::myInstance = nullptr;
CSingleton::CGarbo CSingleton::m_garbo;
int main()
{
    CSingleton *   ct1 = CSingleton::getInstance();
    CSingleton *   ct2 = CSingleton::getInstance();
    CSingleton *   ct3 = CSingleton::getInstance();
    
    return 0;
}

運行結果如下:

對於懶漢式這兩種情況,當調用getInstance()函數時,如果對象還沒產生(第一種狀態),就需要產生對象,然後返回對象指針。如果對象已經存在了(第二種狀態),就直接返回對象指針。當單線程時,沒有問題。但是,多線程情況下,如果一個函數中不同狀態有不同操作,就要考慮線程同步的問題了。因此,我們需要修改一下getInstance中的實現。

舉例如下:

第一種懶漢式:

static CSingleton * getInstance()
{    
    lock();
    static CSingleton myInstance;
    unlock();
    return &myInstance;
}

第二種懶漢式:

static CSingleton * getInstance()
{    
    if (nullptr == myInstance)
    {
        lock();// 需要自己採用適當的互斥方式
        if (nullptr == myInstance)
        {
            myInstance = new CSingleton();
        }
        unlock();
    }
    return myInstance;
}

注意!由於線程和使用的操作系統有關,因此這裏的lock()和unlock()函數僅作說明示意,並未實現。都是常見的線程同步方法,可以查詢其他資料來實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章