目錄
智能指針的使用場景分析
RAII和智能指針的設計思路
C++標準庫智能指針的使用
auto_ptr的使用:
unique_ptr的使用:
定制刪除器:
weak_ptr
智能指針的使用場景分析
下面這段程序在“cout << Divide(len, time) << endl;“可能會拋出異常,如果不捕獲這個異常,也就是不加對應的catch語句,那么后面的delete[]就執行不到位,這樣就會造成內存泄漏。所以,我們應該加上對應的catch語句,將異常捕獲后釋放資源,再將異常重新拋出。
除了Divide會拋出異常,new的部分也會拋出異常,若是”int* array1 = new int[10];“處拋異常,倒沒什么事,因為拋出異常代表內存申請失敗,但若是”int* array2 = new int[10];“處拋異常呢?此時array1已經成功申請內存了,如果不delete掉array1的資源,就會造成內存泄漏,為了避免這樣的情況,還需在array2處在寫一個try語句。但是當存在多個變量進行new時,代碼會變的很搓,所以這里就可以使用到智能指針。
double Divide(int a, int b)
{// 當b == 0時拋出異常if (b == 0){throw "Divide by zero condition!";}else{return (double)a / (double)b;}
}
void Func()
{// 這?可以看到如果發?除0錯誤拋出異常,另外下?的array和array2沒有得到釋放。// 所以這?捕獲異常后并不處理異常,異常還是交給外?處理,這?捕獲了再重新拋出去。// 但是如果array2new的時候拋異常呢,就還需要套?層捕獲釋放邏輯,這?更好解決?案// 是智能指針,否則代碼太戳了int* array1 = new int[10];int* array2 = new int[10]; // 拋異常呢try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (...){cout << "delete []" << array1 << endl;cout << "delete []" << array2 << endl;//先將異常捕獲,釋放資源,再重新拋出,這里捕獲異常的目的就是為了釋放資源,避免內存泄漏delete[] array1;delete[] array2;throw; // 異常重新拋出,捕獲到什么拋出什么}// ...cout << "delete []" << array1 << endl;delete[] array1;cout << "delete []" << array2 << endl;delete[] array2;
}
int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知異常" << endl;}return 0;
}
RAII和智能指針的設計思路
RAII是ResourceAcquisition Is Initialization的縮寫,本質是一種利用對象?命周期來管理獲取到的動態資源。RAII在獲取資源時把資源委托給?個對象,接著控制對資源的訪問, 資源在對象的?命周期內始終保持有效,最后在對象析構的時候釋放資源,這樣保障了資源的正常 釋放,避免資源泄漏問題。
簡單講就是,申請了內存空間,但這個內存空間不需要自己管理,而是交給一個對象進行管理,當這個對象生命周期結束時,會析構,同時也會把管理資源釋放掉。
構造函數保存資源,析構函數釋放資源
#include<iostream>using namespace std;template<class T>
class SmartPtr//智能指針
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){//析構的是被智能指針管理的資源cout << "delete[]" << _ptr << endl;delete[] _ptr;}// 重載運算符,模擬指針的?為,?便訪問資源T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}private:T* _ptr;
};double Divide(int a,int b)
{//如果這里拋出異常,會跳到main函數中去。根據智能指針資源在對象的?命周期內始終保持有效,最后在對象析構的時候釋放資源。所以在跳轉到main函數中去之前,會調用智能指針的析構函數將資源釋放掉if (b==0){throw "Divide:分母不能為0";}else{return ((double)a / (double)b);}}void Func()
{SmartPtr<int>sp1 = new int[10];SmartPtr<int>sp2 = new int[10];SmartPtr<int>sp3 = new int[10];SmartPtr<pair<int,int>>sp4 = new pair<int, int>[10];int len, time;cin >> len >> time;cout << Divide(len,time) << endl;sp1[5] = 50;sp4->first = 1;sp4->second = 2;cout << sp1[5] << endl;}int main()
{try{Func();}catch(const char*errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知異常" << endl;}return 0;
}
C++標準庫智能指針的使用
智能指針解決了拋出異常可能會導致內存泄漏的問題,但是它自身也存在一些問題。比如:智能指針的拷貝:
template<class T>
class SmartPtr
{
public:// RAIISmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete[] " << _ptr << endl;delete[] _ptr;}// 重載運算符,模擬指針的?為,?便訪問資源T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}private:T* _ptr;
};int main()
{//我們自己沒有實現拷貝構造,編譯器自己生成的默認拷貝構造是淺拷貝//這樣就會導致兩個智能指針指向同一塊資源,析構時就會析構兩次SmartPtr<int>sp1 = new int[10];SmartPtr<int>sp2(sp1);return 0;
}
根據前面所學,當存在申請空間時,就要調用深拷貝。但是智能指針模擬的是原生指針,原生指針1拷貝給原生指針2的目的是賦值,他們指向的資源依然是同一塊。智能指針1拷貝給智能指針2的目的是為了讓兩個智能指針共同管理這塊資源
-
C++標準庫中的智能指針都在這個頭?件下?,我們包含了就可以使用。 智能指針有好?種,除了weak_ptr他們都符合RAII和像指針?樣訪問的?為,原理上??主要是解決智能指針拷?時的思路不同的問題
-
auto_ptr是C++98時設計出來的智能指針,他的特點是拷?時把被拷?對象的資源的管理權轉移給拷貝對象,這是?個?常糟糕的設計,因為他會使被拷?對象懸空,訪問報錯的問題,也就是出現野指針的問題
-
unique_ptr是C++11設計出來的智能指針,他的名字翻譯出來是唯?指針,他的特點是不?持拷?,只?持移動。如果不需要拷貝的場景就非常建議使用他。
-
shared_ptr是C++11設計出來的智能指針,他的名字翻譯出來是共享指針,他的特點是?持拷?, 也?持移動。如果需要拷貝的場景就需要使?他了。底層是用引用計數的?式實現的。
-
weakptr是C++11設計出來的智能指針,他的名字翻譯出來是弱指針,他完全不同于上?的智能指針,他不?持RAII,也就意味著不能用它直接管理資源,weakptr的產生本質是要解決shared_ptr 的?個循環引用導致內存泄漏的問題。
auto_ptr的使用:
#include<iostream>
#include<memory>
using namespace std;struct Date
{
public:Date(int year=1,int month=1,int day=1):_year(year),_month(month),_day(day){ }~Date(){cout << "析構" << endl;}int _year;int _month;int _day;
};int main()
{auto_ptr<Date>ap1(new Date);//拷貝發生資源管理權的轉移,ap1懸空auto_ptr<Date>ap2(ap1);//空指針的訪問,這也是auto_ptr會存在的問題//ap1->_year;return 0;
}
拷貝完后,在對ap1進行訪問,這就是對空指針進行訪問:
unique_ptr的使用:
struct Date
{
public:Date(int year=1,int month=1,int day=1):_year(year),_month(month),_day(day){ }~Date(){cout << "析構" << endl;}int _year;int _month;int _day;
};int main()
{//unique_ptr只支持移動構造,不支持拷貝unique_ptr<Date>up1(new Date);//當移動完后,up1會懸空unique_ptr<Date>up2(move(up1));return 0;
}
unique_ptr與auto_ptr的區別:前者是告知使用者,自己只支持移動構造,不支持拷貝構造,使用完后會造成指針懸空的情況;后者是告知使用者自己是拷貝構造,但是會造成指針懸空的問題并未告知使用者
shared_ptr的使用:
struct Date
{
public:Date(int year=1,int month=1,int day=1):_year(year),_month(month),_day(day){ }~Date(){cout << "析構" << endl;}int _year;int _month;int _day;
};int main()
{shared_ptr<Date>sp1(new Date);shared_ptr<Date>sp2(sp1);shared_ptr<Date>sp3(sp1);//輸出引用計數cout << sp1.use_count() << endl;sp1->_year++;cout << sp1->_year << endl;cout << sp2->_year << endl;cout << sp3->_year << endl;//shared_ptr還能像如下這樣賦值shared_ptr<Date>sp4=make_share<Date>(2024,1,1);return 0;
}
引用計數:當多個對象管理同一塊資源時,用一個count記錄對象個數,當count==0時,再將資源釋放
模擬shared_ptr:
namespace liu
{template<class T>class shared_ptr{public:shared_ptr(T*ptr=nullptr):_ptr(ptr),_pcount(new int(1)){ }~shared_ptr(){if (--(*_pcount) == 0){cout << "析構" << endl;delete _pcount;delete _ptr;}}shared_ptr(const shared_ptr<T>&sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>&sp){if (_ptr==sp._ptr){return *this;}if (--(*_pcount)==0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;};
}
如果以int count的方式來計數:
如果以靜態成員變量的方式來計數:
定制刪除器:
智能指針析構時默認是進?delete釋放資源,這也就意味著如果不是new出來的資源,交給智能指 針管理,析構時就會崩潰。
//原生指針的空間、malloc來的空間、new[]出來的空間或者其他不是new出來的空間,都不能交給智能指針管理
shared_ptr<Date>sp1(new Date[10]);
面對這種情況,有兩種解決方案:
第一種:
//模板特化
shared_ptr<Date[]>sp2(new Date[10]);
第二種:
//定制刪除器struct Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}~Date(){cout << "析構" << endl;}int _year;int _month;int _day;
};class Fclose
{
public:void operator()(FILE*ptr){cout << "flcose()" << endl;fclose(ptr);}};template<class T>
void DeleteArrayFunc(T *ptr)
{cout << "函數指針" << endl;delete[] ptr;
}int main()
{//傳仿函數shared_ptr<FILE>sp3(fopen("智能指針.cpp","r"),Fclose());//傳lambdashared_ptr<FILE>sp4(fopen("智能指針.cpp", "r"), [](FILE* ptr) {cout << "fclose()" << endl;shared_ptr<Date>sp5(new Date[10], [](Date* ptr) {cout << "delete[]" << endl;delete[] ptr; })shared_ptr<Date>sp6(new Date[10], DeleteArrayFunc<Date>);fclose(ptr); });return 0;
}
unique_ptr定制刪除器智是在模板處,shared_ptr定制刪除器是在構造函數參數處
uniqueptr傳定制刪除器如果不想傳仿函數想以其他形式傳如刪除器的話,會很麻煩。 sharedptr定制刪除器的位置是在函數參數的位置,編譯器會自動推導類型。
//unique_ptr不以傳仿函數的方式傳入定制刪除器
//lambda
auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
unique_ptr<FILE, decltype(fcloseFunc)>up1(fopen("智能指針.cpp", "r"), fcloseFunc);
//函數指針
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr的循環引用
shared_ptr?多數情況下管理資源非常合適,?持RAII,也?持拷貝。但是在循環引用的場景下會 導致資源沒得到釋放造成內存泄漏,
- 如下圖所述場景:這樣循環引用的問題就會造成內存泄漏
- 把ListNode結構體中的_next和_prev改成weak_ptr,weak_ptr綁定到shared_ptr時不會增加它的引用計數,_next和_prev不參與資源釋放管理邏輯,就成功打破了循環引用,解決了這?的問題
weak_ptr
weakptr不?持RAII,也不?持訪問資源,所以我們看?檔發現weakptr構造時不?持綁定到資 源,只?持綁定到sharedptr,綁定到sharedptr時,不增加shared_ptr的引用計數,那么就可以 解決上述的循環引?問題。
-
weakptr也沒有重載operator*和operator->等,因為他不參與資源管理,那么如果他綁定的 sharedptr已經釋放了資源,那么他去訪問資源就是很危險的。
-
weakptr?持expired檢查指向的 資源是否過期,usecount也可獲取sharedptr的引?數,weakptr想訪問資源時,可以調用lock返回?個管理資源的sharedptr,如果資源已經被釋放,返回的sharedptr是?個空對象,如 果資源沒有釋放,則通過返回的shared_ptr訪問資源是安全的。
sharedptr中的count計數在sharedptr釋放時不會立即釋放,因為它還需要提供給weakptr使用,如果立即釋放了,就會造成weakptr野指針的情況。
weak_ptr中還有expired接口來檢查資源是否過期。
shared_ptr<string>sp1(new string("11111"));shared_ptr<string>sp2(sp1);weak_ptr<string>wp1 = sp1;cout << wp1.expired() << endl;cout << wp1.use_count() << endl;
如果shareptr的資源是weakptr所需要的,那么可以使用lock()接口在資源釋放前將鎖住鎖住。
鎖住資源實際上就是再用一個shared_ptr指針來管理該資源。