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