C++ 單例模式的幾種實(shí)現(xiàn)方式研究
單例模式
單例模式,可以說設(shè)計(jì)模式中最常應(yīng)用的一種模式了,據(jù)說也是面試官最喜歡的題目。但是如果沒有學(xué)過設(shè)計(jì)模式的人,可能不會(huì)想到要去應(yīng)用單例模式,面對(duì)單例模式適用的情況,可能會(huì)優(yōu)先考慮使用全局或者靜態(tài)變量的方式,這樣比較簡(jiǎn)單,也是沒學(xué)過設(shè)計(jì)模式的人所能想到的最簡(jiǎn)單的方式了。
一般情況下,我們建立的一些類是屬于工具性質(zhì)的,基本不用存儲(chǔ)太多的跟自身有關(guān)的數(shù)據(jù),在這種情況下,每次都去new一個(gè)對(duì)象,即增加了開銷,也使得代碼更加臃腫。其實(shí),我們只需要一個(gè)實(shí)例對(duì)象就可以。如果采用全局或者靜態(tài)變量的方式,會(huì)影響封裝性,難以保證別的代碼不會(huì)對(duì)全局變量造成影響。
考慮到這些需要,我們將默認(rèn)的構(gòu)造函數(shù)聲明為私有的,這樣就不會(huì)被外部所new了,甚至可以將析構(gòu)函數(shù)也聲明為私有的,這樣就只有自己能夠刪除自己了。在Java和C#這樣純的面向?qū)ο蟮恼Z(yǔ)言中,單例模式非常好實(shí)現(xiàn),直接就可以在靜態(tài)區(qū)初始化instance,然后通過getInstance返回,這種就被稱為餓漢式單例類。也有些寫法是在getInstance中new instance然后返回,這種就被稱為懶漢式單例類,但這涉及到第一次getInstance的一個(gè)判斷問題。
下面的代碼只是表示一下,跟具體哪種語(yǔ)言沒有關(guān)系。
單線程中:
Singleton* getInstance()
{
if (instance == NULL)
instance = new Singleton();
return instance;
}
這樣就可以了,保證只取得了一個(gè)實(shí)例。但是在多線程的環(huán)境下卻不行了,因?yàn)楹芸赡軆蓚€(gè)線程同時(shí)運(yùn)行到if (instance == NULL)這一句,導(dǎo)致可能會(huì)產(chǎn)生兩個(gè)實(shí)例。于是就要在代碼中加鎖。
Singleton* getInstance()
{
lock();
if (instance == NULL)
{
instance = new Singleton();
}
unlock();
return instance;
}
但這樣寫的話,會(huì)稍稍映像性能,因?yàn)槊看闻袛嗍欠駷榭斩夹枰绘i定,如果有很多線程的話,就愛會(huì)造成大量線程的阻塞。于是大神們又想出了雙重鎖定。
Singleton* getInstance()
{
if (instance == NULL)
{
lock();
if (instance == NULL)
{
instance = new Singleton();
}
unlock();
}
return instance;
}
這樣只夠極低的幾率下,通過越過了if (instance == NULL)的線程才會(huì)有進(jìn)入鎖定臨界區(qū)的可能性,這種幾率還是比較低的,不會(huì)阻塞太多的線程,但為了防止一個(gè)線程進(jìn)入臨界區(qū)創(chuàng)建實(shí)例,另外的線程也進(jìn)去臨界區(qū)創(chuàng)建實(shí)例,又加上了一道防御if (instance == NULL),這樣就確保不會(huì)重復(fù)創(chuàng)建了。
常用的場(chǎng)景
單例模式常常與工廠模式結(jié)合使用,因?yàn)楣S只需要?jiǎng)?chuàng)建產(chǎn)品實(shí)例就可以了,在多線程的環(huán)境下也不會(huì)造成任何的沖突,因此只需要一個(gè)工廠實(shí)例就可以了。
優(yōu)點(diǎn)
1.減少了時(shí)間和空間的開銷(new實(shí)例的開銷)。
2.提高了封裝性,使得外部不易改動(dòng)實(shí)例。
缺點(diǎn)
1.懶漢式是以時(shí)間換空間的方式。
2.餓漢式是以空間換時(shí)間的方式。
C++實(shí)現(xiàn)代碼
#ifndef _SINGLETON_H_
#define _SINGLETON_H_
class Singleton{
public:
static Singleton* getInstance();
private:
Singleton();
//把復(fù)制構(gòu)造函數(shù)和=操作符也設(shè)為私有,防止被復(fù)制
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
static Singleton* instance;
};
#endif
#include "Singleton.h"
Singleton::Singleton(){
}
Singleton::Singleton(const Singleton&){
}
Singleton& Singleton::operator=(const Singleton&){
}
//在此處初始化
Singleton* Singleton::instance = new Singleton();
Singleton* Singleton::getInstance(){
return instance;
}
#include "Singleton.h"
#include <stdio.h>
int main(){
Singleton* singleton1 = Singleton::getInstance();
Singleton* singleton2 = Singleton::getInstance();
if (singleton1 == singleton2)
fprintf(stderr,"singleton1 = singleton2\n");
return 0;
}
1 g++ -o client Singleton.cpp client.cpp
運(yùn)行結(jié)果
下面給大家補(bǔ)充一下
單例模式有兩種實(shí)現(xiàn)模式:
1)懶漢模式: 就是說當(dāng)你第一次使用時(shí)才創(chuàng)建一個(gè)唯一的實(shí)例對(duì)象,從而實(shí)現(xiàn)延遲加載的效果。
2)餓漢模式: 就是說不管你將來用不用,程序啟動(dòng)時(shí)就創(chuàng)建一個(gè)唯一的實(shí)例對(duì)象。
所以,從實(shí)現(xiàn)手法上看, 懶漢模式是在第一次使用單例對(duì)象時(shí)才完成初始化工作。因?yàn)榇藭r(shí)可能存在多線程競(jìng)態(tài)環(huán)境,如不加鎖限制會(huì)導(dǎo)致重復(fù)構(gòu)造或構(gòu)造不完全問題。
餓漢模式則是利用外部變量,在進(jìn)入程序入口函數(shù)之前就完成單例對(duì)象的初始化工作,此時(shí)是單線程所以不會(huì)存在多線程的競(jìng)態(tài)環(huán)境,故而無需加鎖。
以下是典型的幾種實(shí)現(xiàn)
一、 懶漢模式,標(biāo)準(zhǔn)的 ”雙檢鎖“ + ”自動(dòng)回收“ 實(shí)現(xiàn)
class Singleton
{
public:
static Singleton* GetInstance()
{
if (m_pInstance == NULL )
{
Lock(); // 加鎖
if (m_pInstance == NULL )
{
m_pInstance = new Singleton ();
}
UnLock(); // 解鎖
}
return m_pInstance;
}
// 實(shí)現(xiàn)一個(gè)內(nèi)嵌垃圾回收類
class CGarbo
{
public:
~CGarbo()
{
if(Singleton::m_pInstance)
delete Singleton::m_pInstance;
}
};
static CGarbo Garbo; // 定義一個(gè)靜態(tài)成員變量,程序結(jié)束時(shí),系統(tǒng)會(huì)自動(dòng)調(diào)用它的析構(gòu)函數(shù)從而釋放單例對(duì)象
private:
Singleton(){};
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
static Singleton* m_pInstance;
};
Singleton* Singleton::m_pInstance = NULL;
Singleton::CGarbo Garbo;
二、靜態(tài)局部變量的懶漢模式 ,而不是new在堆上創(chuàng)建對(duì)象,避免自己回收資源。
這里仍然要注意的是局部變量初始化的線程安全性問題,在C++0X以后,要求編譯器保證靜態(tài)變量初始化的線程安全性,可以不加鎖。但C++ 0X以前,仍需要加鎖。
class Singleton
{
public:
static Singleton* GetInstance()
{
Lock(); // not needed after C++0x
static Singleton instance;
UnLock(); // not needed after C++0x
return &instance;
}
private:
Singleton() {};
Singleton(const Singleton &);
Singleton & operator = (const Singleton &);
};
在懶漢模式里,如果大量并發(fā)線程獲取單例對(duì)象,在進(jìn)行頻繁加鎖解鎖操作時(shí),必然導(dǎo)致效率低下。
三、餓漢模式,基礎(chǔ)版本
因?yàn)槌绦蛞婚_始就完成了單例對(duì)象的初始化,所以后續(xù)不再需要考慮多線程安全性問題,就可以避免懶漢模式里頻繁加鎖解鎖帶來的開銷。
class Singleton
{
public:
static Singleton* GetInstance()
{
return &m_instance;
}
private:
Singleton(){};
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
static Singleton m_instance;
};
Singleton Singleton::m_instance; // 在程序入口之前就完成單例對(duì)象的初始化
雖然這種實(shí)現(xiàn)在一定程度下能良好工作,但是在某些情況下會(huì)帶來問題 --- 就是在C++中 ”非局部靜態(tài)對(duì)象“ 的 ”初始化“ 順序 的 ”不確定性“, 參見Effective c++ 條款47。
考慮: 如果有兩個(gè)這樣的單例類,將分別生成單例對(duì)象A, 單例對(duì)象B. 它們分別定義在不同的編譯單元(cpp中), 而A的初始化依賴于B 【 即A的構(gòu)造函數(shù)中要調(diào)用B::GetInstance() ,而此時(shí)B::m_instance 可能還未初始化,顯然調(diào)用結(jié)果就是非法的 】, 所以說只有B在A之前完成初始化程序才能正確運(yùn)行,而這種跨編譯單元的初始化順序編譯器是無法保證的。
四、餓漢模式,增強(qiáng)版本(boost實(shí)現(xiàn))
在前面的方案中:餓漢模式中,使用到了類靜態(tài)成員變量,但是遇到了初始化順序的問題; 懶漢模式中,使用到了靜態(tài)局部變量,但是存在著線程安全等問題。
boost 的實(shí)現(xiàn)方式是:?jiǎn)卫龑?duì)象作為靜態(tài)局部變量,然后增加一個(gè)輔助類,并聲明一個(gè)該輔助類的類靜態(tài)成員變量,在該輔助類的構(gòu)造函數(shù)中,初始化單例對(duì)象。以下為代碼
class Singleton
{
public:
static Singleton* GetInstance()
{
static Singleton instance;
return &instance;
}
protected:
// 輔助代理類
struct Object_Creator
{
Object_Creator()
{
Singleton::GetInstance();
}
};
static Object_Creator _object_creator;
Singleton() {}
~Singleton() {}
};
Singleton::Object_Creator Singleton::_object_creator;
首先,代理類這個(gè)外部變量初始化時(shí),在其構(gòu)造函數(shù)內(nèi)部調(diào)用 Singleton::GetInstance();從而間接完成單例對(duì)象的初始化,這就通過該代理類實(shí)現(xiàn)了餓漢模式的特性。
其次,仍然考慮第三種模式的缺陷。 當(dāng)A的初始化依賴于B, 【 即A的構(gòu)造函數(shù)中要調(diào)用B::GetInstance() ,而此時(shí)B::m_instance 可能還未初始化,顯然調(diào)用結(jié)果就是非法的 】 現(xiàn)在就變?yōu)椤驹贏的構(gòu)造函數(shù)中要調(diào)用B::GetInstance() ,如果B尚未初始化,就會(huì)引發(fā)B的初始化】,所以在不同編譯單元內(nèi)全局變量的初始化順序不定的問題就隨之解決。
最后,關(guān)于使用懶漢還是餓漢模式,我的理解:
如果這個(gè)單例對(duì)象構(gòu)造十分耗時(shí)或者占用很多資源,比如加載插件啊, 初始化網(wǎng)絡(luò)連接啊,讀取文件啊等等,而有可能該對(duì)象程序運(yùn)行時(shí)不會(huì)用到,那么也要在程序一開始就進(jìn)行初始化,也是一種資源浪費(fèi)吧。 所以這種情況懶漢模式(延遲加載)更好。
如果這個(gè)單例對(duì)象在多線程高并發(fā)環(huán)境下頻繁使用,性能要求較高,那么顯然使用餓漢模式來避免資源競(jìng)爭(zhēng),提高響應(yīng)速度更好。
上一篇:OpenCV如何提取圖片中曲線
欄 目:C語(yǔ)言
下一篇:Opencv Hough算法實(shí)現(xiàn)圖片中直線檢測(cè)
本文標(biāo)題:C++ 單例模式的幾種實(shí)現(xiàn)方式研究
本文地址:http://www.jygsgssxh.com/a1/Cyuyan/554.html
您可能感興趣的文章
- 04-02c語(yǔ)言沒有round函數(shù) round c語(yǔ)言
- 01-10深入理解C++中常見的關(guān)鍵字含義
- 01-10使用C++實(shí)現(xiàn)全排列算法的方法詳解
- 01-10c++中inline的用法分析
- 01-10用C++實(shí)現(xiàn)DBSCAN聚類算法
- 01-10全排列算法的非遞歸實(shí)現(xiàn)與遞歸實(shí)現(xiàn)的方法(C++)
- 01-10C++大數(shù)模板(推薦)
- 01-10淺談C/C++中的static與extern關(guān)鍵字的使用詳解
- 01-10深入C/C++浮點(diǎn)數(shù)在內(nèi)存中的存儲(chǔ)方式詳解
- 01-10深入理解C/C++混合編程


閱讀排行
- 1C語(yǔ)言 while語(yǔ)句的用法詳解
- 2java 實(shí)現(xiàn)簡(jiǎn)單圣誕樹的示例代碼(圣誕
- 3利用C語(yǔ)言實(shí)現(xiàn)“百馬百擔(dān)”問題方法
- 4C語(yǔ)言中計(jì)算正弦的相關(guān)函數(shù)總結(jié)
- 5c語(yǔ)言計(jì)算三角形面積代碼
- 6什么是 WSH(腳本宿主)的詳細(xì)解釋
- 7C++ 中隨機(jī)函數(shù)random函數(shù)的使用方法
- 8正則表達(dá)式匹配各種特殊字符
- 9C語(yǔ)言十進(jìn)制轉(zhuǎn)二進(jìn)制代碼實(shí)例
- 10C語(yǔ)言查找數(shù)組里數(shù)字重復(fù)次數(shù)的方法
本欄相關(guān)
- 04-02c語(yǔ)言函數(shù)調(diào)用后清空內(nèi)存 c語(yǔ)言調(diào)用
- 04-02func函數(shù)+在C語(yǔ)言 func函數(shù)在c語(yǔ)言中
- 04-02c語(yǔ)言的正則匹配函數(shù) c語(yǔ)言正則表達(dá)
- 04-02c語(yǔ)言用函數(shù)寫分段 用c語(yǔ)言表示分段
- 04-02c語(yǔ)言中對(duì)數(shù)函數(shù)的表達(dá)式 c語(yǔ)言中對(duì)
- 04-02c語(yǔ)言編寫函數(shù)冒泡排序 c語(yǔ)言冒泡排
- 04-02c語(yǔ)言沒有round函數(shù) round c語(yǔ)言
- 04-02c語(yǔ)言分段函數(shù)怎么求 用c語(yǔ)言求分段
- 04-02C語(yǔ)言中怎么打出三角函數(shù) c語(yǔ)言中怎
- 04-02c語(yǔ)言調(diào)用函數(shù)求fibo C語(yǔ)言調(diào)用函數(shù)求
隨機(jī)閱讀
- 01-11ajax實(shí)現(xiàn)頁(yè)面的局部加載
- 01-10C#中split用法實(shí)例總結(jié)
- 04-02jquery與jsp,用jquery
- 01-10delphi制作wav文件的方法
- 01-10SublimeText編譯C開發(fā)環(huán)境設(shè)置
- 08-05DEDE織夢(mèng)data目錄下的sessions文件夾有什
- 08-05dedecms(織夢(mèng))副欄目數(shù)量限制代碼修改
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 01-10使用C語(yǔ)言求解撲克牌的順子及n個(gè)骰子
- 08-05織夢(mèng)dedecms什么時(shí)候用欄目交叉功能?


