索引
- 靜態化並不是單例 (Singleton) 模式
- 餓漢模式
- 懶漢模式 (堆棧-粗糙版)
- 懶漢模式 (局部靜態變數-最佳版)
- 範例代碼和注意事項 (最優實現)
- 擴充閱讀
- 參考資料
我非常贊成合理的使用 設計模式 能讓代碼更容易理解和維護, 不過我自己除了簡單的 單例 (Singleton) 模式 外, 其它都很少用
可恥的是, 直到前段時間拜讀了 C++ In Theory: The Singleton Pattern, Part I, 我才發現自己的 單例 (Singleton) 模式 寫法還有改進空間.
文章作者 J. Nakamura 以 Log 日誌類列舉了 單例 (Singleton) 模式 的三種寫法:
// log.h#ifndef __LOG_H#define __LOG_H#include <list>#include <string>class Log {public: virtual void Write(char const *logline); virtual bool SaveTo(char const *filename);private: std::list<std::string> m_data;};#endif // __LOG_H靜態化並不是單例 (Singleton) 模式
初學者可能會犯的錯誤, 誤以為把所有的成員變數和成員方法都用 static 修飾後, 就是單例模式了:
class Log {public: static void Write(char const *logline); static bool SaveTo(char const *filename);private: static std::list<std::string> m_data;};In log.cpp we need to addstd::list<std::string> Log::m_data;
乍一看確實具備單例模式的很多條件, 不過它也有一些問題. 第一, 靜態成員變數初始化順序不依賴建構函式, 得看編譯器心情的, 沒法保證初始化順序 (極端情況: 有 a b 兩個成員對象, b 需要把 a 作為初始化參數傳入, 你的類就 必須 得要有建構函式, 並確保初始化順序).
第二, 最嚴重的問題, 失去了面對對象的重要特性 — “多態”, 靜態成員方法不可能是 virtual 的. Log 類的子類沒法享受 “多態” 帶來的便利.
餓漢模式
餓漢模式 是指單例執行個體在程式運行時被立即執行初始化:
class Log {public: static Log* Instance() { return &m_pInstance; } virtual void Write(char const *logline); virtual bool SaveTo(char const *filename);private: Log(); // ctor is hidden Log(Log const&); // copy ctor is hidden static Log m_pInstance; static std::list<std::string> m_data;};// in log.cpp we have to addLog Log::m_pInstance;
這種模式的問題也很明顯, 類現在是多態的, 但靜態成員變數初始化順序還是沒保證.
還引起另外一個問題 (我之前碰到過的真實事件, 以後便一直採用下面提到的 “懶漢模式”): 有兩個單例模式的類 ASingleton 和 BSingleton, 某天你想在 BSingleton 的建構函式中使用 ASingleton 執行個體, 這就出問題了. 因為 BSingleton m_pInstance 靜態對象可能先 ASingleton 一步調用初始化建構函式, 結果 ASingleton::Instance() 返回的就是一個未初始化的記憶體地區, 程式還沒跑就直接崩掉.
懶漢模式 (堆棧-粗糙版)
J. Nakamura 把它叫作 “Gamma Singleton”, 因為這是 Gamma 在他大名鼎鼎的 <<設計模式>> (<<Design Patterns>>) [Gamma] 一書採用的方法. 稱它為 “懶漢模式” 是因為單例執行個體只在第一次被使用時進行初始化:
class Log {public: static Log* Instance() { if (!m_pInstance) m_pInstance = new Log; return m_pInstance; } virtual void Write(char const *logline); virtual bool SaveTo(char const *filename);private: Log(); // ctor is hidden Log(Log const&); // copy ctor is hidden static Log* m_pInstance; static std::list<std::string> m_data;};// in log.cpp we have to addLog* Log::m_pInstance = NULL;
Instance() 只在第一次被調用時為 m_pInstance 分配記憶體並初始化. 嗯, 看上去所有的問題都解決了, 初始化順序有保證, 多態也沒問題.
不過細心的你可能已經發現了一個問題, 程式退出時, 解構函式沒被執行. 這在某些設計不可靠的系統上會導致資源泄漏, 比如檔案控制代碼, socket 串連, 記憶體等等. 幸好 Linux / Windows 2000/XP 等常用系統都能在程式退出時自動釋放佔用的系統資源. 不過這仍然可能是個隱患, 至少 J. Nakamura 印象中, 有些系統是不會自動釋放的.
對於這個問題, 比較土的解決方案是, 給每個 Singleton 類添加一個 destructor() 方法:
virtual bool destructor() { // ... release resource if (NULL!= m_pInstance) { delete m_pInstance; m_pInstance = NULL; }}
然後在程式退出時確保調用了每個 Singleton 類的 destructor() 方法, 這麼做雖然可靠, 但卻很是繁瑣. 幸運的是, Meyers 大師有個更簡便的方法.
懶漢模式 (局部靜態變數-最佳版)
它也被稱為 Meyers Singleton [Meyers]:
class Log {public: static Log& Instance() { static Log theLog; return theLog; } virtual void Write(char const *logline); virtual bool SaveTo(char const *filename);private: Log(); // ctor is hidden Log(Log const&); // copy ctor is hidden Log& operator=(Log const&); // assign op is hidden static std::list<std::string> m_data;};
在 Instance() 函數內定義局部靜態變數的好處是, theLog “ 的建構函式只會在第一次調用 “Instance() 時被初始化, 達到了和 “堆棧版” 相同的動態初始化效果, 保證了成員變數和 Singleton 本身的初始化順序.
它還有一個潛在的安全措施, Instance() 返回的是對局部靜態變數的引用, 如果返回的是指標, Instance() 的調用者很可能會誤認為他要檢查指標的有效性, 並負責銷毀. 建構函式和拷貝建構函式也私人化了, 這樣類的使用者不能自行執行個體化.
另外, 多個不同的 Singleton 執行個體的析構順序與構造順序相反.
範例代碼和注意事項 (最優實現)
把下面 C++ 程式碼片段中的 Singleton 替換成實際類名, 快速得到一個單例類:
class Singleton {public: static Singleton& Instance() { static Singleton theSingleton; return theSingleton; } /* more (non-static) functions here */private: Singleton(); // ctor hidden Singleton(Singleton const&); // copy ctor hidden Singleton& operator=(Singleton const&); // assign op. hidden ~Singleton(); // dtor hidden};
Note
-
任意兩個 Singleton 類的建構函式不能相互引用對方的執行個體, 否則會導致程式崩潰. 如:
ASingleton& ASingleton::Instance() { const BSingleton& b = BSingleton::Instance(); static ASingleton theSingleton; return theSingleton;}BSingleton& BSingleton::Instance() { const ASingleton & b = ASingleton::Instance(); static BSingleton theSingleton; return theSingleton;}
-
在多線程的應用場合下必須小心使用. 如果唯一執行個體尚未建立時, 有兩個線程同時調用建立方法, 且它們均沒有檢測到唯一執行個體的存在, 便會同時各自建立一個執行個體, 這樣就有兩個執行個體被構造出來, 從而違反了單例模式中執行個體唯一的原則. 解決這個問題的辦法是為指示類是否已經執行個體化的變數提供一個互斥鎖 (雖然這樣會降低效率).
-
多個 Singleton 執行個體相互引用的情況下, 需要謹慎處理解構函式. 如: 初始化順序為 ASingleton » BSingleton » CSingleton 的三個 Singleton 類, 其中 ASingleton BSingleton 的解構函式調用了 CSingleton 執行個體的成員函數, 程式退出時, CSingleton 的解構函式 將首先被調用, 導致執行個體無效, 那麼後續 ASingleton BSingleton 的析構都將失敗, 導致程式異常退出.
擴充閱讀
- 反模式 : 在實踐中明顯出現但又低效或是有待最佳化的設計模式, 是用來解決問題的帶有共同性的不良方法. 它們已經經過研究並分類, 以防止日後重蹈覆轍, 並能在研發尚未投產的系統時辨認出來. (其中的一些反模式還挺有意思的);
- C++ In Theory: The Singleton Pattern, Part 2 : “C++ In Theory” 系列的第二部分, 主要內容是泛型程式設計中的單例模式, 我對泛型不太感冒, 感興趣的朋友可以看看.
參考資料
| [Gamma] |
Design Patterns: E.Gamma, R.Helm, R.Johnson and J.Vlissides. |
| [Meyers] |
More Effective C++: S.Meyers. |