1. 程式人生 > >C++面試 設計模式之單例模式(C++11)

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;