??小新課堂開課了,歡迎歡迎~??
🎈🎈養成好習慣,先贊后看哦~🎈🎈
所屬專欄:C++:由淺入深篇
小新的主頁:編程版小新-CSDN博客
引言:為什么引入智能指針?
1.C++手動釋放內存的痛點:
- 內存泄漏:忘記delete或異常導致未釋放。
- 野指針:訪問已經釋放的資源。
- 重復釋放:同一內存被釋放多次。
- 資源泄漏:不僅限于內存。
- 代碼復雜性與維護困難。
2.RAII(Resource Acquisition Is Initialization)原則:獲取資源即初始化。
- 核心思想:將資源的生命周期綁定到對象的生命周期。
- 對象構造時獲取資源,對象析構時自動釋放資源。
3.智能指針作為RAII的實踐者:
- 智能指針是類模板,封裝了原始指針,顧名思義就是比原始指針更智能。
- 通過重載運算符(->,*)模擬原始指針的行為。
- 核心價值:在析構函數中自動釋放管理的資源,確保資源安全釋放。
- 引如現代C++標準(auto_ptr的教訓與C++11的革新)。
一.智能指針的場景引入
在下面的程序中我們可以看到,new了以后,我們也delete了。但是new本身也有可能拋異常,如果是第一個那還好,array1未被成功分配,就無需釋放資源,異常被捕獲,無內存泄漏。但是如果第二個new失敗,array1成功分配內存,array2拋異常,如果不做特殊處理,異常被main函數的catch捕獲,array1的內存就泄漏了。在沒有學智能指針之前,我們是按如下方式解決的,但是這讓我們處理起來很麻煩。
double Divide(int a, int b)
{// 當b == 0時拋出異常if (b == 0){throw "Divide by zero condition!";} else{return (double)a / (double)b;}
}
void Func()
{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是一種管理資源的類的設計思想,本質是一種利用對象生命周期來代管(做到共同管理)獲取到的動態資源,避免資源泄漏,這里的資源可以是內存、文件指針、網絡連接、互斥鎖等等。
RAII在獲取資源時把資源委托給一個對象,接著控制對資源的訪問,資源在對象的生命周期內始終保持有效,最后在對象析構的時候釋放資源,這樣保障了資源的正常釋放,避免資源泄漏問題。
智能指針類除了滿足了RAII的設計思路,還要方便了資源的訪問,所以智能指針類還會像迭代器類一樣,重載 operator*/operator->/operator[] 等運算符,方便訪問資源。
下面我們就來看一下是怎么用智能智能解決上面new的問題的。
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;
};
double Divide(int a, int b)
{// 當b == 0時拋出異常if (b == 0){throw "Divide by zero condition!";} else{return (double)a / (double)b;}
} void Func()
{// 這里使用RAII的智能指針類管理new出來的數組以后,程序簡單多了//將資源的生命周期綁定到對象的生命周期//對象構造時獲取資源,對象析構時自動釋放資源SmartPtr<int> sp1 = new int[10];SmartPtr<int> sp2 = new int[10];for (size_t i = 0; i < 10; i++){sp1[i] = sp2[i] = i;} int len, time;cin >> len >> time;cout << Divide(len, time) << 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++標準庫智能指針的使用及原理
C++標準庫中的智能指針都在<memory>這個頭文件下面,我們包含<memory>就可以是使用了,智能指針有好幾種,除了weak_ptr他們都符合RAII和像指針一樣訪問的行為。
原理上而言主要是解決智能指針拷貝時的思路不同。
auto_ptr
auto_ptr - C++ Reference是C++98時設計出來的智能指針,他的特點是拷貝時把被拷貝對象的資源的管理權轉移給拷貝對象,這是一個非常糟糕的設計,因為他會導致被拷貝對象懸空,訪問報錯的問題。
struct Date
{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}~Date(){cout << "~Date()" << endl;}
};int main()
{auto_ptr<Date> ap1(new Date);// 拷貝時,管理權限轉移,被拷貝對象ap1懸空auto_ptr<Date> ap2(ap1);// 空指針訪問,ap1對象已經懸空//ap1->_year++;return 0;
}
**視頻演示**
auto_ptr屏幕錄制
**原理**
拷貝時,資源管理權轉移,ap2代管資源,被拷貝對象ap1懸空。
**模擬實現**
namespace xin
{template<class T>class auto_ptr{public:auto_ptr(T* ptr): _ptr(ptr){}auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){sp._ptr = nullptr;//管理權轉移}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (*this != ap){if (_ptr){//釋放當前資源delete _ptr;}//將ap的資源轉移給當前對象_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指針?樣使?T & operator*(){return *_ptr;} T* operator->(){return _ptr;}private:T* _ptr;};
}
unique_ptr
unique_ptr - C++ Reference是C++11設計出來的智能指針,他的名字翻譯出來是唯一的指針,他的特點的不支持拷貝,只支持移動。如果不需要拷貝的場景就非常建議使用他。
int main()
{unique_ptr<Date> up1(new Date);// 不支持拷貝//unique_ptr<Date> up2(up1);// 支持移動,但是移動后up1也懸空,所以使用移動要謹慎//因為移動構造有被掠奪資源的風險,這里默認是你知道//你自己move的,就說明你知道有風險,所有才說他們本質是設計思路的不同unique_ptr<Date> up3(move(up1));return 0;
}
**視屏演示**
unique_ptr
**原理**
unique_ptr不支持拷貝,只支持移動。
**模擬實現**
template<class T>
class unique_ptr
{
public:explicit unique_ptr(T* ptr)//不支持隱士類型轉化,避免原始指針隱士轉化為智能指針:_ptr(ptr){}~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}//不支持拷貝unique_ptr(const unique_ptr<T>& up) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;//支持移動unique_ptr(unique_ptr<T>&& up):_ptr(up._ptr){up._ptr = nullptr;}unique_ptr<T>& operator=( unique_ptr<T>&& up){delete _ptr;_ptr = up._ptr;up._ptr = nullptr;}T& operator*(){return *_ptr;}T& operator->(){return _ptr;}private:T* _ptr;};
shared_ptr
shared_ptr - C++ Reference是C++11設計出來的智能指針,他的名字翻譯出來是共享指針,他的特點是支持拷貝,也支持移動。如果需要拷貝的場景就需要使用他了。底層是用引用計數的方式實現的。
int main()
{shared_ptr<Date> sp1(new Date);// 支持拷貝shared_ptr<Date> sp2(sp1);shared_ptr<Date> sp3(sp2);cout << sp1.use_count() << endl;sp1->_year++;cout << sp1->_year << endl;cout << sp2->_year << endl;cout << sp3->_year << endl;// 支持移動,但是移動后sp1也懸空,所以使用移動要謹慎shared_ptr<Date> sp4(move(sp1));cout << sp4.use_count() << endl;return 0;
}
**視屏演示**
shared_ptr
**運行結果**
**原理**
他的特點是支持拷貝,也支持移動,底層是用引用計數的方式實現的。
引用計數就是統計有幾個智能智能共同管理這塊資源的,一個資源對應一個引用計數,不是sp1有一個自己的引用計數,sp2有一個自己的引用計數這種。看了圖片大家就大概知道怎么理解引用計數了。這個跟操作系統里的文件系統里的硬鏈接,軟鏈接計算引用計數那個挺像的。
智能指針析構時默認是用delete釋放資源,這也就意味著如果不是new出來的資源,交給智能指針管理,析構時就會崩潰。但是因為new []經常使用,為了簡潔一點,unique_ptr和shared_ptr都特化了一份[]的版本。
int main()
{//這樣實現程序會崩潰/*unique_ptr<Date> up1(new Date[10]);shared_ptr<Date> sp1(new Date[10]);*/// 解決?案1// 因為new[]經常使?,所以unique_ptr和shared_ptr// 實現了?個特化版本,這個特化版本析構時用的delete[]unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]);return 0;
}
智能指針支持在構造時給個刪除器,所謂刪除器本質就是一個可調用對象,這個可調用對象中實現你想要的釋放資源的方式,當構造智能指針時,給了定制的刪除器,在智能指針析構時就會調用刪除器去釋放資源。
template<class T>
void DeleteArrayFunc(T* ptr)
{delete[] ptr;
}template<class T>
class DeleteArray
{public :void operator()(T* ptr){delete[] ptr;}
};
class Fclose
{public :void operator()(FILE* ptr){cout << "fclose:" << ptr << endl;fclose(ptr);}
};int main()
{// 解決方案2// 仿函數對象做刪除器// unique_ptr和shared_ptr支持刪除器的方式有所不同// unique_ptr是在類模板參數支持的,shared_ptr是構造函數參數支持的// unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());// 這里沒有使用相同的方式還是挺坑的// 使用仿函數unique_ptr可以不在構造函數傳遞,因為仿函數類型構造的對象直接就可以調用// 但是下面的函數指針和lambda的類型不可以unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);//可以不在構造函數傳遞shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());//在構造函數傳遞// 函數指針做刪除器unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);// lambda表達式做刪除器auto delArrOBJ = [](Date* ptr) {delete[] ptr; };//我們無法知道lambda的類型unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);////但是這里要顯示傳類型,就用了decltype,其作用是查詢表達式的類型shared_ptr<Date> sp4(new Date[5], delArrOBJ);// 實現其他資源管理的刪除器shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {cout << "fclose:" << ptr << endl;fclose(ptr);});return 0;
}
shared_ptr 除了支持用指向資源的指針構造,還支持?make_shared 用初始化資源對象的值直接構造。
shared_ptr 和 unique_ptr 都支持了operator bool的類型轉換,如果智能指針對象是一個空對象沒有管理資源,則返回false,否則返回true,意味著我們可以直接把智能指針對象給if判斷是否為空。
int main()
{shared_ptr<Date> sp1(new Date(2024, 9, 11));shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);auto sp3 = make_shared<Date>(2024, 9, 11);shared_ptr<Date> sp4;//支持無參構造// if (sp1.operator bool())if (sp1)cout << "sp1 is not nullptr" << endl;if (!sp4)cout << "sp4 is nullptr" << endl;// 報錯 因為它們的構造函數都不支持隱士類型轉化//shared_ptr<Date> sp5 = new Date(2024, 9, 11);//unique_ptr<Date> sp6 = new Date(2024, 9, 11);return 0;
}
**模擬實現**
下面的代碼中使用了atomic<int>而不是普通的int是為了實現線程安全的引用計數,后面會更詳細介紹。注意這里是不能用static的,static成員是所有同一類型實例共享的,而不是每個資源獨立的。
template<class T>
class shared_ptr
{
public:explicit shared_ptr(T* ptr = nullptr )//標準庫里支持無參構造:_ptr(ptr),_pcount(new atomic<int>(1))//_pcount(new int(1)){}template<class D>shared_ptr(T* ptr ,D del):_ptr(ptr),_pcount(new int(1)),_del(del){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount), _del(sp._del){++(*_pcount);}void release(){if (--(*_pcount)==0){//最后一個管理的對象,釋放資源_del(_ptr);delete _pcount;_ptr = nullptr;_pcount = nullptr;}}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);_del = sp._del;}return *this;}~shared_ptr(){release();}T* get() const{return _ptr;}int use_count() const{return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;atomic<int>* _pcount; //原子操作//int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };//包裝器來包裝刪除器,默認使用lambda
};
四.循環引用和weak_ptr
shared_ptr導致的循環引用問題
shared_ptr大多數情況下管理資源非常合適,支持RAII,也支持拷貝。但是在循環引用的場景下會導致資源沒得到釋放內存泄漏,所以我們要認識循環引用的場景和資源沒釋放的原因,并且學會使用weak_ptr解決這種問題。
struct ListNode
{int _data;std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{// 循環引? -- 內存泄露shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}
沒有析構,內存泄漏。
如上圖所述場景,n1和n2析構后,管理兩個節點的引用計數減到1
1. 右邊的節點什么時候釋放呢,左邊節點中的_next管著呢,_next析構后,右邊的節點就釋放了。
2. _next什么時候析構呢,_next是左邊節點的的成員,左邊節點釋放,_next就析構了。
3. 左邊節點什么時候釋放呢,左邊節點由右邊節點中的_prev管著呢,_prev析構后,左邊的節點就釋放了。
4. _prev什么時候析構呢,_prev是右邊節點的成員,右邊節點釋放,_prev就析構了。
? 至此邏輯上成功形成回旋鏢似的循環引用,誰都不會釋放就形成了循環引用,導致內存泄漏。
weak_ptr版本:
struct ListNode
{int _data;// 這?改成weak_ptr,當n1->_next = n2;綁定shared_ptr時// 不增加n2的引用計數,不參與資源釋放的管理,就不會形成循環引用了std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{// 循環引? -- 內存泄露std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}
weak_ptr
weak_ptr - C++ Reference是C++11設計出來的智能指針,他的名字翻譯出來是弱指針,他完全不同于上面的智能指針,他不支持RAII,也就意味著不能用它直接管理資源。
weak_ptr構造時不支持綁定到資源,只支持綁定到shared_ptr,綁定到shared_ptr時,不增加shared_ptr的引用計數,那么就可以解決上述的循環引用問題。
int main()
{shared_ptr<string> sp1(new string("111111"));shared_ptr<string> sp2(sp1);weak_ptr<string> wp = sp1;cout << wp.expired() << endl;cout << wp.use_count() << endl;// sp1和sp2都指向了其他資源,則weak_ptr就過期了sp1 = make_shared<string>("222222");cout << wp.expired() << endl;cout << wp.use_count() << endl;sp2 = make_shared<string>("333333");cout << wp.expired() << endl;cout << wp.use_count() << endl;return 0;
}
**原理**
**模擬實現**
template<class T>
class weak_ptr
{
public:weak_ptr(){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}private:T* _ptr = nullptr;
};
我們這里實現的shared_ptr和weak_ptr都是以最簡潔的方式實現的, 只能滿足基本的功能,這里的weak_ptr lock等功能是無法實現的,想要實現就要/把shared_ptr和weak_ptr一起改了,把引用計數拿出來放到一個單獨類型,shared_ptr 和weak_ptr都要存儲指向這個類的對象才能實現,有興趣可以去翻翻源代碼。
五.shared_ptr的線程安全問題
還記得我們在上面shared_ptr的模擬實現部分使用的atomic。原子操作(atomic operation)指的是在多線程環境下不會被中斷的操作。這里的atomic<int>是C++11引入的原子類型,用于保證對引用計數的增減操作是原子性的,從而使得shared_ptr的引用計數在多線程環境下是線程安全的,當然這個也可以用加鎖來實現。這個和操作系統處理訪問臨界資源的原理高度相似。
簡單來說,就是shared_ptr的引用計數本身是線程安全的,但是shared_ptr管理的對象本身并不是線程安全的。因為多個線程同時修改同一個shared_ptr管理的對象時,需要額外的同步措施。
創作不易,還請各位大佬支持~