單例模式
文章目錄
- 單例模式
- 一、餓漢模式(Eager Initialization)
- 1. 定義
- 2. 特點
- 3. 餓漢單例模式(定義時-類外初始化)
- 4. 實現細節
- 二、懶漢模式(Lazy Initialization)
- 1. 定義
- 2. 特點
- 3. 懶漢單例模式(第一次調用時-初始化)
- 4. 多線程不安全(需加鎖)
- 三、對比 & 使用建議
一、餓漢模式(Eager Initialization)
1. 定義
類加載時就創建實例,不管你用不用,先創建再說。
2. 特點
- 線程安全(因為類加載是線程安全的)
- 啟動時就分配資源,資源消耗可能較大
3. 餓漢單例模式(定義時-類外初始化)
#include <iostream>class TaskQueue {
public:// 靜態方法:獲取唯一實例指針static TaskQueue* getInstance() {return m_taskQ; // 返回靜態成員變量指針}// 刪除拷貝構造函數:防止復制實例(例如 TaskQueue b = a)TaskQueue(const TaskQueue&) = delete;// 刪除賦值運算符:防止賦值復制(例如 a = b)TaskQueue& operator=(const TaskQueue&) = delete;private:// 默認構造函數私有化:禁止類外部構造對象// 外部無法通過 new TaskQueue() 或 TaskQueue t; 構造對象TaskQueue() = default;// 靜態成員變量聲明:用于保存唯一實例的指針static TaskQueue* m_taskQ;
};// ?? 類外定義并初始化靜態成員變量:這一行非常關鍵!
// ? 這是 TaskQueue 類的“靜態成員變量定義+初始化”
// ? new TaskQueue 調用了 private 構造函數,但因為這是類自己的代碼(初始化自己的靜態成員),所以**允許訪問私有成員**
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
// --------------------------------------------
// ?? 雖然這行“寫在類外”(語法上),但它是類的一部分(靜態成員初始化),它仍然被認為是類自己的代碼(類內部行為),所以可以訪問私有構造函數。
// C++ 標準允許它訪問類的 private 構造函數。
// 所以不會報錯,而是合法的。int main() {// 獲取單例對象的指針TaskQueue* q1 = TaskQueue::getInstance();TaskQueue* q2 = TaskQueue::getInstance();// 打印地址驗證是否為同一實例std::cout << "q1 地址: " << q1 << std::endl;std::cout << "q2 地址: " << q2 << std::endl;// 輸出地址肯定一樣return 0;
}
注意:
// 靜態成員變量定義和初始化(在類外完成)
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
這句代碼在程序啟動時就執行,立即創建了 TaskQueue
的唯一實例:
- 是靜態變量,生命周期貫穿整個程序;
- 實例在任何
getInstance()
調用之前就已創建完成; getInstance()
只是簡單地返回這個已創建好的指針。
因此,它就是一個標準的餓漢單例模式實現。
4. 實現細節
- 為什么
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
屬于類內訪問,可以訪問private構造函數?
是因為它是“靜態變量”?“私有變量”?還是“初始化”這件事本身?
條件 | 是否是關鍵 | 解釋 |
---|---|---|
? 這是類的成員定義 | ? 是關鍵 | 初始化 TaskQueue::m_taskQ 是類的一部分,因此有權限訪問類的私有成員 |
是 static 成員 | ? 不是核心原因 | 雖然需要類外初始化,但并不是 static 帶來了訪問權限 |
是 private 變量 | ? 更不是原因 | private 表示“只能被類的代碼訪問”,而這行被視為類的代碼 |
不管是 static
還是 private
,關鍵原因在于:這是類的“成員變量定義”,屬于類的內部實現,因此它擁有類的訪問權限。
- 延申:若把
new TaskQueue
寫在main()
中
? 非法代碼(main 函數中訪問私有構造函數)
int main() {TaskQueue* q = new TaskQueue(); // ? 錯!構造函數是 private
}
為什么報錯?
- main() 是類外部的普通代碼。
- 它不是類成員,不被視為類內部實現。
- 因此沒有權限訪問私有構造函數,編譯器會直接報錯。
? 合法代碼(類外定義靜態成員時調用私有構造函數)
TaskQueue* TaskQueue::m_taskQ = new TaskQueue; // ? 對!
為什么合法?
- 這是類在定義和初始化自己的靜態成員變量。
- 雖然代碼寫在類外,但它被視為類的一部分(屬于 TaskQueue 類實現)。
- 所以有權訪問 private 構造函數。
- C++ 語法明確允許這種訪問。
二、懶漢模式(Lazy Initialization)
1. 定義
在第一次訪問時才創建實例,延遲到真正需要的時候再進行初始化。
2. 特點
- 延遲加載:只有在首次調用
getInstance()
時才會創建實例,節省系統資源; - 線程不安全(默認實現),但可以通過加鎖、雙重檢查、
std::call_once
等方式實現線程安全; - 相較于餓漢模式,更靈活、更節省資源,但實現稍復雜。
3. 懶漢單例模式(第一次調用時-初始化)
#include <iostream>class TaskQueue {
public:// ? 沒有加鎖,線程不安全 ******不同點******static TaskQueue* getInstance() {if (m_taskQ == nullptr) { m_taskQ = new TaskQueue(); // ?不安全,可能多個線程同時執行這里,創建多個實例}return m_taskQ;}TaskQueue(const TaskQueue&) = delete;TaskQueue& operator=(const TaskQueue&) = delete;private:TaskQueue() = default;static TaskQueue* m_taskQ;
};// 初始化靜態實例指針 ******不同點******
TaskQueue* TaskQueue::m_taskQ = nullptr;int main() {TaskQueue* q1 = TaskQueue::getInstance();TaskQueue* q2 = TaskQueue::getInstance();std::cout << "q1 地址: " << q1 << std::endl;std::cout << "q2 地址: " << q2 << std::endl;// 輸出地址一樣(如果線程不沖突)return 0;
}
4. 多線程不安全(需加鎖)
線程沖突時,多個線程可能在getInstance()
創建多個對象,需要加鎖!!!
三、對比 & 使用建議
對比項 | 餓漢模式(Eager Singleton) | 懶漢模式(Lazy Singleton) |
---|---|---|
實例創建時機 | 程序啟動時 / 類加載時立即創建 | 第一次調用 getInstance() 時才創建 |
資源占用 | 無論是否使用都會占用資源 | 僅在需要時才占用資源,更節省內存 |
線程安全 | ? 天然線程安全(由 C++ 靜態初始化保證) | ? 默認線程不安全,需手動加鎖處理 |
實現難度 | 實現簡單,邏輯清晰 | 實現復雜(涉及鎖、雙檢、或 call_once) |
性能開銷 | 啟動時略高,占用資源可能浪費 | 每次調用 getInstance() 可能涉及鎖(效率略低) |
適用場景 | 實例始終會用到,資源占用可接受 | 實例可能不一定會用到,或實例化代價較高 |
常用實現 | 類外初始化靜態成員指針(如:new Singleton; ) | 內部判斷是否為 null + 加鎖后 new Singleton(); |
示例構造代碼 | TaskQueue* m = new TaskQueue; (類外直接構造) | if (!m) m = new TaskQueue; (函數內延遲構造) |
可擴展性 | 不容易擴展為參數化構造 | 初始化時可自定義參數(但需額外設計) |
使用場景 | 推薦模式 |
---|---|
實例一定會被頻繁使用 | ? 餓漢模式(簡單穩定) |
實例創建代價高或可能不用 | ? 懶漢模式(延遲創建) |
多線程訪問高頻 | ? 餓漢 或 call_once 懶漢 |
希望按需控制生命周期 | ? 懶漢更靈活 |