1. 程式人生 > >劍指offer 面試題2 Singleton模式 C++實現

劍指offer 面試題2 Singleton模式 C++實現

題目:實現Singleton模式

​ 以下內容是我在看《劍指offer》的面試題2時,遇到的問題,因為書中使用C#實現,所以想用C++重新實現一下,Test方法不夠全,後續還要完善。C++實現過程主要參考:C++設計模式——單例模式

​ 程式碼中的註釋一般是我的筆記,或一些發現。

​ PS: 感謝勤勞的慵懶君~~ @亦餘心之所向兮

1 解法一:單執行緒解法

缺點:多執行緒情況下,每個執行緒可能創建出不同的Singleton例項

// 劍指offer 面試題2 實現Singleton模式
#include <iostream>
using namespace std; class Singleton { public: static Singleton* getInstance() { // 在後面的Singleton例項初始化時,若後面是new Singleton(),則此處不必new;(廢話) // 若後面是賦值成NULL,則此處需要判斷,需要時new // 注意!然而這兩種方式並不等價!後面的Singleton例項初始化時,new Singleton(),其實是執行緒安全的,因為static初始化是在主函式main()之前,那麼後面的方法豈不是很麻煩。。。。這也是我測試的時候想到的
/* if(m_pInstance == NULL) { m_pInstance = new Singleton(); } */ return m_pInstance; } static void destroyInstance() { if(m_pInstance != NULL) { delete m_pInstance; m_pInstance = NULL; } } private
: Singleton(){} static Singleton* m_pInstance; }; // Singleton例項初始化 Singleton* Singleton::m_pInstance = new Singleton(); // 前面不能加static,會和類外全域性static混淆 // 單執行緒獲取多次例項 void Test1(){ // 預期結果:兩個例項指標指向的地址相同 Singleton* singletonObj = Singleton::getInstance(); cout << singletonObj << endl; Singleton* singletonObj2 = Singleton::getInstance(); cout << singletonObj2 << endl; Singleton::destroyInstance(); } int main(){ Test1(); return 0; }

2 解法二:多執行緒+加鎖

​ 解法1是最簡單,也是最普遍的實現方式,也是現在網上各個部落格中記述的實現方式,但是,這種實現方式,有很多問題,比如:沒有考慮到多執行緒的問題,在多執行緒的情況下,就可能建立多個Singleton例項,以下版本是改善的版本。
​ 注意:下面的程式碼涉及互斥鎖以及多執行緒測試,使用了C++11的多執行緒庫,std::thread,,std::mutex,請使用支援C++11多執行緒的編譯器,並確認開啟了C++11的編譯選項,具體方法見:http://blog.csdn.net/huhaijing/article/details/51753085

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
using namespace std;

class Singleton
{
private:
    static mutex m_mutex; // 互斥量

    Singleton(){}
    static Singleton* m_pInstance;

public:
    static Singleton* getInstance(){
        if(m_pInstance == NULL){
            m_mutex.lock(); // 使用C++11中的多執行緒庫
            if(m_pInstance == NULL){ // 兩次判斷是否為NULL的雙重檢查
                m_pInstance = new Singleton();
            }
            m_mutex.unlock();
        }
        return m_pInstance;
    }

    static void destroyInstance(){
        if(m_pInstance != NULL){
            delete m_pInstance;
            m_pInstance = NULL;
        }
    }
};

Singleton* Singleton::m_pInstance = NULL; // 所以說直接new 多好啊,可以省去Lock/Unlock的時間
mutex Singleton::m_mutex;


void print_singleton_instance(){
    Singleton *singletonObj = Singleton::getInstance();
    cout << singletonObj << endl;
}

// 多個程序獲得單例
void Test1(){
    // 預期結果,打印出相同的地址,之間可能缺失換行符,也屬正常現象
    vector<thread> threads;
    for(int i = 0; i < 10; ++i){
        threads.push_back(thread(print_singleton_instance));
    }

    for(auto& thr : threads){
        thr.join();
    }
}

int main(){
    Test1();
    Singleton::destroyInstance();
    return 0;
}

​ 此處進行了兩次m_pInstance == NULL的判斷,是借鑑了Java的單例模式實現時,使用的所謂的“雙檢鎖”機制。因為進行一次加鎖和解鎖是需要付出對應的代價的,而進行兩次判斷,就可以避免多次加鎖與解鎖操作,同時也保證了執行緒安全。但是,如果進行大資料的操作,加鎖操作將成為一個性能的瓶頸;為此,一種新的單例模式的實現也就出現了。

3 解法三:const static 型例項

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

class Singleton
{
private:
    Singleton(){}
    static const Singleton* m_pInstance;
public:
    static Singleton* getInstance(){

        return const_cast<Singleton *>(m_pInstance); // 去掉“const”特性
        // 注意!若該函式的返回值改為const static型,則此處不必進行const_cast靜態轉換
        // 所以該函式可以改為:
        /*
        const static Singleton* getInstance(){
            return m_pInstance;
        }
        */
    }

    static void destroyInstance(){
        if(m_pInstance != NULL){
            delete m_pInstance;
            m_pInstance = NULL;
        }
    }
};
const Singleton* Singleton::m_pInstance = new Singleton(); // 利用const只能定義一次,不能再次修改的特性,static繼續保持類內只有一個例項

void print_singleton_instance(){
    Singleton *singletonObj = Singleton::getInstance();
    cout << singletonObj << endl;
}

// 多個程序獲得單例
void Test1(){
    // 預期結果,打印出相同的地址,之間可能缺失換行符,也屬正常現象
    vector<thread> threads;
    for(int i = 0; i < 10; ++i){
        threads.push_back(thread(print_singleton_instance));
    }

    for(auto& thr : threads){
        thr.join();
    }
}

int main(){
    Test1();
    Singleton::destroyInstance();
    return 0;
}

​ 因為靜態初始化在程式開始時,也就是進入主函式之前,由主執行緒以單執行緒方式完成了初始化,所以靜態初始化例項保證了執行緒安全性。在效能要求比較高時,就可以使用這種方式,從而避免頻繁的加鎖和解鎖造成的資源浪費。由於上述三種實現,都要考慮到例項的銷燬,關於例項的銷燬,待會在分析。

4 解法四:在get函式中建立並返回static臨時例項的引用

PS:該方法不能人為控制單例例項的銷燬

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

class Singleton
{
private:
    Singleton(){}

public:
    static Singleton* getInstance(){
        static Singleton m_pInstance; // 注意,宣告在該函式內
        return &m_pInstance;
    }
};

void print_singleton_instance(){
    Singleton *singletonObj = Singleton::getInstance();
    cout << singletonObj << endl;
}

// 多個程序獲得單例
void Test1(){
    // 預期結果,打印出相同的地址,之間可能缺失換行符,也屬正常現象
    vector<thread> threads;
    for(int i = 0; i < 10; ++i){
        threads.push_back(thread(print_singleton_instance));
    }

    for(auto& thr : threads){
        thr.join();
    }
}

// 單個程序獲得多次例項
void Test2(){
    // 預期結果,打印出相同的地址,之間換行符分隔
    print_singleton_instance();
    print_singleton_instance();
}

int main(){
    cout << "Test1 begins: " << endl;
    Test1();
    cout << "Test2 begins: " << endl;
    Test2();
    return 0;
}

以上就是四種主流的單例模式的實現方式。

5 解法五:最終方案,最簡&顯式控制例項銷燬

​ 在上述的四種方法中,除了第四種沒有使用new操作符例項化物件以外,其餘三種都使用了;

​ 我們一般的程式設計觀念是,new操作是需要和delete操作進行匹配的;是的,這種觀念是正確的。在上述的實現中,是添加了一個destoryInstance的static函式,這也是最簡單,最普通的處理方法了;但是,很多時候,我們是很容易忘記呼叫destoryInstance函式,就像你忘記了呼叫delete操作一樣。由於怕忘記delete操作,所以就有了智慧指標;那麼,在單例模型中,沒有“智慧單例”,該怎麼辦?怎麼辦?

​ 在實際專案中,特別是客戶端開發,其實是不在乎這個例項的銷燬的。因為,全域性就這麼一個變數,全域性都要用,它的生命週期伴隨著軟體的生命週期,軟體結束了,它也就自然而然的結束了,因為一個程式關閉之後,它會釋放它佔用的記憶體資源的,所以,也就沒有所謂的記憶體洩漏了。

​ 但是,有以下情況,是必須需要進行例項銷燬的:

  1. 在類中,有一些檔案鎖了,檔案控制代碼,資料庫連線等等,這些隨著程式的關閉而不會立即關閉的資源,必須要在程式關閉前,進行手動釋放;
  2. 具有強迫症的程式設計師。

​ 在程式碼實現部分的第四種方法能滿足第二個條件,但是無法滿足第一個條件。好了,接下來,就介紹一種方法,這種方法也是我從網上學習而來的,程式碼實現如下:

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

class Singleton
{
private:
    Singleton(){}
    static Singleton* m_pInstance;

    // **重點在這**
    class GC // 類似Java的垃圾回收器
    {
    public:
        ~GC(){
            // 可以在這裡釋放所有想要釋放的資源,比如資料庫連線,檔案控制代碼……等等。
            if(m_pInstance != NULL){
                cout << "GC: will delete resource !" << endl;
                delete m_pInstance;
                m_pInstance = NULL;
            }
        };
    };

    // 內部類的例項
    static GC gc;

public:
    static Singleton* getInstance(){
        return m_pInstance;
    }
};


Singleton* Singleton::m_pInstance = new Singleton();
Singleton::GC Singleton::gc;

void print_instance(){
    Singleton* obj1 = Singleton::getInstance();
    cout << obj1 << endl;
}

// 多執行緒獲取單例
void Test1(){
    // 預期輸出:相同的地址,中間可能缺失換行符,屬於正常現象
    vector<thread> threads;
    for(int i = 0; i < 10; ++i){
        threads.push_back(thread(print_instance));
    }

    for(auto& thr : threads){
        thr.join();
    }
}

// 單執行緒獲取單例
void Test2(){
    // 預期輸出:相同的地址,換行符分隔
    print_instance();
    print_instance();
    print_instance();
    print_instance();
    print_instance();
}

int main()
{
    cout << "Test1 begins: " << endl;
    cout << "預期輸出:相同的地址,中間可以缺失換行(每次執行結果的排列格式通常不一樣)。" << endl;
    Test1();
    cout << "Test2 begins: " << endl;
    cout << "預期輸出:相同的地址,每行一個。" << endl;
    Test2();
    return 0;
}

​ 在程式執行結束時,系統會呼叫Singleton的靜態成員GC的解構函式,該解構函式會進行資源的釋放,而這種資源的釋放方式是在程式設計師“不知道”的情況下進行的,而程式設計師不用特別的去關心,使用單例模式的程式碼時,不必關心資源的釋放。

​ 那麼這種實現方式的原理是什麼呢?由於程式在結束的時候,系統會自動析構所有的全域性變數,系統也會析構所有類的靜態成員變數,因為靜態變數和全域性變數在記憶體中,都是儲存在靜態儲存區的,所有靜態儲存區的變數都會被釋放。

​ 由於此處使用了一個內部GC類,而該類的作用就是用來釋放資源,而這種使用技巧在C++中是廣泛存在的,參見《C++中的RAII機制》

執行結果:
這裡寫圖片描述