- 前言
- 無法呼叫解構函式的原因
- 改進方法
- 內嵌回收類
- 智慧指標
- 區域性靜態變數
- 參考文章
前言
在《單例模式學習》中提到了,在單例物件是透過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;
}