1. 程式人生 > >C++實現單例模式(包括採用C++11中的智慧指標)

C++實現單例模式(包括採用C++11中的智慧指標)

    對於設計模式來說,以前只是看過基礎的理論,很多都沒有實現和使用過。這段時間看到了別人C++程式碼中使用了單例模式,發現了很多新的東西,特此總結記錄一下。說話比較囉嗦,希望由淺入深,幫助大家理解!

    單例模式,顧名思義,即一個類只有一個例項物件。C++一般的方法是將建構函式、拷貝建構函式以及賦值操作符函式宣告為private級別,從而阻止使用者例項化一個類。那麼,如何才能獲得該類的物件呢?這時,需要類提供一個public&static的方法,通過該方法獲得這個類唯一的一個例項化物件。這就是單例模式基本的一個思想。

    下面首先討論不考慮執行緒安全的問題(即:單執行緒環境),這樣能體現出單例模式的本質思想。常見的單例模式分為兩種:

    1、餓漢式:即類產生的時候就建立好例項物件,這是一種空間換時間的方式

    2、懶漢式:即在需要的時候,才建立物件,這是一種時間換空間的方式

首先說一下餓漢式:餓漢式的物件在類產生的時候就建立了,一直到程式結束才釋放。即物件的生存週期和程式一樣長,因此 該例項物件需要儲存在記憶體的全域性資料區,故使用static修飾。程式碼如下(注:類的定義都放在了一個頭檔案CSingleton.h中,為了節省空間,該類有些實現和定義就放在測試的主檔案中,沒有單獨建立一個CSingleton.cpp檔案):

標頭檔案:

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H

#include<iostream>
using namespace std;
class CSingleton
{
private:
	CSingleton(){ cout << "單例物件建立!" << endl; };
	CSingleton(const CSingleton &);
	CSingleton& operator=(const CSingleton &);
	~CSingleton(){ cout << "單例物件銷燬!" << endl; };

	static CSingleton myInstance; // 單例物件在這裡!
public:
	static CSingleton* getInstance()
	{		
		return &myInstance;
	}
};

#endif
原始檔:
//主檔案,用於測試用例的生成
#include<iostream>
#include"CSingleton.h"

using namespace std;
CSingleton CSingleton::myInstance;
int main()
{
	CSingleton *ct1 = CSingleton::getInstance();
	CSingleton *ct2 = CSingleton::getInstance();
	CSingleton *ct3 = CSingleton::getInstance();

	return 0;
}
對於餓漢式來說,是執行緒安全
的。執行結果如下所示:


能夠看出,類的例項只有一個,並且正常銷燬。這裡,問題來了!!!如果在類裡面的定義改成如下形式:

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H

#include<iostream>
using namespace std;
class CSingleton
{
private:
	CSingleton(){ cout << "單例物件建立!" << endl; };
	CSingleton(const CSingleton &);
	CSingleton& operator=(const CSingleton &);
	~CSingleton(){ cout << "單例物件銷燬!" << endl; };

	static CSingleton *myInstance; // 這裡改了!
public:
	static CSingleton* getInstance()
	{		
		return myInstance; // 這裡也改了!
	}
};

#endif

同樣原始檔改成如下:
//主檔案,用於測試用例的生成
#include<iostream>
#include"CSingleton.h"

using namespace std;
CSingleton * CSingleton::myInstance=new CSingleton();
int main()
{
	CSingleton *ct1 = CSingleton::getInstance();
	CSingleton *ct2 = CSingleton::getInstance();
	CSingleton *ct3 = CSingleton::getInstance();

	return 0;
}
結果如下:



咦!怎麼沒有進入解構函式?這裡就有問題了,如果單例模式的類中申請了其他資源,就無法釋放,導致記憶體洩漏!

原因:此時全域性資料區中,儲存的並不是一個例項物件,而是一個例項物件的指標,即一個地址變數而已!例項物件呢?在堆區,因為是通過new得來的!雖然這樣能夠減小全域性資料區的佔用,把例項物件這一大坨都放到堆區。可是!如何釋放資源呢?

首先能想到的第一個方法:我自己手動釋放呀!我在程式結束的時候delete不就可以了?對!這是可以的,程式如下:

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H

#include<iostream>
using namespace std;
class CSingleton
{
private:
	CSingleton(){ cout << "單例物件建立!" << endl; };
	CSingleton(const CSingleton &);
	CSingleton& operator=(const CSingleton &);
	~CSingleton(){ cout << "單例物件銷燬!" << endl; };

	static CSingleton *myInstance; 
public:
	static CSingleton* getInstance()
	{		
		return myInstance;
	}
	static void releaseInstance() // 這裡加了個方法
	{
		delete myInstance;
	}
};

#endif

原始檔:

//主檔案,用於測試用例的生成
#include<iostream>
#include"CSingleton.h"

using namespace std;
CSingleton * CSingleton::myInstance=new CSingleton();
int main()
{
	CSingleton *ct1 = CSingleton::getInstance();
	CSingleton *ct2 = CSingleton::getInstance();
	CSingleton *ct3 = CSingleton::getInstance();
        CSingleton::releaseInstance(); // 手動釋放
	return 0;
}


執行結果如下


執行結果沒問題!可是,要是我寫著寫著我忘記了,沒有顯式呼叫釋放的函式怎麼辦?如果有一個自動釋放的方法就好了!天無絕人之路,方法二如下:
// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H

#include<iostream>
using namespace std;
class CSingleton
{
private:
	CSingleton(){ cout << "單例物件建立!" << endl; };
	CSingleton(const CSingleton &);
	CSingleton& operator=(const CSingleton &);
	~CSingleton(){ cout << "單例物件銷燬!" << endl; };

	static CSingleton *myInstance; 
public:
	static CSingleton* getInstance()
	{		
		return myInstance;
	}

private:
	// 定義一個內部類
	class CGarbo{
	public:
		CGarbo(){};
		~CGarbo()
		{
			if (nullptr != myInstance)
			{
				delete myInstance;
				myInstance = nullptr;
			}
		}
	};
	// 定義一個內部類的靜態物件
	// 當該物件銷燬時,順帶就釋放myInstance指向的堆區資源
	static CGarbo m_garbo; 
};

#endif

原始檔如下:
//主檔案,用於測試用例的生成
#include<iostream>
#include"CSingleton.h"

using namespace std;
CSingleton * CSingleton::myInstance=new CSingleton();
CSingleton::CGarbo CSingleton::m_garbo; // 注意這裡!!!
int main()
{
	CSingleton *ct1 = CSingleton::getInstance();
	CSingleton *ct2 = CSingleton::getInstance();
	CSingleton *ct3 = CSingleton::getInstance();

	return 0;
}
執行結果如下:


可見能夠正常釋放堆區申請的資源了!問題解決!這裡又會想到,C++11中把boost庫中的智慧指標變成標準庫的東西。智慧指標可以在引用計數為0的時候自動釋放記憶體,方便使用者管理記憶體,為什麼不嘗試用一下智慧指標呢?現在修改程式碼如下(不可以正常執行):

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H

#include<iostream>
#include<memory>
using namespace std;
class CSingleton
{
private:
	CSingleton(){ cout << "單例物件建立!" << endl; };
	CSingleton(const CSingleton &);
	CSingleton& operator=(const CSingleton &);
	~CSingleton(){ cout << "單例物件銷燬!" << endl; };

	static shared_ptr<CSingleton> myInstance; 
public:
	static shared_ptr<CSingleton> getInstance()
	{		
		return myInstance;
	}

};

#endif
主檔案
//主檔案,用於測試用例的生成
#include<iostream>
#include<memory>
#include"CSingleton.h"

using namespace std;
shared_ptr<CSingleton> CSingleton::myInstance(new CSingleton());
int main()
{
	shared_ptr<CSingleton>  ct1 = CSingleton::getInstance();
	shared_ptr<CSingleton>  ct2 = CSingleton::getInstance();
	shared_ptr<CSingleton>  ct3 = CSingleton::getInstance();
	
	return 0;
}

結果編譯都過不了。仔細一看,原來智慧指標shared_ptr無法訪問私有化的解構函式。當shared_ptr內部的引用計數為零時,會自動呼叫所指物件的解構函式來釋放記憶體。然而,此時單例模式類的解構函式為private,故出現編譯錯誤。如何修改呢?當然,最簡單的方法是把解構函式變成public。但是如果某使用者不小心手殘,顯式呼叫了解構函式,這不就悲劇了。第二種方法,就是不通過解構函式來釋放物件的資源。怎麼辦呢?不要忘了shared_ptr在定義的時候可以指定刪除器(deleter)。可是通過測試,無法直接傳入解構函式。我現在想到的一個方法是,在單例函式內部再定義一個Destory函式,該函式也要為static的,即通過類名可直接呼叫。當然,在使用該單例類的時候,也需要使用shared_ptr獲取單一例項物件。程式碼如下:
// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H

#include<iostream>
#include<memory>
using namespace std;
class CSingleton
{
private:
	CSingleton(){ cout << "單例物件建立!" << endl; };
	CSingleton(const CSingleton &);
	CSingleton& operator=(const CSingleton &);
	~CSingleton(){ cout << "單例物件銷燬!" << endl; };
	static void Destory(CSingleton *){ cout << "在這裡銷燬單例物件!" << endl; };//注意這裡
	static shared_ptr<CSingleton> myInstance; 
public:
	static shared_ptr<CSingleton> getInstance()
	{		
		return myInstance;
	}

};

#endif
主檔案
//主檔案,用於測試用例的生成
#include<iostream>
#include<memory>
#include"CSingleton.h"

using namespace std;
shared_ptr<CSingleton> CSingleton::myInstance(new CSingleton(), CSingleton::Destory);
int main()
{
	shared_ptr<CSingleton>  ct1 = CSingleton::getInstance();
	shared_ptr<CSingleton>  ct2 = CSingleton::getInstance();
	shared_ptr<CSingleton>  ct3 = CSingleton::getInstance();
	
	return 0;
}

執行結果如下:


刪除器宣告時(即static void Destory(CSingleton *)),需要傳入該物件的指標,否則編譯出錯。這裡僅作說明,用不上該指標,故沒有給形參取名字。詳細說明一下,通過去掉形參的宣告,出錯後,就能定位到 標頭檔案<memory>中釋放物件的位置,程式碼如下:

	template<class _Ux>
		void _Resetp(_Ux *_Px)
		{	// release, take ownership of _Px
		_TRY_BEGIN	// allocate control block and reset
		_Resetp0(_Px, new _Ref_count<_Ux>(_Px));
		_CATCH_ALL	// allocation failed, delete resource
		delete _Px;
		_RERAISE;
		_CATCH_END
		}

	template<class _Ux,
		class _Dx>
		void _Resetp(_Ux *_Px, _Dx _Dt)
		{	// release, take ownership of _Px, deleter _Dt
		_TRY_BEGIN	// allocate control block and reset
		_Resetp0(_Px, new _Ref_count_del<_Ux, _Dx>(_Px, _Dt));
		_CATCH_ALL	// allocation failed, delete resource
		_Dt(_Px);
		_RERAISE;
		_CATCH_END
		}
可以看出,上面程式碼中第一個_Resetp就是沒有指明deleter時呼叫的函式。第二個_Resetp是指明deleter時呼叫的函式,此時deleter _Dt需要接收一個引數_Px(即指向shared_ptr內部物件的指標),從而釋放shared_ptr內部物件的內容。

扯遠了!

迴歸正題,到這裡,我們可以看到:餓漢式的單例模式原理很簡單,也很好寫,並且執行緒安全,不需要考慮執行緒同步!

然後,聊一聊懶漢式單例模式。

懶漢式單例模式,是在第一次呼叫getInstance()的時候,才建立例項物件。想到這裡,是不是直接把物件定義為static,然後放在getInstance()中。第一次進入該函式,就建立例項物件,然後一直到程式結束,釋放該物件。動手試一試。程式碼如下:

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H

#include<iostream>
using namespace std;
class CSingleton
{
private:
	CSingleton(){ cout << "單例物件建立!" << endl; };
	CSingleton(const CSingleton &);
	CSingleton& operator=(const CSingleton &);
	~CSingleton(){ cout << "單例物件銷燬!" << endl; };


public:
	static CSingleton * getInstance()
	{	
		static CSingleton myInstance;
		return &myInstance;
	}

};

#endif
主檔案
//主檔案,用於測試用例的生成
#include<iostream>
#include"CSingleton.h"

using namespace std;

int main()
{
	CSingleton *   ct1 = CSingleton::getInstance();
	CSingleton *   ct2 = CSingleton::getInstance();
	CSingleton *   ct3 = CSingleton::getInstance();
	
	return 0;
}

執行結果如下:

程式正常執行。此時,如果想把物件放在堆區,也可以這麼實現:

// 餓漢式單例的實現
#ifndef C_SINGLETON_H
#define C_SINGLETON_H

#include<iostream>
#include<memory>
using namespace std;
class CSingleton
{
private:
	CSingleton(){ cout << "單例物件建立!" << endl; };
	CSingleton(const CSingleton &);
	CSingleton& operator=(const CSingleton &);
	~CSingleton(){ cout << "單例物件銷燬!" << endl; };

	static CSingleton *myInstance;


public:
	static CSingleton * getInstance()
	{	
		if (nullptr == myInstance)
		{
			myInstance = new CSingleton();
		}
		return myInstance;
	}

private:
	// 定義一個內部類
	class CGarbo{
	public:
		CGarbo(){};
		~CGarbo()
		{
			if (nullptr != myInstance)
			{
				delete myInstance;
				myInstance = nullptr;
			}
		}
	};
	// 定義一個內部類的靜態物件
	// 當該物件銷燬時,順帶就釋放myInstance指向的堆區資源
	static CGarbo m_garbo;
};

#endif
主檔案如下:
//主檔案,用於測試用例的生成
#include<iostream>
#include<memory>
#include"CSingleton.h"

using namespace std;
CSingleton * CSingleton::myInstance = nullptr;
CSingleton::CGarbo CSingleton::m_garbo;
int main()
{
	CSingleton *   ct1 = CSingleton::getInstance();
	CSingleton *   ct2 = CSingleton::getInstance();
	CSingleton *   ct3 = CSingleton::getInstance();
	
	return 0;
}
執行結果如下:

咳咳!!

對於懶漢式這兩種情況,當呼叫getInstance()函式時,如果物件還沒產生(第一種狀態),就需要產生物件,然後返回物件指標。如果物件已經存在了(第二種狀態),就直接返回物件指標。當單執行緒時,沒有問題。但是,多執行緒情況下,如果一個函式中不同狀態有不同操作,就要考慮執行緒同步的問題了。因此,我們需要修改一下getInstance中的實現。

舉例如下:

第一種懶漢式:

static CSingleton * getInstance()
{	
	lock();
	static CSingleton myInstance;
	unlock();
	return &myInstance;
}


第二種懶漢式

static CSingleton * getInstance()
	{	
		if (nullptr == myInstance)
		{
			lock();// 需要自己採用適當的互斥方式
			if (nullptr == myInstance)
			{
				myInstance = new CSingleton();
			}
			unlock();
		}
		return myInstance;
	}

注意!由於執行緒和使用的作業系統有關,因此這裡的lock()和unlock()函式僅作說明示意,並未實現。都是常見的執行緒同步方法,可以查詢其他資料來實現。這裡不再贅述。

當然,懶漢式的實現也可以採用shared_ptr來實現。但是很多思想與實現方法和餓漢式的有一定重複,因此這裡也不再給出。

結束語

本人水平有限,如果程式碼有問題或者更好的方法,歡迎留言!大家一起討論,一起進步!