劍指offer面試題(2)——實現Singleton模式

轉自:我想有個長長的名字的博客,劍指offer 面試題2 Singleton模式 C++實現ios

題目:實現Singleton模式程序員

​ 如下內容是我在看《劍指offer》的面試題2時,遇到的問題,由於書中使用C#實現,因此想用C++從新實現一下,Test方法不夠全,後續還要完善。C++實現過程主要參考:C++設計模式——單例模式面試

​ 代碼中的註釋通常是個人筆記,或一些發現。數據庫

​ PS: 感謝勤勞的慵懶君~~ @亦餘心之所向兮編程

1 解法一:單線程解法

缺點:多線程狀況下,每一個線程可能建立出不一樣的Singleton實例設計模式

 
  1. // 劍指offer 面試題2 實現Singleton模式安全

  2. #include <iostream>多線程

  3. using namespace std;函數

  4.  
  5. class Singleton性能

  6. {

  7. public:

  8. static Singleton* getInstance()

  9. {

  10. // 在後面的Singleton實例初始化時,若後面是new Singleton(),則此處沒必要new;(廢話)

  11. // 若後面是賦值成NULL,則此處須要判斷,須要時new

  12. // 注意!然而這兩種方式並不等價!後面的Singleton實例初始化時,new Singleton(),實際上是線程安全的,由於static初始化是在主函數main()以前,那麼後面的方法豈不是很麻煩。。。。這也是我測試的時候想到的

  13. /*

  14. if(m_pInstance == NULL)

  15. {

  16. m_pInstance = new Singleton();

  17. }

  18. */

  19. return m_pInstance;

  20. }

  21.  
  22. static void destroyInstance()

  23. {

  24. if(m_pInstance != NULL)

  25. {

  26. delete m_pInstance;

  27. m_pInstance = NULL;

  28. } }

  29.  
  30. private:

  31. Singleton(){}

  32. static Singleton* m_pInstance;

  33. };

  34.  
  35. // Singleton實例初始化

  36. Singleton* Singleton::m_pInstance = new Singleton(); // 前面不能加static,會和類外全局static混淆

  37.  
  38. // 單線程獲取屢次實例

  39. void Test1(){

  40. // 預期結果:兩個實例指針指向的地址相同

  41. Singleton* singletonObj = Singleton::getInstance();

  42. cout << singletonObj << endl;

  43.  
  44. Singleton* singletonObj2 = Singleton::getInstance();

  45. cout << singletonObj2 << endl;

  46.  
  47. Singleton::destroyInstance();

  48. }

  49.  
  50. int main(){

  51. Test1();

  52. return 0;

  53. }

2 解法二:多線程+加鎖

​ 解法1是最簡單,也是最廣泛的實現方式,也是如今網上各個博客中記述的實現方式,可是,這種實現方式,有不少問題,好比:沒有考慮到多線程的問題,在多線程的狀況下,就可能建立多個Singleton實例,如下版本是改善的版本。 
​ 注意:下面的代碼涉及互斥鎖以及多線程測試,使用了C++11的多線程庫,std::thread,,std::mutex,請使用支持C++11多線程的編譯器,並確認開啓了C++11的編譯選項,具體方法見:http://blog.csdn.net/huhaijing/article/details/51753085

 
  1. #include <iostream>

  2. #include <mutex>

  3. #include <thread>

  4. #include <vector>

  5. using namespace std;

  6.  
  7. class Singleton

  8. {

  9. private:

  10. static mutex m_mutex; // 互斥量

  11.  
  12. Singleton(){}

  13. static Singleton* m_pInstance;

  14.  
  15. public:

  16. static Singleton* getInstance(){

  17. if(m_pInstance == NULL){

  18. m_mutex.lock(); // 使用C++11中的多線程庫

  19. if(m_pInstance == NULL){ // 兩次判斷是否爲NULL的雙重檢查

  20. m_pInstance = new Singleton();

  21. }

  22. m_mutex.unlock();

  23. }

  24. return m_pInstance;

  25. }

  26.  
  27. static void destroyInstance(){

  28. if(m_pInstance != NULL){

  29. delete m_pInstance;

  30. m_pInstance = NULL;

  31. }

  32. }

  33. };

  34.  
  35. Singleton* Singleton::m_pInstance = NULL; // 因此說直接new 多好啊,能夠省去Lock/Unlock的時間

  36. mutex Singleton::m_mutex;

  37.  
  38.  
  39. void print_singleton_instance(){

  40. Singleton *singletonObj = Singleton::getInstance();

  41. cout << singletonObj << endl;

  42. }

  43.  
  44. // 多個進程得到單例

  45. void Test1(){

  46. // 預期結果,打印出相同的地址,之間可能缺失換行符,也屬正常現象

  47. vector<thread> threads;

  48. for(int i = 0; i < 10; ++i){

  49. threads.push_back(thread(print_singleton_instance));

  50. }

  51.  
  52. for(auto& thr : threads){

  53. thr.join();

  54. }

  55. }

  56.  
  57. int main(){

  58. Test1();

  59. Singleton::destroyInstance();

  60. return 0;

  61. }

​ 此處進行了兩次m_pInstance == NULL的判斷,是借鑑了Java的單例模式實現時,使用的所謂的「雙檢鎖」機制。由於進行一次加鎖和解鎖是須要付出對應的代價的,而進行兩次判斷,就能夠避免屢次加鎖與解鎖操做,同時也保證了線程安全。可是,若是進行大數據的操做,加鎖操做將成爲一個性能的瓶頸;爲此,一種新的單例模式的實現也就出現了。

3 解法三:const static 型實例

 
  1. #include <iostream>

  2. #include <thread>

  3. #include <vector>

  4. using namespace std;

  5.  
  6. class Singleton

  7. {

  8. private:

  9. Singleton(){}

  10. static const Singleton* m_pInstance;

  11. public:

  12. static Singleton* getInstance(){

  13.  
  14. return const_cast<Singleton *>(m_pInstance); // 去掉「const」特性

  15. // 注意!若該函數的返回值改成const static型,則此處沒必要進行const_cast靜態轉換

  16. // 因此該函數能夠改成:

  17. /*

  18. const static Singleton* getInstance(){

  19. return m_pInstance;

  20. }

  21. */

  22. }

  23.  
  24. static void destroyInstance(){

  25. if(m_pInstance != NULL){

  26. delete m_pInstance;

  27. m_pInstance = NULL;

  28. }

  29. }

  30. };

  31. const Singleton* Singleton::m_pInstance = new Singleton(); // 利用const只能定義一次,不能再次修改的特性,static繼續保持類內只有一個實例

  32.  
  33. void print_singleton_instance(){

  34. Singleton *singletonObj = Singleton::getInstance();

  35. cout << singletonObj << endl;

  36. }

  37.  
  38. // 多個進程得到單例

  39. void Test1(){

  40. // 預期結果,打印出相同的地址,之間可能缺失換行符,也屬正常現象

  41. vector<thread> threads;

  42. for(int i = 0; i < 10; ++i){

  43. threads.push_back(thread(print_singleton_instance));

  44. }

  45.  
  46. for(auto& thr : threads){

  47. thr.join();

  48. }

  49. }

  50.  
  51. int main(){

  52. Test1();

  53. Singleton::destroyInstance();

  54. return 0;

  55. }

​ 由於靜態初始化在程序開始時,也就是進入主函數以前,由主線程以單線程方式完成了初始化,因此靜態初始化實例保證了線程安全性。在性能要求比較高時,就可使用這種方式,從而避免頻繁的加鎖和解鎖形成的資源浪費。因爲上述三種實現,都要考慮到實例的銷燬,關於實例的銷燬,待會在分析。

4 解法四:在get函數中建立並返回static臨時實例的引用

PS:該方法不能人爲控制單例實例的銷燬

 
  1. #include <iostream>

  2. #include <thread>

  3. #include <vector>

  4. using namespace std;

  5.  
  6. class Singleton

  7. {

  8. private:

  9. Singleton(){}

  10.  
  11. public:

  12. static Singleton* getInstance(){

  13. static Singleton m_pInstance; // 注意,聲明在該函數內

  14. return &m_pInstance;

  15. }

  16. };

  17.  
  18. void print_singleton_instance(){

  19. Singleton *singletonObj = Singleton::getInstance();

  20. cout << singletonObj << endl;

  21. }

  22.  
  23. // 多個進程得到單例

  24. void Test1(){

  25. // 預期結果,打印出相同的地址,之間可能缺失換行符,也屬正常現象

  26. vector<thread> threads;

  27. for(int i = 0; i < 10; ++i){

  28. threads.push_back(thread(print_singleton_instance));

  29. }

  30.  
  31. for(auto& thr : threads){

  32. thr.join();

  33. }

  34. }

  35.  
  36. // 單個進程得到屢次實例

  37. void Test2(){

  38. // 預期結果,打印出相同的地址,之間換行符分隔

  39. print_singleton_instance();

  40. print_singleton_instance();

  41. }

  42.  
  43. int main(){

  44. cout << "Test1 begins: " << endl;

  45. Test1();

  46. cout << "Test2 begins: " << endl;

  47. Test2();

  48. return 0;

  49. }

以上就是四種主流的單例模式的實現方式。

5 解法五:最終方案,最簡&顯式控制實例銷燬

​ 在上述的四種方法中,除了第四種沒有使用new操做符實例化對象之外,其他三種都使用了;

​ 咱們通常的編程觀念是,new操做是須要和delete操做進行匹配的;是的,這種觀念是正確的。在上述的實現中,是添加了一個destoryInstance的static函數,這也是最簡單,最普通的處理方法了;可是,不少時候,咱們是很容易忘記調用destoryInstance函數,就像你忘記了調用delete操做同樣。因爲怕忘記delete操做,因此就有了智能指針;那麼,在單例模型中,沒有「智能單例」,該怎麼辦?怎麼辦?

​ 在實際項目中,特別是客戶端開發,實際上是不在意這個實例的銷燬的。由於,全局就這麼一個變量,全局都要用,它的生命週期伴隨着軟件的生命週期,軟件結束了,它也就天然而然的結束了,由於一個程序關閉以後,它會釋放它佔用的內存資源的,因此,也就沒有所謂的內存泄漏了。

​ 可是,有如下狀況,是必須須要進行實例銷燬的:

  1. 在類中,有一些文件鎖了,文件句柄,數據庫鏈接等等,這些隨着程序的關閉而不會當即關閉的資源,必需要在程序關閉前,進行手動釋放;
  2. 具備強迫症的程序員。

​ 在代碼實現部分的第四種方法能知足第二個條件,可是沒法知足第一個條件。好了,接下來,就介紹一種方法,這種方法也是我從網上學習而來的,代碼實現以下:

 
  1. #include <iostream>

  2. #include <thread>

  3. #include <vector>

  4. using namespace std;

  5.  
  6. class Singleton

  7. {

  8. private:

  9. Singleton(){}

  10. static Singleton* m_pInstance;

  11.  
  12. // **重點在這**

  13. class GC // 相似Java的垃圾回收器

  14. {

  15. public:

  16. ~GC(){

  17. // 能夠在這裏釋放全部想要釋放的資源,好比數據庫鏈接,文件句柄……等等。

  18. if(m_pInstance != NULL){

  19. cout << "GC: will delete resource !" << endl;

  20. delete m_pInstance;

  21. m_pInstance = NULL;

  22. }

  23. };

  24. };

  25.  
  26. // 內部類的實例

  27. static GC gc;

  28.  
  29. public:

  30. static Singleton* getInstance(){

  31. return m_pInstance;

  32. }

  33. };

  34.  
  35.  
  36. Singleton* Singleton::m_pInstance = new Singleton();

  37. Singleton::GC Singleton::gc;

  38.  
  39. void print_instance(){

  40. Singleton* obj1 = Singleton::getInstance();

  41. cout << obj1 << endl;

  42. }

  43.  
  44. // 多線程獲取單例

  45. void Test1(){

  46. // 預期輸出:相同的地址,中間可能缺失換行符,屬於正常現象

  47. vector<thread> threads;

  48. for(int i = 0; i < 10; ++i){

  49. threads.push_back(thread(print_instance));

  50. }

  51.  
  52. for(auto& thr : threads){

  53. thr.join();

  54. }

  55. }

  56.  
  57. // 單線程獲取單例

  58. void Test2(){

  59. // 預期輸出:相同的地址,換行符分隔

  60. print_instance();

  61. print_instance();

  62. print_instance();

  63. print_instance();

  64. print_instance();

  65. }

  66.  
  67. int main()

  68. {

  69. cout << "Test1 begins: " << endl;

  70. cout << "預期輸出:相同的地址,中間能夠缺失換行(每次運行結果的排列格式一般不同)。" << endl;

  71. Test1();

  72. cout << "Test2 begins: " << endl;

  73. cout << "預期輸出:相同的地址,每行一個。" << endl;

  74. Test2();

  75. return 0;

  76. }

​ 在程序運行結束時,系統會調用Singleton的靜態成員GC的析構函數,該析構函數會進行資源的釋放,而這種資源的釋放方式是在程序員「不知道」的狀況下進行的,而程序員不用特別的去關心,使用單例模式的代碼時,沒必要關心資源的釋放。

​ 那麼這種實現方式的原理是什麼呢?因爲程序在結束的時候,系統會自動析構全部的全局變量,系統也會析構全部類的靜態成員變量,由於靜態變量和全局變量在內存中,都是存儲在靜態存儲區的,全部靜態存儲區的變量都會被釋放。

​ 因爲此處使用了一個內部GC類,而該類的做用就是用來釋放資源,而這種使用技巧在C++中是普遍存在的,參見《C++中的RAII機制》

運行結果: 
這裏寫圖片描述

相關文章
相關標籤/搜索