談談 C++ 單例模式
單例模式是一個很常見的設計模式,也廣泛應用于程序開發。其具有如下特點:
一個類只有一個實例化對象
全局可以使用
那么有人要問,那我不就定義一個類,程序只初始化一個全局的實例就好了嗎?沒錯,這樣是可以的。但是我們都知道程序會經過多人的接手維護和開發,比如第N個接手程序的時候,并不知道這個類定義的時候只能初始化一個實例,然后又實例化了新的對象, 則可能會造成意想不到的場景。那么這時候就要提到防御性編程,個人認為單例模式的實現也是防御性編程的一種方式,讓這個類保證只有一個實例化對象,并且如果試圖構造多個對象的時候,在程序的編譯期報錯。題外話,這也是為什么本人在進行一些稍大規模開發的時候,只會去選擇強類型語言,而不會選擇弱類型語言的原因,強類型語言會在編譯期間幫我們避免很多運行時可能產生的的Bug。
本文我們將探討如下內容:
單例模式的基本實現:包含單例模式的實現,線程安全,以及生命周期等
單例模式的模板實現, 多模塊調用單例存在的問題
單例模式的基本實現
在程序開發中,比較常見的單例就是程序啟動的相關配置信息了。比如我們定義一個SingletonConfig類。注意這個類有如下特點:
私有的構造函數, 拷貝構造函數,以及operator=, 保證其不能夠在類的外部進程對象構造,拷貝等操作。
GetInstance是一個公有的靜態成員函數,用來構造這個類唯一的實例對象m_objConfig, 并且返回給使用者。
我們來看下代碼實現:
class SingletonConfig
{
public:
static SingletonConfig * GetInstance()
{
if (m_objConfig = = nullptr)
m_objConfig = new SingletonConfig;
return m_objConfig;
}
private:
SingletonConfig() { ; };
SingletonConfig(const SingletonConfig&) { ; };
SingletonConfig& operator= (const SingletonConfig&) { ; };
private:
static SingletonConfig *m_objConfig;
};
SingletonConfig* SingletonConfig::m_objConfig = nullptr;
這也就是單例模式的基本實現了,然后我們需要考慮的就是單例的模式的生命周期問題,?單例的實例何時創建?何時銷毀?
單例模式生命周期
單例創建的時機
根據單例的創建時間,可以分為餓漢模式和懶漢模式。
上一節所展示的代碼則是懶漢模式: 當你的應用程序在需要調用GetInstance()方法的時候才創建實例化的對象,類似于懶加載。這種方式的好處在于,有一些單例模式的實例,可能在整個進程的聲明周期內可能不一定用到。那么這種懶漢模式就省去了不必要的資源創建過程。
餓漢模式一般的實現方式為,在進程或者模塊加載的時候就會創建全局的實例。比如將上述單例模式修改為。像這種進程啟動必須要使用的單例對象,使用餓漢模式實現比較簡單。
class SingletonConfig
{
public:
static SingletonConfig * GetInstance()
{
return m_objConfig;
}
private:
SingletonConfig() { ; };
SingletonConfig(const SingletonConfig&) { ; };
SingletonConfig& operator= (const SingletonConfig&) { ; };
private:
static SingletonConfig *m_objConfig;
};
SingletonConfig* SingletonConfig::m_objConfig = new SingletonConfig;
綜合來看什么情況下該使用餓漢模式,什么情況下該使用懶漢模式呢?個人認為大多數實現的場景下應該使用懶漢模式,其更加靈活,可以自己定義單例對象的創建時間;對于初始化對象時間比較長的單例,可以在進程啟動的時候手動的調用GetInstance()方法來完成初始化,避免在服務過程中導致第一個初始化示例對象的任務處理速度變慢。
單例釋放的時機
接下來查看,那么單例模式應該何時釋放其資源呢?一般情況下當進程退出的時候,一般的資源也都會隨之釋放,大多數場景單例模式即使不手動去調用析構函數也不會帶來很大的問題。但是有一些場景想在進程退出前把資源處理完善,比如這個單例對象有內存中的內容需要刷新到磁盤。那么有兩種方法,一種是全局static對象由進程退出的時候調用析構函數,另一種是讓單例使用者自己進行析構函數調用。
先說說全局static對象,一種是直接在類成員里面定義一個static成員,或者是在GetInstance()中定義一個static單例對象,比如:
class SingletonConfig
{
public:
static SingletonConfig * GetInstance()
{
static SingletonConfig objConfig;
return &objConfig;
}
virtual ~SingletonConfig()
{
std::cout << "~SingletonConfig()" << std::endl;
}
private:
SingletonConfig() { ; };
SingletonConfig(const SingletonConfig&) { ; };
SingletonConfig& operator= (const SingletonConfig&) { ; };
};
這種方法在程序退出的時候,將會調用SingletonConfig的析構函數,看下匯編,可以看到利用atexit注冊了一個方法,這個方法中會調用SingletonConfig的析構函數,并且在程序退出的時候執行。
如果想自己去控制單例模式的釋放時間可以實現如下, 在合適的時機調用ReleaseInstance方法去釋放單例對象。
class SingletonConfig
{
public:
static SingletonConfig * GetInstance()
{
if (m_objConfig == nullptr)
m_objConfig = new SingletonConfig;
return m_objConfig;
}
static void ReleaseInstance()
{
if (m_objConfig)
{
delete m_objConfig;
m_objConfig = nullptr;
}
}
virtual ~SingletonConfig()
{
std::cout << "~SingletonConfig()" << std::endl;
}
private:
SingletonConfig() { ; };
SingletonConfig(const SingletonConfig&) { ; };
SingletonConfig& operator= (const SingletonConfig&) { ; };
private:
static SingletonConfig* m_objConfig;
};
SingletonConfig* SingletonConfig::m_objConfig = nullptr;
這里我要留一個問題給讀者如果有兩個單例模式SingletonA和SingletonB, 他們都采用static的方式實現單例,那么如果SingletonA調用了SingletonB,有沒有可能產生什么問題?如果有如何避免這個問題?如果不知道的可以看看書籍**<
線程安全
如果是如下方式使用static對象方式實現的單例模式,在C++ 11之前是非線程安全的,而在C++ 11之后是線程安全的。
static SingletonConfig * GetInstance()
{
static SingletonConfig objConfig;
return &objConfig;
}
但如果不使用static對象,采用下述方式,那么在單例對象還沒初始化的時候,當多線程同時調用GetInstance可能會出現線程安全問題,導致創建了多個SingletonConfig。
static SingletonConfig * GetInstance()
{
if (m_objConfig == nullptr)
m_objConfig = new SingletonConfig;
return m_objConfig;
}
而一般的實現如下:
使用std::lock_guard去多線程保證互斥
雙重的m_objConfig == nullptr檢查,第一次是為了效率,當單例對象已經在的時候,就不需要互斥鎖了;第二次是進入鎖范圍之后,要查看下,是否有其他線程已經創建了單例對象,如果還沒有創建才進行創建。
class SingletonConfig
{
public:
static SingletonConfig * GetInstance()
{
if (m_objConfig == nullptr)
{
std::lock_guard
if (m_objConfig == nullptr)
{
m_objConfig = new SingletonConfig;
}
}
return m_objConfig;
}
static void ReleaseInstance()
{
if (m_objConfig)
{
delete m_objConfig;
m_objConfig = nullptr;
}
}
virtual ~SingletonConfig()
{
std::cout << "~SingletonConfig()" << std::endl;
}
private:
SingletonConfig() { ; };
SingletonConfig(const SingletonConfig&) { ; };
SingletonConfig& operator= (const SingletonConfig&) { ; };
private:
static SingletonConfig* m_objConfig;
static std::mutex m_mutex;
};
SingletonConfig* SingletonConfig::m_objConfig = nullptr;
std::mutex SingletonConfig::m_mutex;
單例模式的模板實現以及可能的問題
在網上或者一些書上,會使用模板去實現通用的單例模式,大致如下:
template
class CommonSingleton
{
public:
static T* GetInstance()
{
if (m_objSingle == nullptr)
{
std::lock_guard
if (m_objSingle == nullptr)
{
m_objSingle = new T;
}
}
return m_objSingle;
}
static void ReleaseInstance()
{
if (m_objSingle)
{
delete m_objSingle;
m_objSingle = nullptr;
}
}
private:
CommonSingleton() { ; };
CommonSingleton(const CommonSingleton&) { ; };
CommonSingleton& operator= (const CommonSingleton&) { ; };
private:
static T* m_objSingle;
static std::mutex m_mutex;
};
template
T* CommonSingleton
template
std::mutex CommonSingleton
如果有一個類需要單例模式,比如TestClass, 則調用方式如下:
CommonSingleton
以上的模板實現大家注意到了沒,這個實例化的對象有一個沒有參數的構造函數,如果一個類是必須有參數的構造函數呢?這個時候其實可以借助C++ 11 中的可變參數的完美轉發。具體實現讀者可以思考下,如果不清楚的可以參考?《深入應用C++11代碼優化及工程級應用》中的改進單例模式這一章節。
不過本人認為這一種的模板化實現,并不是一個特別好的方案,我也并不會優先選擇模板化的單例模式實現,主要有兩點原因:
模板參數接受的類,可以是這種:默認暴露給用戶,可以構造,拷貝,賦值的類,這樣便可以重新創造多個對象。這種方式缺乏了本人所理解的防御性編程的思路。
當使用模板實例化的時候,同一種模板參數的類,在多個不同的模塊中其實都會有自己的實例化對象。比如有A和B兩個模塊,并且均調用了CommonSingleton
總結
單例模式除了其具有程序中單個實例化對象的特點,也具有防御式編程的思想在其中。使用中一定要注意單例模式的生命周期,以及模板實現的跨模塊調用的問題。以上僅是一家之言,歡迎一起討論。
參考
<
<<深入應用C++11代碼優化及工程級應用>>的改進單例模式這一章節
C++ Java
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。