設計模式學習(一)單例模式補充——單例模式析構

paw5zx發表於2024-03-19

目錄
  • 前言
  • 無法呼叫解構函式的原因
  • 改進方法
    • 內嵌回收類
    • 智慧指標
    • 區域性靜態變數
  • 參考文章

前言

《單例模式學習》中提到了,在單例物件是透過new關鍵字動態分配在堆上的情況下,當程式退出時,不會透過C++的RAII機制自動呼叫其解構函式。本文討論一下這種現象的原因以及解決方法。

無法呼叫解構函式的原因

在DCLP(雙檢查鎖模式)中,CSingleton中的instance是一個靜態指標變數,被分配在全域性/靜態儲存區。而instance所指向的CSingleton例項是透過new建立在堆上的,只能手動呼叫delete來釋放相關資源(對於單例模式這是無法實現的,因為解構函式私有),無法透過RAII釋放相關資源。
在程式結束時,instance這個指標變數被銷燬了,但它所指向的記憶體空間中的CSingleton物件並沒有被顯式銷燬,而是由作業系統去回收這一塊記憶體(不會呼叫其解構函式)。然而依賴作業系統來清理資源並不是一個優雅的結束方式,可能會造成檔案控制代碼未關閉、網路連線未斷開等資源洩漏。

class CSingleton
{
public:
    static CSingleton* getInstance();
    static std::mutex mtx;
private:
    CSingleton(){}
    ~CSingleton(){}
    CSingleton(const CSingleton&)			 = delete;
    CSingleton& operator=(const CSingleton&) = delete;

    static CSingleton* instance;
};

CSingleton* CSingleton::instance;

CSingleton* CSingleton::getInstance()
{
    if(nullptr == instance)
    {
        mtx.lock();
        if(nullptr == instance)
        {
            instance = new CSingleton();
        }
        mtx.unlock();
    }
    return instance;
}

改進方法

在討論改進方法時,我們還是傾向於利用C++的RAII機制,而不是手動去控制釋放的時機。

內嵌回收類

我們的單例類物件生命週期的開始是在第一次呼叫時,結束是在程式結束時。

而且我們知道①靜態成員變數的生命週期是從程式啟動到結束②在靜態成員變數被銷燬時會呼叫其解構函式

因此我們可以在單例類中定義一個用於釋放單例類資源的內嵌類,將其解構函式定義為顯式刪除單例物件的操作,然後在單例類中新增一個內嵌類型別的靜態成員變數garbo

這樣的話,在程式結束時garbo就會被銷燬,而RAII機制確保了在銷燬時會呼叫內嵌類CGarbo的解構函式。

因為在~CGarbo()中delete了CSingleton::instance,所以~CSingleton()就會被呼叫,相關資源得以釋放。

class CSingleton
{
public:
    static CSingleton* getInstance();
private:
    CSingleton(){std::cout<<"建立了一個物件"<< std::endl;}
    ~CSingleton(){std::cout<<"銷燬了一個物件"<< std::endl;}
    CSingleton(const CSingleton&) 			 = delete;
    CSingleton& operator=(const CSingleton&) = delete;
        
    static CSingleton* instance;
    static std::mutex mtx;
        
    class CGarbo    
    {
    public:
        CGarbo(){}
        ~CGarbo()
        {
            if(nullptr != CSingleton::instance)    //巢狀類可訪問外層類的私有成員
            {
                delete CSingleton::instance;
                instance = nullptr;
            }
             std::cout<<"Garbo worked"<< std::endl;
        }
    };
  
    static CGarbo garbo; //定義一個靜態成員,程式結束時,系統會自動呼叫它的解構函式
    
};
  
CSingleton* CSingleton::instance;

std::mutex CSingleton::mtx;

CSingleton* CSingleton::getInstance()
{
    ...
}

CSingleton::CGarbo CSingleton::garbo;    //還需要初始化一個垃圾清理的靜態成員變數

執行結果:

智慧指標

我們還可以利用智慧指標引用計數機制,對資源自動管理:

//編譯不透過
class CSingleton
{
public:
    static std::shared_ptr<CSingleton> getInstance(); 
private:
    CSingleton(){std::cout<<"建立了一個物件"<<std::endl;}
    ~CSingleton(){std::cout<<"銷燬了一個物件"<<std::endl;}
    CSingleton(const CSingleton&)            = delete;
    CSingleton& operator=(const CSingleton&) = delete;

    static std::shared_ptr<CSingleton> instance;
    static std::mutex mutex;
};

std::shared_ptr<CSingleton> CSingleton::instance;

std::mutex CSingleton::mutex;

std::shared_ptr<CSingleton> CSingleton::getInstance()
{
    if (nullptr == instance)
    {
        std::lock_guard<std::mutex> lock(mutex);
        if (nullptr == instance)
        {
            instance = std::shared_ptr<CSingleton>(new CSingleton());
        }
    }
    return instance; 
}

注意上述程式碼無法透過編譯,原因是當std::shared_ptr被銷燬時,它會嘗試使用delete來銷燬管理的物件。但因為CSingleton的解構函式是私有的,所以無法從外部手動銷燬CSingleton例項。

要解決這個問題,我們需要在CSingleton中自定義一個刪除器,讓std::shared_ptr能夠呼叫私有解構函式。

class CSingleton
{
public:
    static std::shared_ptr<CSingleton> getInstance(); 
private:
    CSingleton(){std::cout<<"建立了一個物件"<<std::endl;}
    ~CSingleton(){std::cout<<"銷燬了一個物件"<<std::endl;}
    CSingleton(const CSingleton&)            = delete;
    CSingleton& operator=(const CSingleton&) = delete;

    static std::shared_ptr<CSingleton> instance;
    static std::mutex mutex;

    static void deleter(CSingleton* p); //自定義刪除器
};

std::shared_ptr<CSingleton> CSingleton::instance;

std::mutex CSingleton::mutex;

std::shared_ptr<CSingleton> CSingleton::getInstance()
{
    if (nullptr == instance)
    {
        std::lock_guard<std::mutex> lock(mutex);
        if (nullptr == instance)
        {
            instance = std::shared_ptr<CSingleton>(new CSingleton(),CSingleton::deleter);
        }
    }
    return instance; 
}

void CSingleton::deleter(CSingleton* p)
{
    delete p;
    std::cout<<"deleter worked"<<std::endl;
}

測試結果:

區域性靜態變數

區域性靜態變數形式的單例模式也可以完成資源的釋放,詳見《單例模式學習》

static CSingleton& getInstance() 
{
    static CSingleton instance;
    return instance;
}

參考文章

相關文章