1. 程式人生 > >C++實現懶漢式單例模式

C++實現懶漢式單例模式

單例模式無論在生活中還是在工程中都有很廣泛的應用,在C++專案中,很多時候我們只希望整個工程中某個類僅有一個實體物件,設計這種類的時候就需要使用單例模式來設計。下面是實現的一個懶漢式單例模式的程式碼:

#include <iostream>
#include <cstdio>

using std::cout;
using std::endl;

class Singleton {
    private :
        static Singleton* p_obj;
        Singleton(void){

        }
    public :
        static
Singleton* GetInstance(void){ if(p_obj == NULL){ p_obj = new Singleton(); cout << "New Singleton();" << endl; return p_obj; } } ~Singleton(void){ if(p_obj!=NULL){ delete p_obj; p_obj = NULL; cout
<< "~Singleton" << endl; } } }; Singleton* Singleton::p_obj = NULL; void run(void){ Singleton* s1 = Singleton::GetInstance(); Singleton* s2 = Singleton::GetInstance(); cout << "ADDR s1:" << s1 << endl; cout << "ADDR s2:" << s2 << endl; } int
main(){ run(); cout << "Press enter to continue..." << endl; getchar(); return 0; }

在設計類的時候,聲明瞭一個static Singleton型別的指標在類的私有成員區,需要注意的是,這個指標的記憶體單元並不會在物件中開闢,需要程式設計師自己在全域性定義一個Singleton指標,如果不是單例模式的類,這種static型別的成員是每一個物件共享的,即所有的物件都可以訪問在全域性資料區的這一段static記憶體。

在類私有成員區還定義了建構函式,顯然地,這樣設計時不想讓程式設計師可以直接建立Singleton物件,而是需要建立Singleton指標使用GetInstance方法去建立一個物件。這裡需要我們知道C++編譯器的一個特性:當程式設計師在設計類的時候,沒有顯示地宣告建構函式,編譯器會預設提供一個空的建構函式。這種懶漢式單例模式的實現,就是利用了C++的這一特性,將建構函式宣告在類私有成員區,如果程式設計師在外部建立Singleton物件,編譯器將報錯:Singleton成員函式時私有的,不可訪問。

如果想要呼叫Singleton類中的GetInstance函式建立物件,首先會判斷一下p_obj指標是不是為空,即是不是還沒有開闢過這個物件,如果確實目前還沒有開闢過這個物件,那麼使用new關鍵字在堆空間中開闢一段Sngleton大小的記憶體給申請者,而如果程式中早已存在Singleotn物件,則把已經存在的Singleton物件在堆空間中的地址返回給申請者。

其實這段程式是有漏洞的,在類中實現的解構函式並不會釋放堆記憶體,因為它根本就沒有被呼叫。這就造成了堆記憶體洩露的災難。不能夠在析構中釋放記憶體,而是需要呼叫者手動地釋放堆記憶體。改進這個漏洞後的程式碼如下:

#include <iostream>
#include <cstdio>

using std::cout;
using std::endl;

class Singleton {
    private :
        static Singleton* p_obj;
        Singleton(){

        }
    public :
        static Singleton* GetInstance(){
            if(p_obj == NULL){
                p_obj = new Singleton();
                cout << "New Singleton();" << endl;
                return p_obj;
            }
        }

};

Singleton* Singleton::p_obj = NULL;

void run(){
    Singleton* s1 = Singleton::GetInstance();
    Singleton* s2 = Singleton::GetInstance();

    cout << "ADDR s1:" << s1 << endl;
    cout << "ADDR s2:" << s2 << endl;    

    delete s1;
}

int main(){

    run();    

    cout << "Press enter to continue..." << endl;
    getchar();

    return 0;
}

顯然,改進了的程式碼在單執行緒系統中不會再造成堆記憶體洩露。為什麼要強調一下是單執行緒的系統中呢?因為它在多執行緒的系統中還是可能出問題,想象一下,同時兩個執行緒執行到了if(p_obj == NULL),這時候會怎樣?顯然,會在堆記憶體中申請兩個Singleton大小的記憶體段。然而我們只釋放了一個,想象中他們是同一段,這就造成了洩露了一段Singleton大小的記憶體。改進這個漏洞的方法是在if(NULL != p_obj)前後加鎖,改進如下:

#include <iostream>
#include <cstdio>

using std::cout;
using std::endl;

class Singleton {
    private :
        static Singleton* p_obj;
        Singleton(){            
        }
    public :
        static Singleton* GetInstance(){
            lock();
            if(p_obj == NULL){
                p_obj = new Singleton();
                cout << "New Singleton();" << endl;
                return p_obj;
            }
            unlock();
        }
};

Singleton* Singleton::p_obj = NULL;

void run(){
    Singleton* s1 = Singleton::GetInstance();
    Singleton* s2 = Singleton::GetInstance();

    cout << "ADDR P1:" << s1 << endl;
    cout << "ADDR P2:" << s2 << endl;    

    delete s1;
}

int main(){

    run();    

    cout << "Press enter to continue..." << endl;
    getchar();

    return 0;
}

然而這樣做的線上程很多的時候會影響程式碼的效率,試想當多個執行緒都想要申請這麼這段記憶體的時候,都被排程器阻塞,堆在一起等待,這是一件多可怕的事情。為了提高程式碼的執行效率,可以使用雙重判斷加鎖,改進如下:

static Singleton* GetInstance(){
    if(p_obj == NULL){
        lock();
        if(p_obj == NULL){
            p_obj = new Singleton();
            cout << "New Singleton();" << endl;
            return p_obj;
        }
        unlock();
    }
}

多判斷一次,減少多執行緒共同排隊到這個位置的可能性。