管理類的指針成員

設計具備指針成員的類時,類設計者必須首先須要決定的是該指針應提供什麼行爲。
將一個指針複製到另外一個指針時,兩個指針指向同一對象。
當兩個指針指向同一對象時,可能使用任一指針改變基礎對象。
相似地,極可能一個指針刪除了一對象時,另外一指針的用戶還認爲基礎對象仍然存在。
指針成員默認具備與指針對象一樣的行爲。安全

然而,經過不一樣的複製控制策略,能夠爲指針成員實現不一樣的行爲。
大多數 C++ 類採用如下三種方法之一管理指針成員:app

1. 指針成員採起常規指針型行爲。這樣的類具備指針的全部缺陷但無需特殊的複製控制。
2. 類能夠實現所謂的「智能指針」行爲。指針所指向的對象是共享的,但類可以防止懸垂指針。
3. 類採起值型行爲。指針所指向的對象是惟一的,由每一個類對象獨立管理。函數

[1. 一個帶指針成員的簡單類]fetch

爲了闡明所涉及的問題,咱們將實現一個簡單類,該類包含一個 int 值和一個指針:ui

       // class that has a pointer member that behaves like a plain pointer
       class HasPtr {
       public:
              // copy of the values we're given
              HasPtr(int *p, int i): ptr(p), val(i) { }
 
              // const members to return the value of the indicated data member
              int *get_ptr() const { return ptr; }
              int get_int() const { return val; }

              // non const members to change the indicated data member
              void set_ptr(int *p) { ptr = p; }
              void set_int(int i) { val = i; }

              // return or change the value pointed to, so ok for const objects
              int get_ptr_val() const { return *ptr; }
              void set_ptr_val(int val) const { *ptr = val; }

       private:
              int *ptr;
              int val;
       };

1.1 默認複製/賦值與指針成員this

由於 HasPtr 類沒有定義複製構造函數,因此複製一個 HasPtr 對象將複製兩個成員:spa

     int obj = 0;
     HasPtr ptr1(&obj, 42); // int* member points to obj, val is 42
     HasPtr ptr2(ptr1);     // int* member points to obj, val is 42

複製以後,ptr1 和 ptr2 中的指針指向同一對象且兩個對象中的 int 值相同。設計

可是,由於指針的值不一樣於它所指對象的值,這兩個成員的行爲看來很是不一樣。指針

複製以後,int 值是清楚和獨立的,而指針則糾纏在一塊兒。code

注意:具備指針成員且使用默認合成複製構造函數的類具備普通指針的全部缺陷。

         尤爲是,類自己沒法避免懸垂指針。

1.2 指針共享同一對象

複製一個算術值時,副本獨立於原版,能夠改變一個副本而不改變另外一個:

     ptr1.set_int(0); // changes val member only in ptr1
     ptr2.get_int();  // returns 42
     ptr1.get_int();  // returns 0

複製指針時,地址值是可區分的,但指針指向同一基礎對象。
若是在任一對象上調用 set_ptr_val,則兩者的基礎對象都會改變:

     ptr1.set_ptr_val(42); // sets object to which both ptr1 and ptr2 point
     ptr2.get_ptr_val();   // returns 42

兩個指針指向同一對象時,其中任意一個均可以改變共享對象的值。

1.3 懸垂指針(Dangling Pointers)

由於類直接複製指針,會使用戶面臨潛在的問題:HasPtr 保存着給定指針。
用戶必須保證只要 HasPtr 對象存在,該指針指向的對象就存在:

     int *ip = new int(42); // dynamically allocated int initialized to 42
     HasPtr ptr(ip, 10);    // Has Ptr points to same object as ip does
     delete ip;             // object pointed to by ip is freed
     ptr.set_ptr_val(0); // disaster: The object to which Has Ptr points was freed!

這裏的問題是 ip 和 ptr 中的指針指向同一對象。
刪除了該對象時,ptr 中的指針再也不指向有效對象。
然而,ptr 卻沒有辦法得知對象已經不存在了。

[2. 定義智能指針類]

智能指針除了增長功能外,其行爲像普通指針同樣。
本例中讓智能指針負責刪除共享對象。
用戶將動態分配一個對象並將該對象的地址傳給新的 HasPtr 類。
用戶仍然能夠經過普通指針訪問對象,但毫不能刪除指針。 

HasPtr 類將保證在撤銷指向對象的最後一個 HasPtr 對象時刪除對象。
具體而言,複製對象時,副本和原對象將指向同一基礎對象,
若是經過一個副本改變基礎對象,則經過另外一對象訪問的值也會改變。 

新的 HasPtr 類須要一個析構函數來刪除指針,可是,析構函數不能無條件地刪除指針。
若是兩個 HasPtr 對象指向同一基礎對象,
那麼在兩個對象都撤銷以前,咱們並不但願刪除基礎對象。
爲了編寫析構函數,須要知道這個 HasPtr 對象是否爲指向給定對象的最後一個。

2.1 引入使用計數

定義智能指針的通用技術是採用一個使用計數(或引用計數)。
智能指針類將一個計數器與類指向的對象相關聯。
使用計數跟蹤該類有多少個對象共享同一指針。
使用計數爲 0 時,刪除對象。每次建立類的新對象時,初始化指針並將使用計數置爲 1。
當對象做爲另外一對象的副本而建立時,複製構造函數複製指針並增長與之相應的使用計數的值。
對一個對象進行賦值時,
賦值操做符減小左操做數所指對象的使用計數的值(若是使用計數減至 0,則刪除對象),
並增長右操做數所指對象的使用計數的值。
最後,調用析構函數時,析構函數減小使用計數的值,若是計數減至 0,則刪除基礎對象。
惟一的創新在於決定將使用計數放在哪裏。
計數器不能直接放在 HasPtr 對象中,爲何呢?考慮下面的狀況:

       int obj;
       HasPtr p1(&obj, 42);
       HasPtr p2(p1);  // p1 and p2 both point to same int object
       HasPtr p3(p1);  // p1, p2, and p3 all point to same int object

若是使用計數保存在 HasPtr 對象中,建立 p3 時怎樣更新它?
能夠在 p1 中將計數增量並複製到 p3,但怎樣更新 p2 中的計數?

2.2 使用計數類

實現使用計數有兩種經典策略,另外一種方法將在後續章節中講述。
這裏所用的方法中,須要定義一個單獨的具體類用以封閉使用計數和相關指針:

       // private class for use by HasPtr only
       class U_Ptr {
              friend class HasPtr;
              int *ip;
              size_t use;
              U_Ptr(int *p): ip(p), use(1) { }
              ~U_Ptr() { delete ip; }
       };

這個類的全部成員均爲 private。

咱們不但願用戶使用 U_Ptr 類,因此它沒有任何 public 成員。
將 HasPtr 類設置爲友元,使其成員能夠訪問 U_Ptr 的成員。
U_Ptr 類保存指針和使用計數,每一個 HasPtr 對象將指向一個 U_Ptr 對象,
使用計數將跟蹤指向每一個 U_Ptr 對象的 HasPtr 對象的數目。
U_Ptr 定義的構造函數複製指針,而析構函數刪除它。
構造函數還將使用計數置爲 1,表示一個 HasPtr 對象指向這個 U_Ptr 對象。
假定剛從指向 int 值 42 的指針建立一個 HasPtr 對象,能夠畫出這些對象,以下圖:

若是複製這個對象,則對象以下圖所示。

2.3 使用計數類的使用

新的 HasPtr 類保存一個指向 U_Ptr 對象的指針,U_Ptr 對象指向實際的 int 基礎對象。
必須改變每一個成員以說明的 HasPtr 類指向一個 U_Ptr 對象而不是一個 int* 值。
先看看構造函數和複製控制成員:

       /* smart pointer class: takes ownership of the dynamically allocated
        * object to which it is bound
        * User code must dynamically allocate an object to initialize a HasPtr
        * and must not delete that object; the HasPtr class will delete it
        */
       class HasPtr {
       public:
              // HasPtr owns the pointer; pmust have been dynamically allocated
              HasPtr(int *p, int i): ptr(new U_Ptr(p)), val(i) { }

              // copy members and increment the use count
              HasPtr(const HasPtr &orig):ptr(orig.ptr), val(orig.val) { ++ptr->use; }

              HasPtr& operator=(const HasPtr&);

              // if use count goes to zero, delete the U_Ptr object
              ~HasPtr() { if (--ptr->use == 0) delete ptr; }
private: U_Ptr *ptr; // points to use-counted U_Ptr class int val; };

接受一個指針和一個 int 值的 HasPtr 構造函數使用其指針形參建立一個新的 U_Ptr 對象。
HasPtr 構造函數執行完畢後,HasPtr 對象指向一個新分配的 U_Ptr 對象,該 U_Ptr 對象存儲給定指針。
新 U_Ptr 中的使用計數爲 1,表示只有一個 HasPtr 對象指向它。
複製構造函數從形參複製成員並增長使用計數的值。
複製構造函數執行完畢後,新建立對象與原有對象指向同一 U_Ptr 對象,該 U_Ptr 對象的使用計數加 1。
析構函數將檢查 U_Ptr 基礎對象的使用計數。
若是使用計數爲 0,則這是最後一個指向該 U_Ptr 對象的 HasPtr 對象,
在這種狀況下,HasPtr 析構函數刪除其 U_Ptr 指針。
刪除該指針將引發對 U_Ptr 析構函數的調用,U_Ptr 析構函數刪除 int 基礎對象。

2.4 賦值與使用計數

賦值操做符比複製構造函數複雜一點:

       HasPtr& HasPtr::operator=(const HasPtr &rhs)
       {
              ++rhs.ptr->use;     // increment use count on rhs first
              if (--ptr->use == 0)
                     delete ptr;    // if use count goes to 0 on this object, delete it
              ptr = rhs.ptr;      // copy the U_Ptr object
              val = rhs.val;      // copy the int member
             
              return *this;
       }

在這裏,首先將右操做數中的使用計數加 1,而後將左操做數對象的使用計數減 1 並檢查這個使用計數。
若是這是指向 U_Ptr 對象的最後一個對象,就刪除該對象,這會依次撤銷 int 基礎對象。
將左操做數中的當前值減 1(可能撤銷該對象)以後,
再將指針從 rhs 複製到這個對象。賦值照常返回對這個對象的引用。
這個賦值操做符在減小左操做數的使用計數以前使 rhs 的使用計數加 1,從而防止自身賦值。

注意:若是左右操做數相同,賦值操做符的效果將是 U_Ptr 基礎對象的使用計數加 1 以後當即減 1。

2.5 改變其餘成員

如今須要改變訪問 int* 的其餘成員,以便經過 U_Ptr 指針間接獲取 int:

       class HasPtr {
       public:
              // copy control and constructors as before
              // accessors must change to fetch value from U_Ptr object
              int *get_ptr() const { return ptr->ip; }
              int get_int() const { return val; }

              // change the appropriate data member
              void set_ptr(int *p) { ptr->ip = p; }
              void set_int(int i) { val = i; }

              // return or change the value pointed to, so ok for const objects
              // Note: *ptr->ip is equivalent to *(ptr->ip)
              int get_ptr_val() const { return *ptr->ip; }
              void set_ptr_val(int i) { *ptr->ip = i; }

       private:
              U_Ptr *ptr;        // points to use-counted U_Ptr class
              int val;
       };

那些使用指針操做的函數必須對 U_Ptr 解引用,以便獲取 int* 基礎對象。
複製 HasPtr 對象時,int 成員的行爲與第一個類中同樣。
所複製的是 int 成員的值,各成員是獨立的,副本和原對象中的指針仍指向同一基礎對象,
對基礎對象的改變將影響經過任一 HasPtr 對象所看到的值。
然而,HasPtr 的用戶無須擔憂懸垂指針,只要他們讓 HasPtr 類負責釋放對象,
HasPtr 類將保證只要有指向基礎對象的 HasPtr 對象存在,基礎對象就存在。

爲了管理具備指針成員的類,
必須定義複製構造函數、賦值操做符和析構函數這三個複製控制成員。
值型類將指針成員所指基礎值的副本給每一個對象。
複製構造函數分配新元素並從被複制對象處複製值,
賦值操做符撤銷所保存的原對象並從右操做數向左操做數複製值,析構函數撤銷對象。 

「智能指針」的類在對象間共享同一基礎值,從而提供了指針型行爲。
使用計數是管理智能指針類的通用技術。
同一基礎值的每一個副本都有一個使用計數。
複製構造函數將指針從舊對象複製到新對象時,會將使用計數加 1。
賦值操做符將左操做數的使用計數減 1 並將右操做數的使用計數加 1,
若是左操做數的使用計數減至 0,賦值操做符必須刪除它所指向的對象,
最後,賦值操做符將指針從右操做數複製到左操做數。
析構函數將使用計數減 1,而且,若是使用計數減至 0,就刪除基礎對象。

 [3. 定義值型類]

處理指針成員的另外一個方法是給指針成員提供值語義。
具備值語義的類所定義的對象,其行爲很像算術類型的對象:
複製值型對象時,會獲得一個不一樣的新副本。
對副本所作的改變不會反映在原有對象上,反之亦然。string 類是值型類的一個例子。
要使指針成員表現得像一個值,複製 HasPtr 對象時必須複製指針所指向的對象:

      /*
       * Valuelike behavior even though HasPtr has a pointer member:
       * Each time we copy a HasPtr object, we make a new copy of the
       * underlying int object to which ptr points.
       */
       class HasPtr {
       public:
              // no point to passing a pointer if we're going to copy it anyway
              // store pointer to a copy of the object we're given
              HasPtr(const int &p, int i): ptr(new int(p)), val(i) {}

              // copy members and increment the use count
              HasPtr(const HasPtr &orig):ptr(new int (*orig.ptr)), val(orig.val) { }

              HasPtr& operator=(const HasPtr&);
              ~HasPtr() { delete ptr; }

              // accessors must change to fetch value from Ptr object
              int get_ptr_val() const { return *ptr; }
              int get_int() const { return val; }

              // change the appropriate data member
              void set_ptr(int *p) { ptr = p; }
              void set_int(int i) { val = i; }

              // return or change the value pointed to, so ok for const objects
              int *get_ptr() const { return ptr; }
              void set_ptr_val(int p) const { *ptr = p; }

       private:
              int *ptr;        // points to an int
              int val;
       };

複製構造函數再也不復制指針,它將分配一個新的 int 對象,並初始化該對象以保存與被複制對象相同的值。
每一個對象都保存屬於本身的 int 值的不一樣副本,因此析構函數將無條件刪除指針。
賦值操做符不須要分配新對象,
它只是必須記得給其指針所指向的對象賦新值,而不是給指針自己賦值:

       HasPtr& HasPtr::operator=(const HasPtr &rhs)
       {
              // Note: Every HasPtr is guaranteed to point at an actual int;
              // We know that ptr cannot be a zero pointer
              *ptr = *rhs.ptr;       // copy the value pointed to
              val = rhs.val;         // copy the int
return *this; }

換句話說,改變的是指針所指向的值,而不是指針。即便要將一個對象賦值給它自己,賦值操做符也必須老是保證正確。本例中,即便左右操做數相同,操做本質上也是安全的,所以,不須要顯式檢查自身賦值。

相關文章
相關標籤/搜索