@TOC

零、前言
本章主要講解學習C++中智能指針的概念及使用
一、為什么需要智能指針
示例:
double Division(int a, int b) { // 當b == 0時拋出異常 if (b == 0) { throw "Division by zero condition!"; } return (double)a / (double)b; } void Func() { //如果發生除0錯誤拋出異常在外部進行捕獲,那么下面的array沒有得到釋放 int* array = new int[10]; int len, time; cin >> len >> time; cout << Division(len, time) << endl; cout << "delete []" << array << endl; delete[] array; } int main() { try { Func(); } catch (const char* errmsg) { cout << errmsg << endl; } return 0; }
效果:
異常安全問題:
如果在malloc和free之間如果存在拋異常,那么還是有內存泄漏
一般解決辦法:重新拋出時處理
double Division(int a, int b) { // 當b == 0時拋出異常 if (b == 0) { throw "Division by zero condition!"; } return (double)a / (double)b; } void Func() { // 這里捕獲異常后并不處理異常,異常還是交給外面處理 // 這里捕獲了再重新拋出去 int* array = new int[10]; try { int len, time; cin >> len >> time; cout << Division(len, time) << endl; } catch (...) { cout << "delete []" << array << endl; delete[] array; throw; } // ... cout << "delete []" << array << endl; delete[] array; } int main() { try { Func(); } catch (const char* errmsg) { cout << errmsg << endl; } return 0; }
注:這種方式比較麻煩,不實用,由此引入了智能指針
二、內存泄漏
什么是內存泄漏:
內存泄漏指因為疏忽或錯誤造成程序未能釋放已經不再使用的內存的情況。內存泄漏并不是指內存在物理上的消失,而是應用程序分配某段內存后,因為設計錯誤,失去了對該段內存的控制,因而造成了內存的浪費
內存泄漏的危害:
長期運行的程序出現內存泄漏,影響很大,如操作系統、后臺服務等等,出現內存泄漏會導致響應越來越慢,最終卡死
C/C++程序中一般我們關心兩種方面的內存泄漏:
堆內存泄漏:
堆內存指的是程序執行中依據須要分配通過malloc / calloc / realloc / new等從堆中分配的一塊內存,用完后必須通過調用相應的 free或者delete 刪掉。假設程序的設計錯誤導致這部分內存沒有被釋放,那么以后這部分空間將無法再被使用,就會產生Heap Leak
系統資源泄漏:
指程序使用系統分配的資源,比方套接字、文件描述符、管道等沒有使用對應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能減少,系統執行不穩定
如何避免內存泄漏:
工程前期良好的設計規范,養成良好的編碼規范,申請的內存空間記著匹配的去釋放。ps:這個理想狀態。但是如果碰上異常時,就算注意釋放了,還是可能會出問題
采用RAII思想或者智能指針來管理資源
有些公司內部規范使用內部實現的私有內存管理庫。這套庫自帶內存泄漏檢測的功能選項
出問題了使用內存泄漏工具檢測。ps:不過很多工具都不夠靠譜,或者收費昂貴
總結:
內存泄漏非常常見,解決方案分為兩種:
1、事前預防型。如智能指針等
2、事后查錯型。如泄漏檢測工具
三、智能指針
1、RAII
概念及介紹:
RAII(Resource Acquisition Is Initialization)是一種利用對象生命周期來控制程序資源(如內存、文件句柄、網絡連接、互斥量等等)的簡單技術
在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最后在對象析構的時候釋放資源。即我們實際上把管理一份資源的責任托管給了一個對象
好處:
不需要顯式地釋放資源
對象所需的資源在其生命期內始終保持有效
示例:
// 使用RAII思想設計的SmartPtr類 template class SmartPtr { public: SmartPtr(T* ptr = nullptr) : _ptr(ptr) {} ~SmartPtr() { if (_ptr) delete _ptr; } private: T* _ptr; };
注意:
RAII思想除了可以用來設計智能指針,還可以用來設計守衛鎖,防止異常安全導致的死鎖問題
C++11中引入了lock_guard和unique_lock來解決死鎖的問題
大部分情況下,兩者的功能是一樣的,不過unique_lock比lock_guard更靈活:unique_lock提供了lock, unlock, try_lock等接口. lock_guard沒有多余的接口;構造函數時拿到鎖,析構函數時釋放鎖 lock_guard比unique_lock要省時
模擬實現lock_guard:
template class LockGuard { public: LockGuard(Mutex& mtx) :_mutex(mtx) { _mutex.lock(); } ~LockGuard() { _mutex.unlock(); } LockGuard(const LockGuard&) = delete; private: // 注意這里必須使用引用,否則鎖的就不是一個互斥量對象 Mutex& _mutex; };
2、智能指針的原理
上述的SmartPtr還不能將其稱為智能指針,因為它還不具有指針的行為
指針可以解引用,也可以通過->去訪問所指空間中的內容,因此模板類中還得需要將***** 、**->**重載下,才可讓其像指針一樣去使用
示例:
template class SmartPtr { public: SmartPtr(T* ptr = nullptr) : _ptr(ptr) {} ~SmartPtr() { if (_ptr) delete _ptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };
智能指針的原理:
RAII特性(構造時初始化,析構時釋放)
重載operator*和opertaor->,具有像指針一樣的行為
3、std::auto_ptr
概念及介紹:
C++98版本的庫中就提供了auto_ptr的智能指針
auto_ptr的實現原理:管理權轉移的思想,即當拷貝和賦值時將智能指針管理的內存地址進行轉移,也就是一份空間內存只有一個智能指針進行管理
示例:
class Date { public: Date() { cout << "Date()" << endl; } ~Date() { cout << "~Date()" << endl; } int _year; int _month; int _day; }; int main() { auto_ptr ap(new Date); auto_ptr copy(ap); // auto_ptr的問題:當對象拷貝或者賦值后,前面的對象就懸空了 // C++98中設計的auto_ptr問題是非常明顯的,所以實際中很多公司明確規定了不能使用auto_ptr ap->_year = 2018; return 0; }
效果:
模擬實現:
//auto_ptr:管理權轉移-存在問題 template class auto_ptr { public: auto_ptr(T* ptr) :_ptr(ptr) {} auto_ptr(auto_ptr& ap) :_ptr(ap._ptr)// 轉移資源 { ap._ptr = nullptr; } auto_ptr& operator=(auto_ptr& ap) { if (this != &ap)//防止自我賦值 { cout << "~auto_ptr:" << _ptr << endl; delete _ptr; _ptr = ap._ptr; ap._ptr = nullptr; } return *this; } ~auto_ptr() { if (_ptr) { cout << "~auto_ptr:" << _ptr << endl; delete _ptr; } } T& operator*() { return *_ptr; } T* operator->() { return &_ptr; } private: T* _ptr; };
4、std::unique_ptr

概念及介紹:
C++11中開始提供更靠譜的unique_ptr
unique_ptr的實現原理:簡單粗暴的防拷貝,即只有一個智能指針管理資源,并且不會發生拷貝和賦值
示例:
int main() { unique_ptr up(new Date); // unique_ptr的設計思路非常的粗暴-防拷貝,也就是不讓拷貝和賦值。 unique_ptr copy(up);//error return 0; }
效果:
模擬實現:
//unique_ptr:不存在拷貝和賦值-沒有管理權的轉移 template class unique_ptr { public: unique_ptr(T* ptr) :_ptr(ptr) {} unique_ptr(unique_ptr& ap) = delete; unique_ptr& operator=(unique_ptr& ap) = delete; ~unique_ptr() { if (_ptr) { cout << "~unique_ptr:" << _ptr << endl; delete _ptr; } } T& operator*() { return *_ptr; } T* operator->() { return &_ptr; } private: T* _ptr; };
注:C++98防拷貝的方式:只聲明不實現+聲明成私有;C++11防拷貝的方式修飾函數為delete
5、std::shared_ptr
概念及介紹:
C++11中開始提供更靠譜的并且支持拷貝的shared_ptr
shared_ptr的原理:是通過引用計數的方式來實現多個shared_ptr對象之間共享資源,只有最后一個智能指針析構才進行資源的釋放
注意:
shared_ptr在其內部,給每個資源都維護了著一份計數,用來記錄該份資源被幾個對象共享
在對象被銷毀時(也就是析構函數調用),就說明自己不使用該資源了,對象的引用計數減一
如果引用計數是0,就說明自己是最后一個使用該資源的對象,必須釋放該資源
如果不是0,就說明除了自己還有其他對象在使用該份資源,不能釋放該資源,否則其他對象就成野指針了
由于資源共享,需要使用引用計數,也就是計數也是共享的,那么對計數的操作需要保證原子性,否則會造成數據混亂
示例:
int main() { // shared_ptr通過引用計數支持智能指針對象的拷貝 shared_ptr sp(new Date); shared_ptr copy(sp); cout << "ref count:" << sp.use_count() << endl; cout << "ref count:" << copy.use_count() << endl; return 0; }
效果:
模擬實現:
//shared_ptr:多個智能指針管理一個內存資源,最后一個智能指針釋放內存 template class shared_ptr { public: explicit shared_ptr(T* ptr) :_ptr(ptr) , _pCount(new int(1)) , _mtx(new mutex) {} void add_pCount() { // 加鎖或者使用加1的原子操作 _mtx->lock(); ++(*_pCount); _mtx->unlock(); } void release_pCount() { // 引用計數減1,如果減到0,則釋放資源 bool flg = false; _mtx->lock(); --(*_pCount); if ((*_pCount) == 0 && _ptr) { cout << "~shared_ptr:" << _ptr << endl; D del; delete _ptr; delete _pCount; flg = true; } _mtx->unlock(); if (flg) delete _mtx; } shared_ptr(shared_ptr& sp) :_ptr(sp._ptr) , _pCount(sp._pCount) , _mtx(sp._mtx) { add_pCount(); } //shared_ptr& operator=(shared_ptr& sp) //{ // if (_ptr != sp._ptr)//管理資源地址相同則不用處理 // { // release_pCount(); // _ptr = sp._ptr; // _pCount = sp._pCount; // _mtx = sp._mtx; // add_pCount(); // } // return *this; //} void swap(shared_ptr& sp) { std::swap(_ptr, sp._ptr); std::swap(_pCount, sp._pCount); std::swap(_mtx, sp._mtx); } //現代式寫法 shared_ptr& operator=(shared_ptr sp) { swap(sp); return *this; } ~shared_ptr() { release_pCount(); } T& operator*() { return *_ptr; } T* operator->() { return &_ptr; } T* get() { return _ptr; } size_t use_count() { return *_pCount; } private: T* _ptr;//管理的內存資源 int* _pCount;//計數 mutex* _mtx;//多線程互斥 //堆上開辟-多個智能指針共享計數和互斥鎖 };
shared_ptr的線程安全分為兩方面:
智能指針對象中引用計數是多個智能指針對象共享的,引用計數同時++或–操作不是原子的,存在線程數據安全問題,會導致資源未釋放或者程序崩潰的問題,即內部計數操作需要加鎖
智能指針管理的對象存放在堆上,兩個線程中同時去訪問,會導致線程安全問題,即調用指針對象去訪問資源時需要自行保證訪問的互斥,確保線程安全
示例:
// 1.演示引用計數線程安全問題,就把AddRefCount和SubRefCount中的鎖去掉 // 2.演示可能不出現線程安全問題,因為線程安全問題是偶現性問題,main函數的n改大一些概率就變大了,就容易出現了。 // 3.下面代碼我們使用SharedPtr演示,是為了方便演示引用計數的線程安全問題,將代碼中的SharedPtr換成shared_ptr進行測試,可以驗證庫的shared_ptr,發現結論是一樣的 void SharePtrFunc(SharedPtr& sp, size_t n) { cout << sp.Get() << endl; for (size_t i = 0; i < n; ++i) { // 這里智能指針拷貝會++計數,智能指針析構會--計數,這里是線程安全的。 SharedPtr copy(sp); // 這里智能指針訪問管理的資源,不是線程安全的。所以我們看看這些值兩個線程++了2n次,但是最終看到的結果,并一定是加了2n copy->_year++; copy->_month++; copy->_day++; } } int main() { SharedPtr p(new Date); cout << p.Get() << endl; const size_t n = 100; thread t1(SharePtrFunc, p, n); thread t2(SharePtrFunc, p, n); t1.join(); t2.join(); cout << p->_year << endl; cout << p->_month << endl; cout << p->_day << endl; return 0; }
6、std::weak_ptr
概念及引入:
一般來說shared_ptr可以滿足資源管理的大部分情況,但是也有些情況是shared_ptr不能處理的,這時候就需要使用weak_ptr
示例:std::shared_ptr的循環引用
struct ListNode { int _data; shared_ptr _prev; shared_ptr _next; ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { shared_ptr node1(new ListNode); shared_ptr node2(new ListNode); cout << node1.use_count() << endl; cout << node2.use_count() << endl; node1->_next = node2; node2->_prev = node1; cout << node1.use_count() << endl; cout << node2.use_count() << endl; return 0; }
效果:
循環引用分析:
node1和node2兩個智能指針對象指向兩個節點,引用計數變成1,我們不需要手動delete
node1的_ next指向node2,node2的_prev指向node1,引用計數變成2
node1和node2析構,引用計數減到1,但是_ next還指向下一個節點,_ prev還指向上一個節點
也就是說_ next析構了,node2的計數減到0,node2就釋放了;_prev析構了,node1就釋放了
但是_ next屬于node1的成員,node1釋放了,_ next才會析構,而node1由_ prev管理,_prev屬于node2的成員,所以這就叫循環引用,誰也不會釋放
示圖:
解決方案:
在引用計數的場景下,把節點中的_ prev和_ next改成weak_ptr就可以了
weak_ptr原理:
node1->_ next = node2;和node2->_ prev = node1;時weak_ptr的_ next和_ prev不會增加node1和node2的引用計數,即weak_ptr不會參與空間資源的管理,只是作為一個解決循環引用的工具
示例:
struct ListNode { int _data; weak_ptr _prev; weak_ptr _next; ~ListNode(){ cout << "~ListNode()" << endl; } }; int main() { shared_ptr node1(new ListNode); shared_ptr node2(new ListNode); cout << node1.use_count() << endl; cout << node2.use_count() << endl; node1->_next = node2; node2->_prev = node1; cout << node1.use_count() << endl; cout << node2.use_count() << endl; return 0; }
效果:
模擬實現:
template class weak_ptr { public: weak_ptr() :_ptr(nullptr) {} weak_ptr(const weak_ptr& wp) :_ptr(wp._ptr) ,_pCount(wp._pCount) {} weak_ptr(const shared_ptr& sp) :_ptr(sp._ptr) , _pCount(sp._pCount) {} weak_ptr& operator=(const weak_ptr& wp) { _ptr = wp._ptr; _pCount = wp._pCount; } weak_ptr& operator=(const shared_ptr& sp) { _ptr = sp._ptr; _pCount = sp._pCount; } private: T* _ptr; int* _pCount; };
7、刪除器
概念及引入:
對于管理的資源并不一定就只是通過new出來的,還存在其他的方式獲取的資源對象該如何通過智能指針管理呢
這里就需要shared_ptr設計一個刪除器來解決,對于不一樣的資源使用其對應的方式進行資源的回收
示例:
// 仿函數的刪除器 template struct FreeFunc { void operator()(T* ptr) { cout << "free:" << ptr << endl; free(ptr); } }; template struct DeleteArrayFunc { void operator()(T* ptr) { cout << "delete[]" << ptr << endl; delete[] ptr; } }; int main() { FreeFunc freeFunc; shared_ptr sp1((int*)malloc(4), freeFunc); DeleteArrayFunc deleteArrayFunc; shared_ptr sp2((int*)malloc(4), deleteArrayFunc); return 0; }
效果:
簡單模擬實現刪除器shared_ptr:
template class DelRef { public: void operator()(T*& ptr) { if (ptr) { delete ptr; ptr = nullptr; } } }; template class Free { public: void operator()(T*& ptr) { if (ptr) { free(ptr); ptr = nullptr; } } }; class FClose { public: void operator()(FILE*& pf) { if (pf) { fclose(pf); pf = nullptr; } } }; //shared_ptr:多個智能指針管理一個內存資源,最后一個智能指針釋放內存 template> class shared_ptr { friend class weak_ptr; public: explicit shared_ptr(T* ptr) :_ptr(ptr) , _pCount(new int(1)) , _mtx(new mutex) {} void add_pCount() { _mtx->lock(); ++(*_pCount); _mtx->unlock(); } void release_pCount() { bool flg = false; _mtx->lock(); --(*_pCount); if ((*_pCount) == 0 && _ptr) { cout << "~shared_ptr:" << _ptr << endl; D del; del(_ptr); delete _pCount; flg = true; } _mtx->unlock(); if (flg) delete _mtx; } shared_ptr(shared_ptr& sp) :_ptr(sp._ptr) , _pCount(sp._pCount) , _mtx(sp._mtx) { add_pCount(); } //shared_ptr& operator=(shared_ptr& sp) //{ // if (_ptr != sp._ptr)//管理資源地址相同則不用處理 // { // release_pCount(); // _ptr = sp._ptr; // _pCount = sp._pCount; // _mtx = sp._mtx; // add_pCount(); // } // return *this; //} void swap(shared_ptr& sp) { std::swap(_ptr, sp._ptr); std::swap(_pCount, sp._pCount); std::swap(_mtx, sp._mtx); } //現代式寫法 shared_ptr& operator=(shared_ptr sp) { swap(sp); return *this; } ~shared_ptr() { release_pCount(); } T& operator*() { return *_ptr; } T* operator->() { return &_ptr; } T* get() { return _ptr; } size_t use_count() { return *_pCount; } private: T* _ptr;//管理的內存資源 int* _pCount;//計數 mutex* _mtx;//多線程互斥 //堆上開辟-多個智能指針共享計數和互斥鎖 };
注:這里的模擬并不一定就是C++中真真的底層實現
8、C++11和boost中智能指針的關系
C++ 98 中產生了第一個智能指針auto_ptr
C++ boost給出了更實用的scoped_ptr和shared_ptr和weak_ptr
C++ TR1,引入了shared_ptr等。不過注意的是TR1并不是標準版
C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr對應boost的scoped_ptr。并且這些智能指針的實現原理是參考boost中的實現的
C++
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。