C++ 單例模式

前 言

我們在寫程序時,經(jīng)常需要實現(xiàn)某一個類對象能夠全局訪問心赶,但又需要保證其唯一的設(shè)計严沥。這就需要使用常說的單例模式來實現(xiàn)。

實 現(xiàn)

《設(shè)計模式》書中給出了一種實現(xiàn)方式拷沸,即定義一個單例類色查,使用類的私有靜態(tài)指針變量指向類的唯一實例,并用一個公有的靜態(tài)方法來獲取該實例堵漱。
根據(jù)書中的實現(xiàn)方式综慎,示例如下:

class CSingleton
{
private:
    CSingleton()   //將構(gòu)造函數(shù)設(shè)為私有
    static CSingleton *m_pInstance;  //私有靜態(tài)指針變量,指向唯一實例
public:
    static CSingleton* GetInstance()  //公有靜態(tài)方法勤庐,獲取該實例
    {
        if(m_pInstance == nullptr)  //判斷是否第一次調(diào)用
            m_pInstance = new CSingleton();
        return m_pInstance;
    }
};

因為類的構(gòu)造函數(shù)是私有的示惊,所以外部任何創(chuàng)建實例的嘗試都將失敗,訪問實例的唯一方法愉镰,即通過公有靜態(tài)方法GetInstance()米罚。GetInstance()返回的實例是當(dāng)這個函數(shù)首次被訪問時創(chuàng)建的。這就是常說的懶漢模式丈探。

以上的實現(xiàn)方式滿足了我們的需求录择,但是存在著諸多問題,比如:該實例如何刪除碗降?
我們可以在程序結(jié)束時調(diào)用GetInstance()并delete掉返回的實例指針隘竭。但是這樣的操作很容易出錯,因為我們很難保證在程序執(zhí)行的最后刪除讼渊;也不能保證刪除掉實例后动看,程序不再調(diào)用創(chuàng)建實例。

我們知道爪幻,系統(tǒng)會在程序結(jié)束后釋放所有全局變量并析構(gòu)所有類的靜態(tài)對象菱皆。利用這一個特性须误,我們可以在類中設(shè)計一個靜態(tài)成員變量,在其析構(gòu)函數(shù)中刪除唯一實例仇轻。在程序結(jié)束時京痢,系統(tǒng)將會調(diào)用這個靜態(tài)成員變量的析構(gòu)函數(shù),從而幫助我們自動的刪除唯一實例篷店,且不會出現(xiàn)人為的意外失誤祭椰。如下面實例中的CGarbo類:

class CSingleton
{
//經(jīng)典單例(Singleton)設(shè)計模式,只創(chuàng)建一個對象疲陕,并且自動釋放
private:
    CSingleton(void)
    static CSingleton *m_pInstance;
    //其唯一作用就是在析構(gòu)函數(shù)中刪除CSingleton實例
    class CGarbo 
    {
    public:
        ~CGarbo()
        {
            if(CSingleton::m_pInstance) 
                delete CSingleton::m_pInstance;
        }
    };
    static CGarbo Garbo;  //程序結(jié)束時吭产,系統(tǒng)會調(diào)用其析構(gòu)函數(shù)
public:
    static CSingleton * GetInstance()
    {
        if(m_pInstance == nullptr)
            m_pInstance = new CSingleton();
        return m_pInstance;
    }
};

以上的寫法,即滿足了全局訪問唯一實例鸭轮,也保證了在程序結(jié)束時,系統(tǒng)幫助我們選擇正確的釋放時機橄霉,不必我們關(guān)心此實例的釋放窃爷。但是依舊存在缺陷,因為此方式是線程不安全的姓蜂。在多線程中按厘,當(dāng)多個線程同時訪問時,會同時判斷實例未創(chuàng)建钱慢,從而創(chuàng)建出多個實例逮京,很明顯違背了我們實例唯一的需求。

不難想出束莫,此時可以使用線程鎖來保證線程的安全懒棉。如以下示例:

static CSingleton * GetInstance()
{
    Lock();  //可以使用臨界區(qū)CRITICAL_SECTION或者互斥量MUTEX來實現(xiàn)線程鎖
    if(m_pInstance == nullptr)
        m_pInstance = new CSingleton();
    UnLock();
    return m_pInstance;
}

上面的寫法依舊存在缺陷,因為當(dāng)某個線程要訪問時览绿,就立即上鎖策严,這樣導(dǎo)致了不必要的鎖的消耗。所以我們可以先判斷下實例是否存在饿敲,再進行是否上鎖的操作妻导。這就是所謂的雙檢查鎖(DCL)思想,即Double Checked Locking怀各。優(yōu)化的寫法如下實例:

static CSingleton * GetInstance()
{  
    if(m_pInstance == nullptr) {  
        Lock();
        if(m_pInstance == nullptr) {  
            m_pInstance = new CSingleton();  
        }  
        UnLock();  
    }  
    return m_pInstance ;  
}

此時一個完整的單例模式就實現(xiàn)了倔韭,但事實證明,此實現(xiàn)的寫法依舊存在著重大的問題瓢对,而問題就在于m_pInstance = new CSingleton;這一句寿酌,具體如下:

分析: m_pInstance = new CSingleton()這句話可以分成三個步驟來執(zhí)行:
1.分配了一個CSingleton類型對象所需要的內(nèi)存。
2.在分配的內(nèi)存處構(gòu)造CSingleton類型的對象沥曹。
3.把分配的內(nèi)存的地址賦給指針m_pInstance份名。

可能會認為這三個步驟是按順序執(zhí)行的,但實際上只能確定步驟 1 是最先執(zhí)行的,步驟2,3卻不一定碟联。
問題就出現(xiàn)在這。假如某個線程A在調(diào)用執(zhí)行m_pInstance = new CSingleton()的時候是按照1, 3, 2的順序的,
那么剛剛執(zhí)行完步驟3給singleton類型分配了內(nèi)存(此時m_ instance就不是nullptr了 )就切換到了線程B,
由于m_pInstance已經(jīng)不是nullptr了,所以線程B會直接執(zhí)行return m_ instance得到一個對象,而這個對象并沒有真正的被構(gòu)造! ! 
嚴重bug就這么發(fā)生了僵腺。

參考:https://segmentfault.com/a/1190000015950693

進一步探討

著名的《Effective C++》系列書籍的作者 Meyers 提出了C++ 11版本最簡潔的跨平臺方案鲤孵,即Meyers' Singleton
實現(xiàn)如下:

class CSingleton
{
private:
    CSingleton(void)
    
public:
    static CSingleton & getInstance()
    {
        static CSingleton m_pInstance;  //局部靜態(tài)變量
        return m_pInstance;
    }
};

這樣的寫法即簡潔又完美!需要注意的是此寫法需要支持C++11以上辰如、GCC4.0編譯器以上普监。

以上的所有內(nèi)容是本人的一些思考和領(lǐng)悟,如有不正確的地方琉兜,歡迎大佬指正凯正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市豌蟋,隨后出現(xiàn)的幾起案子廊散,更是在濱河造成了極大的恐慌,老刑警劉巖梧疲,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件允睹,死亡現(xiàn)場離奇詭異,居然都是意外死亡幌氮,警方通過查閱死者的電腦和手機缭受,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來该互,“玉大人米者,你說我怎么就攤上這事∮钪牵” “怎么了蔓搞?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長随橘。 經(jīng)常有香客問我败明,道長,這世上最難降的妖魔是什么太防? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任妻顶,我火速辦了婚禮,結(jié)果婚禮上蜒车,老公的妹妹穿的比我還像新娘讳嘱。我一直安慰自己,他們只是感情好酿愧,可當(dāng)我...
    茶點故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布沥潭。 她就那樣靜靜地躺著,像睡著了一般嬉挡。 火紅的嫁衣襯著肌膚如雪钝鸽。 梳的紋絲不亂的頭發(fā)上汇恤,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天,我揣著相機與錄音拔恰,去河邊找鬼因谎。 笑死,一個胖子當(dāng)著我的面吹牛颜懊,可吹牛的內(nèi)容都是我干的财岔。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼河爹,長吁一口氣:“原來是場噩夢啊……” “哼匠璧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起咸这,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤夷恍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后媳维,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體裁厅,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年侨艾,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拓挥。...
    茶點故事閱讀 40,769評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡唠梨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出侥啤,到底是詐尸還是另有隱情当叭,我是刑警寧澤,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布盖灸,位于F島的核電站蚁鳖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赁炎。R本人自食惡果不足惜醉箕,卻給世界環(huán)境...
    茶點故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望徙垫。 院中可真熱鬧讥裤,春花似錦、人聲如沸姻报。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吴旋。三九已至损肛,卻和暖如春厢破,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背治拿。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工摩泪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人忍啤。 一個月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓加勤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親同波。 傳聞我的和親對象是個殘疾皇子鳄梅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,781評論 2 361