當前位置:
首頁 > 知識 > C++ 單例模式總結與剖析

C++ 單例模式總結與剖析

目錄

  • C++ 單例模式總結與剖析
  • 一、什麼是單例
  • 二、C++單例的實現
  • 2.1 基礎要點
  • 2.2 C++ 實現單例的幾種方式
  • 2.3 單例的模板
  • 三、何時應該使用或者不使用單例
  • 反對單例的理由
  • 參考文章

C++ 單例模式總結與剖析

單例可能是最常用的簡單的一種設計模式,實現方法多樣,根據不同的需求有不同的寫法; 同時單例也有其局限性,因此有很多人是反對使用單例的。本文對C++ 單例的常見寫法進行了一個總結, 包括懶漢式、線程安全、單例模板等; 按照從簡單到複雜,最終回歸簡單的的方式循序漸進地介紹,並且對各種實現方法的局限進行了簡單的闡述,大量用到了C++ 11的特性如智能指針, magic static,線程鎖; 從頭到尾理解下來,對於學習和鞏固C++語言特性還是很有幫助的。本文的全部代碼在 g++ 5.4.0 編譯器下編譯運行通過,可以在我的github 倉庫中找到。

一、什麼是單例

單例 Singleton 是設計模式的一種,其特點是只提供唯一一個類的實例,具有全局變數的特點,在任何位置都可以通過介面獲取到那個唯一實例;

具體運用場景如:

  1. 設備管理器,系統中可能有多個設備,但是只有一個設備管理器,用於管理設備驅動;
  2. 數據池,用來緩存數據的數據結構,需要在一處寫,多處讀取或者多處寫,多處讀取;

二、C++單例的實現

2.1 基礎要點

  • 全局只有一個實例:static 特性,同時禁止用戶自己聲明並定義實例(把構造函數設為 private)
  • 線程安全
  • 禁止賦值和拷貝
  • 用戶通過介面獲取實例:使用 static 類成員函數

2.2 C++ 實現單例的幾種方式

2.2.1 有缺陷的懶漢式

懶漢式(Lazy-Initialization)的方法是直到使用時才實例化對象,也就說直到調用get_instance() 方法的時候才 new 一個單例的對象。好處是如果被調用就不會佔用內存。

#include <iostream>
// version1:
// with problems below:
// 1. thread is not safe
// 2. memory leak
class Singleton{
private:
Singleton(){
std::cout<<"constructor called!"<<std::endl;
}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Singleton* m_instance_ptr;
public:
~Singleton(){
std::cout<<"destructor called!"<<std::endl;
}
static Singleton* get_instance(){
if(m_instance_ptr==nullptr){
m_instance_ptr = new Singleton;
}
return m_instance_ptr;
}
void use() const { std::cout << "in use" << std::endl; }
};
Singleton* Singleton::m_instance_ptr = nullptr;
int main(){
Singleton* instance = Singleton::get_instance();
Singleton* instance_2 = Singleton::get_instance();
return 0;
}

運行的結果是

constructor called!

可以看到,獲取了兩次類的實例,卻只有一次類的構造函數被調用,表明只生成了唯一實例,這是個最基礎版本的單例實現,他有哪些問題呢?

  1. 線程安全的問題

    ,當多線程獲取單例時有可能引發競態條件:第一個線程在if中判斷 m_instance_ptr是空的,於是開始實例化單例;同時第2個線程也嘗試獲取單例,這個時候判斷m_instance_ptr還是空的,於是也開始實例化單例;這樣就會實例化出兩個對象,這就是線程安全問題的由來;

    解決辦法

    :加鎖
  2. 內存泄漏

    . 注意到類中只負責new出對象,卻沒有負責delete對象,因此只有構造函數被調用,析構函數卻沒有被調用;因此會導致內存泄漏。

    解決辦法

    : 使用共享指針;

因此,這裡提供一個改進的,線程安全的、使用智能指針的實現;

2.2.2 線程安全、內存安全的懶漢式單例 (智能指針,鎖)

#include <iostream>
#include <memory> // shared_ptr
#include <mutex> // mutex
// version 2:
// with problems below fixed:
// 1. thread is safe now
// 2. memory doesn"t leak
class Singleton{
public:
typedef std::shared_ptr<Singleton> Ptr;
~Singleton(){
std::cout<<"destructor called!"<<std::endl;
}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Ptr get_instance(){
// "double checked lock"
if(m_instance_ptr==nullptr){
std::lock_guard<std::mutex> lk(m_mutex);
if(m_instance_ptr == nullptr){
m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
}
return m_instance_ptr;
}
}
private:
Singleton(){
std::cout<<"constructor called!"<<std::endl;
}
static Ptr m_instance_ptr;
static std::mutex m_mutex;
};
// initialization static variables out of class
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::m_mutex;
int main(){
Singleton::Ptr instance = Singleton::get_instance();
Singleton::Ptr instance2 = Singleton::get_instance();
return 0;
}

運行結果如下,發現確實只構造了一次實例,並且發生了析構。

constructor called!
destructor called!

shared_ptr和mutex都是C++11的標準,以上這種方法的優點是

  • 基於 shared_ptr, 用了C++比較倡導的 RAII思想,用對象管理資源,當 shared_ptr 析構的時候,new 出來的對象也會被 delete掉。以此避免內存泄漏。
  • 加了鎖,使用互斥量來達到線程安全。這裡使用了兩個 if判斷語句的技術稱為

    雙檢鎖

    ;好處是,只有判斷指針為空的時候才加鎖,避免每次調用 get_instance的方法都加鎖,鎖的開銷畢竟還是有點大的。

不足之處在於: 使用智能指針會要求用戶也得使用智能指針,非必要不應該提出這種約束; 使用鎖也有開銷; 同時代碼量也增多了,實現上我們希望越簡單越好。

還有更加嚴重的問題,在某些平台(與編譯器和指令集架構有關),==雙檢鎖會失效==!具體可以看這篇文章,解釋了為什麼會發生這樣的事情。

因此這裡還有第三種的基於 Magic Staic的方法達到線程安全

2.2.3 最推薦的懶漢式單例(magic static )——局部靜態變數

#include <iostream>
class Singleton
{
public:
~Singleton(){
std::cout<<"destructor called!"<<std::endl;
}
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Singleton& get_instance(){
static Singleton instance;
return instance;
}
private:
Singleton(){
std::cout<<"constructor called!"<<std::endl;
}
};
int main(int argc, char *argv[])
{
Singleton& instance_1 = Singleton::get_instance();
Singleton& instance_2 = Singleton::get_instance();
return 0;
}

運行結果

constructor called!
destructor called!

這種方法又叫做 Meyers" SingletonMeyer"s的單例, 是著名的寫出《Effective C++》系列書籍的作者 Meyers 提出的。所用到的特性是在C++11標準中的Magic Static特性:


If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

如果當變數在初始化的時候,並發同時進入聲明語句,並發線程將會阻塞等待初始化結束。

這樣保證了並發線程在獲取靜態局部變數的時候一定是初始化過的,所以具有線程安全性。

C++靜態變數的生存期 是從聲明到程序結束,這也是一種懶漢式。

這是最推薦的一種單例實現方式:

  1. 通過局部靜態變數的特性保證了線程安全 (C++11, GCC > 4.3, VS2015支持該特性);
  2. 不需要使用共享指針,代碼簡潔;
  3. 注意在使用的時候需要聲明單例的引用 Single& 才能獲取對象。

另外網上有人的實現返回指針而不是返回引用

static Singleton* get_instance(){
static Singleton instance;
return &instance;
}

這樣做並不好,理由主要是無法避免用戶使用delete instance導致對象被提前銷毀。還是建議大家使用返回引用的方式。

2.2.4 函數返回引用

有人在網上提供了這樣一種單例的實現方式;

#include <iostream>
class A
{
public:
A() {
std::cout<<"constructor" <<std::endl;
}
~A(){
std::cout<<"destructor"<<std::endl;
}
};
A& ret_singleton(){
static A instance;
return instance;
}
int main(int argc, char *argv[])
{
A& instance_1 = ret_singleton();
A& instance_2 = ret_singleton();
return 0;
}

嚴格來說,這不屬於單例了,因為類A只是個尋常的類,可以被定義出多個實例,但是亮點在於提供了ret_singleton的方法,可以返回一個全局(靜態)變數,起到類似單例的效果,這要求用戶必須保證想要獲取 全局變數A ,只通過ret_singleton()的方法。

以上是各種方法實現單例的代碼和說明,解釋了各種技術實現的初衷和原因。這裡會比較推薦 C++11 標準下的 2.2.3 的方式,即使用static local的方法,簡單的理由來說是因為其足夠簡單卻滿足所有需求和顧慮。

在某些情況下,我們系統中可能有多個單例,如果都按照這種方式的話,實際上是一種重複,有沒有什麼方法可以只實現一次單例而能夠復用其代碼從而實現多個單例呢? 很自然的我們會考慮使用模板技術或者繼承的方法,

在我的博客中有介紹過如何使用單例的模板。

2.3 單例的模板

2.3.1 CRTP 奇異遞歸模板模式實現

代碼示例如下:

// brief: a singleton base class offering an easy way to create singleton
#include <iostream>
template<typename T>
class Singleton{
public:
static T& get_instance(){
static T instance;
return instance;
}
virtual ~Singleton(){
std::cout<<"destructor called!"<<std::endl;
}
Singleton(const Singleton&)=delete;
Singleton& operator =(const Singleton&)=delete;
protected:
Singleton(){
std::cout<<"constructor called!"<<std::endl;
}
};
/********************************************/
// Example:
// 1.friend class declaration is requiered!
// 2.constructor should be private
class DerivedSingle:public Singleton<DerivedSingle>{
// !!!! attention!!!
// needs to be friend in order to
// access the private constructor/destructor
friend class Singleton<DerivedSingle>;
public:
DerivedSingle(const DerivedSingle&)=delete;
DerivedSingle& operator =(const DerivedSingle&)= delete;
private:
DerivedSingle()=default;
};
int main(int argc, char* argv[]){
DerivedSingle& instance1 = DerivedSingle::get_instance();
DerivedSingle& instance2 = DerivedSingle::get_instance();
return 0;
}

以上實現一個單例的模板基類,使用方法如例子所示意,子類需要將自己作為模板參數T 傳遞給 Singleton<T> 模板; 同時需要將基類聲明為友元,這樣才能調用子類的私有構造函數。

基類模板的實現要點是:

  1. 構造函數需要是

    protected

    ,這樣子類才能繼承;
  2. 使用了奇異遞歸模板模式CRTP(Curiously recurring template pattern)
  3. get instance 方法和 2.2.3 的static local方法一個原理。
  4. 在這裡基類的析構函數可以不需要 virtual ,因為子類在應用中只會用 Derived 類型,保證了析構時和構造時的類型一致

2.3.2 不需要在子類聲明友元的實現方法

在 stackoverflow上, 有大神給出了不需要在子類中聲明友元的方法,在這裡一併放出;精髓在於使用一個代理類 token,子類構造函數需要傳遞token類才能構造,但是把 token保護其起來, 然後子類的構造函數就可以是公有的了,這個子類只有 Derived(token)的這樣的構造函數,這樣用戶就無法自己定義一個類的實例了,起到控制其唯一性的作用。代碼如下。

// brief: a singleton base class offering an easy way to create singleton
#include <iostream>
template<typename T>
class Singleton{
public:
static T& get_instance() noexcept(std::is_nothrow_constructible<T>::value){
static T instance{token()};
return instance;
}
virtual ~Singleton() =default;
Singleton(const Singleton&)=delete;
Singleton& operator =(const Singleton&)=delete;
protected:
struct token{}; // helper class
Singleton() noexcept=default;
};
/********************************************/
// Example:
// constructor should be public because protected `token` control the access
class DerivedSingle:public Singleton<DerivedSingle>{
public:
DerivedSingle(token){
std::cout<<"destructor called!"<<std::endl;
}
~DerivedSingle(){
std::cout<<"constructor called!"<<std::endl;
}
DerivedSingle(const DerivedSingle&)=delete;
DerivedSingle& operator =(const DerivedSingle&)= delete;
};
int main(int argc, char* argv[]){
DerivedSingle& instance1 = DerivedSingle::get_instance();
DerivedSingle& instance2 = DerivedSingle::get_instance();
return 0;
}

2.3.3 函數模板返回引用

在 2.2.4 中提供了一種類型的全局變數的方法,可以把一個一般的類,通過這種方式提供一個類似單例的

全局性效果(但是不能阻止用戶自己聲明定義這樣的類的對象);在這裡我們把這個方法變成一個 template 模板函數,然後就可以得到任何一個類的全局變數。

#include <iostream>
class A
{
public:
A() {
std::cout<<"constructor" <<std::endl;
}
~A(){
std::cout<<"destructor"<<std::endl;
}
};
template<typename T>
T& get_global(){
static T instance;
return instance;
}
int main(int argc, char *argv[])
{
A& instance_1 = get_global<A>();
A& instance_2 = get_global<A>();
return 0;
}

可以看到這種方式確實非常簡潔,同時類仍然具有一般類的特點而不受限制,當然也因此失去了單例那麼強的約束(禁止賦值、構造和拷貝構造)。

這裡把函數命名為 get_global() 是為了強調,這裡可以通過這種方式獲取得到單例最重要的全局變數特性;但是並不是單例的模式。

三、何時應該使用或者不使用單例

根據stackoverflow上的一個高票答案 singleton-how-should-it-be-used:

You need to have one and only one object of a type in system

==你需要系統中只有唯一一個實例存在的類的全局變數的時候才使用單例==。

  • 如果使用單例,應該用什麼樣子的
  • How to create the best singleton:
  • The smaller, the better. I am a minimalist
  • Make sure it is thread safe
  • Make sure it is never null
  • Make sure it is created only once
  • Lazy or system initialization? Up to your requirements
  • Sometimes the OS or the JVM creates singletons for you (e.g. in Java every class definition is a singleton)
  • Provide a destructor or somehow figure out how to dispose resources
  • Use little memory
  • ==越小越好,越簡單越好,線程安全,內存不泄露==

反對單例的理由

當然程序員是分流派的,有些是反對單例的,有些人是反對設計模式的,有些人甚至連面向對象都反對 :).

反對單例的理由有哪些:

著作權聲明:本文系作者原創,歡迎轉載分享,轉載請標明來源。

C++ 單例模式總結與剖析

打開今日頭條,查看更多圖片

作者:行者孫

原文:https://www.cnblogs.com/sunchaothu/p/10389842.html

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

核心交易鏈路架構設計與演進
一步步封裝實現自己的網路請求框架

TAG:程序員小新人學習 |