【C++】 ——C++單例模式中的餓漢和懶漢模式

一、 單例模式的定義

單例模式是一種常見的軟件設計模式。它的核心結構只包含一個被稱爲單例的特殊類。它的目的是保證一個類僅有一個實例並提供一個訪問它的全局訪問點,該實例被所有程序模塊共享

有很多地方都需要這樣的功能模塊,如系統的日誌輸出,操作系統只能有一個窗口管理器,一臺PC連接一個鍵盤等。

單例模式是通過類本身來管理其唯一實例,這種特性提供瞭解決問題的辦法。唯一的實例是類的一個普通對象,但涉及這個類的時候,讓它只能創建一個實例並提供對此實例的全局訪問。創建實例的操作我們通常把這個成員函數叫做instance(),他的返回值是唯一實例的指針,另外,我們可以提供一個public靜態方法來幫助我們獲得這個類的唯一的一個實例化對象

二、 單例模式的懶漢模式

咱先記住第一句話:第一次用到類的實例的時候纔回去實例化

什麼意思呢?像一個懶漢一樣,需要用到創建實例了的程序再去創建實例,不需要創建實例程序就不去創建實例,這是一個時間換空間的做法,同時體現了懶漢本性。

實現方法:定義一個單例類,使用類的私有靜態指針變量指向類的唯一實例,並用一個公有的靜態方法獲取該實例
如以下代碼所示:

#include <iostream>
#include <stdlib.h>
using namespace std;

class singleton  //實現單例模式的類
{
private:
	singleton() //私有的構造函數,這樣就不能再其他地方創建該實例
	{

	}
	static singleton* instance;  //定義一個唯一指向實例的指針,並且是私有的
	static int b; 
public:
	static singleton* GetInstance()  //定義一個公有函數,可以獲取這個唯一實例
	{
		if (instance == NULL)  //判斷是不是第一次使用
			instance = new singleton;
		return instance;
	}
	static void show()
	{
		cout << b << endl;
	}
};
int singleton::b = 10; //靜態成員變量在類外進行初始化,它是整個類的一部分並不屬於某個類
singleton* singleton::instance = NULL;
int main()
{
	singleton* a1 = singleton::GetInstance();
	cout << a1 << endl;
	a1->show();

	singleton* a2 = singleton::GetInstance();
	cout << a2 << endl;
	a2->show();
	system("pause");
	return 0;
}

我們來看看執行結果:
在這裏插入圖片描述
顯而易見,我們看到實例的兩個對象的地址都是一樣的,也就是說單例模式的實現是成功的。

從以上實例中我們可以看出,懶漢模式的singleton類有以下特點:

  • 他有一個指向唯一實例的靜態指針,並且是私有的
  • 它有一個公有的函數,可以獲取這個唯一的實例,並且在需要的時候創建該實例
  • 它的構造函數是私有的,這樣就不能從別處創建該類的實例

改進

但是這存在一個缺點,也就是說它在單線程下是正確的,但是在多線程情況下,如果兩個線程同時首次調用GetInstance()方法,那麼就會同時監測到instance爲NULL,則兩個線程會同時構造一個實例給instance,這樣就會發生錯誤。所以,我們對以上的單例模式進行改進——沒錯,就是加鎖,當instance不爲空的時候就不需要進行加鎖的操作,代碼如下:

class singleton  //實現單例模式的類
{
private:
	singleton() //私有的構造函數,這樣就不能再其他地方創建該實例
	{
	}
	static singleton* instance;  //定義一個唯一指向實例的指針,並且是私有的
	static int b; 
public:
	static singleton* GetInstance()
	{
		Lock(); //上鎖
		if (instance == NULL)
		{
			instance = new singleton;
		}
		Unlock();  //解鎖
		return instance;
	}
};

三、 單例模式的餓漢模式

咱再記住第二句話:單例類定義的時候就進行實例化

這又是什麼意思呢?像一個餓漢一樣,不管需不需要用到實例都要去創建實例,即在類產生的時候就創建好實例,這是一種空間換時間的做法。作爲一個餓漢而言,體現了它的本質——“我全都要”

在餓漢模式中,實例對象儲存在全局數據區,所以要用static來修飾,所以對於餓漢模式來說,是線程安全的,因爲在線程創建之前實例就已經創建好了。 我們直接來看看代碼:

#include <iostream>
#include <stdlib.h>
using namespace std;

class singleton
{
private:
	static singleton* instance; //這是我們的單例對象,它是一個類對象的指針
	singleton()
	{
		cout << "創建一個單例對象" << endl;
	}
	~singleton()
	{
		cout << "析構掉一個單例對象" << endl;
	}
public:
	static singleton* getinstance();
};
//下面這個靜態成員變量在類加載的時候就已經初始化好了
singleton* singleton::instance = new singleton();
singleton* singleton::getinstance()
{
	return instance;  //直接返回inatance
}
int main()
{
	cout << "we get the instance" << endl;
	singleton* a1 = singleton::getinstance();
	singleton* a2 = singleton::getinstance();
	singleton* a3 = singleton::getinstance();
	cout << "we destroy the instance" << endl;
	system("pause");
	return 0;
}

看看結果截圖:
在這裏插入圖片描述
第一行使我們執行構造函數的結果,這應該沒有問題,後兩句是主函數的執行結果,但是沒有執行析構函數!發現了嗎?會導致內存泄漏,這個大家應該都能想到,什麼原因?

經過博主的學習,發現此時全局數據區中,存儲的並不是一個實例對象,而是一個實例對象的指針,它是一個地址而已,我們真正佔有資源的實例對象是存在堆中,我們需要手動的去調用delete釋放申請的資源。

但是也不能去手動調用析構,因爲析構我們已經聲明爲private了,根本調不動。這裏給出的一個解決辦法是在類中寫一個主動釋放資源的方法:
在這裏插入圖片描述
紅框的是我加的delete函數,我們再來看看效果截圖:
在這裏插入圖片描述
這下就調用了我們的析構函數,那其實我們自己寫的時候要手動的去調用這個函數來釋放資源,是很不方便的,於是大佬們又想出另外一種更加優化的方法——直接聲明一個內部類:

#include <iostream>
#include <stdlib.h>
using namespace std;

class singleton
{
private:
	singleton()
	{
		cout << "創建一個單例對象" << endl;
	}
	~singleton()
	{
		cout << "析構掉一個單例對象" << endl;
	}
	static singleton* instance; //這是我們的單例對象,它是一個類對象的指針
public:
	static singleton* getinstance();
	//static void deleteInstance();
private:
	class Garbo   //內部類
	{
	public:
		Garbo()
		{}
		~Garbo()
		{
			if (instance != NULL)
			{
				delete instance;
				instance = NULL;
			}
		}
	};
	static Garbo gar; //定義一個內部類的靜態對象,當該對象銷燬的時候,調用析構函數順便銷燬我們的單例對象
};
//下面這個靜態成員變量在類加載的時候就已經初始化好了
singleton* singleton::instance = new singleton();
singleton::Garbo singleton::gar; //初始化gar靜態成員變量
singleton* singleton::getinstance()
{
	return instance;  //直接返回inatance
}

int main()
{
	cout << "we get the instance" << endl;
	singleton* a1 = singleton::getinstance();
	singleton* a2 = singleton::getinstance();
	singleton* a3 = singleton::getinstance();
	cout << "we destroy the instance" << endl;
	//singleton::deleteInstance();
	system("pause");
	return 0;
}

截圖如下:
在這裏插入圖片描述
我們並沒有手動的去調用delete函數,還銷燬了這個對象,有的博主說智能指針還可以解決這個問題,我只能說,這個確實可以,是大佬無疑了。其實思路也很簡單,博主直接把人家的文章貼出來,有興趣盆友自行查看智能指針實現餓漢模式

四、 單例模式的應用場景

優點
(1)在單利模式中,活動的實例只有一個實例,對單例類的所有實例化得到的都是相同的一個實例,這樣就防止其他對象自己實例化,確保所有對象都訪問一個實例
(2)單例模式中的類自己來控制實例化進程,類就在改變實例化進程上有相應的伸縮性。
(3)提供了對唯一實例的受控訪問
(4)避免對共享資源的多重使用
(5)由於在系統只存在一個對象,因此可以節約資源,當需要偏飯創建和銷燬對象時,單例模式無疑可以提高系統性能
缺點
(1)不適用於變化的對象,如果同一類型的對象總是要在不同的用例場景發生變化,單例就會引起數據的錯誤,不能保存彼此的狀態。
(2)濫用單例將帶來一些負面問題,如爲了節省資源將數據庫連接池對象設計爲的單例類,可能會導致共享連接池對象的程序過多而出現連接池溢出;如果實例化的對象長時間不被利用,系統會認爲是垃圾而被回收,這將導致對象狀態的丟失。
(3)由於單利模式中沒有抽象層,因此單例類的擴展有很大的困難。

應用場景
(1)資源共享的情況下,避免由於資源操作時導致的性能或損耗等。如上述中的日誌文件,應用配置。
(2)控制資源的情況下,方便資源之間的互相通信。如線程池等。

本文吸取了很多優秀博主的文章,但是博主的文章只適合初學者入個門,慚愧…我把引用把鏈接貼出來,大家可以理解的更透徹C++單利模式中的餓漢模式以及單例模式的優缺點

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章