C++實現懶漢式單例模式
單例模式無論在生活中還是在工程中都有很廣泛的應用,在C++專案中,很多時候我們只希望整個工程中某個類僅有一個實體物件,設計這種類的時候就需要使用單例模式來設計。下面是實現的一個懶漢式單例模式的程式碼:
#include <iostream>
#include <cstdio>
using std::cout;
using std::endl;
class Singleton {
private :
static Singleton* p_obj;
Singleton(void){
}
public :
static Singleton* GetInstance(void){
if(p_obj == NULL){
p_obj = new Singleton();
cout << "New Singleton();" << endl;
return p_obj;
}
}
~Singleton(void){
if(p_obj!=NULL){
delete p_obj;
p_obj = NULL;
cout << "~Singleton" << endl;
}
}
};
Singleton* Singleton::p_obj = NULL;
void run(void){
Singleton* s1 = Singleton::GetInstance();
Singleton* s2 = Singleton::GetInstance();
cout << "ADDR s1:" << s1 << endl;
cout << "ADDR s2:" << s2 << endl;
}
int main(){
run();
cout << "Press enter to continue..." << endl;
getchar();
return 0;
}
在設計類的時候,聲明瞭一個static Singleton型別的指標在類的私有成員區,需要注意的是,這個指標的記憶體單元並不會在物件中開闢,需要程式設計師自己在全域性定義一個Singleton指標,如果不是單例模式的類,這種static型別的成員是每一個物件共享的,即所有的物件都可以訪問在全域性資料區的這一段static記憶體。
在類私有成員區還定義了建構函式,顯然地,這樣設計時不想讓程式設計師可以直接建立Singleton物件,而是需要建立Singleton指標使用GetInstance方法去建立一個物件。這裡需要我們知道C++編譯器的一個特性:當程式設計師在設計類的時候,沒有顯示地宣告建構函式,編譯器會預設提供一個空的建構函式。這種懶漢式單例模式的實現,就是利用了C++的這一特性,將建構函式宣告在類私有成員區,如果程式設計師在外部建立Singleton物件,編譯器將報錯:Singleton成員函式時私有的,不可訪問。
如果想要呼叫Singleton類中的GetInstance函式建立物件,首先會判斷一下p_obj指標是不是為空,即是不是還沒有開闢過這個物件,如果確實目前還沒有開闢過這個物件,那麼使用new關鍵字在堆空間中開闢一段Sngleton大小的記憶體給申請者,而如果程式中早已存在Singleotn物件,則把已經存在的Singleton物件在堆空間中的地址返回給申請者。
其實這段程式是有漏洞的,在類中實現的解構函式並不會釋放堆記憶體,因為它根本就沒有被呼叫。這就造成了堆記憶體洩露的災難。不能夠在析構中釋放記憶體,而是需要呼叫者手動地釋放堆記憶體。改進這個漏洞後的程式碼如下:
#include <iostream>
#include <cstdio>
using std::cout;
using std::endl;
class Singleton {
private :
static Singleton* p_obj;
Singleton(){
}
public :
static Singleton* GetInstance(){
if(p_obj == NULL){
p_obj = new Singleton();
cout << "New Singleton();" << endl;
return p_obj;
}
}
};
Singleton* Singleton::p_obj = NULL;
void run(){
Singleton* s1 = Singleton::GetInstance();
Singleton* s2 = Singleton::GetInstance();
cout << "ADDR s1:" << s1 << endl;
cout << "ADDR s2:" << s2 << endl;
delete s1;
}
int main(){
run();
cout << "Press enter to continue..." << endl;
getchar();
return 0;
}
顯然,改進了的程式碼在單執行緒系統中不會再造成堆記憶體洩露。為什麼要強調一下是單執行緒的系統中呢?因為它在多執行緒的系統中還是可能出問題,想象一下,同時兩個執行緒執行到了if(p_obj == NULL),這時候會怎樣?顯然,會在堆記憶體中申請兩個Singleton大小的記憶體段。然而我們只釋放了一個,想象中他們是同一段,這就造成了洩露了一段Singleton大小的記憶體。改進這個漏洞的方法是在if(NULL != p_obj)前後加鎖,改進如下:
#include <iostream>
#include <cstdio>
using std::cout;
using std::endl;
class Singleton {
private :
static Singleton* p_obj;
Singleton(){
}
public :
static Singleton* GetInstance(){
lock();
if(p_obj == NULL){
p_obj = new Singleton();
cout << "New Singleton();" << endl;
return p_obj;
}
unlock();
}
};
Singleton* Singleton::p_obj = NULL;
void run(){
Singleton* s1 = Singleton::GetInstance();
Singleton* s2 = Singleton::GetInstance();
cout << "ADDR P1:" << s1 << endl;
cout << "ADDR P2:" << s2 << endl;
delete s1;
}
int main(){
run();
cout << "Press enter to continue..." << endl;
getchar();
return 0;
}
然而這樣做的線上程很多的時候會影響程式碼的效率,試想當多個執行緒都想要申請這麼這段記憶體的時候,都被排程器阻塞,堆在一起等待,這是一件多可怕的事情。為了提高程式碼的執行效率,可以使用雙重判斷加鎖,改進如下:
static Singleton* GetInstance(){
if(p_obj == NULL){
lock();
if(p_obj == NULL){
p_obj = new Singleton();
cout << "New Singleton();" << endl;
return p_obj;
}
unlock();
}
}
多判斷一次,減少多執行緒共同排隊到這個位置的可能性。