C++面試 設計模式之單例模式(C++11)
單例模式
確保一個類只有一個例項,並提供了一個全域性訪問點。
單例模式,可以說設計模式中最常應用的一種模式了,據說也是面試官最喜歡的題目。但是如果沒有學過設計模式的人,可能不會想到要去應用單例模式,面對單例模式適用的情況,可能會優先考慮使用全域性或者靜態變數的方式,這樣比較簡單,也是沒學過設計模式的人所能想到的最簡單的方式了。
一般情況下,我們建立的一些類是屬於工具性質的,基本不用儲存太多的跟自身有關的資料,在這種情況下,每次都去new一個物件,即增加了開銷,也使得程式碼更加臃腫。其實,我們只需要一個例項物件就可以。如果採用全域性或者靜態變數的方式,會影響封裝性,難以保證別的程式碼不會對全域性變數造成影響。
考慮到這些需要,我們將預設的建構函式宣告為私有的,這樣就不會被外部所new了,甚至可以將解構函式也宣告為私有的,這樣就只有自己能夠刪除自己了。在Java和C#這樣純的面向物件的語言中,單例模式非常好實現,直接就可以在靜態區初始化instance,然後通過getInstance返回,這種就被稱為餓漢式單例類。也有些寫法是在getInstance中new instance然後返回,這種就被稱為懶漢式單例類,但這涉及到第一次getInstance的一個判斷問題。
建構函式宣告為private或protect防止被外部函式例項化,內部儲存一個private static的類指標儲存唯一的例項,例項的動作由一個public的類方法代勞,該方法也返回單例類唯一的例項。
//單執行緒
//這是一個很棒的實現,簡單易懂。但這是一個完美的實現嗎?
//不!該方法是執行緒不安全的,考慮兩個執行緒同時首次呼叫instance方法且同時檢測到p是NULL值,
//則兩個執行緒會同時構造一個例項給p,這是嚴重的錯誤!同時,這也不是單例的唯一實現!
class singleton
{
protected:
singleton(){}
private:
static singleton* p;
public:
static singleton* instance();
};
singleton* singleton::p = NULL;
singleton* singleton::instance()
{
if (p == NULL)
p = new singleton();
return p;
}
懶漢與餓漢
單例大約有兩種實現方法:懶漢與餓漢。
懶漢:故名思義,不到萬不得已就不會去例項化類,也就是說在第一次用到類例項的時候才會去例項化,所以上邊的經典方法被歸為懶漢實現;
餓漢:餓了肯定要飢不擇食。所以在單例類定義的時候就進行例項化。
特點與選擇:
由於要進行執行緒同步,所以在訪問量比較大,或者可能訪問的執行緒比較多時,採用餓漢實現,可以實現更好的效能。這是以空間換時間。
在訪問量較小時,採用懶漢實現。這是以時間換空間。
執行緒安全的懶漢實現
執行緒不安全,怎麼辦呢?最直觀的方法:加鎖。
加鎖的經典懶漢實現:
class singleton
{
protected:
singleton()
{
pthread_mutex_init(&mutex);
}
private:
static singleton* p;
public:
static pthread_mutex_t mutex;
static singleton* initance();
};
pthread_mutex_t singleton::mutex;
singleton* singleton::p = NULL;
singleton* singleton::initance()
{
//因為每次判斷是否為空都需要被鎖定,如果有很多執行緒的話,就會造成大量執行緒的阻塞。
//於是出現了雙重鎖定。
if (p == NULL)
{
pthread_mutex_lock(&mutex);
if (p == NULL)
p = new singleton();
pthread_mutex_unlock(&mutex);
}
return p;
}
內部靜態變數的懶漢實現
此方法也很容易實現,在instance函式裡定義一個靜態的例項,也可以保證擁有唯一例項,在返回時只需要返回其指標就可以了。推薦這種實現方法,真得非常簡單。
class singleton
{
protected:
singleton()
{
pthread_mutex_init(&mutex);
}
public:
static pthread_mutex_t mutex;
static singleton* initance();
int a;
};
pthread_mutex_t singleton::mutex;
singleton* singleton::initance()
{
pthread_mutex_lock(&mutex);
static singleton obj;
pthread_mutex_unlock(&mutex);
return &obj;
}
餓漢實現
因為餓漢實現本來就是執行緒安全的,不用加鎖。
//假如有一個全域性物件A 建構函式裡引用上文中餓漢形式的指標,
//若在A建構函式構造之前以上單例並未構造出來,那就會有問題。
class singleton
{
protected:
singleton()
{}
private:
static singleton* p;
public:
static singleton* initance();
};
singleton* singleton::p = new singleton;
singleton* singleton::initance()
{
return p;
}
常用的場景
單例模式常常與工廠模式結合使用,因為工廠只需要建立產品例項就可以了,在多執行緒的環境下也不會造成任何的衝突,因此只需要一個工廠例項就可以了。
優點
1.減少了時間和空間的開銷(new例項的開銷)。
2.提高了封裝性,使得外部不易改動例項。
#ifndef _SINGLETON_H_
#define _SINGLETON_H_
class Singleton{
public:
static Singleton* getInstance();
private:
Singleton();
//把複製建構函式和=操作符也設為私有,防止被複制
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;
}
以上使用的方式存在問題:只能例項化沒有引數的型別,其它帶引數的型別就不行了。
c++11 為我們提供瞭解決方案:可變模板引數
template <typename T>
class Singleton
{
public:
template<typename... Args>
static T* Instance(Args&&... args)
{
if(m_pInstance==nullptr)
m_pInstance = new T(std::forward<Args>(args)...);
return m_pInstance;
}
static T* GetInstance()
{
if (m_pInstance == nullptr)
throw std::logic_error("the instance is not init,
please initialize the instance first");
return m_pInstance;
}
static void DestroyInstance()
{
delete m_pInstance;
m_pInstance = nullptr;
}
private:
Singleton(void);
virtual ~Singleton(void);
Singleton(const Singleton&);
Singleton& operator = (const Singleton&);
private:
static T* m_pInstance;
};
template <class T> T* Singleton<T>::m_pInstance = nullptr;
由於原來的介面中,單例物件的初始化和取值都是一個介面,可能會遭到誤用,更新之後,講初始化和取值分為兩個介面,單例的用法為:先初始化,後面取值,如果中途銷燬單例的話,需要重新取值。如果沒有初始化就取值則會丟擲一個異常。
Multiton
#include <map>
#include <string>
#include <memory>
using namespace std;
template < typename T, typename K = string>
class Multiton
{
public:
template<typename... Args>
static std::shared_ptr<T> Instance(const K& key, Args&&... args)
{
return GetInstance(key, std::forward<Args>(args)...);
}
template<typename... Args>
static std::shared_ptr<T> Instance(K&& key, Args&&... args)
{
return GetInstance(key, std::forward<Args>(args)...);
}
private:
template<typename Key, typename... Args>
static std::shared_ptr<T> GetInstance(Key&& key, Args&&...args)
{
std::shared_ptr<T> instance = nullptr;
auto it = m_map.find(key);
if (it == m_map.end())
{
instance = std::make_shared<T>(std::forward<Args>(args)...);
m_map.emplace(key, instance);
}
else
{
instance = it->second;
}
return instance;
}
private:
Multiton(void);
virtual ~Multiton(void);
Multiton(const Multiton&);
Multiton& operator = (const Multiton&);
private:
static map<K, std::shared_ptr<T>> m_map;
};
template <typename T, typename K>
map<K, std::shared_ptr<T>> Multiton<T, K>::m_map;