C++ 實現單例模式
單例模式是任何面嚮物件語言繞不過的,單例模式是很有必要的,接下來我用最樸素的語言來解釋和記錄單例模式的學習。
- 什麼是單例模式?
單例模式就是一個類只能被例項化一次 ,更準確的說是只能有一個例項化的物件的類。
- 建立一個單例模式的類(初想)
一個類只能有一個例項化的物件,那麼這個類就要禁止別人new出來,或者通過直接定義一個物件出來
class CAR { public: CAR(){} ~CAR(){} }; CAR a; CAR *b = new CAR;
很明顯這樣的類可以被程式設計師用上面這兩種方式例項化。那麼考慮,如何禁止用上面的這兩種方式例項化一個類呢?
如果把建構函式私有化,很明顯上面這兩種方法都會預設的去呼叫建構函式,當建構函式是private或者protected時,建構函式將無法從外部呼叫。
class CSingleton { private: CSingleton() { } }; int main() { CSingleton t; CSingleton *tt = new CSingleton; }
上面的程式碼選擇了這樣例項化類,很明顯編譯器會報錯,因為私有化的建構函式無法被外部呼叫
error: ‘CSingleton::CSingleton()’ is private
既然建構函式是私有了,那麼他就只能被類內部的成員函式呼叫,所以我們可以搞一個共有函式去供外部呼叫,然後這個函式返回一個物件,為了保證多次呼叫這個函式返回的是一個物件,我們可以把類內部要返回的物件設定為靜態的,就有了下面的程式碼:
class CSingleton { private: CSingleton() { } static CSingleton *p; public: static CSingleton* getInstance() { if(p == NULL) p = new CSingleton(); return p; } }; CSingleton* CSingleton::p = NULL;
我們在主函式呼叫來測試一下
int main() { CSingleton *t = CSingleton::getInstance(); CSingleton*tt = CSingleton::getInstance(); cout << t << endl << tt << endl; }
結果是
0x1c59c0
0x1c59c0
兩個地址一樣,證明我們的單例類的正確的,原理其實很簡單,第一次呼叫獲取例項的函式時,靜態類的變數指標空,所以會建立一個物件出來,第二次呼叫就不是空了,直接返回第一次的物件指標(地址)。
同時思考另一個問題,如果兩個執行緒同時獲取例項化物件呢?顯然是不行的,會出現兩個執行緒同時要物件的時候指標還都是空的情況就完了,想到這種情況你肯定會毫不猶豫的去加個鎖。(進一步思考)
class CSingleton { private: CSingleton() { pthread_mutex_init(&mtx,0); } static CSingleton *p; public: static pthread_mutex_t mtx; static CSingleton* getInstance() { if(p == NULL) { pthread_mutex_lock(&mtx); p = new CSingleton(); pthread_mutex_unlock(&mtx); } return p; } }; pthread_mutex_t CSingleton::mtx; CSingleton* CSingleton::p = NULL;
上面的程式碼就是加鎖之後的了,你可以用下面的方法呼叫
void* fun1(void *) { while(1) { CSingleton *pt = CSingleton::getInstance(); cout << "fun1: pt_addr = " << pt << endl; Sleep(1000); } } void* fun2(void *) { while(1) { CSingleton *pt = CSingleton::getInstance(); cout << "fun2: pt_addr = " << pt << endl; Sleep(1000); } } void callSingleton() { pthread_mutex_init(&CSingleton::mtx,0); pthread_t pt_1 = 0; pthread_t pt_2 = 0; int ret = pthread_create(&pt_1,0,&fun1,0); if(ret != 0) { printf("error\n"); } ret = pthread_create(&pt_2,0,&fun2,0); if(ret != 0) { printf("error\n"); } pthread_join(pt_1,0); pthread_join(pt_2,0); }
你可以這樣在fun1,fun2中隨意的去例項化這個類了,執行結果如下
fun1: pt_addr = 0xb85a38
fun2: pt_addr = 0xb85a38
fun1: pt_addr = 0xb85a38
fun2: pt_addr = 0xb85a38
fun1: pt_addr = 0xb85a38
fun2: pt_addr = 0xb85a38
總結一下我們的這種實現單例模式的方式:類中宣告一個靜態的本類指標,再寫一個public的函式來讓這個指標指向我們新建立的例項,返回這個指標(這個例項的地址),並進行加鎖,這個物件就永遠只有一份,然後單例模式就實現了。
class CSingleton { private: CSingleton() { pthread_mutex_init(&mtx,0); } public: static pthread_mutex_t mtx; static CSingleton* getInstance() { pthread_mutex_lock(&mtx); static CSingleton obj; pthread_mutex_unlock(&mtx); return &obj; } }; pthread_mutex_t CSingleton::mtx;
也可以像上面的程式碼一樣把靜態物件的放到函式裡面,這樣就省的在去外部宣告一下了,只要返回一個靜態類的地址,就算這個函式執行完也不會被銷燬,它被儲存在靜態區和全域性變數差不多。
再次總結:只要返回一個本類物件的地址就好了,這個地址要是靜態的。別忘記加鎖。
而且上面這種方式也被人們成為懶漢模式,為什麼叫懶漢?因為這樣的方式只有在我呼叫 CSingleton::getInstance(); 的時候才會返回一個例項化的物件,懶死了,我不要你你就不給我,是不是?
下面這種方式就和上面的不同,人家還沒要,我就忍不住先給人家準備好了,如飢似渴,所以也叫餓漢模式。
我們注意到上面第一種方式,類中的靜態變數要先被外部宣告,否則編譯器不會為它分配空間,像這樣 CSingleton* CSingleton::p = NULL; 其實我們可以在這一步就new一個物件出來,因為p是CSingleton的成員,它是可以呼叫建構函式的哦,於是我們改成這樣就是餓漢模式了
class CSingleton { private: CSingleton() { } static CSingleton *p; public: static CSingleton* getInstance() { return p; } }; CSingleton* CSingleton::p = new CSingleton();
我們這樣鎖也不用加了,因為我們呼叫 CSingleton::getInstance() 之前這個類就已經被例項化了,我們呼叫這個函式的目地只是為了得到這個物件的地址。餓漢模式就實現了
總結:利用靜態變數和私有化建構函式的特性來實現單例模式。搞一個靜態的自身類指標,然後把建構函式私有化,這樣new的時候就只能讓本類中的成員呼叫,然後不擇手段在類內部new出這個物件,並提供一種方法供外部得到這個物件的地址。