單例模式是設計模式中最簡單、常見的一種。其主要目的是確保整個進程中,只有一個類的實例,並且提供一個統一的訪問介面。常用於 Logger 類、通信介面類等。 # 基本原理 限制用戶直接訪問類的構造函數,提供一個統一的 public 介面獲取單例對象。 這裡有一個“先有雞還是先有蛋”的問題: - 因為用 ...
單例模式是設計模式中最簡單、常見的一種。其主要目的是確保整個進程中,只有一個類的實例,並且提供一個統一的訪問介面。常用於 Logger 類、通信介面類等。
基本原理
限制用戶直接訪問類的構造函數,提供一個統一的 public 介面獲取單例對象。
這裡有一個“先有雞還是先有蛋”的問題:
- 因為用戶無法訪問構造函數,所以無法創建對象
- 因為無法創建對象,所以不能調用普通的 getInstance() 方法來獲取單例對象
解決這個問題的方法很簡單,將 getInstance() 定義為 static 即可(這也會限制 getInstance() 內只能訪問類的靜態成員)
註意事項
- 所有的構造函數是 private 的
- 拷貝構造、拷貝賦值運算符需要顯式刪除
=delete
,防止編譯器自動合成(即使你顯式定義了析構函數或拷貝構造/拷貝賦值運算符,編譯器依然可能合成拷貝賦值運算符/拷貝構造!新的 C++ 標準已將該行為標記為 deprecated,但為了相容舊代碼,目前 C++23 仍然會合成!後面打算單獨用一篇筆記總結一下編譯器預設合成的函數)
C++ 單例模式的幾種實現方式
版本 1 餓漢式
提前創建單例對象
class Singleton1 {
public:
static Singleton1* getInstance() { return &inst; }
Singleton1(const Singleton1&) = delete;
Singleton1& operator=(const Singleton1&) = delete;
private:
Singleton1() = default;
static Singleton1 inst;
};
Singleton1 Singleton1::inst;
這個版本在程式啟動時創建單例對象,即使沒有使用也會創建,浪費資源。
版本 2 懶漢式
版本 2 通過將單例對象的實例化會推遲到首次調用 getInstance(),解決了上面的問題。
class Singleton2 {
public:
static Singleton2* getInstance() {
if (!pSingleton) {
pSingleton = new Singleton2();
}
return pSingleton;
}
Singleton2(const Singleton2&) = delete;
Singleton2& operator=(const Singleton2&) = delete;
private:
Singleton2() = default;
static Singleton2* pSingleton;
};
Singleton2* Singleton2::pSingleton = nullptr;
版本 3 線程安全
在版本 2 中,如果多個線程同時調用 getInstance() 則有可能創建多個實例。
class Singleton3 {
public:
static Singleton3* getInstance() {
lock_guard<mutex> lck(mtx);
if (!pSingleton) {
pSingleton = new Singleton3();
}
return pSingleton;
}
Singleton3(const Singleton3&) = delete;
Singleton3& operator=(const Singleton3&) = delete;
private:
Singleton3() = default;
static Singleton3* pSingleton;
static mutex mtx;
};
Singleton3* Singleton3::pSingleton = nullptr;
mutex Singleton3::mtx;
加鎖可以解決線程安全的問題,但版本 3 的問題在於效率太低。每次調用 getInstance() 都需要加鎖,而加鎖的開銷又是相當高昂的。
版本 4 DCL (Double-Checked Locking)
版本 4 是版本 3 的改進版本,只有在指針為空的時候才會進行加鎖,然後再次判斷指針是否為空。而一旦首次初始化完成之後,指針不為空,則不再進行加鎖。既保證了線程安全,又不會導致後續每次調用都產生鎖的開銷。
class Singleton4 {
public:
static Singleton4* getInstance() {
if (!pSingleton) {
lock_guard<mutex> lck(mtx);
if (!pSingleton) {
pSingleton = new Singleton4();
}
}
return pSingleton;
}
Singleton4(const Singleton4&) = delete;
Singleton4& operator=(const Singleton4&) = delete;
private:
Singleton4() = default;
static Singleton4* pSingleton;
static mutex mtx;
};
Singleton4* Singleton4::pSingleton = nullptr;
mutex Singleton4::mtx;
DCL 在很長一段時間內被認為是 C++ 單例模式的最佳實踐。但也有文章表示 DCL 的正確性取決於記憶體模型,關於這部分的深入討論可以參考這兩篇文章:
- https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
- https://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/
版本 5 Meyers’ Singleton
這個版本利用局部靜態變數來實現單例模式。最早由 C++ 大佬、Effective C++ 系列的作者 Scott Meyers 提出,因此也被稱為 Meyers’ Singleton
"This approach is founded on C++'s guarantee that local static objects are initialized when the object's definition is first encountered during a call to that function." ... "As a bonus, if you never call a function emulating a non-local static object, you never incur the cost of constructing and destructing the object."
—— Scott Meyers
TLDR:這就是 C++11 之後的單例模式最佳實踐,沒有之一。
- 最簡潔:不需要額外定義類的靜態成員
- 線程安全,不需要額外加鎖
- 沒有煩人的指針
class Singleton5 {
public:
static Singleton5& getInstance() {
static Singleton5 inst;
return inst;
}
Singleton5(const Singleton5&) = delete;
Singleton5& operator=(const Singleton5&) = delete;
private:
Singleton5() = default;
};
我曾見到過有人畫蛇添足地返回單例指針,就像下麵這樣
static Singleton* getInstance() {
static Singleton inst;
return &inst;
}
如果沒什麼正當的理由(我也實在想不到有什麼理由),還是老老實實地返回引用吧。現代 C++ 應當避免使用裸指針,關於這一點,我也有一篇筆記:裸指針七宗罪