智慧指標

東南亞季風發表於2021-02-05

1.智慧指標作用

提出智慧指標時候,不得不提一下棧和堆。我們都知道,棧上的資源是由系統管理的,申請和釋放資源都是由棧的策略來進行的。

而堆上的資源申請,是使用者顯示的呼叫關鍵字new 和 delete來進行申請資源和釋放資源,該資源的生命週期是執行new語句申請資源    到執行 delete釋放資源。(或者程式結束由系統釋放這部分堆資源)。

這時候我們在來談一下智慧指標的作用和原理。

智慧指標就是行為類似指標的棧物件,並非指標型別,在棧物件生命週期即將結束時,智慧指標通過解構函式釋放有它管理的堆記憶體。這樣就能夠不用手動釋放記憶體了。

 

2.智慧指標需要注意什麼

(1)智慧指標是利用了棧物件生命即將結束時,系統呼叫解構函式釋放物件資源時候,管理它所指向的堆記憶體。那麼我們要是把智慧指標本身定義在堆記憶體上,是不是就不滿足上面的特性了。所以智慧指標是寫在棧上使用的。

(2)智慧指標不是指標型別是個物件。所以我們需要它滿足指標的特性。那就是能解引用。我們這裡需要過載一下 -> 操作符   和  * 操作符。這樣我們就讓物件有了指標的功能和特性。

(3)接著我們需要思考的問題就是構造時候問題,我們知道建構函式分為四種,預設建構函式,無引數建構函式,有引數的建構函式,拷貝建構函式。我們需要管理一塊動態記憶體。必須要知道記憶體的起始地址,因此需要有一個引數 記憶體的起始地址。所以不考慮預設和無引數建構函式。有引數的構造,我們如果把兩個相同的地址都傳給了智慧指標讓其構造,產生兩個智慧指標物件都指向同一個動態記憶體地址塊,析構時候豈不是會對相同的動態記憶體塊釋放了兩次,這比記憶體洩漏還要惡劣。假設我們用的是拷貝建構函式。會發生什麼,如果是預設拷貝構造,就是淺拷貝,這個也是對相同的記憶體釋放了兩次。

(4)賦值運算子的過載

第一步是判斷是否是自賦值,然後再釋放舊資源,假設這時候還有其他指標指著同一塊管理的動態記憶體,你就這樣釋放了?然後還有和前面提到一樣的問題。相同動態記憶體地址,被多個智慧指標指著。如何解決多次釋放相同動態記憶體的問題。因此我們要引出計數器。我們下面會講。

 

嗯智慧指標看起來好像有點雞肋,只要自己知道手動釋放資源就夠了。這種想法很明顯是有問題的。比如說你在寫程式碼時候,如果寫了一個if()return 出去了,沒執行到下面的delete呢,或者丟擲異常,沒執行下面的delete呢?

(5)多執行緒

後面會說

(6)是不是應該設計為單例模式

後面會說

 

boost庫的四種智慧指標

auot_ptr   scoped_ptr、shared_ptr、weak_ptr

 

3.auto_ptr

應該有兩種稍稍不同的思路寫auto_ptr,一種是所有者轉移,把原來的智慧指標置為NULL,第二種是是在成員變數裡定義一個_owner變數,來提供所有權管理的方法。

理解第一種的程式碼思路:

就是當發生了構造,我之前說了構造出來的一般都要傳一個指標指向開闢的堆記憶體作為引數,或者預設為NULL。然後把外部傳進來的指標賦給了要構造的物件,然後把外部傳進來的指標賦為NULL。

賦值運算子過載先delete,然後賦值獲取新的堆記憶體的管理權,接著把外部的指標變NULL。這樣就始終只有一個智慧指標是真正管理到堆記憶體地址,其他都為空。解決了同一塊記憶體被多次釋放的問題。但是新的問題就是那些為NULL 的智慧指標怎麼繼續想訪問那塊堆記憶體?

 

我舉個簡單的例子先來理解下aout_ptr,先舉一個置為NULL的例子

房子:管理的堆記憶體     房產證:是否具有管理(刪除堆記憶體的)的權力         鑰匙:是否具有訪問記憶體塊的權力

就是相當於你有一棟房子和這個房子的房產證,然後你現在你轉讓這個房子,把房產證交出去,鑰匙也沒收了。你就變成NULL了。什麼也做不了。而接受轉讓的人有鑰匙和房產證,它可以選擇拆房子和接著轉讓房產證把自己變NULL。

 

#include <iostream>
using namespace std;

template<class T>
class Auto_Ptr
{
public:
    Auto_Ptr(T* ptr = 0):_ptr(ptr)
    {
        ptr = NULL;
    }

    Auto_Ptr(Auto_Ptr<T>& ap):_ptr(ap._ptr)
    {
        ap._ptr = NULL;
    }

    Auto_Ptr<T>& operator=(Auto_Ptr<T>& ap)
    {
        if (this != &ap)
        {
            if (_ptr)
            {
                delete _ptr;
            }

            _ptr = ap._ptr;
            ap._ptr = NULL;
        }

        return *this;
    }

    ~Auto_Ptr()
    {
        if (_ptr)
        {
            delete _ptr;
            _ptr = NULL;
        }
    }

    T& operator*()
    {
        return *_ptr;
	}

    T* operator->()
    {
        return _ptr;
    }

private:
    T* _ptr;

};

 

第二種程式碼思路是在private 里加入了一個bool型別的  _owner,當發生拷貝構造時候,(拷貝物件)判斷一下_owner是否為true,是true的話就delete釋放舊資源。(拷貝物件)然後在獲取ptr,(拷貝物件)獲取和(被拷貝物件)一樣的許可權,也就是把_owner 賦值過去。在把被拷貝_onwer變成false;

這裡舉個例子

房子 :管理的堆記憶體     房產證: 是否具有delete管理堆記憶體許可權 (_owner  是否為true) 鑰匙:物件是否有訪問堆記憶體的地址

我現在買一個房子,得到了一個房產證和鑰匙。有人看上我的房子,但是我又要用房子,於是我就決定把房子轉讓給他,把房產證給他,然後給他配了一把鑰匙。這樣我也能進房間,但是我不能去毀掉這個房子(delete 釋放堆記憶體),因為這個房子不是我的了。其他人找我要房子的時候,我只能給他們配多一把鑰匙,給不了房產證了。

缺陷:

哪天朋友把房子轉讓給一個二貨(臨時物件生命週期短,生命週期一到就呼叫析構,他很快就毀掉房子delete  釋放對記憶體。或者你朋友在你不知情的時候把房子拆了,另外的人蓋了新房子在上面。你下次開不了門了。

 

#include<iostream>
using namespace std;

template <typename T>
class A_ptr
{
public:
    A_ptr(T *ptr = NULL):_ptr(ptr){};
   
    A_ptr(A_ptr<T> &src)
    {
        _ptr = src._ptr;
        _owner = src._owner;
        src._owner = false;
    }
   
    A_ptr& operator= (A_ptr &src)
    {
        if(this != &src)
        {
            if(_owner == true)
            {
                delete _ptr;
            }

            _ptr = src._ptr;
            _owner = src._owner;
            src._owner = false;
        }
        return *this;
    }

    ~A_ptr()
    {
        if(_owner)
        {
            delete _ptr;
        }
    }

    T& operator*(){return *_ptr;}

    const T* operator->(){return _ptr;}

private:
    T *_ptr;
    bool  _owner;
};

scoped_ptr

這個智慧指標是彌補了auot_ptr 的缺陷,就是你賦值和拷貝構造時候,管理許可權發生了轉移,而且只有一個物件能析構delete那塊對記憶體。而這個具有管理許可權的物件我們不確定它什麼時候會呼叫解構函式把那塊堆記憶體釋放掉了,而當這塊堆記憶體被釋放掉了,之前還指向這塊堆記憶體的物件,還想訪問就會出錯。於是我們想出來使用scoped_ptr指標。

(以下個人看法)

我當時認為如果許可權不轉移,一直讓第一個呼叫帶參建構函式的_owner為true,之後發生拷貝和賦值行為的都是false。這樣控制住第一個構造出來的智慧指標物件是最後一個呼叫解構函式就可以了。就相當於我買了一個房子,拿到了房產證。然後我之後想進房子裡的租客,都獲得是鑰匙,我只要保證租客走完了,拆了房子。老師說是和c++的思想不吻合。既然你是拷貝構造和賦值。意味著你是不是應該把你的所有權交出去給下個物件。

scoped_ptr:

它是相當於把拷貝構造和賦值運算子過載放入了私有成員裡面,這樣就不會發生許可權的轉移問題,相當於一棟房子就一個人能用,房子不能轉讓了。這個就不寫程式碼了。這個是治標不治本的,感覺不能從根本上解決問題。

shared_ptr:

我先說一下這個類的框架

它是相當於多弄了一個輔助類_heapManager,這個類就是一個資料結構,可以是連結串列或者容器。最好是帶擴容的資料結構。然後在每個節點或者單元格儲存的是一個void *_paddr指標和一個計數器_refcount。void *指標記錄智慧指標管理的堆記憶體的起始地址。而計數器是記錄該堆記憶體塊被多少個智慧指標引用。這兩個變數是放在一個結構體中,結構體是放在陣列中。然後智慧指標類中有一個靜態的該輔助類的宣告。然後再類外定義並初始化。該類還要負責對計數器的加減和檢視計數器的值。

智慧指標類負責就是第一點它滿足指標的功能,解引用,接著就是構造時候該如何操作,不帶引數和帶引數的因為是剛開始建立所以,如果該指標不是NULL把計數器變1,拷貝構造時候,這時候是需要的就是把計數器也是加1。而不是變1。接著到賦值運算子過載,需要先判斷是否自賦值。如果是賦值的話呢因為要有覆蓋原來資料的因素。所以要考慮釋放舊資源的問題。這裡是計數先減少1,看一下計數器是否為0。不為0那麼不需要釋放資源,因為還有其他的智慧指標同樣在引用這塊記憶體。

 

和上面一樣簡單抽象個例子來解釋下

國家土地緊張,你有個房子,有個房產證和鑰匙。此時房子是大家共有的財產了,意味著房產證也是共有的。那麼就不能隨便拆房子了。每次有人想要過來享受房子,那麼擁有房產證的人會被+1,這個 會被記錄著。他不需要走了時候就-1。當擁有房產證最後一個人要離開房子時候,無論是搬新家還是不住房子了。都需要把原來的房子給拆了,還給政府土地。

 

#include <iostream>
#include <vector>

using namespace std;



//這是一個輔助類,用來記錄智慧指標管理的記憶體的起始地址,和同一塊記憶體被多少個智慧指標共同管理
class CHeapManager
{
public:
        static CHeapManager &getIntance()
        {
                return heapManager;
        }

	void addRef(void *ptr)
	{
		vector<ResItem>::iterator  it = find(_vec.begin(),_vec.end(),ptr);
		if(it == _vec.end())                  //沒找到
		{
			ResItem item(ptr);              //把ptr初始化
			_vec.push_back(item);  //把新的指標放入容器
		    cout<<"new res addr :"<<ptr<<endl;
		}
		else
			it->_refcount++;          //找到了引用計數加1
	}

	void delRef(void *ptr)
	{
		vector<ResItem>::iterator it = find(_vec.begin(),_vec.end(),ptr);
		
		if(it != _vec.end())     //找到了
		{
			it->_refcount--;  //這裡做減減,而刪除工作是由CSmartprt來做的
		}
	}

	int getRef(void *ptr)
	{
		vector<ResItem>::iterator it = find(_vec.begin(),_vec.end(),ptr);
		if(it != _vec.end())
		{
			return it->_refcount; //獲得引用計數個數
		}
		
		throw "no find ";   //沒找到
	}
private:
        
	struct ResItem    //這個是存放進陣列容器的結構體
	{
		ResItem(void *ptr = NULL):_paddr(ptr),_refcount(0)
		{
			if(_paddr != NULL)
			{
				_refcount = 1;
			}
		}

		bool operator == (void *ptr)
		{
			return _paddr == ptr;
		}

		void * _paddr;  //泛型指標,只需要知道管理堆記憶體的起始地址
		int _refcount;   //計數器
	};

	CHeapManager(){};
        CHeapManager(const CHeapManager &src){}
        static CHeapManager heapManager;

	vector<ResItem> _vec;   //容器
};

CHeapManager CHeapManager::heapManager;   //實現單例模式要對這個變數進行初始化


template<typename T>
class CSmartptr                  
{
public:
	CSmartptr(T *ptr = NULL):_ptr(ptr)      
	{
		if(_ptr != NULL)
		{
			addRef();
		}
	}

	CSmartptr( const CSmartptr<T> &src):_ptr(src._ptr)
	{
		if(_ptr != NULL)
		{
			addRef();
		}
	}

	CSmartptr<T>& operator = (const CSmartptr<T> &src)
	{
		if(this == &src)
			return *this;
		delRef();
		if(0 == getRef())
		{
			delete _ptr;
			cout<<"delete 1"<<endl;
		}
		_ptr = src._ptr;
		addRef();
		return *this;
	}

	~CSmartptr()
	{
		delRef();
		if(0 == getRef())
		{
			delete _ptr;
			cout<<"delete 1"<<endl;
		}
	}

	 T& operator*(){return *_ptr;}

    const T* operator->(){return _ptr;}

private:
	void addRef(){_heapManager.addRef(_ptr);}
	void delRef(){_heapManager.delRef(_ptr);}
	int getRef(){return _heapManager.getRef(_ptr);}

	T  *_ptr;
	static CHeapManager  &_heapManager;    //這個計數器類是智慧指標類所共有的
};

template<typename T>
CHeapManager& CSmartptr<T>::_heapManager = CHeapManager::getIntance();

 

多執行緒情況

我們看起來大致實現了智慧指標了,但是考慮下多執行緒的情況呢。我們對有計數器的是不是應該格外小心一點呢,我們想想在計數器加加減減時候是不是原子操作呢?明顯不是,所以加減是臨界區程式碼,需要用鎖來控制。

 

還有比如說

int *p = new int;

CSmartptr<int> p1(p);

CSmartptr<char>p2((char *)p);  

所謂的靜態的成員變數,不是類物件共享,而是同一類物件所共享,不是類生成不同的靜態類物件。上面這種情況是產生了兩個個_heapManage。因此會導致同一記憶體被釋放兩次。

如果定義到全域性變數下可以解決但是這是c的思想,因此使用單例模式。上面程式碼已經使用單例模式了。

 

還有一個問題就是

 

4.weak_ptr

迴圈引用:

什麼是迴圈引用呢,這個很好理解

 

於是引出了弱指標,強指標修改的是_use計數器,而弱指標修改的是_weak計數器。是否釋放記憶體看的是_use計數器。這樣迴圈一方變成了弱指標,迴圈引用問題就可以解決了。

 

 

相關文章