最近遇到幾道相似的筆試題:設計模式
1. 請實現一個單例模式的類,要求線程安全。安全
2. 用C++設計一個不能被繼承的類。多線程
3. 如何定義一個只能在堆上(棧上)生成對象的類?函數
這些題目本質上都跟單例模式相關。測試
單例模式就是保證一個類只有一個實例,並提供一個訪問它的全局訪問點。首先,須要保證一個類只有一個實例;在類中,要構造一個實例,就必須調用類的構造函數,如此,爲了防止在外部調用類的構造函數而構造實例,須要將構造函數的訪問權限標記爲protected或private;最後,須要提供要給全局訪問點,就須要在類中定義一個static函數,返回在類內部惟一構造的實例。ui
下邊就是一個常見的單例模式程序例子:spa
// 程序1
1 class Singleton 2 { 3 private: 4 Singleton(){} 5 ~Singleton(){} 6 static Singleton *pInstance; 7 8 public: 9 static Singleton *GetInstance() // 對GetInstance稍加修改,這個設計模板即可以適用於可變多實例狀況,如一個類容許最多五個實例。 10 { 11 if (pInstance == NULL) //判斷是否第一次調用 12 { 13 pInstance = new Singleton (); 14 } 15 return pInstance; 16 } 17 18 static void DestoryInstance() 19 { 20 if (pInstance != NULL) 21 { 22 delete pInstance; 23 pInstance = NULL; 24 } 25 } 26 27 }; 28 29 Singleton *Singleton ::pInstance = NULL;
該程序保證在不調用類中的靜態函數的狀況下,不可以在類外建立該類的實例(由於構造函數爲私有函數);另外,在非多線程模式下只能建立該類的一個實例。.net
注:線程
1. 由於上述構造函數或析構函數爲私有函數,因此該類是沒法被繼承的,知足文章開頭提到的第二題。設計
2. 該類的實例只能被建立在堆上(new),由於析構函數被聲明爲私有函數,知足文章開頭提到的第三題。具體緣由摘自博文如何限制對象只能創建在堆上或者棧上:
「
在C++中,類的對象創建分爲兩種,一種是靜態創建,如A a;另外一種是動態創建,如A* ptr=new A;這兩種方式是有區別的。
靜態創建一個類對象,是由編譯器爲對象在棧空間中分配內存,是經過直接移動棧頂指針,挪出適當的空間,而後在這片內存空間上調用構造函數造成一個棧對象。使用這種方法,直接調用類的構造函數。
動態創建類對象,是使用new運算符將對象創建在堆空間中。這個過程分爲兩步,第一步是執行operator new()函數,在堆空間中搜索合適的內存並進行分配;第二步是調用構造函數構造對象,初始化這片內存空間。這種方法,間接調用類的構造函數。
... ...
類對象只能創建在堆上,就是不能靜態創建類對象,即不能直接調用類的構造函數。
容易想到將構造函數設爲私有。在構造函數私有以後,沒法在類外部調用構造函數來構造類對象,只能使用new運算符來創建對象。然而,前面已經說過,new運算符的執行過程分爲兩步,C++提供new運算符的重載,實際上是隻容許重載operator new()函數,而operator()函數用於分配內存,沒法提供構造功能。所以,這種方法不能夠。
當對象創建在棧上面時,是由編譯器分配內存空間的,調用構造函數來構造棧對象。當對象使用完後,編譯器會調用析構函數來釋放棧對象所佔的空間。編譯器管理了對象的整個生命週期。若是編譯器沒法調用類的析構函數,狀況會是怎樣的呢?好比,類的析構函數是私有的,編譯器沒法調用析構函數來釋放內存。因此,編譯器在爲類對象分配棧空間時,會先檢查類的析構函數的訪問性,其實不光是析構函數,只要是非靜態的函數,編譯器都會進行檢查。若是類的析構函數是私有的,則編譯器不會在棧空間上爲類對象分配內存。
... ...
只有使用new運算符,對象纔會創建在堆上,所以,只要禁用new運算符就能夠實現類對象只能創建在棧上。將operator new()設爲私有便可。代碼以下:
1 class A 2 { 3 private: 4 void* operator new(size_t t){} // 注意函數的第一個參數和返回值都是固定的 5 void operator delete(void* ptr){} // 重載了new就須要重載delete 6 public: 7 A(){} 8 ~A(){} 9 };
」
咱們知道,對於類Singleton的實例,最後咱們須要顯式調用DestroyInstance函數來釋放內存。那有沒有一種方法可讓程序自動析構實例呢?
要自動析構實例,這裏咱們須要用到C++中的RAII(Resource Acquisition Is Initialization)機制。具體地,咱們在類Singleton中在聲明一個靜態類(析構函數釋放Singleton實例內存)並定義一個該類的靜態實例。這樣,在Singleton實例被析構時,該靜態實例的析構函數會被自動調用,因此最終可以將Singleton實例的內存自動釋放掉。具體程序以下:
// 程序2
1 class Singleton 2 { 3 private: 4 Singleton(){} 5 ~Singleton(){} 6 static Singleton *pInstance; 7 8 class Garbo //它的惟一工做就是在析構函數中刪除Singleton的實例 9 { 10 public: 11 ~Garbo() 12 { 13 if (pInstance != NULL) 14 { 15 delete pInstance; 16 pInstance = NULL; 17 cout << "Delete instance!" << endl; 18 } 19 } 20 }; 21 static Garbo garbo; //定義一個靜態成員變量,程序結束時,系統會自動調用它的析構函數 22 23 public: 24 static Singleton *GetInstance() // 對GetInstance稍加修改,這個設計模板即可以適用於可變多實例狀況,如一個類容許最多五個實例。 25 { 26 if (pInstance == NULL) //判斷是否第一次調用 27 { 28 pInstance = new Singleton(); 29 cout << "Create instance" << endl; 30 } 31 return pInstance; 32 } 33 34 }; 35 36 Singleton *Singleton::pInstance = NULL; 37 Singleton::Garbo Singleton::garbo;
這個程序可能會顯得麻煩臃腫,咱們能夠改進成這個樣子:
// 程序3
1 class Singleton 2 { 3 private: 4 Singleton(){} // 構造函數是私有的 5 // ~Singleton(){} // 在這裏不能夠聲明爲private。由於咱們在函數GetInstance聲明定義了位於棧上的變量, 6 // 這樣程序結束時會自動調用析構函數(爲private則調用不了,編譯不經過). 7 8 public: 9 static Singleton& GetInstance() 10 { 11 static Singleton instance; // 局部靜態變量 12 return instance; 13 } 14 }; 15 16 int main() 17 { 18 Singleton singleton1 = Singleton::GetInstance(); 19 Singleton singleton2 = singleton1; 20 cout << &singleton1 << endl; 21 cout << &singleton2 << endl; 22 23 return 0; 24 }
這一下,程序簡潔又可以在程序運行結束時自動釋放實例內存。但咱們發現,在測試(main函數)時,咱們發現singleton1和singleton2的地址並不同,也就是說,這個程序存在漏洞,即經過默認拷貝函數能夠生成不止一個類的實例。不過咱們能夠考慮將默認拷貝函數和默認賦值函數權限設定爲private或protect:
// 程序4
1 class Singleton 2 { 3 private: 4 Singleton(){} // 構造函數是私有的 5 Singleton(const Singleton& orig){}; 6 Singleton& operator=(const Singleton& orig){}; 7 // ~Singleton(){} // 在這裏不能夠聲明爲private。由於咱們在函數GetInstance聲明定義了位於棧上的變量, 8 // 這樣程序結束時會自動調用析構函數(爲private則調用不了,編譯不經過). 9 10 public: 11 static Singleton& GetInstance() 12 { 13 static Singleton instance; // 局部靜態變量 14 return instance; 15 } 16 }; 17 18 int main() 19 { 20 Singleton singleton1 = Singleton::GetInstance(); // 通不過編譯,實際會調用默認拷貝函數 21 Singleton singleton2 = singleton1; // 通不過編譯,由於會調用默認拷貝函數 22 cout << &singleton1 << endl; 23 cout << &singleton2 << endl; 24 25 return 0; 26 }
接下來,咱們繼續改進這個程序:
// 程序5
1 class Singleton 2 { 3 private: 4 Singleton(){} // 構造函數是私有的 5 // ~Singleton(){} // 在這裏不能夠聲明爲private。由於咱們在函數GetInstance聲明定義了位於棧上的變量, 6 // 這樣程序結束時會自動調用析構函數(爲private則調用不了,編譯不經過). 7 8 public: 9 ~Singleton(){ cout << "~Singleton is called!" << endl; } 10 static Singleton* GetInstance() 11 { 12 static Singleton instance; // 局部靜態變量 13 return &instance; 14 } 15 }; 16 17 int main() 18 { 19 Singleton *singleton1 = Singleton::GetInstance(); 20 Singleton *singleton2 = singleton1; 21 Singleton *singleton3 = Singleton::GetInstance(); 22 cout << singleton1 << endl; 23 cout << singleton2 << endl; 24 cout << singleton3 << endl; 25 26 return 0; 27 }
程序運行結果以下:
結果證實了最後改進的這個程序可以只生成一個類的實例,並且在程序結束時可以自動調用析構函數釋放內存。
對於程序5而言,不存在線程競爭的問題;但對程序1和程序2而言是存在這個問題的。這裏以程序2爲例來講明如何避免線程競爭:
1 class Singleton 2 { 3 private: 4 Singleton(){} 5 ~Singleton(){} 6 static Singleton *pInstance; 7 8 class Garbo //它的惟一工做就是在析構函數中刪除Singleton的實例 9 { 10 public: 11 ~Garbo() 12 { 13 if (pInstance != NULL) 14 { 15 Lock(); 16 if (pInstance != NULL) 17 { 18 delete pInstance; 19 pInstance = NULL; 20 cout << "Delete instance!" << endl; 21 } 22 Unlock(); 23 } 24 } 25 }; 26 static Garbo garbo; //定義一個靜態成員變量,程序結束時,系統會自動調用它的析構函數 27 28 public: 29 static Singleton *GetInstance() // 對GetInstance稍加修改,這個設計模板即可以適用於可變多實例狀況,如一個類容許最多五個實例。 30 { 31 if (pInstance == NULL) //判斷是否第一次調用 32 { 33 Lock(); 34 if (pInstance == NULL) // 此處進行了兩次m_Instance == NULL的判斷,是借鑑了Java的單例模式實現時, 35 // 使用的所謂的「雙檢鎖」機制。由於進行一次加鎖和解鎖是須要付出對應的代價的, 36 // 而進行兩次判斷,就能夠避免屢次加鎖與解鎖操做,同時也保證了線程安全。 37 { 38 pInstance = new Singleton(); 39 cout << "Create instance" << endl; 40 } 41 Unlock(); 42 } 43 return pInstance; 44 } 45 46 }; 47 48 Singleton *Singleton::pInstance = NULL; 49 Singleton::Garbo Singleton::garbo;