最近遇到幾道類似的筆試題:
1. 請實現一個單例模式的類,要求線程安全。
2. 用C++設計一個不能被繼承的類。
3. 如何定義一個只能在堆上(棧上)生成對象的類?
這些題目本質上都跟單例模式相關。
單例模式
單例模式就是保證一個類只有一個實例,並提供一個訪問它的全局訪問點。首先,需要保證一個類只有一個實例;在類中,要構造一個實例,就必須調用類的構造函數,如此,為了防止在外部調用類的構造函數而構造實例,需要將構造函數的訪問權限標記為protected或private;最后,需要提供要給全局訪問點,就需要在類中定義一個static函數,返回在類內部唯一構造的實例。
下邊就是一個常見的單例模式程序例子:
// 程序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;
該程序保證在不調用類中的靜態函數的情況下,不能夠在類外創建該類的實例(因為構造函數為私有函數);另外,在非多線程模式下只能創建該類的一個實例。
注:
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;
參考資料