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

C++ 實現單例模式

單例模式是任何面嚮物件語言繞不過的,單例模式是很有必要的,接下來我用最樸素的語言來解釋和記錄單例模式的學習。

  • 什麼是單例模式?

單例模式就是一個類只能被例項化一次 ,更準確的說是只能有一個例項化的物件的類。

  • 建立一個單例模式的類(初想)

一個類只能有一個例項化的物件,那麼這個類就要禁止別人new出來,或者通過直接定義一個物件出來

複製程式碼
class CAR
{
public:
    CAR(){}
    ~CAR(){}
};
CAR a;
CAR *b = new CAR;
複製程式碼

很明顯這樣的類可以被程式設計師用上面這兩種方式例項化。那麼考慮,如何禁止用上面的這兩種方式例項化一個類呢?

如果把建構函式私有化,很明顯上面這兩種方法都會預設的去呼叫建構函式,當建構函式是private或者protected時,建構函式將無法從外部呼叫。

複製程式碼
class CSingleton
{
private:
    CSingleton()
    {
    }
};
int main()
{
    CSingleton t;
    CSingleton *tt = new CSingleton;
}
複製程式碼

上面的程式碼選擇了這樣例項化類,很明顯編譯器會報錯,因為私有化的建構函式無法被外部呼叫

error: ‘CSingleton::CSingleton()’ is private

既然建構函式是私有了,那麼他就只能被類內部的成員函式呼叫,所以我們可以搞一個共有函式去供外部呼叫,然後這個函式返回一個物件,為了保證多次呼叫這個函式返回的是一個物件,我們可以把類內部要返回的物件設定為靜態的,就有了下面的程式碼:

複製程式碼
class CSingleton
{
private:
    CSingleton()
    {
    }
    static CSingleton *p;
public:
    static CSingleton* getInstance()
    {
        if(p == NULL)
            p = new CSingleton();
        return p;
    }
};
CSingleton* CSingleton::p = NULL;
複製程式碼

我們在主函式呼叫來測試一下

複製程式碼
int main()
{
    CSingleton *t = CSingleton::getInstance();
    CSingleton 
*tt = CSingleton::getInstance(); cout << t << endl << tt << endl; }
複製程式碼

結果是

0x1c59c0
0x1c59c0

兩個地址一樣,證明我們的單例類的正確的,原理其實很簡單,第一次呼叫獲取例項的函式時,靜態類的變數指標空,所以會建立一個物件出來,第二次呼叫就不是空了,直接返回第一次的物件指標(地址)。

同時思考另一個問題,如果兩個執行緒同時獲取例項化物件呢?顯然是不行的,會出現兩個執行緒同時要物件的時候指標還都是空的情況就完了,想到這種情況你肯定會毫不猶豫的去加個鎖。(進一步思考)

複製程式碼
class CSingleton
{
private:
    CSingleton()
    {
        pthread_mutex_init(&mtx,0);
    }
    static CSingleton *p;
public:
    static pthread_mutex_t mtx;
    static CSingleton* getInstance()
    {
        if(p == NULL)
        {
            pthread_mutex_lock(&mtx);
            p = new CSingleton();
            pthread_mutex_unlock(&mtx);
        }
        return p;
    }
};
pthread_mutex_t CSingleton::mtx;
CSingleton* CSingleton::p = NULL;
複製程式碼

上面的程式碼就是加鎖之後的了,你可以用下面的方法呼叫

複製程式碼
void* fun1(void *)
{
    while(1)
    {
        CSingleton *pt = CSingleton::getInstance();
        cout << "fun1: pt_addr = " << pt << endl;
        Sleep(1000);
    }
}
void* fun2(void *)
{
    while(1)
    {
        CSingleton *pt = CSingleton::getInstance();
        cout << "fun2: pt_addr = " << pt << endl;
        Sleep(1000);
    }
}
void callSingleton()
{
    pthread_mutex_init(&CSingleton::mtx,0);
    pthread_t pt_1 = 0;
    pthread_t pt_2 = 0;
    int ret = pthread_create(&pt_1,0,&fun1,0);
    if(ret != 0)
    {
        printf("error\n");
    }
    ret = pthread_create(&pt_2,0,&fun2,0);
    if(ret != 0)
    {
        printf("error\n");
    }
    pthread_join(pt_1,0);
    pthread_join(pt_2,0);
}
複製程式碼

你可以這樣在fun1,fun2中隨意的去例項化這個類了,執行結果如下

fun1: pt_addr = 0xb85a38
fun2: pt_addr = 0xb85a38
fun1: pt_addr = 0xb85a38
fun2: pt_addr = 0xb85a38
fun1: pt_addr = 0xb85a38
fun2: pt_addr = 0xb85a38

總結一下我們的這種實現單例模式的方式:類中宣告一個靜態的本類指標,再寫一個public的函式來讓這個指標指向我們新建立的例項,返回這個指標(這個例項的地址),並進行加鎖,這個物件就永遠只有一份,然後單例模式就實現了。

複製程式碼
class CSingleton
{
private:
    CSingleton()
    {
        pthread_mutex_init(&mtx,0);
    }
public:
    static pthread_mutex_t mtx;
    static CSingleton* getInstance()
    {
        pthread_mutex_lock(&mtx);
        static CSingleton obj;
        pthread_mutex_unlock(&mtx);
        return &obj;
    }
};
pthread_mutex_t CSingleton::mtx;
複製程式碼

也可以像上面的程式碼一樣把靜態物件的放到函式裡面,這樣就省的在去外部宣告一下了,只要返回一個靜態類的地址,就算這個函式執行完也不會被銷燬,它被儲存在靜態區和全域性變數差不多。

再次總結:只要返回一個本類物件的地址就好了,這個地址要是靜態的。別忘記加鎖。

而且上面這種方式也被人們成為懶漢模式,為什麼叫懶漢?因為這樣的方式只有在我呼叫 CSingleton::getInstance(); 的時候才會返回一個例項化的物件,懶死了,我不要你你就不給我,是不是?

下面這種方式就和上面的不同,人家還沒要,我就忍不住先給人家準備好了,如飢似渴,所以也叫餓漢模式。

我們注意到上面第一種方式,類中的靜態變數要先被外部宣告,否則編譯器不會為它分配空間,像這樣 CSingleton* CSingleton::p = NULL; 其實我們可以在這一步就new一個物件出來,因為p是CSingleton的成員,它是可以呼叫建構函式的哦,於是我們改成這樣就是餓漢模式了

複製程式碼
class CSingleton
{
private:
    CSingleton()
    {
    }
    static CSingleton *p;
public:
    static CSingleton* getInstance()
    {
        return p;
    }
};
CSingleton* CSingleton::p = new CSingleton();
複製程式碼

我們這樣鎖也不用加了,因為我們呼叫 CSingleton::getInstance() 之前這個類就已經被例項化了,我們呼叫這個函式的目地只是為了得到這個物件的地址。餓漢模式就實現了

總結:利用靜態變數和私有化建構函式的特性來實現單例模式。搞一個靜態的自身類指標,然後把建構函式私有化,這樣new的時候就只能讓本類中的成員呼叫,然後不擇手段在類內部new出這個物件,並提供一種方法供外部得到這個物件的地址。