01:單例模式——C++實現
目錄
一、介紹
單例模式(Singleton Pattern):這種模式涉及到一個單一的類,該類負責建立自己的物件,同時確保只有單個物件被建立。這個類提供了一種訪問其唯一的物件的方式,可以直接訪問,不需要例項化該類的物件。
二、應用場景
1、Windows 是多程序多執行緒的,在操作一個檔案的時候,就不可避免地出現多個程序或執行緒同時操作一個檔案的現象,所以所有檔案的處理必須通過唯一的例項來進行。
2、一個系統中可以存在多個列印任務,但是隻能有一個正在工作的任務;一個系統只能有一個視窗管理器或檔案系統;一個系統只能有一個計時工具或ID(序號)生成器。如在Windows中就只能開啟一個工作管理員。
一些裝置管理器常常設計為單例模式。
三、要點
單例模式要點有如下三點:
- 單例類只能有一個例項。
- 單例類必須自己建立自己的唯一例項。
- 單例類必須給所有其他物件提供這一例項。
從實現角度即以下三點:
- 單例模式的類只提供私有的建構函式。
- 類定義中含有一個該類的靜態私有物件。
- 該類提供了一個靜態的公有的函式用於建立或獲取它本身的靜態私有物件。
同時還要注意:1、執行緒安全(雙檢鎖DCL:double-checked-lock)。2、資源釋放。
四、樣例Demo
4.1、區域性靜態變數
這種方式很常見,實現簡單,且無需擔心單例的銷燬問題。
// singleton.h #ifndef SINGLETON_H #define SINGLETON_H // 非真正意義上的單例 class Singleton { public: static Singleton& GetInstance() { static Singleton instance; return instance; } private: Singleton() {} };
但這並非真正意義上的單例,當使用如下方式訪問單例時:
Singleton single = Singleton::GetInstance();
因為編譯器會生成一個預設的拷貝建構函式,這時會出現一個類拷貝問題,違背了單例特性。要避免該問題,可以有如下兩種方法:
1、將GetInstance函式的返回型別修改為指標,而非引用。
// singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H
// 單例
class Singleton
{
public:
// 修改返回型別為指標型別
static Singleton* GetInstance()
{
static Singleton instance;
return &instance;
}
private:
Singleton() {}
};
#endif // SINGLETON_H
2、顯示的宣告類的拷貝建構函式並重載賦值運算子。
// singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H
#include <iostream>
using namespace std;
// 單例
class Singleton
{
public:
static Singleton& GetInstance()
{
static Singleton instance;
return instance;
}
void doSomething() {
cout << "Do something" << endl;
}
private:
Singleton() {} // 建構函式(被保護)
Singleton(Singleton const &); // 無需實現
Singleton& operator = (const Singleton &); // 無需實現
};
#endif // SINGLETON_H
這樣就可以保證只有一個例項,又不用擔心記憶體回收問題。
Singleton::GetInstance().doSomething(); // OK
Singleton single = Singleton::GetInstance(); // Error 不能編譯通過
4.2、懶漢式與餓漢式
先看Singleton的標頭檔案,懶漢與餓漢式公用部分:
// singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H
// 單例 - 懶漢式/餓漢式公用
class Singleton
{
public:
static Singleton* GetInstance();
private:
Singleton() {} // 建構函式(被保護)
private:
static Singleton *m_pSingleton; // 指向單例物件的指標
};
#endif // SINGLETON_H
懶漢式
特點:Lazy初始化;非多執行緒安全。
優點:第一次呼叫才初始化,避免記憶體浪費。
缺點:必須加鎖才能保證單例,但加鎖會影響效率。
// singleton.cpp
#include "singleton.h"
// 單例 - 懶漢式
Singleton *Singleton::m_pSingleton = NULL;
Singleton *Singleton::GetInstance()
{
if (m_pSingleton == NULL)
m_pSingleton = new Singleton();
return m_pSingleton;
}
餓漢式
特點:非Lazy初始化;多執行緒安全。
優點:沒有加鎖,執行效率提高。
缺點:類載入時就初始化,浪費記憶體。
// singleton.cpp
#include "singleton.h"
// 單例 - 餓漢式
Singleton *Singleton::m_pSingleton = new Singleton();
Singleton *Singleton::GetInstance()
{
return m_pSingleton;
}
4.3、執行緒安全
在懶漢式下有非多執行緒安全問題,引入DCL機制可以解決。
singleton.h內容:
// singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H
#include <iostream>
#include <mutex>
using namespace std;
// 單例 - 懶漢式/餓漢式公用
class Singleton
{
public:
static Singleton* GetInstance();
private:
Singleton() {} // 建構函式(被保護)
private:
static Singleton *m_pSingleton; // 指向單例物件的指標
static mutex m_mutex; // 鎖
};
#endif // SINGLETON_H
singleton.cpp內容:
// singleton.cpp
#include "singleton.h"
// 單例 - 懶漢式(雙檢鎖 DCL 機制)
Singleton *Singleton::m_pSingleton = NULL;
mutex Singleton::m_mutex;
Singleton *Singleton::GetInstance()
{
if (m_pSingleton == NULL) {
std::lock_guard<std::mutex> lock(m_mutex); // 自解鎖
if (m_pSingleton == NULL) {
m_pSingleton = new Singleton();
}
}
return m_pSingleton;
}
這樣可以保證執行緒安全,只是會帶來一點效能影響。
4.4、資源釋放
有記憶體申請,就有記憶體釋放,可以採用如下方式:
1、主動釋放(手動呼叫介面釋放記憶體)。
2、自動釋放(程式自己釋放)。
要手動釋放資源,新增一個static介面,編寫需要釋放資源的程式碼。
// 單例 - 主動釋放
static void DestoryInstance()
{
if (m_pSingleton != NULL) {
delete m_pSingleton;
m_pSingleton = NULL;
}
}
// 需要釋放資源的時候再手動呼叫
Singleton::GetInstance()->DestoryInstance();
方式雖然簡單,但很多時候,容易忘記呼叫DestoryInstance,這時可以採用更簡便方法。
// singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H
#include <iostream>
using namespace std;
// 單例 - 自動釋放
class Singleton
{
public:
static Singleton* GetInstance();
private:
Singleton() {} // 建構函式(被保護)
private:
static Singleton *m_pSingleton; // 指向單例物件的指標
// GC 機制
class GC
{
public:
~GC()
{
// 可以在這裡銷燬所有的資源,例如:db 連線、檔案控制代碼等
if (m_pSingleton != NULL) {
cout << "Here destroy the m_pSingleton..." << endl;
delete m_pSingleton;
m_pSingleton = NULL;
}
}
static GC gc; // 用於釋放單例
};
};
#endif // SINGLETON_H
只需要宣告Sinleton::GC即可:
// main.cpp
#include "singleton.h"
Singleton::GC Singleton::GC::gc; // 重要
int main()
{
Singleton *pSingleton1 = Singleton::GetInstance();
Singleton *pSingleton2 = Singleton::GetInstance();
cout << (pSingleton1 == pSingleton2) << endl;
return 0;
}
程式執行結束時,系統會呼叫Singleton的靜態成員GC的解構函式,該解構函式會進行資源的釋放。這種方式最大優點就是在“不知不覺”中進行,所以對開發者來說尤為省心。
五、優缺點
優點
1、例項控制:單例模式會阻止其他物件例項化自己的單例物件的副本,從而確保所有物件都訪問唯一例項,避免對資源的多重利用。
2、靈活性:因為類控制了例項化過程,所以類可以靈活更改例項化過程。
缺點:
1、開銷:雖然數量少,但如果每次物件請求都要檢查是否存在類例項,將仍然需要一些開銷。可以通過使用靜態初始化解決(有的也將開銷少劃為優點,應該是當正確使用時為優點。)。
2、可能的開發混淆:使用單例物件時,開發人員必須記住不能使用new關鍵字例項化此物件。因為可能無法訪問庫原始碼,開發人員可能會意外發現無法直接例項化此類。
3、物件生存期:不能解決刪除單個物件的問題。在提供記憶體管理的語言中(例如基於.NET Framework的語言),只有單例類能夠導致例項被取消分配,因為它包含對該例項的私有引用。在某些語言中(如 C++),其他類可以刪除物件例項,但這樣會導致單例類中出現懸浮引用。