C++實現單例模式(包括採用C++11中的智慧指標)
對於設計模式來說,以前只是看過基礎的理論,很多都沒有實現和使用過。這段時間看到了別人C++程式碼中使用了單例模式,發現了很多新的東西,特此總結記錄一下。說話比較囉嗦,希望由淺入深,幫助大家理解!
單例模式,顧名思義,即一個類只有一個例項物件。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()函式僅作說明示意,並未實現。都是常見的執行緒同步方法,可以查詢其他資料來實現。這裡不再贅述。
當然,懶漢式的實現也可以採用shared_ptr來實現。但是很多思想與實現方法和餓漢式的有一定重複,因此這裡也不再給出。
結束語
本人水平有限,如果程式碼有問題或者更好的方法,歡迎留言!大家一起討論,一起進步!