C++中的單例模式


  最近遇到幾道類似的筆試題:

  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;

參考資料

  C++中的單例模式

  C++設計模式——單例模式

  如何限制對象只能建立在堆上或者棧上

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM