由淺入深剖析—智能指針

1.爲什麼出現智能指針?

智能指針的出現是爲了解決,由於異常出現而導致申請的空間沒有釋放,而出現的內存泄漏的問題。

智能指針其針對的情況如下代碼

當我們除數輸入0時,系統就會拋異常中斷程序,但是我們的p指針還沒有被釋放,程序卻終止了,導致了內存泄漏,這是十分危險的。

void Div(double a, double b)
{
	if (b == 0)
	{
		invalid_argument err("除數等於0");
			throw err;
	}
	else
	{
		cout << a / b << endl;
	}
}
void Fun()
{
	int * p = new int;
	double x1, x2;
	cin >> x1 >> x2; //除數爲零,導致內存泄漏
	Div(x1, x2);

	cout << "delete" << endl;
	delete p;
}

int main() 
{
	try
	{
		Fun();
	}
	catch (exception& e)
	{
		e.what();
	}

	system("pause");
	return 0;
}

 

如何才能保證出現意外情況,內存也能出了作用域釋放呢?我們想到了C++中的類,因爲類在出了作用域會自動的調用他的構造函數,所以就有了一種解決方法,將這個無人管理的指針p賦值給類中的成員變量,讓p和類中的成員變量指向同一塊空間,這樣就可以在類生命週期結束前調用析構函數,同時也將指針p所指的空間delete掉,方法如下

 

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)//缺省構造
	:_ptr(ptr)
	{}

	~SmartPtr()
	{
        if(_ptr)//防止釋放空指針
        {
		cout << "~SmartPtr" << endl;
		delete _ptr;
        }
	}
private:
	T* _ptr;
};
void Div(double a, double b)
{
	if (b == 0)
	{
		invalid_argument err("除數爲0");
		throw err;
	}
	else
	{
		cout << a /  b << endl;
	}
}

void fun()
{
	int *p = new int;
	SmartPtr<int> sp(p);
	double a, b;
	cin >> a >> b;
	Div(a, b);
	cout << "delete" << endl;
	delete p;
}
int main()
{
	try
	{
		fun();
	}
	catch (exception(&e))
	{
		e.what();
	}
	system("pause");
	return 0;
}

這時當我們除數輸入0時,系統就會拋異常中斷程序,但是不影響我們的p指針釋放,因爲他已經託管給類SmartPtr了,不會導致了內存泄漏。

 

讀到這裏你應該知道了在某些情況下使用智能指針的本質,和他的好處,就像它的名字,可以智能的管理p指針的內存,防止內存泄漏。

提到智能指針,不得不瞭解的就是RAII(面試常考)

 

RAII:RAII,也稱爲“資源獲取就是初始化”,是c++等編程語言常用的管理資源、避免內存泄露的方法。是一種利用對象生命週期來控制程序資源(如內存、文件句柄、網絡連接、互斥量等等)的簡單技術。 它保證在任何情況下,使用對象時先構造對象,最後析構對象。總的來說,智能指針就是RAII的產物

使用RAII的好處:1.不需要顯式的釋放資源。

                            2.資源和類的生命週期同時存在。

 

但智能指針瞭解到這裏遠遠不夠,要想在面試的時候被問到智能指針想十拿九穩的話,還需要以下的知識,請耐心閱讀。

 

 

2.智能指針的前世今生

上面我寫的智能指針smartptr還不夠智能,意味着不能光實現管理資源的功能,還能滿足指針的其他功能,如像指針一樣可以*p , p->  (可以重載)

於是我們還得完善上面的指針,添加新功能

struct Date
{
	int _year;
	int _month;
};


SmartPtr<int> sp(new int);
*sp = 10;
cout << *sp << endl;
SmartPtr<Date> spp(new Date);
spp.operator->()->_year = 2019; //spa->_year=10  等價於 spa.operator->()->_year=20;編譯器的優化
cout << spp->_year << endl;

結果:

 

 

總結一下智能指針的原理:

1.RAII特性

2.重載operator*和opertaor->,具有像指針一樣的行爲。

 

從早期的 C++98到現在C++11,標準庫裏衍生出來了一系列的智能指針。

 

 

3.智能指針的分類:

1.C++98的  auto_ptr                 特點:管理權轉移,缺陷極大

2.C++11的  unique_ptr             特點:防拷貝 ,簡單粗暴,不支持拷貝

3.C++11的  shared_ptr             特點:引用計數的原理,彌補unique_ptr的缺陷,可拷貝

4.shared_ptr附帶的weak_ptr    特點:weak_ptr   只能由shared_ptr賦值

 

分類剖析:

1.auto_ptr 

模擬實現auto_ptr

template<class T>
class Auto_ptr
{
public:
	Auto_ptr(T* ptr = nullptr)//缺省構造
	:_ptr(ptr)
	{}

	~Auto_ptr()
	{
		cout << "~Auto_ptr" << endl;
		delete _ptr;
	}

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

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

	Auto_ptr(Auto_ptr<T>& sp)
		:_ptr(sp._ptr)
	{
		sp._ptr = nullptr;
	}

	//重載賦值
	Auto_ptr<T>& operator=(Auto_ptr<T>& sp)	//用Auto_ptr<T>返回一個被賦值的類  sp1=sp2
	{
		if (this != &sp)	//防止自己給自己賦值
		{
			if (_ptr)//防止原先的sp1內存泄漏
			{
				delete _ptr;
			}
			_ptr = sp._ptr;
			sp._ptr = nullptr;
		}
		return *this;
	}
private:
	T* _ptr;
};
void test1()
{
	Auto_ptr<int> ap1(new int);
	Auto_ptr<int> ap2(ap1);//等於把內存交給ap2管 管理權轉移//但是這裏的問題就是就沒法實現*ap1=10,因爲ap1這是爲nullptr,所以這是auto_ptr的一個坑  一般公司嚴禁使用auto_ptr,這是一種有缺陷的智能指針
}
int main()
{
	test1();
	system("pause");
	return 0;
}

所以auto_ptr有嚴重缺陷,禁用。

 

2.unique_ptr 

他和aoto_ptr唯一的區別就是

unique_ptr(const unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;

直接將拷貝和賦值函數刪除,或者將其定爲私有,使其他對象訪問不到。他的特點就像他的名字,一旦創建,就指向那塊內存,以後再也不能讓其它的智能指針指向同一塊內存。唯一性,防拷貝 ,簡單粗暴,不支持拷貝。

 

3.shared_ptr

shared_ptr改進了unique_ptr,使其具有拷貝的能力,原理是使用了引用計數,

模擬實現:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using namespace std;

template<class T>
class Shared_ptr
{
public:
	Shared_ptr(T* ptr = nullptr)//缺省構造
		:_ptr(ptr)
		, _pcount(new int(1))
	{}

	~Shared_ptr()
	{
		if (*(_pcount) == 0)
		{
			cout << "~Shared_ptr" << endl;
			delete _ptr;
			delete _pcount;
			_ptr = nullptr;
			_pcount = nullptr;
		}
	}

	//sp1(sp2)
	Shared_ptr(Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
	{
		(*_pcount)++;
	}


	int get_count()
	{
		return *_pcount;
	}

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

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

	//重載賦值
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		//if(this!=&sp)
		if (_ptr != sp._ptr)//這裏是防止上面的代碼出現sp1=sp2的情況,
			//出現sp1=sp2不算錯,但是不夠優化,這樣寫就可以更加優化
		{

			if (--*_pcount == 0)	//加這個if條件的原因是,如果原來sp1的內存不止
				//一個shared_ptr佔用的,如果這時貿然釋放sp1,
				//就會不合理,所以檢測如果他的引用計數爲1shi,
				//就可以釋放,正確處理了內存泄漏
			{
				delete _ptr;
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			(*_pcount)++;
		}
		return *this;
	}

private:
	T* _ptr;
	int* _pcount;
};
void test1()
{
	Shared_ptr<int> ap1(new int);
	Shared_ptr<int> ap2(ap1);
	cout << ap1.get_count() << endl;
	cout << ap2.get_count() << endl;
}
int main()
{
	test1();
	system("pause");
	return 0;
}

結果:

注意:這裏重點理解一下賦值重載函數  shared_ptr<T>& operator=(const shared_ptr<T>& sp)

智能指針改進到這裏,你以爲就萬事大吉了嗎?,錯,還存在一個問題,智能指針管理的對象存放在堆上,兩個線程中同時去訪問,會導致線程安全問題。

shared_ptr的線程安全問題:

智能指針中的引用計數是可以多個對象共享的,當出現兩個線程同時操作_pcount時,這時不是原子操作,shared_ptr 的引用計數本身是安全且無鎖的,但對象的讀寫則不是,因爲 shared_ptr 有兩個數據成員,讀寫操作不能原子化。

再說細一點就是當線程A的引用計數需要加一時,從準備加一到加一完成的這個還沒完成操作的時間段內,切換到另一個線程B,這時線程B也加一,但是這時線程A的加一操作沒有完成但以啓動,但最後引用計數的結果本來應該爲3,但可能因爲這個原因變成2。

改進如下:


template<class T>
class Shared_ptr
{
public:
	Shared_ptr(T* ptr = nullptr)//缺省構造
		:_ptr(ptr)
		, _pcount(new int(1))
		, _pMutex(new mutex)
	{}

	~Shared_ptr()
	{
		Release();
	}
private:
	void Release()
	{
		bool deleteflag = false;
		// 引用計數減1,如果減到0,則釋放資源
		_pMutex.lock();
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
			deleteflag = true;
		}
		if (deleteflag == true)
		{
			delete _pMutex;
		}
	}
	//sp1(sp2)
	Shared_ptr(Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _pMutex(sp._pMutex)
	{
		(*_pcount)++;
	}


	int get_count()
	{
		return *_pcount;
	}

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

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

	//重載賦值
	Shared_ptr<T>& operator=(const Shared_ptr<T>& sp)
	{
		if (_ptr != sp.ptr)
		{
			Release();
			_ptr = sp._ptr;
			_pRefCount = sp._pRefCount;
			_pMutex = sp._pMutex;

			_pMutex->lock();
			++(*_pcount);
			_pMutex->unlock();
		}
		return *this;
	}

private:
	T* _ptr;
	int* _pcount;
	mutex* _pMutex; //互斥鎖
};

注意:

shared_ptr還有一點:

1.shared_ptr<B> sp1(new B); 沒問題

2.shared_ptr<B> sp1((B*)malloc(sizeof( B)));沒問題,因爲delete底層還是調用free

3.stringshared_ptr<> sp1((string*)malloc(sizeof( string)));有問題,因爲string 裏面的 _str原生構造了時隨機值,沒有初始化,出了作用域調用析構,釋放一塊隨機值,這是不可以的。

鎖的拓展(面試常考):

1.互斥鎖     特點:適合粒度大的

2.自旋鎖     特點:適合粒度小的

舉個例子:

for (size_t i = 0; i < 1000000; ++i)//這裏的time大,因爲互斥鎖適合粒度大的
{
LockGuard<mutex> lock(mtx);
++n;
}


LockGuard<mutex> lock(mtx);//這裏的time小,因爲互斥鎖適合粒度大的
for (size_t i = 0; i < 1000000; ++i)
{
++n;
}

到此shared_ptr還有一個最特殊的缺陷,就是循環引用,什麼是循環引用

由於先構造的後釋放,後構造的先釋放可知,先釋放的是sp2,那麼因爲它的引用計數爲2,減去1之後就成爲了1,不能釋放空間,因爲還有其他的對象在管理這塊空間。但是sp2這個變量已經被銷燬,因爲它是棧上的變量,但是sp2管理的堆上的空間並沒有釋放。

接下來釋放sp1,同樣,先檢查引用計數,由於sp1的引用計數也是2,所以減1後成爲1,也不會釋放sp1管理的動態空間。

通俗點講:就是sp2要釋放,那麼必須等p1釋放了,而sp1要釋放,必須等sp2釋放,所以,最終,它們兩個都沒有釋放空間。

所以,造成了內存泄漏。如下圖這樣子

 

爲了解決這一特殊情況,引出了weak_ptr,

 

4.weak_ptr

weak_ptr解決了循環引用的問題,用法如下:

具體用法就是:weak_ptr可以將shared_ptr賦值給weak_ptr,它的特點是不增加引用計數,可以接收shared_ptr作爲參數。

一句話,weak_ptr是爲了解決shared_ptr出現循環引用的問題的。

注意:不能new一個內存給weak_ptr,只能將一個shared_ptr賦值給weak_ptr。

補充:

面試如果讓你寫一個智能指針:

1.一定不要寫auto_ptr,它有嚴重的缺陷

2.一般寫unique_ptr  沒有大的缺陷,只是不能賦值

3.儘量不要寫shared_ptr,因爲寫起來相對複雜,容易出錯 還可能存在線程安全的問題

 

基本上智能指針知道這些對於面試來說就可以了,知識有點多,希望大家看完後慢慢梳理一下。

 

RAII拓展(防止追問RAII的知識):https://blog.csdn.net/y396397735/article/details/81024755

 

 

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