平時習慣使用cocos2d-x的Ref內存模式,回過頭來在控制台項目中覺得c++的智能指針有點生疏,於是便重溫一下。
首先有請c++智能指針們登場: std::auto_ptr、std::unique_ptr、std::shared_ptr 、std::weak_ptr
auto_ptr(已廢棄的指針)
沒有智能指針的c++時代,對堆內存的管理就是簡單的new delete。
但是缺點是容易忘了delete釋放,即使是資深碼農,也可能會在某一個地方忘記delete它,造成內存泄漏。
在實際工程中,我們往往更希望把精力放在應用層上,而不是費盡心思在語言的細枝末節(內存的釋放)。
於是就有了這個最原始的智能指針。
內部大概實現:
做成一個auto_ptr類,包含原始指針成員。
當auto_ptr類型的對象被釋放時,利用析構函數,將擁有的原始指針delete掉。
//大概長這個樣子(化簡版) template<class T> class auto_ptr{ T* ptr; };
示例用法:
void runGame(){ std::auto_ptr<Monster> monster1(new Monster());//monster1 指向 一個怪物 monster1->doSomething();//怪物做某種事 } //runGame函數執行完時,monster1被釋放,然后它的析構函數也把指向的一個怪物釋放了,要死帶着一起死(o_o)
復制auto_ptr對象時,把指針指傳給復制出來的對象,原有對象的指針成員隨后重置為nullptr。
這說明auto_ptr是獨占性的,不允許多個auto_ptr指向同一個資源。
void runGame(){ std::auto_ptr<Monster> monster1(new Monster());//monster1 指向 一個怪物 monster1->doSomething();//怪物做某種事 std::auto_ptr<Monster> monster2 = monster1;//轉移指針 monster2->doSomething();//怪物做某種事 monster1->doSomething();//Oops!monster1智能指針指向了nullptr,運行期崩潰。 }
注意:
雖然本文簡單介紹了auto_ptr。
但是不要用auto_ptr! 不要用auto_ptr!
雖然它是c++11以前的最原始的智能指針,但是在c++11中已經被棄用(使用的話會被警告)了。
它的替代品,也就是c++11新智能指針unique_ptr,shared_ptr,weak_ptr將在下文出現
unique_ptr(一種強引用指針)
“它是我的所有物,你們都不能碰它!”——魯迅
正如它的名字,獨占 是它最大的特點。
內部大概實現:
它其實算是auto_ptr的翻版(都是獨占資源的指針,內部實現也基本差不多).
但是unique_ptr的名字能更好的體現它的語義,而且在語法上比auto_ptr更安全(嘗試復制unique_ptr時會編譯期出錯,而auto_ptr能通過編譯期從而在運行期埋下出錯的隱患)
假如你真的需要轉移所有權(獨占權),那么你就需要用std::move(std::unique_ptr對象)語法,盡管轉移所有權后 還是有可能出現原有指針調用(調用就崩潰)的情況。
但是這個語法能強調你是在轉移所有權,讓你清晰的知道自己在做什么,從而不亂調用原有指針。
示例用法:
void runGame(){ std::unique_ptr<Monster> monster1(new Monster());//monster1 指向 一個怪物 std::unique_ptr<Monster> monster2 = monster1;//Error!編譯期出錯,不允許復制指針指向同一個資源。 std::unique_ptr<Monster> monster3 = std::move(monster1);//轉移所有權給monster3. monster1->doSomething();//Oops!monster1指向nullptr,運行期崩潰 }
(額外:boost庫的boost::scoped_ptr也是一個獨占性智能指針,但是它不允許轉移所有權,從始而終都只對一個資源負責,它更安全謹慎,但是應用的范圍也更狹窄。)
shared_ptr(一種強引用指針)
“它是我們(shared_ptr)的,也是你們(weak_ptr)的,但實質還是我們的”——魯迅
共享對象所有權是件快樂的事情。
多個shared_ptr指向同一處資源,當所有shared_ptr都全部釋放時,該處資源才釋放。
(有某個對象的所有權(訪問權,生命控制權) 即是 強引用,所以shared_ptr是一種強引用型指針)
內部大概實現:
每個shared_ptr都占指針的兩倍空間,一個裝着原始指針,一個裝着計數區域(SharedPtrControlBlock)的指針
(用原始指針構造時,會new一個SharedPtrControlBlock出來作為計數存放的地方,然后用指針指向它,計數加減都通過SharedPtrControlBlock指針間接操作。)
//shared計數放在這個結構體里面,實際上結構體里還應該有另一個weak計數。下文介紹weak_ptr時會解釋。 struct SharedPtrControlBlock{ int shared_count; };
//大概長這個樣子(化簡版) template<class T> class shared_ptr{ T* ptr; SharedPtrControlBlock* count; };
每次復制,多一個共享同處資源的shared_ptr時,計數+1。每次釋放shared_ptr時,計數-1。
當shared計數為0時,則證明所有指向同一處資源的shared_ptr們全都釋放了,則隨即釋放該資源(哦,還會釋放new出來的SharedPtrControlBlock)。
這也是常說的引用計數技術(好繞口)
示例用法:
void runGame(){ std::shared_ptr<Monster> monster1(new Monster()); //計數加到1
do{std::shared_ptr<Monster> monster2 = monster1; //計數加到2 }while(0);
//該棧退出后,計數減為1,monster1指向的堆對象仍存在
std::shared_ptr<Monster> monster3 = monster1; //計數加到2 } //該棧退出后,shared_ptr都釋放了,計數減為0,它們指向的堆對象也能跟着釋放.
缺陷:模型循環依賴(互相引用或環引用)時,計數會不正常
假如有這么一個怪物模型,它有2個親人關系
class Monster{ std::shared_ptr<Monster> m_father; std::shared_ptr<Monster> m_son; public: void setFather(std::shared_ptr<Monster>& father);//實現細節懶得寫了 void setSon(std::shared_ptr<Monster>& son); //懶 ~Monster(){std::cout << "A monster die!";} //析構時發出死亡的悲鳴 };
然后執行下面函數
void runGame(){ std::shared_ptr<Monster> father = new Monster(); std::shared_ptr<Monster> son = new Monster(); father->setSon(son); son->setFather(father); }
猜猜執行完runGame()函數后,這對怪物父子能正確釋放(發出死亡的悲鳴)嗎?
答案是不能。
那么我們來模擬一遍(自行腦海模擬一遍最好),函數退出時棧的shared_ptr對象陸續釋放后的情形:
開始:
father,son指向的堆對象 shared計數都是為2
son智能指針退出棧:
son指向的堆對象 計數減為1,father指向的堆對象 計數仍為2。
father智能指針退出棧:
father指向的堆對象 計數減為1 , son指向的堆對象 計數仍為1。
函數結束:所有計數都沒有變0,也就是說中途沒有釋放任何堆對象。
為了解決這一缺陷的存在,弱引用指針weak_ptr的出現很有必要。
weak_ptr(一種弱引用指針)
“它是我們(weak_ptr)的,也是你們(shared_ptr)的,但實質還是你們的”——魯迅
weak_ptr是為了輔助shared_ptr的存在,它只提供了對管理對象的一個訪問手段,同時也可以實時動態地知道指向的對象是否存活。
(只有某個對象的訪問權,而沒有它的生命控制權 即是 弱引用,所以weak_ptr是一種弱引用型指針)
內部大概實現:
計數區域(SharedPtrControlBlock)結構體引進新的int變量weak_count,來作為弱引用計數。
每個weak_ptr都占指針的兩倍空間,一個裝着原始指針,一個裝着計數區域的指針(和shared_ptr一樣的成員)。
weak_ptr可以由一個shared_ptr或者另一個weak_ptr構造。
weak_ptr的構造和析構不會引起shared_count的增加或減少,只會引起weak_count的增加或減少。
被管理資源的釋放只取決於shared計數,當shared計數為0,才會釋放被管理資源,
也就是說weak_ptr不控制資源的生命周期。
但是計數區域的釋放卻取決於shared計數和weak計數,當兩者均為0時,才會釋放計數區域。
//shared引用計數和weak引用計數 //之前的計數區域實際最終應該長這個樣子 struct SharedPtrControlBlock{ int shared_count; int weak_count; };
//大概長這個樣子(化簡版) template<class T> class weak_ptr{ T* ptr; SharedPtrControlBlock* count; };
針對空懸指針問題:
空懸指針問題是指:無法知道指針指向的堆內存是否已經釋放。
得益於引入的weak_count,weak_ptr指針可以使計數區域的生命周期受weak_ptr控制,
從而能使weak_ptr獲取 被管理資源的shared計數,從而判斷被管理對象是否已被釋放。(可以實時動態地知道指向的對象是否被釋放,從而有效解決空懸指針問題)
它的成員函數expired()就是判斷指向的對象是否存活。
針對循環引用問題:
class Monster{ //盡管父子可以互相訪問,但是彼此都是獨立的個體,無論是誰都不應該擁有另一個人的所有權。 std::weak_ptr<Monster> m_father; //所以都把shared_ptr換成了weak_ptr std::weak_ptr<Monster> m_son; //同上 public: void setFather(std::shared_ptr<Monster>& father); //實現細節懶得寫了 void setSon(std::shared_ptr<Monster>& son); //懶 ~Monster(){std::cout << "A monster die!";} //析構時發出死亡的悲鳴 };
然后執行下面的函數
void runGame(){ std::shared_ptr<Monster> father(new Monster()); std::shared_ptr<Monster> son(new Monster()); father->setSon(son); son->setFather(father); }
那么我們再來模擬一遍,函數退出時棧的shared_ptr對象陸續釋放后的情形:
一開始:
father指向的堆對象 shared計數為1,weak計數為1
son指向的堆對象 shared計數為1,weak計數為1
son智能指針退出棧:
son指向的堆對象 shared計數減為0,weak計數為1,釋放son的堆對象,發出第一個死亡的悲鳴
father指向的堆對象 shared計數為1,weak計數減為0;
father智能指針退出棧:
father指向的堆對象 shared計數減為0,weak計數為0;釋放father的堆對象和father的計數區域,發出第二個死亡的悲鳴。
son指向的堆對象 shared計數為0,weak計數減為0;釋放son的計數區域。
函數結束,釋放行為正確。
(可以說,當生命控制權沒有彼此互相掌握時,才能正確解決循環引用問題,而弱引用的使用可以使生命控制權互相掌握的情況消失)
此外:
weak_ptr沒有重載 * 和 -> ,所以並不能直接使用資源。但可以使用lock()獲得一個可用的shared_ptr對象,
如果對象已經死了,lock()會失敗,返回一個空的shared_ptr。
void runGame(){ std::shared_ptr<Monster> monster1(new Monster()); std::weak_ptr<Monster> r_monster1 = monster1; r_monster1->doSomething();//Error! 編譯器出錯!weak_ptr沒有重載* 和 -> ,無法直接當指針用 std::shared_ptr<Monster> s_monster1 = r_monster1.lock();//OK!可以通過weak_ptr的lock方法獲得shared_ptr。
}
總結(語義)
1、不要使用std::auto_ptr(已經在C++11或以上標准中棄用)
2、當你需要一個獨占資源所有權(訪問權+生命控制權)的指針,且不允許任何外界訪問,使用std::unique_ptr
3、當你需要一個共享資源所有權(訪問權+生命控制權)的指針,使用std::shared_ptr
4、當你需要一個能訪問資源,但不控制其生命周期的指針,使用std::weak_ptr
推薦用法:
一個shared_ptr和n個weak_ptr搭配使用而不是n個shared_ptr。
因為一般模型中,最好總是被一個指針控制生命周期,然后可以被n個指針控制訪問。
邏輯上,大部分模型的生命在直觀上總是受某一樣東西直接控制而不是多樣東西共同控制。
程序上,能夠完全避免生命周期互相控制引發的 循環引用問題。