一篇文章帶你瞭解C++智能指針詳解
為什麼要有智能指針?
因為普通的指針存在以下幾個問題:
- 資源泄露
- 野指針
- 未初始化
- 多個指針指向同一塊內存,某個指針將內存釋放,別的指針不知道
- 異常安全問題
- 如果在 malloc和free 或者 new和delete 之間如果存在拋異常,那麼也會導致內存泄漏。
資源泄漏示例代碼:
int main(){ int *p = new int; *p = 1; p = new int; // 未釋放之前申請的資源,導致內存泄漏 delete p; return 0; }
野指針示例代碼:
int main(){ int *p1 = new int; int *p2 = p1; delete p1; *p2 = 1; // 申請的內存已經被釋放掉瞭, return 0; }
int main(){ int *p; *p = 1; // 程序直接報錯, 使用瞭未初始化的變量 return 0; }
解決方法:智能指針
智能指針的使用及原理
- 具有RALL 特性
- 重載瞭 operator* 和 operator ->,使其具有瞭指針一樣的行為
RALL
RALL(Resource Acquistion Is Initialization)是一種利用對象生命周期來控制程序資源(如內存,文件句柄,網絡連接,互斥量等)的簡單技術。
在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最後在對象析構的時候釋放資源。相當於利用 對象 管理瞭一份資源。這樣的優勢在於
1.不需要顯式的釋放資源(對象析構時,自動釋放資源)
2.采用這種方式,對象所需的資源在其生命周期內始終保持有效。
智能指針就是一個實例出來的對象
C++98版本的庫中就提供瞭auto_ptr的智能指針。但是 auto_ptr存在當對象拷貝或者賦值之後,前面的對象就懸空瞭。
C++11 提供更靠譜的並且支持拷貝的 shared_ptr
shared_ptr :
通過引用計數的方式實現多個shared_ptr 對象之間共享資源。
shared_ptr在其內部,給每個資源都維護瞭著一份計數,用來記錄該份資源被幾個對象共享。在對象被銷毀時(也就是析構函數調用),就說明自己不使用該資源瞭,對象的引用計數減一。如果引用計數是0,就說明自己是最後一個使用該資源的對象,必須釋放該資源;如果不是0,就說明除瞭自己還有其他對象在使用該份資源,不能釋放該資源,否則其他對象就成野指針瞭
unique_ptr :
確保一個對象同一時刻隻能被一個智能指針引用,可以轉移所有權(可以從一個智能指針轉移到另一個智能指針)
auto_ptr :
C++11 已棄用, 與unique_ptr 類似
使用時,需包含頭文件
#include <memory>
shared_ptr的使用註意事項
創建
1. shared_ptr<int> ptr{new int(3)}; 2. shared_ptr<int> ptr; ptr.reset(new int(3)); 3. shared_ptr<int> ptr = make_shared<int>(3);
shared_ptr 支持使用比較運算符,使用時,會調用共享指針內部封裝的原始指針的比較運算符。
支持
==、!=、<、<=、>、>=
使用 比較運算符 的前提 必須是 同類型
示例:
shared_ptr<int> p1 = make_shared<int>(1); shared_ptr<int> p2 = make_shared<int>(2); shared_ptr<int> p3; shared_ptr<double> p4 = make_shared<double>(1); bool b1 = p1 < p2; // true bool b2 = p1 > p3; // true, 非NULL 指針與 NULL 指針相比 ,都是大於 bool b3 = p3 == p3; // true bool b4 = p4 < p2 // 編譯失敗,類型不一致
shared_ptr 可以使用強制類型轉換,但是不能使用普通的強制類型轉換符
1.shared_ptr 強制類型轉換符 允許將其中包含的指針強制轉換為其它類型
2.不能使用普通的強制類型轉換運算符,否則會導致未定義行為
3.shared_ptr 的強制類型轉換運算符包括
static_pointer_cast
dynamic_pointer_cast
const_pointer_cast
示例:
shared_ptr<void> p(new int); // 內部保留 void* 指針 static_pointer_cast<int*>(p); // 正確的 強制類型轉換方式 shared_ptr<int> p1(static_cast<int*>(p.get())); // 錯誤的強制類型轉換方式,未定義錯誤
多個 shared_ptr 不能擁有同一個對象
利用代碼理解
示例:
class Mytest{ public: Mytest(const string& str) :_str(str){} ~Mytest(){ std::cout << _str << "destory" << std::endl; } private: string _str; }; int main(){ Mytest* p = new Mytest("shared_test"); shared_ptr<Mytest> p1(p); // 該對象可以正常析構 shared_ptr<Mytest> p2(p); // 對象銷毀時,錯誤,讀取位置 0xDDDDDDDD 時發生訪問沖突。 return 0; }
上述代碼, 共享指針 p1 對象在程序 結束時,調用析構,釋放瞭p 所指向的空間, 當 p2 進行析構的時候,又釋放p所指向的空間, 但是由於已經釋放過瞭, 重復釋放已經釋放過的內存,導致段錯誤。
可以使用 shared_from_this 避免這種問題
改進代碼:
class Mytest:public enable_shared_from_this<Mytest> { public: Mytest(const string& str) :_str(str) {} ~Mytest() { std::cout << _str << "destory" << std::endl; } shared_ptr<Mytest> GetSharedptr() { return shared_from_this(); } private: string _str; }; int main() { Mytest* p = new Mytest("shared_test"); shared_ptr<Mytest> p1(p); shared_ptr<Mytest> p2 = p->GetSharedptr(); // 正確做法 return 0; }
shared_ptr 的銷毀
shared_ptr 在初始化的時候,可以定義刪除器,刪除器可以定義為 普通函數、匿名函數、函數指針等符合要求的可調用對象
示例代碼:
void delFun(string* p) { std::cout << "Fun delete " << *p << endl; delete p; } int main() { std::cout << "begin" << std::endl; shared_ptr<string> p1; { shared_ptr<string> p2(new string("p1"), [](string* p) { std::cout << "Lamda delete " << *p << std::endl; delete p; }); p1 = p2; shared_ptr<string> p3(new string("p3"), delFun); } std::cout << "end" << std::endl; return 0; }
執行結果:
begin
Fun delete p3
end
Lamda deletep1
分析結果:
首先 ,p3在{ }作用域內 ,生命周期最先結束,調用delFun作為刪除器
其次,p2 也在{ } 作用域內,生命周期也結束瞭,但是因為 p1 和 p2 指向瞭同一個對象,所以p2 銷毀隻是將其 對象 引用計數 -1。
最後,程序運行結束,p1銷毀,其對象引用計數-1 變為0,調用 刪除器,銷毀對象。
shared_ptr<char> p(new char[10]); // 編譯能夠通過,但是會造成資源泄漏 // 正確做法 shared_ptr<char> p(new char[10], [](char* p){ delete p[]; }); // 正確做法 shared_ptr<char> p(new char[10], default_delete<char[]>());
- 可以為數組創建一個shared_ptr ,但是這樣會造成資源泄露。因為 shared_ptr 提供默認的刪除調用的是 delete,而不是 delete[]
- 可以使用自定義刪除器,刪除器中使用 delete[]
- 可以使用 default_delete 作為刪除器,因為它使用 delete[]
shared_ptr 存在的問題:
1.循環引用
不同對象相互引用,形成環路
2.想要共享但是不想擁有對象
shared_ptr 的線程安全問題
1. shared_ptr 對象中引用計數是多個shared_ptr對象共享的,兩個線程中shared_ptr的引用計數同時++或–,這個操作不是原子的,引用計數原來是1,++瞭兩次,可能還是2 這樣引用計數就錯亂瞭。會導致資源未釋放或者程序崩潰的問題。所以隻能指針中引用計數++、–是需要加鎖的,也就是說引用計數的操作是線程安全的。
2.shared_ptr 管理的對象存放在堆上,兩個線程中同時去訪問,會導致線程安全問題。
// 1.因為線程安全問題是偶現性問題,main函數的n改大一些概率就變大瞭,就 容易出現瞭。 void SharePtrFunc(shared_ptr<Date>& sp, size_t n) { cout << sp.Get() << endl; for (size_t i = 0; i < n; ++i) { // 這裡智能指針拷貝會++計數,智能指針析構會--計數,這裡是線程安全的。 shared_ptr<Date> copy(sp); // 這裡智能指針訪問管理的資源,不是線程安全的。所以我們看看這些值兩個線程++瞭2n次,但是最終看到的結果,並一定是加瞭2n copy->_year++; copy->_month++; copy->_day++; } } int main() { shared_ptr<Date> 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; }
shared_ptr 的循環引用
struct ListNode { int _data; shared_ptr<ListNode> _prev; shared_ptr<ListNode> _next; ~ListNode(){ cout << "~ListNode()" << endl; } }; int main() { shared_ptr<ListNode> node1(new ListNode); shared_ptr<ListNode> 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就釋放瞭。
也就是說_prev析構瞭,node1就釋放瞭。
但是_next屬於node的成員,node1釋放瞭,_next才會析構,而node1由_prev管理,_prev屬於node2成員,所以這就叫循環引用,誰也不會釋放。
解決方案:在引用計數的場景下,把節點中的_prev和_next改成weak_ptr就可以瞭
原理:
node1->_next = node2;和node2->_prev = node1;時weak_ptr的_next和_prev不會增加
node1和node2的引用計數。
struct ListNode { int _data; weak_ptr<ListNode> _prev; weak_ptr<ListNode> _next; ~ListNode(){ cout << "~ListNode()" << endl; } }; int main() { shared_ptr<ListNode> node1(new ListNode); shared_ptr<ListNode> 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; }
unique_ptr
- 同一個對象,隻能有唯一的一個 unique_ptr 指向它
- 繼承瞭自動指針 auto_ptr,
- 有助於避免發生異常時導致的資源泄漏
unique_ptr的使用
unique_ptr 定義瞭*、-> 運算符,沒有定義 ++ 之類的指針算法
unique_ptr 不允許使用賦值語法進行初始化,必須使用普通指針直接初始化
unique_ptr 可以為 空
unique_ptr 不能使用普通的復制語義賦值, 可以使用 C++11 的 move() 函數
unique_ptr 獲得新對象時,會銷毀之前的對象
unique_ptr 防止拷貝的原理:
// C++98防拷貝的方式:隻聲明不實現+聲明成私有 UniquePtr(UniquePtr<T> const &); UniquePtr & operator=(UniquePtr<T> const &); // C++11防拷貝的方式:delete UniquePtr(UniquePtr<T> const &) = delete; UniquePtr & operator=(UniquePtr<T> const &) = delete;
總結
本篇文章就到這裡瞭,希望能給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!