包含指針的類需要特别注意複制控制,原因是複制指針時隻是複制了指針中的位址,而不會複制指針指向的對象!
将一個指針複制到另一個指針時,兩個指針指向同一對象。當兩個指針指向同一對象時,可能使用任一指針改變基礎對象。類似地,很可能一個指針删除了一對象時,另一指針的使用者還認為基礎對象仍然存在。指針成員預設具有與指針對象同樣的行為。
大多數C++類采用以下三種方法之一管理指針成員:
1)指針成員采取正常指針型行為:這樣的類具有指針的所有缺陷但無需特殊的複制控制!
2)類可以實作所謂的“智能指針”行為:通過對共享指針的對象進行計數,指針所指向的對象是共享的,但類能夠防止懸垂指針。
3)類采取值型行為:每個對象在建立的時候,不管是初始化或者複制構造,指針所指向的對象是唯一的,有每個類對象獨立管理。
1 正常指針:
[cpp] view plain copy
- class HasPtr
- {
- public:
- HasPtr(int *p,int i):ptr(p),val(i) {} //構造函數
- ......
- private:
- int *ptr;
- int val;
- };
因為 HasPtr 類沒有定義複制構造函數 , 是以複制一個 HasPtr 對象将複制兩個成員,因為采取的正常的合成構造函數
[cpp] view plain copy
- int obj = 0;
- HasPtr ptr1(&obj,42);
- HasPtr ptr2(ptr1);
複制之後,int值是清楚且獨立的,但是指針則糾纏在一起!
複制一個算術值時,副本獨立于原版,可以改變一個副本而不改變另一個
複制指針時,位址值是可區分的,但指針指向同一基礎對象。是以,如果在任意對象上調用set_ptr_val,則兩者的基礎對象都會改變
因為類直接複制指針,會使使用者面臨潛在的問題:HasPtr儲存着給定指針。使用者必須保證隻要HasPtr對象存在,該指針指向的對象就存在:
[cpp] view plain copy
- int *ip = new int(42);
- HasPtr ptr(ip,42);
- delete ip; //會造成懸垂指針
- ptr.set_ptr_val(0); //Error,但是編譯器檢測不出來
- 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
- int obj;
- HasPtr p1(&obj,42);
- HasPtr p2(p1);
- HasPtr p3(p2);
如果使用計數儲存在HasPtr對象中,建立p3時怎樣更新它?可以在p1中将計數增量并複制到p3,但怎樣更新p2中的計數?
定義一個單獨的具體類用以封裝使用計數和相關指針:
[cpp] view plain copy
- class U_Ptr
- {
- //将HasPtr設定成為友元類,使其成員可以通路U_Ptr的成員
- friend class HasPtr;
- int *ip;
- size_t use;
- U_Ptr(int *p):ip(p),use(1) {} //<初始化為1
- ~U_Ptr()
- {
- delete ip;
- }
- };
将所有的成員都設定成為private:我們不希望普通使用者使用U_Ptr類,是以他沒有任何public成員!
使用計數類的使用
新的HasPtr類儲存一個指向U_Ptr對象的指針,U_Ptr對象指向實際的int基礎對象:
[cpp] view plain copy
- class HasPtr
- {
- public:
- HasPtr(int *p,int i):ptr(new U_Ptr(p)),val(i){}
- HasPtr(const HasPtr &orig):ptr(orig.ptr),val(orig.val)
- {
- ++ ptr->use;
- }
- HasPtr &operator=(const HasPtr &orig);
- ~HasPtr()
- {
- if ( -- ptr -> use == 0 )
- {
- delete ptr;
- }
- }
- private:
- U_Ptr *ptr;
- 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基礎對象。
上面介紹了複制構造函數和析構函數中計數器的作用
之後再介紹在指派操作符中的作用
[cpp] view plain copy
- HasPtr &HasPtr::operator=(const HasPtr &rhs)
- {
- ++ rhs.ptr -> use; //
- if ( -- ptr -> use == 0)
- delete ptr;
- ptr = rhs.ptr;
- val = rhs.val;
- return *this;
- }
在這裡,首先将右操作數中的使用計數加1,然後将左操作數對象的使用計數減1并檢查這個使用計數。像析構函數中那樣,如果這是指向U_Ptr對象的最後一個對象,就删除該對象,這會依次撤銷int基礎對象。将左操作數中的目前值減1(可能撤銷該對象)之後,再将指針從rhs複制到這個對象。
這個指派操作符在減少左操作數的使用計數之前使rhs的使 用計數加1,進而防止自身指派。如果左右操作數相同,指派操作符的效果将是U_Ptr基礎對象的使用計數加1之後立即減 1。
總結:
為了管理具有指針成員的類,必須定義三個複制控制成員:複制構造函數、指派操作符和析構函數。這些成員可以定義指針成員的指針型行為或值型行為。
值型類将指針成員所指基礎值的副本給每個對象。複制構造函數配置設定新元素并從被複制對象處複制值,指派操作符撤銷所儲存的原對象并從右操作數向左操作數複制值,析構函數撤銷對象。
作為定義值型行為或指針型行為的另一選擇,是使用稱為“智能指針”的一些類。這些類在對象間共享同一基礎值,進而提供了指針型行為。但它們使用複制控制技術以避免正常指針的一些缺陷。為了實作智能指針行為,類需要保證基礎對象一直存在,直到最後一個副本消失。使用計數是管理智能指針類的通用技術。
3 定義值型類
[cpp] view plain copy
- class HasPtr
- {
- private:
- HasPtr(const int &p,int i):ptr(new int(p)),val(i) {}
- //複制控制
- HasPtr(const HasPtr &rhs):ptr(new int(*rhs.ptr)),val(rhs.val) {} //<複制構造函數中同樣申請一個指針
- HasPtr &operator=(const HasPtr &rhs);
- ~HasPtr()
- {
- delete ptr;
- }
- ........
- public:
- int *ptr;
- int val;
- };
複制構造函數不再複制指針,它将配置設定一個新的int對象,并初始化該對象以儲存與被複制對象相同的值。每個對象都儲存屬于自己的int值的不同副本。因為每個對象儲存自己的副本,是以析構函數将無條件删除指針。
指派操作符也因而不用配置設定新對象,它隻是必須記得給其指針所指向的對象賦新值,而不是給指針本身指派:
[cpp] view plain copy
- HasPtr &HasPtr::operator=(const HasPtr &rhs)
- {
- *ptr = *rhs.ptr; //<初始化對象就可以了
- val = rhs.val;
- return *this;
- }
即使要将一個對象指派給它本身,指派操作符也必須總是保證正确。本例中,即使左右操作數相同,操作本質上也是安全的,是以,不需要顯式檢查自身指派。