單例模式,顧名思義,即一個類只有一個實例對象。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()函數僅作說明示意,並未實現。都是常見的線程同步方法,可以查詢其他資料來實現。