天天看點

C++基礎—複制控制,指針成員管理

包含指針的類需要特别注意複制控制,原因是複制指針時隻是複制了指針中的位址,而不會複制指針指向的對象!

    将一個指針複制到另一個指針時,兩個指針指向同一對象。當兩個指針指向同一對象時,可能使用任一指針改變基礎對象。類似地,很可能一個指針删除了一對象時,另一指針的使用者還認為基礎對象仍然存在。指針成員預設具有與指針對象同樣的行為。

大多數C++類采用以下三種方法之一管理指針成員:

    1)指針成員采取正常指針型行為:這樣的類具有指針的所有缺陷但無需特殊的複制控制!

    2)類可以實作所謂的“智能指針”行為:通過對共享指針的對象進行計數,指針所指向的對象是共享的,但類能夠防止懸垂指針。

    3)類采取值型行為:每個對象在建立的時候,不管是初始化或者複制構造,指針所指向的對象是唯一的,有每個類對象獨立管理。

1 正常指針:

[cpp]  view plain copy

C++基礎—複制控制,指針成員管理
C++基礎—複制控制,指針成員管理
  1. class HasPtr  
  2. {  
  3. public:  
  4.     HasPtr(int *p,int i):ptr(p),val(i) {}  //構造函數
  5.   ......
  6. private:  
  7.     int *ptr;  
  8.     int val;  
  9. };  

因為 HasPtr 類沒有定義複制構造函數 , 是以複制一個 HasPtr 對象将複制兩個成員,因為采取的正常的合成構造函數

[cpp]  view plain copy

C++基礎—複制控制,指針成員管理
C++基礎—複制控制,指針成員管理
  1. int obj = 0;  
  2. HasPtr ptr1(&obj,42);  
  3. HasPtr ptr2(ptr1);  

複制之後,int值是清楚且獨立的,但是指針則糾纏在一起!

 複制一個算術值時,副本獨立于原版,可以改變一個副本而不改變另一個

複制指針時,位址值是可區分的,但指針指向同一基礎對象。是以,如果在任意對象上調用set_ptr_val,則兩者的基礎對象都會改變

  因為類直接複制指針,會使使用者面臨潛在的問題:HasPtr儲存着給定指針。使用者必須保證隻要HasPtr對象存在,該指針指向的對象就存在:

[cpp]  view plain copy

C++基礎—複制控制,指針成員管理
C++基礎—複制控制,指針成員管理
  1. int *ip = new int(42);  
  2. HasPtr ptr(ip,42);  
  3. delete ip;              //會造成懸垂指針  
  4. ptr.set_ptr_val(0);     //Error,但是編譯器檢測不出來  
  5. cout << ptr.get_ptr_val() << endl;   //Error,但是編譯器檢測不出來  

 2 智能指針

除了增加功能外,其行為像普通指針一樣。本例中讓智能指針負責删除共享對象。使用者将動态配置設定一個對象并将該對象的位址傳給新的HasPtr類。使用者仍然可以通過普通指針通路對象,但絕不能删除指針。HasPtr類将保證在撤銷指向對象的最後一個HasPtr對象時删除對象。

   HasPtr在其他方面的行為與普通指針一樣。具體而言,複制對象時,副本和原對象将指向同一基礎對象,如果通過一個副本改變基礎對象,則通過另一對象通路的值也會改變(類似于上例中的普通指針成員)。

    新的HasPtr類需要一個析構函數來删除指針,但是,析構函數不能無條件地删除指針。如果兩個HasPtr對象指向同一基礎對象,那麼,在兩個對象都撤銷之前,我們并不希望删除基礎對象。為了編寫析構函數,需要知道這個HasPtr對象是否為指向給定對象的最後一個。

1、引入使用計數

    定義智能指針的通用技術是采用一個使用計數[引用計數]。智能指針類将一個計數器與類指向的對象相關聯。使用計數跟蹤該類有多少個對象共享同一指針。使用計數為0時,删除對象。

    1)每次建立類的新對象時,初始化指針并将使用計數置為1。

    2)當對象作為另一對象的副本而建立時,複制構造函數複制指針并增加與之相應的使用計數的值。

    3)對一個對象進行指派時,指派操作符減少左操作數所指對象的使用計數的值(如果使用計數減至0,則删除對象),并增加右操作數所指對象的使用計數的值。

    4)最後,調用析構函數時,析構函數減少使用計數的值,如果計數減至0,則删除基礎對象。

唯一的創新在于決定将使用計數放在哪裡。計數器不能直接放在HasPtr對象中:

[cpp]  view plain copy

C++基礎—複制控制,指針成員管理
C++基礎—複制控制,指針成員管理
  1. int obj;  
  2. HasPtr p1(&obj,42);  
  3. HasPtr p2(p1);  
  4. HasPtr p3(p2);  

如果使用計數儲存在HasPtr對象中,建立p3時怎樣更新它?可以在p1中将計數增量并複制到p3,但怎樣更新p2中的計數?

定義一個單獨的具體類用以封裝使用計數和相關指針:

[cpp]  view plain copy

C++基礎—複制控制,指針成員管理
C++基礎—複制控制,指針成員管理
  1. class U_Ptr  
  2. {  
  3.     //将HasPtr設定成為友元類,使其成員可以通路U_Ptr的成員  
  4.     friend class HasPtr;  
  5.     int *ip;  
  6.     size_t use;  
  7.     U_Ptr(int *p):ip(p),use(1) {}  //<初始化為1
  8.     ~U_Ptr()  
  9.     {  
  10.         delete ip;  
  11.     }  
  12. };  

将所有的成員都設定成為private:我們不希望普通使用者使用U_Ptr類,是以他沒有任何public成員!

使用計數類的使用

   新的HasPtr類儲存一個指向U_Ptr對象的指針,U_Ptr對象指向實際的int基礎對象:

[cpp]  view plain copy

C++基礎—複制控制,指針成員管理
C++基礎—複制控制,指針成員管理
  1. class HasPtr  
  2. {  
  3. public:  
  4.     HasPtr(int *p,int i):ptr(new U_Ptr(p)),val(i){}  
  5.     HasPtr(const HasPtr &orig):ptr(orig.ptr),val(orig.val)  
  6.     {  
  7.         ++ ptr->use;  
  8.     }  
  9.     HasPtr &operator=(const HasPtr &orig);  
  10.     ~HasPtr()  
  11.     {  
  12.         if ( -- ptr -> use == 0 )  
  13.         {  
  14.             delete ptr;  
  15.         }  
  16.     }  
  17. private:  
  18.     U_Ptr *ptr;  
  19.     int val;  
  20. };  

接受一個指針和一個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基礎對象。

上面介紹了複制構造函數和析構函數中計數器的作用

之後再介紹在指派操作符中的作用

[cpp]  view plain copy

C++基礎—複制控制,指針成員管理
C++基礎—複制控制,指針成員管理
  1. HasPtr &HasPtr::operator=(const HasPtr &rhs)  
  2. {  
  3.     ++ rhs.ptr -> use;  //
  4.     if ( -- ptr -> use == 0)  
  5.         delete ptr;  
  6.     ptr = rhs.ptr;  
  7.     val = rhs.val;  
  8.     return *this;  
  9. }  

    在這裡,首先将右操作數中的使用計數加1,然後将左操作數對象的使用計數減1并檢查這個使用計數。像析構函數中那樣,如果這是指向U_Ptr對象的最後一個對象,就删除該對象,這會依次撤銷int基礎對象。将左操作數中的目前值減1(可能撤銷該對象)之後,再将指針從rhs複制到這個對象。

    這個指派操作符在減少左操作數的使用計數之前使rhs的使 用計數加1,進而防止自身指派。如果左右操作數相同,指派操作符的效果将是U_Ptr基礎對象的使用計數加1之後立即減 1。

總結:

 為了管理具有指針成員的類,必須定義三個複制控制成員:複制構造函數、指派操作符和析構函數。這些成員可以定義指針成員的指針型行為或值型行為。

    值型類将指針成員所指基礎值的副本給每個對象。複制構造函數配置設定新元素并從被複制對象處複制值,指派操作符撤銷所儲存的原對象并從右操作數向左操作數複制值,析構函數撤銷對象。

    作為定義值型行為或指針型行為的另一選擇,是使用稱為“智能指針”的一些類。這些類在對象間共享同一基礎值,進而提供了指針型行為。但它們使用複制控制技術以避免正常指針的一些缺陷。為了實作智能指針行為,類需要保證基礎對象一直存在,直到最後一個副本消失。使用計數是管理智能指針類的通用技術。

3 定義值型類

[cpp]  view plain copy

C++基礎—複制控制,指針成員管理
C++基礎—複制控制,指針成員管理
  1. class HasPtr  
  2. {  
  3. private:  
  4.     HasPtr(const int &p,int i):ptr(new int(p)),val(i) {}  
  5.     //複制控制  
  6.     HasPtr(const HasPtr &rhs):ptr(new int(*rhs.ptr)),val(rhs.val) {}  //<複制構造函數中同樣申請一個指針
  7.     HasPtr &operator=(const HasPtr &rhs);  
  8.     ~HasPtr()  
  9.     {  
  10.         delete ptr;  
  11.     }  
  12.    ........
  13. public:  
  14.     int *ptr;  
  15.     int val;  
  16. };  

  複制構造函數不再複制指針,它将配置設定一個新的int對象,并初始化該對象以儲存與被複制對象相同的值。每個對象都儲存屬于自己的int值的不同副本。因為每個對象儲存自己的副本,是以析構函數将無條件删除指針。

    指派操作符也因而不用配置設定新對象,它隻是必須記得給其指針所指向的對象賦新值,而不是給指針本身指派:

[cpp]  view plain copy

C++基礎—複制控制,指針成員管理
C++基礎—複制控制,指針成員管理
  1. HasPtr &HasPtr::operator=(const HasPtr &rhs)  
  2. {  
  3.     *ptr = *rhs.ptr;  //<初始化對象就可以了
  4.     val = rhs.val;  
  5.     return *this;  
  6. }  

    即使要将一個對象指派給它本身,指派操作符也必須總是保證正确。本例中,即使左右操作數相同,操作本質上也是安全的,是以,不需要顯式檢查自身指派。

繼續閱讀