目錄
1. 智能指針的使?場景分析
2. RAII和智能指針的設計思路
3. C++標準庫智能指針的使?
4. 智能指針的原理
5.2 weak_ptr
7. C++11和boost中智能指針的關系
8. 內存泄漏
8.1 什么是內存泄漏,內存泄漏的危害
8.2?如何避免內存泄漏
1. 智能指針的使?場景分析
下?程序中我們可以看到,new了以后,我們也delete了,但是因為拋異常導,后?的delete沒有得到執?,所以就內存泄漏了,所以我們需要new以后捕獲異常,捕獲到異常后delete內存,再把異常拋出,但是因為new本?也可能拋異常,連續的兩個new和下?的Divide都可能會拋異常,讓我們處理起來很?煩。智能指針放到這樣的場景??就讓問題簡單多了。
#include <iostream>
using namespace std;
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錯誤拋出異常,另外下?的array1和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;
}
2. RAII和智能指針的設計思路
? RAII是Resource Acquisition Is Initialization的縮寫,它是?種管理資源的類的設計思想,本質是?種利?對象?命周期來管理獲取到的動態資源,避免資源泄漏,這?的資源可以是內存、?件指針、?絡連接、互斥鎖等等。RAII在獲取資源時把資源委托給?個對象,接著控制對資源的訪問,資源在對象的?命周期內始終保持有效,最后在對象析構的時候釋放資源,這樣保障了資源的正常釋放,避免資源泄漏問題。
? 智能指針類除了滿?RAII的設計思路,還要?便資源的訪問,所以智能指針類還會想迭代器類?樣,重載 operator*/operator->/operator[] 等運算符,?便訪問資源。
#include <iostream>
using namespace std;
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;
}
3. C++標準庫智能指針的使?
? C++標準庫中的智能指針都在<memory>這個頭?件下?,我們包含<memory>就可以是使?了,智能指針有好?種,除了weak_ptr它們都符合RAII和像指針?樣訪問的?為,原理上??主要是解決智能指針拷?時的思路不同。
? auto_ptr是C++98時設計出來的智能指針,它的特點是拷?時把被拷?對象的資源的管理權轉移給拷?對象,這是?個?常糟糕的設計,因為它會到被拷?對象懸空,產生訪問報錯的問題,C++11設計出新的智能指針后,強烈建議不要使?auto_ptr。其他C++11出來之前很多公司也是明令禁?使?這個智能指針的。
? unique_ptr是C++11設計出來的智能指針,它的名字翻譯出來是唯?指針,它的特點的不?持拷?,只?持移動。如果不需要拷?的場景就?常建議使?。
? shared_ptr是C++11設計出來的智能指針,它的名字翻譯出來是共享指針,它的特點是?持拷?,也?持移動。如果需要拷?的場景就需要使?它了。底層是?引?計數的?式實現的。
? weak_ptr是C++11設計出來的智能指針,它的名字翻譯出來是弱指針,它完全不同于上?的智能指針,他不?持RAII,也就意味著不能?它直接管理資源,weak_ptr的產?本質是要解決shared_ptr的?個循環引?導致內存泄漏的問題。具體細節下?我們再細講。
? 智能指針析構時默認是進?delete釋放資源,這也就意味著如果不是new出來的資源,交給智能指針管理,析構時就會崩潰。智能指針?持在構造時給?個刪除器,所謂刪除器本質就是?個可調?對象,這個可調?對象中實現你想要的釋放資源的?式,當構造智能指針時,給了定制的刪除器, 在智能指針析構時就會調?刪除器去釋放資源。因為new[]經常使?,所以為了簡潔?點, unique_ptr和shared_ptr都特化了?份[]的版本,使?時 unique_ptr<Date[]> up1(new Date[5]) ; shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的資源。
? unique_ptr和shared_ptr?持刪除器的?式有所不同:unique_ptr是在類模板參數?持的,shared_ptr是構造函數參數?持的。
? template <class T, class... Args> shared_ptr<T> make_shared (Args&&... args);
? shared_ptr 除了?持?指向資源的指針構造,還?持make_shared ?初始化資源對象的值直接構造。
? shared_ptr 和 unique_ptr 都?持了 operator bool的類型轉換,如果智能指針對象是?個空對象沒有管理資源,則返回false,否則返回true,意味著我們可以直接把智能指針對象給if判斷是否為空。
? shared_ptr 和 unique_ptr 都得構造函數都使?explicit修飾,防?普通指針隱式類型轉換成智能指針對象。
#include <iostream>
#include <memory>
using namespace std;
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);// 空指針訪問error,ap1對象已經懸空//ap1->_year++;unique_ptr<Date> up1(new Date);// 不?持拷?//unique_ptr<Date> up2(up1);// ?持移動,但是移動后up1也懸空,所以使?移動要謹慎unique_ptr<Date> up3(move(up1));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));return 0;
}
unique_ptr定制刪除器 建議仿函數 ;shared_ptr定制刪除器 都可以 相對建議lambda
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <memory>
using namespace std;
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;}
};
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()
{// 這樣實現程序會崩潰// 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]);// 解決?案2// 仿函數對象做刪除器// unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());// unique_ptr和shared_ptr?持刪除器的?式有所不同// unique_ptr是在類模板參數?持的,shared_ptr是構造函數參數?持的// 這?沒有使?相同的?式還是挺坑的// 使?仿函數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; };//decltype與auto關鍵字一樣,用于進行編譯時類型推導,//總是以一個普通表達式作為參數,返回該表達式的類型,//而且decltype并不會對表達式進行求值unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);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;
}
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 << "sp1 is nullptr" << endl;//shared_ptr 和 unique_ptr 都得構造函數都使?explicit修飾//防?普通指針隱式類型轉換成智能指針對象//不存在從"Date*"轉換到"std:shared_ptr<Date>"的適當構造函數//報錯shared_ptr<Date> sp5 = new Date(2024, 9, 11);//不存在從"Date*"轉換到"std:unique_ptr<Date,std::default_delete < Date >> "的適當構造函數unique_ptr<Date> sp6 = new Date(2024, 9, 11);return 0;
}
4. 智能指針的原理
? 下?我們模擬實現了auto_ptr和unique_ptr的核?功能,這兩個智能指針的實現?較簡單,我們了解?下原理即可。auto_ptr的思路是拷?時轉移資源管理權給被拷?對象,這種思路是不被認可的,也不建議使?。unique_ptr的思路是不?持拷?。
? 我們重點要看看shared_ptr是如何設計的,尤其是引?計數的設計,主要這??份資源就需要?個引?計數,所以引?計數采?靜態成員的?式是?法實現的,要使?堆上動態開辟的?式,構造智能指針對象時來?份資源,就要new?個引?計數出來。多個shared_ptr指向資源時就++引?計數,shared_ptr對象析構時就--引?計數,引?計數減到0時代表當前析構的shared_ptr是最后?個管理資源的對象,則析構資源。

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
#include<memory>
#include<functional>
#include<atomic>
using namespace std;
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;}
};
namespace sy
{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}//p2(p1)//拷?時把被拷?對象的資源的管理權轉移給拷?對象auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){// 管理權轉移sp._ptr = nullptr;}// p1 = p2;auto_ptr<T>& operator=(auto_ptr<T>& ap){// 檢測是否為??給??賦值if (this != &ap){// 釋放當前對象中資源if (_ptr)delete _ptr;// 轉移ap中資源到當前對象中_ptr = ap._ptr;ap._ptr = NULL;}return *this;}~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指針?樣使?T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};template<class T>class unique_ptr{public://explicit 防?普通指針隱式類型轉換成智能指針對象explicit unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr() {if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指針?樣使?T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//不?持拷?,只?持移動unique_ptr(const unique_ptr<T>& sp) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;unique_ptr(unique_ptr<T>&& sp):_ptr(sp._ptr){sp._ptr = nullptr;}unique_ptr<T>& operator=(unique_ptr<T>&& sp){delete _ptr;_ptr = sp._ptr;sp._ptr = nullptr;}private:T* _ptr;};template<class T>class shared_ptr{public:explicit shared_ptr(T* ptr = nullptr): _ptr(ptr), _pcount(new atomic<int>(1)){}//刪除器template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new atomic<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;//int* _pcount;atomic<int>* _pcount; // 原子操作function<void(T*)> _del = [](T* ptr) {delete ptr; }; };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;};
}int main()
{sy::auto_ptr<Date> ap1(new Date);// 拷?時,管理權限轉移,被拷?對象ap1懸空sy::auto_ptr<Date> ap2(ap1);// 空指針訪問,ap1對象已經懸空 error//ap1->_year++;sy::unique_ptr<Date> up1(new Date);// 不?持拷?//unique_ptr<Date> up2(up1);// ?持移動,但是移動后up1也懸空,所以使?移動要謹慎sy::unique_ptr<Date> up3(move(up1));sy::shared_ptr<Date> sp1(new Date);// ?持拷? 也支持移動sy::shared_ptr<Date> sp2(sp1);sy::shared_ptr<Date> sp3(sp2);cout << sp1.use_count() << endl;sp1->_year++;cout << sp1->_year << endl;cout << sp2->_year << endl;cout << sp3->_year << endl;return 0;
}
5. shared_ptr和weak_ptr
5.1 shared_ptr循環引?問題
? shared_ptr?多數情況下管理資源?常合適,?持RAII,也?持拷?。但是在循環引?的場景下會導致資源沒得到釋放內存泄漏,所以我們要認識循環引?的場景和資源沒釋放的原因,并且學會使?weak_ptr解決這種問題。
? 如下圖所述場景,n1和n2析構后,管理兩個節點的引?計數減到1。
1. 右邊的節點什么時候釋放呢,左邊節點中的_next管著,_next析構后,右邊的節點就釋放了。
2. _next什么時候析構呢,_next是左邊節點的的成員,左邊節點釋放,_next就析構了。
3. 左邊節點什么時候釋放呢,左邊節點由右邊節點中的_prev管著呢,_prev析構后,左邊的節點就釋放了。
4. _prev什么時候析構呢,_prev是右邊節點的成員,右邊節點釋放,_prev就析構了。
? ?此邏輯上成功形成回旋鏢似的循環引?,誰都不會釋放就形成了循環引?,導致內存泄漏。
? 把ListNode結構體中的_next和_prev改成weak_ptr,weak_ptr綁定到shared_ptr時不會增加它的引?計數,_next和_prev不參與資源釋放管理邏輯,就成功打破了循環引?,解決了這?的問題。

weak_ptr是專?綁定shared_ptr,不增加它的引?計數,作為?些場景的輔助管理。
struct ListNode
{int _data;/*std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;*/// 這?改成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;// weak_ptr不?持管理資源,不?持RAII// weak_ptr是專?綁定shared_ptr,不增加他的引?計數,作為?些場景的輔助管理//std::weak_ptr<ListNode> wp(new ListNode);return 0;
}
5.2 weak_ptr
? weak_ptr不?持RAII,也不?持訪問資源,所以我們看?檔發現weak_ptr構造時不?持綁定到資源,只?持綁定到shared_ptr,綁定到shared_ptr時,不增加shared_ptr的引?計數,那么就可以解決上述的循環引?問題。
? weak_ptr也沒有重載operator*和operator->等,因為它不參與資源管理,那么如果它綁定的shared_ptr已經釋放了資源,那么它去訪問資源就是很危險的。weak_ptr?持expired檢查指向的資源是否過期,use_count也可獲取shared_ptr的引?計數,weak_ptr想訪問資源時,可以調?lock(鎖)返回?個管理資源的shared_ptr,如果資源已經被釋放,返回的shared_ptr是?個空對象,如果資源沒有釋放,則通過返回的shared_ptr訪問資源是安全的。
//過期為非0 1
int main()
{std::shared_ptr<string> sp1(new string("111111"));std::shared_ptr<string> sp2(sp1);std::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;wp = sp1;//std::shared_ptr<string> sp3 = wp.lock();auto sp3 = wp.lock();cout << wp.expired() << endl;cout << wp.use_count() << endl;*sp3 += "###";cout << *sp1 << endl;return 0;
}
6. shared_ptr的線程安全問題
? shared_ptr的引?計數對象在堆上,如果多個shared_ptr對象在多個線程中,進?shared_ptr的拷?析構時會訪問修改引?計數,就會存在線程安全問題,所以shared_ptr引?計數是需要加鎖或者原?操作保證線程安全的。
? shared_ptr指向的對象也是有線程安全的問題的,但是這個對象的線程安全問題不歸shared_ptr管,它也管不了,應該有外層使?shared_ptr的?進?線程安全的控制。
? 下?的程序會崩潰或者A資源沒釋放,sy::shared_ptr引?計數從int*改成atomic<int>*就可以保證引?計數的線程安全問題,或者使?互斥鎖加鎖也可以。
#include<thread>
#include<mutex>
struct AA
{int _a1 = 0;int _a2 = 0;~AA(){cout << "~AA()" << endl;}
};
int main()
{sy::shared_ptr<AA> p(new AA);const size_t n = 100000;mutex mtx;auto func = [&](){for (size_t i = 0; i < n; ++i){// 這?智能指針拷?會++計數sy::shared_ptr<AA> copy(p);{unique_lock<mutex> lk(mtx);copy->_a1++;copy->_a2++;}}};thread t1(func);thread t2(func);t1.join();t2.join();cout << p->_a1 << endl;cout << p->_a2 << endl;cout << p.use_count() << endl;return 0;
}
7. C++11和boost中智能指針的關系
? Boost庫是為C++語?標準庫提供擴展的?些C++程序庫的總稱,Boost社區建?的初衷之?就是為C++的標準化?作提供可供參考的實現,Boost社區的發起?Dawes本?就是C++標準委員會的成員之?。在Boost庫的開發中,Boost社區也在這個?向上取得了豐碩的成果,C++11及之后的新語法和庫有很多都是從Boost中來的。
? C++ 98 中產?了第?個智能指針auto_ptr。
? C++ boost給出了更實?的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等。
? C++ TR1,引?了shared_ptr等,不過注意的是TR1并不是標準版。
? C++ 11,引?了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr對應boost的scoped_ptr。并且這些智能指針的實現原理是參考boost中的實現的。
8. 內存泄漏
8.1 什么是內存泄漏,內存泄漏的危害
什么是內存泄漏:內存泄漏指因為疏忽或錯誤造成程序未能釋放已經不再使?的內存,?般是忘記釋放或者發?異常釋放程序未能執?導致的。內存泄漏并不是指內存在物理上的消失,?是應?程序分配某段內存后,因為設計錯誤,失去了對該段內存的控制,因?造成了內存的浪費。
內存泄漏的危害:普通程序運??會就結束了出現內存泄漏問題也不?,進程正常結束,?表的映射關系解除,物理內存也可以釋放。?期運?的程序出現內存泄漏,影響很?,如操作系統、后臺服務、?時間運?的客?端等等,不斷出現內存泄漏會導致可?內存不斷變少,各種功能響應越來越慢,最終卡死。
int main()
{// 申請?個1G未釋放,這個程序多次運?也沒啥危害// 因為程序?上就結束,進程結束各種資源也就回收了char* ptr = new char[1024 * 1024 * 1024];cout << (void*)ptr << endl;return 0;
}
8.2?如何避免內存泄漏
? ?程前期良好的設計規范,養成良好的編碼規范,申請的內存空間記著匹配的去釋放。ps:這個理想狀態。但是如果碰上異常時,就算注意釋放了,還是可能會出問題。需要下?條智能指針來管理才有保證。
? 盡量使?智能指針來管理資源,如果??場景?較特殊,采?RAII思想??造個輪?管理。