1. 程式人生 > >【C++11】4種智慧指標

【C++11】4種智慧指標

|auto_ptr(不要使用的指標)

沒有智慧指標的c++時代,對堆記憶體的管理就是簡單的new delete。
但是缺點是容易忘了delete釋放,即使是資深碼農,也可能會在某一個地方忘記delete它,造成記憶體洩漏。
在實際工程中,我們往往更希望把精力放在應用層上,而不是費盡心思在語言的細枝末節(記憶體的釋放)。
於是就有了這個最原始的智慧指標。

由於 auto_ptr 基於【排他所有權模式】,這意味著:兩個指標(同類型)不能指向同一個資源,複製或賦值都會改變資源的所有權。

複製auto_ptr物件時,把指標指傳給複製出來的物件,原有物件的指標成員隨後重置為nullptr。

auto_ptr<TYPE> A,B;
A = B;

第二行執行完畢後,B的地址為0;

這就是智慧指標auto_ptr是轉移語義造成的,容易讓人用錯的地方。

使用auto_ptr要知道:
1. 智慧指標不能共享指向物件的所有權
2. 智慧指標不能指向陣列。因為其實現中呼叫的是delete而非delete[]
3. 智慧指標不能作為容器類的元素。

所以注意:不要用auto_ptr! 不要用auto_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

2、當你需要一個獨佔資源所有權(訪問權+生命控制權)的指標,且不允許任何外界訪問,請使用std::unique_ptr

3、當你需要一個共享資源所有權(訪問權+生命控制權)的指標,請使用std::shared_ptr

當你需要一個能訪問資源,但不控制其生命週期的指標,請使用std::weak_ptr

 

推薦用法:
一個shared_ptr和n個weak_ptr搭配使用 而不是n個shared_ptr
因為一般模型中,最好總是被一個指標控制生命週期,然後可以被n個指標控制訪問。

邏輯上,大部分模型的生命在直觀上總是受某一樣東西直接控制而不是多樣東西共同控制。
程式上,能夠完全避免生命週期互相控制引發的 迴圈引用問題。