單例模式是設計模式中最簡單、常見的一種,其主要目的是確保整個行程中,只有一個類的實體,并且提供一個統一的訪問介面,常用于 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++ 應當避免使用裸指標,關于這一點,我也有一篇筆記:裸指標七宗罪
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/554926.html
標籤:設計模式
上一篇:策 略 模 式「指 鼠 為 鴨」
下一篇:返回列表
