單例模式是任何面向對象語言繞不過的,單例模式是很有必要的,接下來我用最朴素的語言來解釋和記錄單例模式的學習。
- 什么是單例模式?
單例模式就是一個類只能被實例化一次 ,更准確的說是只能有一個實例化的對象的類。
- 創建一個單例模式的類(初想)
一個類只能有一個實例化的對象,那么這個類就要禁止別人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出這個對象,並提供一種方法供外部得到這個對象的地址。
