1. 程式人生 > >劍指offer面試題(2)——實現Singleton模式

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

浪費了“黃金五年”的Java程式設計師,還有救嗎? >>>   

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

題目:實現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機制》

執行結果: 
這裡寫圖片描述

相關推薦

offer試題2——實現Singleton模式

浪費了“黃金五年”的Java程式設計師,還有救嗎? >>>   

offer試題:賦值運算子函式

對於定義一個賦值運算子函式時,需要注意一下幾點: (1)函式的返回型別必須是一個引用,因為只有返回引用,才可以連續賦值 (2)傳入的引數宣告為常量引用,可以提高程式碼效率,同時賦值運算函式內不會改變傳入的例項狀態 (3)一定要記得釋放例項自身已有的記憶體,否則程式容易出現記

Offer-連結串列-2

知識點/資料結構:連結串列 題目描述 輸入一個連結串列,輸出該連結串列中倒數第k個結點。 思路:如下圖 程式碼如下: /* public class ListNode { int val; ListNode next = null; ListNod

offer牛客2替換空格

(18.12.28) 劍指offer牛客(2)替換空格 這個較為簡單。 時間限制:1秒 空間限制:32768K 熱度指數:764593 本題知識點: 字串 題目描述 請實現一個函式,將一個字串中的每個空格替換成“%20”。例如,當字串為We Are Happy.則經過替換之後的字

Offer試題2.二維陣列中的查詢

一、題目:二維陣列中的查詢 題目:在一個二維陣列中,每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函式,輸入這樣的一個二維陣列和一個整數,判斷陣列中是否含有該整數。     例如下面的二維陣列就是每行、每列都遞增排序。如果在這個陣列中查詢數字7,則返回true;

offer——試題15.2:判斷兩個整數m和n的二進制中相差多少位

end aps alt 試題 namespace different hide 判斷 img 1 #include"iostream" 2 using namespace std; 3 4 int CountDifferentBit(int m,int n)

Offer試題15Java版:鏈表中倒數第K個結點

head 計數器 easy sta 相同 ret white style 輸出 題目: 輸入一個鏈表。輸出該鏈表中倒數第k哥結點。 為了符合大多數人的習慣,本題從1開始計數。即鏈表的尾結點是倒數第1個結點。 比如一個鏈表有6個結點。從頭結點開始它們的值依次是1。2。

Offer試題43Java版:n個骰子的點數

pac pos max mod ins pri class pro bili 題目:把n個骰子仍在地上。全部骰子朝上一面的點數之和為s,輸入n,打印出s的全部可能的值出現的概率。 解法一:基於遞歸求骰子的點數,時間效率不夠高 如今我們考慮怎樣統計每個點數出現的次數。要向

offer試題9:用兩個棧實現佇列兩個佇列模擬棧

 題目描述: 用兩個棧來實現一個佇列,完成佇列的Push和Pop操作。 佇列中的元素為int型別。 思路一:有點死腦筋,每次pop後都預設下次是push操作,,,,。233主要是由於沒把握好兩個棧模擬時入隊和出隊的時機。考慮stack1和stack2的大小和入隊出隊的關係即可改

offer試題三 :陣列中重複的數字 google 面試

目錄 參考部落格: 題目一:找出陣列中重複的數字 思路一 思路二 題目二:不修改陣列找出重複的數字 測試: 牛客:  牛客高贊(和思路二類似都是hash對映,網友思路真是腦洞大開,這裡相關溢位問題考慮的只有~(1<<31)>>1,

offer試題7:重建二叉樹java實現

題目:輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建該二叉樹,假設輸入的前序遍歷和中序遍歷的結果都不含重複的數字。例如:輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6}則重建二叉樹:其中二叉樹的定義如下:  * publi

offer試題13:機器人的運動範圍java

題目:地上有一個m行n列的方格。一個機器人從座標(0,0)的格子開始移動,他每次可以向上,下,左,右移動一格。但不能進入行座標和列座標的數位之和大於k的格子。例如,當K=18時,機器人能夠進入方格(35,37),因為3+5+3+7 =18,但是它不能進入方格(35,38),

offer試題30:包含min函式的棧Java實現

題目:定義棧的資料結構,請在該型別中實現一個能夠得到棧的最小元素的min函式,在該棧中,呼叫min,push,及pop的時間複雜度都為O(1)。 直接上程式碼: import java.util.Stack; public class Solution {

Offer試題40Java版:陣列出現一次的數字

題目:一個整型數組裡除了兩個數字之外,其他的數字都出現了兩次。 * 請些程式找出這兩個只出現一次的數字。要求時間複雜度為O(n),空間複雜度為O(1) 例如輸入陣列{2,4,3,6,3,2,5,5},

Offer試題10Java版:二進位制中的1的個數

題目:請實現一個函式,輸入一個整數,輸出該數二進位制表示中1的個數。例如把9表示成二進位制是1001,有2位是1,因此如果輸入9,該函式輸出2. 1、可能引起死迴圈的解法 這是一道很基本的考察二進位制

Offer試題39Java版:二叉樹的深度

題目:輸入一棵二叉樹的根節點,求該數的深度。從根節點到葉結點依次進過的結點(含根,葉結點)形成樹的一條路徑,最長路徑的長度為樹的深度。 例如,如下圖的二叉樹的深度為4,因為它從根節點到葉結點的最長的路徑包含4個結點(從根結點1開始,經過2和結點5,最終到達葉結點7) 我們

Offer試題36Java版:陣列中的逆序對

題目:在陣列中的兩個數字如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個陣列中的逆序對的總數 例如在陣列{7,5,6,4}中,一共存在5對逆序對,分別是{7,6},{7,5},{7,4},{6,4},{5,4}。 看到這個題目,我們的第一反

offer試題8:二叉樹的下一個節點Java 實現

題目:給定一個二叉樹和其中的一個節點,如何找出中序遍歷序列的下一個節點?樹中的節點除了左右子節點外,還包含父節點。 思路: 節點分為有右子樹和沒有右子樹兩大類: 如果節點有右子樹,那麼它的下一個節點為它右子樹的最左節點 如果節點沒有右子樹,也可以分為兩類:(程

offer試題7:重建二叉樹Java實現

題目:輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。 思路:可以把二叉樹分為左右子樹分別構建,前序

offer試題6:從尾到頭列印連結串列Java實現

題目:輸入一個連結串列的頭結點,從尾到頭反過來打印出每個結點的值。 思路:因為要實現從頭到尾遍歷,然後從尾到頭列印,也就是說第一個遍歷到的最後一個列印,最後遍歷到的第一個列印,這很明顯符合棧 “先進後出” 的特點,所以我們可以利用棧來實現這種順序。 測試用例: 功能測試: