單例模式是設(shè)計模式中最簡單、常見的一種。其主要目的是確保整個進程中,只有一個類的實例,并且提供一個統(tǒng)一的訪問接口。常用于 Logger 類、通信接口類、線程池等。
基本原理
限制用戶直接訪問類的構(gòu)造函數(shù),提供一個統(tǒng)一的 public 接口獲取單例對象。
這里有一個“先有雞還是先有蛋”的問題:
- 因為用戶無法訪問構(gòu)造函數(shù),所以無法創(chuàng)建對象
- 因為無法創(chuàng)建對象,所以不能調(diào)用普通的 getInstance() 方法來獲取單例對象
解決這個問題的方法很簡單,將 getInstance() 定義為 static 即可(這也會限制 getInstance() 內(nèi)只能訪問類的靜態(tài)成員)
注意事項
- 所有的構(gòu)造函數(shù)是 private 的
- 拷貝構(gòu)造、拷貝賦值運算符需要顯式刪除
=delete
,防止編譯器自動合成(即使你顯式定義了析構(gòu)函數(shù)或拷貝構(gòu)造/拷貝賦值運算符,編譯器依然可能合成拷貝賦值運算符/拷貝構(gòu)造!新的 C++ 標準已將該行為標記為 deprecated,但為了兼容舊代碼,目前 C++23 仍然會合成!后面打算單獨用一篇筆記總結(jié)一下編譯器默認合成的函數(shù))
C++ 單例模式的幾種實現(xiàn)方式
版本 1 餓漢式
提前創(chuàng)建單例對象
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;
這個版本在程序啟動時創(chuàng)建單例對象,即使沒有使用也會創(chuàng)建,浪費資源。
版本 2 懶漢式
版本 2 通過將單例對象的實例化會推遲到首次調(diào)用 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 中,如果多個線程同時調(diào)用 getInstance() 則有可能創(chuàng)建多個實例。
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 的問題在于效率太低。每次調(diào)用 getInstance() 都需要加鎖,而加鎖的開銷又是相當高昂的。
版本 4 DCL (Double-Checked Locking)
版本 4 是版本 3 的改進版本,只有在指針為空的時候才會進行加鎖,然后再次判斷指針是否為空。而一旦首次初始化完成之后,指針不為空,則不再進行加鎖。既保證了線程安全,又不會導(dǎo)致后續(xù)每次調(diào)用都產(chǎn)生鎖的開銷。
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 在很長一段時間內(nèi)被認為是 C++ 單例模式的最佳實踐。但也有文章表示 DCL 的正確性取決于內(nèi)存模型,關(guān)于這部分的深入討論可以參考這兩篇文章:
- https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
- https://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/
版本 5 Meyers’ Singleton
這個版本利用局部靜態(tài)變量來實現(xiàn)單例模式。最早由 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 之后的單例模式最佳實踐,沒有之一。
- 最簡潔:不需要額外定義類的靜態(tài)成員
- 線程安全,不需要額外加鎖
- 沒有煩人的指針
class Singleton5 {
public:
static Singleton5& getInstance() {
static Singleton5 inst;
return inst;
}
Singleton5(const Singleton5&) = delete;
Singleton5& operator=(const Singleton5&) = delete;
private:
Singleton5() = default;
};
我曾見到過有人畫蛇添足地返回單例指針,就像下面這樣文章來源:http://www.zghlxwxcb.cn/news/detail-479160.html
static Singleton* getInstance() {
static Singleton inst;
return &inst;
}
如果沒什么正當?shù)睦碛桑ㄎ乙矊嵲谙氩坏接惺裁蠢碛桑€是老老實實地返回引用吧?,F(xiàn)代 C++ 應(yīng)當避免使用裸指針,關(guān)于這一點,我也有一篇筆記:裸指針七宗罪文章來源地址http://www.zghlxwxcb.cn/news/detail-479160.html
到了這里,關(guān)于C++ 單例模式的各種坑及最佳實踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!