C++的4种智能指针剖析使用
?
1. 智能指針背后的設計思想
我們先來看一個簡單的例子:
void remodel(std::string & str) {std::string * ps = new std::string(str);...if (weird_thing()){return;}str = *ps; delete ps;return; }當(weird_thing()返回true)時,delete將不被執行,因此將導致內存泄露。
如何避免這種問題?有人會說,這還不簡單,直接在return;之前加上delete ps;不就行了。是的,你本應如此,問題是很多人都會忘記在適當的地方加上delete語句(連上述代碼中最后的那句delete語句也會有很多人忘記吧),如果你要對一個龐大的工程進行review,看是否有這種潛在的內存泄露問題,那就是一場災難!
這時我們會想:當remodel這樣的函數終止(不管是正常終止,還是由于出現了異常而終止),本地變量都將自動從棧內存中刪除—因此指針ps占據的內存將被釋放,如果ps指向的內存也被自動釋放,那該有多好啊。
我們知道析構函數有這個功能。如果ps有一個析構函數,該析構函數將在ps過期時自動釋放它指向的內存。但ps的問題在于,它只是一個常規指針,不是有析構凼數的類對象指針。如果它指向的是對象,則可以在對象過期時,讓它的析構函數刪除指向的內存。
這正是 auto_ptr、unique_ptr和shared_ptr這幾個智能指針背后的設計思想。我簡單的總結下就是:將基本類型指針封裝為類對象指針(這個類肯定是個模板,以適應不同基本類型的需求),并在析構函數里編寫delete語句刪除指針指向的內存空間。
因此,要轉換remodel()函數,應按下面3個步驟進行:
- 包含頭義件memory(智能指針所在的頭文件);
- 將指向string的指針替換為指向string的智能指針對象;
- 刪除delete語句。
下面是使用auto_ptr修改該函數的結果:
# include <memory> void remodel (std::string & str) {std::auto_ptr<std::string> ps (new std::string(str));...if (weird_thing ())return; str = *ps; // delete ps; NO LONGER NEEDEDreturn; }?
2. shared_ptr簡單介紹
STL一共給我們提供了四種智能指針:auto_ptr、unique_ptr、shared_ptr和weak_ptr。
模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,并提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了好多年:同時,如果您的編譯器不支持其他兩種解決力案,auto_ptr將是唯一的選擇。
使用注意點
- 所有的智能指針類都有一個explicit構造函數,以指針作為參數。比如auto_ptr的類模板原型為:
- templet<class T>
class auto_ptr {explicit auto_ptr(X* p = 0) ; ...
};
?
-
因此不能自動將指針轉換為智能指針對象,必須顯式調用:
- shared_ptr<double> pd;
double *p_reg = new double;
pd = p_reg; // not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg); // allowed (explicit conversion)
shared_ptr<double> pshared = p_reg; // not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg); // allowed (explicit conversion)
?
-
對全部三種智能指針都應避免的一點:
- string vacation("I wandered lonely as a cloud.");
shared_ptr<string> pvac(&vacation); // No
pvac過期時,程序將把delete運算符用于非堆內存,這是錯誤的。
?
使用舉例
#include <iostream> #include <string> #include <memory>class report { private:std::string str; public:report(const std::string s) : str(s) {std::cout << "Object created.\n";}~report() {std::cout << "Object deleted.\n";}void comment() const {std::cout << str << "\n";} };int main() {{std::auto_ptr<report> ps(new report("using auto ptr"));ps->comment();}{std::shared_ptr<report> ps(new report("using shared ptr"));ps->comment();}{std::unique_ptr<report> ps(new report("using unique ptr"));ps->comment();}return 0; }?
?
3. 為什么摒棄auto_ptr?
先來看下面的賦值語句:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”); auto_ptr<string> vocation; vocaticn = ps;上述賦值語句將完成什么工作呢?如果ps和vocation是常規指針,則兩個指針將指向同一個string對象。這是不能接受的,因為程序將試圖刪除同一個對象兩次——一次是ps過期時,另一次是vocation過期時。要避免這種問題,方法有多種:
- 定義陚值運算符,使之執行深復制。這樣兩個指針將指向不同的對象,其中的一個對象是另一個對象的副本,缺點是浪費空間,所以智能指針都未采用此方案。
- 建立所有權(ownership)概念。對于特定的對象,只能有一個智能指針可擁有,這樣只有擁有對象的智能指針的構造函數會刪除該對象。然后讓賦值操作轉讓所有權。這就是用于auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更嚴格。
- 創建智能更高的指針,跟蹤引用特定對象的智能指針數。這稱為引用計數。例如,賦值時,計數將加1,而指針過期時,計數將減1,。當減為0時才調用delete。這是shared_ptr采用的策略。
當然,同樣的策略也適用于復制構造函數。
每種方法都有其用途,但為何說要摒棄auto_ptr呢?
下面舉個例子來說明。
?
?
運行下發現程序崩潰了,原因在上面注釋已經說的很清楚,films[2]已經是空指針了,下面輸出訪問空指針當然會崩潰了。但這里如果把auto_ptr換成shared_ptr或unique_ptr后,程序就不會崩潰,原因如下:
- 使用shared_ptr時運行正常,因為shared_ptr采用引用計數,pwin和films[2]都指向同一塊內存,在釋放空間時因為事先要判斷引用計數值的大小因此不會出現多次刪除一個對象的錯誤。
- 使用unique_ptr時編譯出錯,與auto_ptr一樣,unique_ptr也采用所有權模型,但在使用unique_ptr時,程序不會等到運行階段崩潰,而在編譯器因下述代碼行出現錯誤:
- unique_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership.
?
- 指導你發現潛在的內存錯誤。
-
這就是為何要摒棄auto_ptr的原因,一句話總結就是:避免潛在的內存崩潰問題。
?
?
4. unique_ptr為何優于auto_ptr?
?
可能大家認為前面的例子已經說明了unique_ptr為何優于auto_ptr,也就是安全問題,下面再敘述的清晰一點。
請看下面的語句:
?
?
在語句#3中,p2接管string對象的所有權后,p1的所有權將被剝奪。前面說過,這是好事,可防止p1和p2的析構函數試圖刪同—個對象;
但如果程序隨后試圖使用p1,這將是件壞事,因為p1不再指向有效的數據。
下面來看使用unique_ptr的情況:
unique_ptr<string> p3 (new string ("auto"); //#4 unique_ptr<string> p4; //#5 p4 = p3; //#6編譯器認為語句#6非法,避免了p3不再指向有效數據的問題。因此,unique_ptr比auto_ptr更安全。
但unique_ptr還有更聰明的地方。
有時候,會將一個智能指針賦給另一個并不會留下危險的懸掛指針。假設有如下函數定義:
并假設編寫了如下代碼:
unique_ptr<string> ps; ps = demo('Uniquely special");demo()返回一個臨時unique_ptr,然后ps接管了原本歸返回的unique_ptr所有的對象,而返回時臨時的 unique_ptr 被銷毀,也就是說沒有機會使用 unique_ptr 來訪問無效的數據,換句話來說,這種賦值是不會出現任何問題的,即沒有理由禁止這種賦值。實際上,編譯器確實允許這種賦值,這正是unique_ptr更聰明的地方。
總之,黨程序試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這么做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這么做,比如:
unique_ptr<string> pu1(new string ("hello world")); unique_ptr<string> pu2; pu2 = pu1; // #1 not allowed unique_ptr<string> pu3; pu3 = unique_ptr<string>(new string ("You")); // #2 allowed其中#1留下懸掛的unique_ptr(pu1),這可能導致危害。而#2不會留下懸掛的unique_ptr,因為它調用 unique_ptr 的構造函數,該構造函數創建的臨時對象在其所有權讓給 pu3 后就會被銷毀。這種隨情況而已的行為表明,unique_ptr 優于允許兩種賦值的auto_ptr 。
當然,您可能確實想執行類似于#1的操作,僅當以非智能的方式使用摒棄的智能指針時(如解除引用時),這種賦值才不安全。要安全的重用這種指針,可給它賦新值。C++有一個標準庫函數std::move(),讓你能夠將一個unique_ptr賦給另一個。下面是一個使用前述demo()函數的例子,該函數返回一個unique_ptr<string>對象:
使用move后,原來的指針仍轉讓所有權變成空指針,可以對其重新賦值。
?
5. weak_ptr的簡單介紹
weak_ptr 是一種不控制對象生命周期的智能指針, 它指向一個 shared_ptr 管理的對象. 進行該對象的內存管理的是那個強引用的 shared_ptr. weak_ptr只是提供了對管理對象的一個訪問手段.?
weak_ptr 設計的目的是為配合 shared_ptr 而引入的一種智能指針來協助 shared_ptr 工作, 它只可以從一個 shared_ptr 或另一個 weak_ptr 對象構造, 它的構造和析構不會引起引用記數的增加或減少.?
定義在 memory 文件中(非memory.h), 命名空間為 std.
?
weak_ptr成員函數
weak_ptr 沒有重載*和->但可以使用 lock 獲得一個可用的 shared_ptr 對象. 注意, weak_ptr 在使用前需要檢查合法性.
expired 用于檢測所管理的對象是否已經釋放, 如果已經釋放, 返回 true; 否則返回 false.
lock 用于獲取所管理的對象的強引用(shared_ptr). 如果 expired 為 true, 返回一個空的 shared_ptr; 否則返回一個 shared_ptr, 其內部對象指向與 weak_ptr 相同.
use_count 返回與 shared_ptr 共享的對象的引用計數.
reset 將 weak_ptr 置空.
weak_ptr 支持拷貝或賦值, 但不會影響對應的 shared_ptr 內部對象的計數.
?
weak_ptr的使用更為復雜一點,它可以指向shared_ptr指針指向的對象內存,卻并不擁有該內存,而使用weak_ptr成員lock,則可返回其指向內存的一個share_ptr對象,且在所指對象內存已經無效時,返回指針空值nullptr。
注意:weak_ptr并不擁有資源的所有權,所以不能直接使用資源。
可以從一個weak_ptr構造一個shared_ptr以取得共享資源的所有權。
?
weak_ptr 使用實例:
#define _CRT_SECURE_NO_WARNINGS#include <iostream> #include <string> #include <memory> #include <vector> #include <map>void check(std::weak_ptr<int> &wp) {std::shared_ptr<int> sp = wp.lock(); // 轉換為shared_ptr<int>if (sp != nullptr){std::cout << "still: " << *sp << std::endl;} else{std::cout << "still: " << "pointer is invalid" << std::endl;} }void mytest() {std::shared_ptr<int> sp1(new int(22));std::shared_ptr<int> sp2 = sp1;std::weak_ptr<int> wp = sp1; // 指向shared_ptr<int>所指對象std::cout << "count: " << wp.use_count() << std::endl; // count: 2std::cout << *sp1 << std::endl; // 22std::cout << *sp2 << std::endl; // 22check(wp); // still: 22sp1.reset();std::cout << "count: " << wp.use_count() << std::endl; // count: 1std::cout << *sp2 << std::endl; // 22check(wp); // still: 22sp2.reset();std::cout << "count: " << wp.use_count() << std::endl; // count: 0check(wp); // still: pointer is invalidreturn; }int main() {mytest();system("pause");return 0; }?
使用?weak_ptr 解決 shared_ptr 因循環引有不能釋放資源的問題
使用 shared_ptr 時, shared_ptr 為強引用, 如果存在循環引用, 將導致內存泄露. 而 weak_ptr 為弱引用, 可以避免此問題, 其原理:
對于弱引用來說, 當引用的對象活著的時候弱引用不一定存在. 僅僅是當它存在的時候的一個引用, 弱引用并不修改該對象的引用計數, 這意味這弱引用它并不對對象的內存進行管理.
weak_ptr 在功能上類似于普通指針, 然而一個比較大的區別是, 弱引用能檢測到所管理的對象是否已經被釋放, 從而避免訪問非法內存。
注意: 雖然通過弱引用指針可以有效的解除循環引用, 但這種方式必須在程序員能預見會出現循環引用的情況下才能使用, 也可以是說這個僅僅是一種編譯期的解決方案, 如果程序在運行過程中出現了循環引用, 還是會造成內存泄漏.
?
另一個循環依賴的例子,來自<C++標準庫(第2版)>
class Person : public enable_shared_from_this<Person> { public:Person(const string& name): m_name{ name }{}~Person(){cout << "release " << m_name << endl;}string getName() const{return m_name;}void setFather(shared_ptr<Person> f){m_father = f;if (f){f->m_kids.push_back(shared_from_this());}}void setMother(shared_ptr<Person> m){m_mother = m;if (m){m->m_kids.push_back(shared_from_this());}}shared_ptr<Person> getKid(size_t idx){if (idx < m_kids.size()){weak_ptr<Person> p = m_kids.at(idx);if (!p.expired()){return p.lock();}}return nullptr;}private:string m_name;shared_ptr<Person> m_father;shared_ptr<Person> m_mother;//vector<shared_ptr<Person>> m_kids; // 循環依賴vector<weak_ptr<Person>> m_kids; };// 測試代碼 shared_ptr<Person> jack{ make_shared<Person>("Jack") }; shared_ptr<Person> lucy{ make_shared<Person>("Lucy") }; shared_ptr<Person> john{ make_shared<Person>("John") }; john->setFather(jack); john->setMother(lucy);auto p = jack->getKid(0); if (p) {cout << p->getName() << endl; }?
附源碼實現:
template<class _Ty> class weak_ptr: public _Ptr_base<_Ty> { // class for pointer to reference counted resourcetypedef typename _Ptr_base<_Ty>::_Elem _Elem;public:weak_ptr(){ // construct empty weak_ptr object}template<class _Ty2>weak_ptr(const shared_ptr<_Ty2>& _Other,typename enable_if<is_convertible<_Ty2 *, _Ty *>::value,void *>::type * = 0){ // construct weak_ptr object for resource owned by _Otherthis->_Resetw(_Other);}weak_ptr(const weak_ptr& _Other){ // construct weak_ptr object for resource pointed to by _Otherthis->_Resetw(_Other);}template<class _Ty2>weak_ptr(const weak_ptr<_Ty2>& _Other,typename enable_if<is_convertible<_Ty2 *, _Ty *>::value,void *>::type * = 0){ // construct weak_ptr object for resource pointed to by _Otherthis->_Resetw(_Other);}~weak_ptr(){ // release resourcethis->_Decwref();}weak_ptr& operator=(const weak_ptr& _Right){ // assign from _Rightthis->_Resetw(_Right);return (*this);}template<class _Ty2>weak_ptr& operator=(const weak_ptr<_Ty2>& _Right){ // assign from _Rightthis->_Resetw(_Right);return (*this);}template<class _Ty2>weak_ptr& operator=(shared_ptr<_Ty2>& _Right){ // assign from _Rightthis->_Resetw(_Right);return (*this);}void reset(){ // release resource, convert to null weak_ptr objectthis->_Resetw();}void swap(weak_ptr& _Other){ // swap pointersthis->_Swap(_Other);}bool expired() const{ // return true if resource no longer existsreturn (this->_Expired());}shared_ptr<_Ty> lock() const{ // convert to shared_ptrreturn (shared_ptr<_Elem>(*this, false));} };?
?
6. 如何選擇智能指針?
在掌握了這幾種智能指針后,大家可能會想另一個問題:在實際應用中,應使用哪種智能指針呢?
下面給出幾個使用指南。
(1)如果程序要使用多個指向同一個對象的指針,應選擇shared_ptr。這樣的情況包括:
- 有一個指針數組,并使用一些輔助指針來標示特定的元素,如最大的元素和最小的元素;
- 兩個對象包含都指向第三個對象的指針;
- STL容器包含指針。很多STL算法都支持復制和賦值操作,這些操作可用于shared_ptr,但不能用于unique_ptr(編譯器發出warning)和auto_ptr(行為不確定)。如果你的編譯器沒有提供shared_ptr,可使用Boost庫提供的shared_ptr。
(2)如果程序不需要多個指向同一個對象的指針,則可使用unique_ptr。如果函數使用new分配內存,并返還指向該內存的指針,將其返回類型聲明為unique_ptr是不錯的選擇。這樣,所有權轉讓給接受返回值的unique_ptr,而該智能指針將負責調用delete。可將unique_ptr存儲到STL容器在那個,只要不調用將一個unique_ptr復制或賦給另一個算法(如sort())。例如,可在程序中使用類似于下面的代碼段。
unique_ptr<int> make_int(int n) {return unique_ptr<int>(new int(n)); } void show(unique_ptr<int> &p1) {cout << *a << ' '; } int main() {//...vector<unique_ptr<int> > vp(size);for (int i = 0; i < vp.size(); i++)vp[i] = make_int(rand() % 1000); // copy temporary unique_ptrvp.push_back(make_int(rand() % 1000)); // ok because arg is temporaryfor_each(vp.begin(), vp.end(), show); // use for_each()//... }?
其中push_back調用沒有問題,因為它返回一個臨時unique_ptr,該unique_ptr被賦給vp中的一個unique_ptr。另外,如果按值而不是按引用給show()傳遞對象,for_each()將非法,因為這將導致使用一個來自vp的非臨時unique_ptr初始化pi,而這是不允許的。前面說過,編譯器將發現錯誤使用unique_ptr的企圖。
在unique_ptr為右值時,可將其賦給shared_ptr,這與將一個unique_ptr賦給一個需要滿足的條件相同。與前面一樣,在下面的代碼中,make_int()的返回類型為unique_ptr<int>:
unique_ptr<int> pup(make_int(rand() % 1000)); // ok shared_ptr<int> spp(pup); // not allowed, pup as lvalue shared_ptr<int> spr(make_int(rand() % 1000)); // ok模板shared_ptr包含一個顯式構造函數,可用于將右值unique_ptr轉換為shared_ptr。shared_ptr將接管原來歸unique_ptr所有的對象。
在滿足unique_ptr要求的條件時,也可使用auto_ptr,但unique_ptr是更好的選擇。如果你的編譯器沒有unique_ptr,可考慮使用Boost庫提供的scoped_ptr,它與unique_ptr類似。
?
總結
以上是生活随笔為你收集整理的C++的4种智能指针剖析使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Fragment嵌套Fragment
- 下一篇: 用 TableModel Free 框架