單例可以說是眾多設計模式中最常用的了,同時單例設計模式也是一個老生常談的問題,這是因為寫一個單例卻是很簡單,但是想要寫好一個單例卻比較難。
首先我們先來理一下在C++中實現單例最基本的幾個步驟:
#include <iostream>class Singleton {private: // 聲明 static Singleton* instance; int a{0}; Singleton() { std::cout << "Singleton 構造函數" << std::endl; } Singleton(const Singleton& temp) { std::cout << "Singleton 拷貝構造函數" << std::endl; } Singleton& operator = (const Singleton& temp){ return *instance; }public: static Singleton* getInstance() { return instance; } void addA(){ a++; } void printA(){ std::cout << "printA:" << a << std::endl; }};// 類靜態變量要在類內聲明,類外定義Singleton *Singleton::instance = new Singleton;int main() { Singleton* singleton = Singleton::getInstance(); singleton->addA(); singleton->printA(); return 0;}
這種寫法常用,但是也藏了一些隱患,比如如果使用者自作聰明在通過函數getInstance獲取到了單例指針,使用完畢后調用delete刪除了指針那怎么辦? 請問作為"資深"的復制粘貼工程師你知道怎么避免這種情況嗎?
一把情況下我們如果不希望開發者調用delete刪除指針,可以直接重載delete函數,并且將其設置偉私有方法,或者在C++11以上我們直接使用delete關鍵字將delete函數禁用掉。
上面的代碼例子是指針形式的單例,當然你也可以試試非指針式的單例書寫,其實更推薦非指針式的單例。
#include <iostream>#include <mutex>class Singleton {private: int a{0}; // 聲明 static std::mutex mtx; static Singleton* instance; Singleton(){ } Singleton(const Singleton& temp) { std::cout << "Singleton 拷貝構造函數" << std::endl; } Singleton& operator=(const Singleton& temp){ return *instance; }public: static Singleton* getInstance() { // 鎖、雙重判斷 if(nullptr == instance){ mtx.lock(); if (nullptr == instance) { instance = new Singleton(); } mtx.unlock(); } return instance; } void addA(){ a++; } void printA(){ std::cout << "printA:" << a << std::endl; }};// 需要定義std::mutex Singleton::mtx;Singleton *Singleton::instance{nullptr};int main() { Singleton* singleton = Singleton::getInstance(); singleton->addA(); singleton->printA(); return 0;}
想用懶加載模式,同時為了保證線程安全,以上代碼是很多童鞋會寫出的示例代碼,然而在C++上述代碼卻并不能一定保證正確。
這是因為程序在執行的過程中,出于效率的考量,兩個(在當前線程中)沒有依賴的指令可能會調換順序執行也就是 CPU 動態調度。對于 CPU 來說,這已經是幾十年的老技術了, 這里就不多說了。
因此以上這個鎖加雙重判斷的懶漢模式既繁瑣又不安全,并不推薦。
在單例的實現中,我們實際上是希望實現「執行且只執行一次」的語義。這在 C++11 之后,標準庫實際已經提供了這樣的實現。 那就是std::once_flag和std::call_once。它們內部利用互斥量和條件變量組合,實現了「執行且只執行一次」這樣的語義。
下面我們看看使用std::once_flag和std::call_once實現的單例代碼實例:
#include <iostream>#include <mutex>class Singleton {private: int a{0}; // 聲明 static std::once_flag flag; static Singleton* instance; Singleton(){ std::cout << "Singleton 構造函數" << std::endl; } Singleton(const Singleton& temp) { std::cout << "Singleton 拷貝構造函數" << std::endl; } Singleton& operator=(const Singleton& temp){ return *instance; }public: static Singleton* getInstance() { std::call_once(flag, [&]() -> void { instance = new Singleton; }); return instance; } void addA(){ a++; } void printA(){ std::cout << "printA:" << a << std::endl; }};// 需要定義std::once_flag Singleton::flag;Singleton *Singleton::instance{nullptr};int main() { Singleton* singleton = Singleton::getInstance(); singleton->addA(); singleton->printA(); Singleton::getInstance()->addA(); Singleton::getInstance()->printA(); return 0;}
實例代碼運行結果:
需要注意的是,所有的 std::once_flag 內部共享了同一對互斥量和條件變量。因此當存在很多 std::call_once 的時候,性能會有所下降。 但是從另外一個角度想想如果一個程序中存在很多的std::call_once,那么這個程序本身就設計得很不合理,這種情況更應該從程序設計的源頭上避免。
在 C++11 之后,C++標準保證函數靜態成員的初始化是線程安全的,對其讀寫則不保證線程安全。既然如此,那么我在直接在函數內部使用static 修飾一個單例變量不就好了么?
精簡一下代碼如下:
#include <iostream>class Singleton {private: int a{0}; Singleton(){ std::cout << "Singleton 構造函數" << std::endl; } Singleton(const Singleton& temp) { std::cout << "Singleton 拷貝構造函數" << std::endl; } Singleton& operator=(const Singleton& temp){ return *this; }public: static Singleton* getInstance() { static Singleton instance; return &instance; } void addA(){ a++; } void printA(){ std::cout << "printA:" << a << std::endl; }};int main() { Singleton* singleton = Singleton::getInstance(); singleton->addA(); singleton->printA(); Singleton::getInstance()->addA(); Singleton::getInstance()->printA(); return 0;}
以上代碼實現的單例即是線程安全,同時也是懶加載的,這就是在C++11之后,Effective C++最推薦的單例模式寫法。
實現一個類模板,其模板參數是希望由單例管理的類的名字,并提供 getInstance 之類的靜態接口。這種做法的好處是希望被單例管理的類,可以自由編寫,而無需繼承基類;并且在需要的時候,可以隨時脫去單例外衣。
#include <iostream>template <typename T>struct Singleton { static T* getInstance() { static T ins; return &ins; }};class A{private: int a{0}; A(const A& tmp){ std::cout << "A拷貝構造函數" << std::endl; } A& operator=(const A& tmp){ std::cout << "A賦值運算符" << std::endl; return *this; }public: A(){ std::cout << "A構造函數" << std::endl; } void addA(){ a++; } void printA(){ std::cout << "printA:" << a << std::endl; }};int main() { A* singleton = Singleton<A>::getInstance(); singleton->addA(); singleton->printA(); A* singleton1 = Singleton<A>::getInstance(); singleton1->addA(); singleton1->printA(); return 0;}
由上面的代碼可以看出,單例管理就交給了模版Singleton去控制了,類A本身就不知乎嚴格控制自己是否是單例了,這種實現就比較的靈活,如果你想使用單例的類A就搭配Singleton的模版進行使用即可, 如果你想使用非單例的類A就像正常那樣使用即可。
本文鏈接:http://www.www897cc.com/showinfo-26-23604-0.htmlC++之單例的幾種寫法
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: C++異常處理:如何使用try、catch、throw
下一篇: 深入理解序列化:概念、應用與技術