文章目錄
- 一、單例模式的概念
- 二、單例模式的結構
- 三、常見實現方式
- 3.1 餓漢式單例
- 3.2 懶漢式單例
一、單例模式的概念
單例模式(Singleton Pattern)是一種創建型設計模式,它的核心思想是:保證在一個進程中,某個類僅有一個實例,并提供全局訪問點。
問題:
單例模式同時解決了兩個問題, 所以違反了單一職責原則:
1.保證一個類只有一個實例。 為什么會有人想要控制一個類所擁有的實例數量? 最常見的 原因是控制某些共享資源 (例如數據庫或文件) 的訪問權限。
它的運作方式是這樣的: 如果你創建了一個對象, 同時過一會兒后你決定再創建一個新對象, 此時你會獲得之前已創建的對象, 而不是一個新對象。
注意, 普通構造函數無法實現上述行為, 因為構造函數的設計決定了它必須總是返回一個新對象。
2.為該實例提供一個全局訪問節點。 還記得你用過的那些存儲重要對象的全局變量嗎? 它們在使用上十分方便, 但同時也非常不安全, 因為任何代碼都有可能覆蓋掉那些變量的內容, 從而引發程序崩潰。
和全局變量一樣, 單例模式也允許在程序的任何地方訪問特定對象。 但是它可以保護該實例不被其他代碼覆蓋。
還有一點: 你不會希望解決同一個問題的代碼分散在程序各處的。 因此更好的方式是將其放在同一個類中, 特別是當其他代碼已經依賴這個類時更應該如此。
常見應用場景:
- 日志系統:全局唯一的日志記錄器。
- 配置管理:全局讀取配置文件。
- 線程池、數據庫連接池:全局資源管理。
- 設備驅動對象:保證唯一控制入口。
單例模式的關鍵要素:
- 構造函數私有化:防止外部隨意 new。
- 拷貝構造與賦值運算符刪除:防止復制對象。
- 靜態成員指針/對象:存儲唯一實例。
- 公共靜態方法:提供獲取實例的入口。
二、單例模式的結構
所有單例的實現都包含以下兩個相同的步驟:
- 將默認構造函數設為私有, 防止其他對象使用單例類的 new運算符。
- 新建一個靜態構建方法作為構造函數。 該函數會 “偷偷” 調用私有構造函數來創建對象, 并將其保存在一個靜態成員變量中。 此后所有對于該函數的調用都將返回這一緩存對象。
如果你的代碼能夠訪問單例類, 那它就能調用單例類的靜態方法。 無論何時調用該方法, 它總是會返回相同的對象。
三、常見實現方式
- 在類中添加一個私有靜態成員變量用于保存單例實例。
- 聲明一個公有靜態構建方法用于獲取單例實例。
- 在靜態方法中實現"延遲初始化"。 該方法會在首次被調用時創建一個新對象, 并將其存儲在靜態成員變量中。 此后該方法每次被調用時都返回該實例。
- 將類的構造函數設為私有。 類的靜態方法仍能調用構造函數, 但是其他對象不能調用。
- 檢查客戶端代碼, 將對單例的構造函數的調用替換為對其靜態構建方法的調用。
函數內靜態變量初始化的線程安全問題:
在 C++98/03 中:
- 函數內的靜態局部變量在第一次調用函數時初始化。
- 多線程調用時,如果兩個線程同時進入函數,就可能同時執行初始化,造成多次構造,屬于競態條件(Race Condition)。
所以在 C++98/03 里,需要手動加鎖才能保證安全。
C++11 標準明確規定:函數內靜態局部變量在第一次初始化時,初始化過程是線程安全的。
也就是說:
- 即使多個線程同時調用 getInstance(),只有一個線程會執行構造函數。
- 其他線程會等待構造完成,然后使用同一個實例。
3.1 餓漢式單例
餓漢式的特點:
- 類加載時就創建實例,不管你用不用它,它都存在。
- 線程安全:因為實例在程序開始時就初始化了,不存在多線程并發創建的問題。
- 資源消耗:如果對象很大而一直沒用到,會浪費內存。
- 適用場景:對象創建成本低,且在程序運行中幾乎一定會用到。
靜態成員對象
#include <iostream>class Singleton {
private:Singleton() { std::cout << "Singleton Created\n"; } // 構造函數私有化~Singleton() { std::cout << "Singleton Destroyed\n"; }Singleton(const Singleton&) = delete; // 禁止拷貝Singleton& operator=(const Singleton&) = delete; // 禁止賦值static Singleton instance; // 靜態成員,類加載時即初始化public:static Singleton& getInstance() {return instance; // 返回唯一實例}void show() {std::cout << "Hello Hungry Singleton!" << std::endl;}
};// 靜態成員初始化(在程序啟動時創建)
Singleton Singleton::instance;int main() {Singleton& s1 = Singleton::getInstance();Singleton& s2 = Singleton::getInstance();s1.show();std::cout << "s1 addr = " << &s1 << ", s2 addr = " << &s2 << std::endl;return 0;
}
輸出結果:
實現原理(簡要):
- 編譯器在生成代碼時,會在靜態變量前加上一次性標志(guard variable)。
- 第一個線程進入函數時,檢查標志:
- 如果未初始化 → 執行構造函數 → 設置標志
- 如果已初始化 → 直接返回實例
- 編譯器會保證對標志的寫入和檢查是原子操作或通過內部鎖完成,從而保證線程安全。
3.2 懶漢式單例
懶漢式的特點:
- 延遲初始化:只有第一次調用 getInstance() 時才創建對象。
- 線程安全問題:多線程環境下,需要注意可能出現多個實例的問題。
- 優點:節省資源,只有在真正需要時才創建。
- 缺點:實現稍復雜,需要考慮多線程安全。
懶漢式實現(C++11 推薦寫法)
C++11 后,使用 函數內靜態變量 可以保證線程安全:
#include <iostream>class Singleton {
private:Singleton() { std::cout << "Singleton Created\n"; } // 構造私有~Singleton() { std::cout << "Singleton Destroyed\n"; }Singleton(const Singleton&) = delete; // 禁止拷貝Singleton& operator=(const Singleton&) = delete; // 禁止賦值public:static Singleton& getInstance() {static Singleton instance; // 第一次調用時創建,C++11線程安全return instance;}void show() {std::cout << "Hello Lazy Singleton!" << std::endl;}
};int main() {Singleton& s1 = Singleton::getInstance();Singleton& s2 = Singleton::getInstance();s1.show();std::cout << "s1 addr = " << &s1 << ", s2 addr = " << &s2 << std::endl;return 0;
}
輸出結果:
懶漢式的多線程安全寫法(C++11 前)
如果在 C++11 之前,需要手動加鎖防止多線程同時創建多個實例:
#include <mutex>class Singleton {
private:Singleton() {}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;static Singleton* instance;static std::mutex mtx;public:static Singleton* getInstance() {if (instance == nullptr) {std::lock_guard<std::mutex> lock(mtx);if (instance == nullptr) {instance = new Singleton();}}return instance;}
};// 靜態成員初始化
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;