2023-06-11 21:21:35 來源 : 博客園
單例模式是設(shè)計(jì)模式中最簡(jiǎn)單、常見的一種。其主要目的是確保整個(gè)進(jìn)程中,只有一個(gè)類的實(shí)例,并且提供一個(gè)統(tǒng)一的訪問接口。常用于 Logger 類、通信接口類等。
基本原理限制用戶直接訪問類的構(gòu)造函數(shù),提供一個(gè)統(tǒng)一的 public 接口獲取單例對(duì)象。
(相關(guān)資料圖)
這會(huì)有一個(gè)“先有雞還是先有蛋”的問題:
因?yàn)橛脩魺o法訪問構(gòu)造函數(shù),所以無法創(chuàng)建對(duì)象因?yàn)闊o法創(chuàng)建對(duì)象,所以不能調(diào)用普通的 getInstance() 方法來獲取單例對(duì)象解決這個(gè)問題的方法很簡(jiǎn)單,將 getInstance() 定義為 static 即可(這也會(huì)限制 getInstance() 內(nèi)只能訪問類的靜態(tài)成員)
注意事項(xiàng)所有的構(gòu)造函數(shù)是 private 的拷貝構(gòu)造、拷貝賦值運(yùn)算符需要顯式刪除=delete
,防止編譯器自動(dòng)合成(即使你顯式定義了析構(gòu)函數(shù)或拷貝構(gòu)造/拷貝賦值運(yùn)算符,編譯器依然可能合成拷貝賦值運(yùn)算符/拷貝構(gòu)造!新的 C++ 標(biāo)準(zhǔn)已將該行為標(biāo)記為 deprecated,但為了兼容舊代碼,目前 C++23 仍然會(huì)合成!后面打算單獨(dú)用筆記總結(jié)一下編譯器默認(rèn)合成的函數(shù))C++ 單例模式的幾種實(shí)現(xiàn)方式版本 1 餓漢式提前創(chuàng)建單例對(duì)象
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;
這個(gè)版本在程序啟動(dòng)時(shí)創(chuàng)建單例對(duì)象,即使沒有使用也會(huì)創(chuàng)建,浪費(fèi)資源。
版本 2 懶漢式版本 2 通過將單例對(duì)象的實(shí)例化會(huì)推遲到首次調(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 中,如果多個(gè)線程同時(shí)調(diào)用 getInstance() 則有可能創(chuàng)建多個(gè)實(shí)例。
class Singleton3 { public: static Singleton3* getInstance() { lock_guard 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() 都需要加鎖,而加鎖的開銷又是相當(dāng)?shù)母甙旱摹?/p>版本 4 DCL (Double-Checked Locking)
版本 4 是版本 3 的改進(jìn)版本,只有在指針為空的時(shí)候才會(huì)進(jìn)行加鎖,然后再次判斷指針是否為空。而一旦首次初始化完成之后,指針不為空,則不再進(jìn)行加鎖。既保證了線程安全,又不會(huì)導(dǎo)致后續(xù)每次調(diào)用都產(chǎn)生鎖的開銷。
class Singleton4 { public: static Singleton4* getInstance() { if (!pSingleton) { lock_guard 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 在很長一段時(shí)間內(nèi)被認(rèn)為是 C++ 單例模式的最佳實(shí)踐。但也有文章表示 DCL 的正確性取決于內(nèi)存模型,關(guān)于這部分的討論可以參考這兩篇文章:
https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.htmlhttps://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/版本 5 Meyers’ Singleton這個(gè)版本利用局部靜態(tài)變量來實(shí)現(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 之后的單例模式最佳實(shí)踐,沒有之一。
最簡(jiǎn)潔:不需要額外定義類的靜態(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;};
我曾見到過有人畫蛇添足地返回單例指針,就像下面這樣
static Singleton* getInstance() { static Singleton inst; return &inst;}
如果沒什么正當(dāng)?shù)睦碛桑ㄎ乙矊?shí)在想不到有什么理由),還是老老實(shí)實(shí)地返回引用吧?,F(xiàn)代 C++ 應(yīng)當(dāng)避免使用裸指針,關(guān)于這一點(diǎn),我也有一篇筆記:裸指針七宗罪