管理类的指针成员
設計具有指針成員的類時,類設計者必須首先需要決定的是該指針應提供什么行為。
將一個指針復制到另一個指針時,兩個指針指向同一對象。
當兩個指針指向同一對象時,可能使用任一指針改變基礎對象。
類似地,很可能一個指針刪除了一對象時,另一指針的用戶還認為基礎對象仍然存在。
指針成員默認具有與指針對象同樣的行為。
然而,通過不同的復制控制策略,可以為指針成員實現不同的行為。
大多數 C++ 類采用以下三種方法之一管理指針成員:
1. 指針成員采取常規指針型行為。這樣的類具有指針的所有缺陷但無需特殊的復制控制。
2. 類可以實現所謂的“智能指針”行為。指針所指向的對象是共享的,但類能夠防止懸垂指針。
3. 類采取值型行為。指針所指向的對象是唯一的,由每個類對象獨立管理。
[1. 一個帶指針成員的簡單類]
為了闡明所涉及的問題,我們將實現一個簡單類,該類包含一個 int 值和一個指針:
// class that has a pointer member that behaves like a plain pointerclass HasPtr {public:// copy of the values we're givenHasPtr(int *p, int i): ptr(p), val(i) { }// const members to return the value of the indicated data memberint *get_ptr() const { return ptr; }int get_int() const { return val; }// non const members to change the indicated data membervoid set_ptr(int *p) { ptr = p; }void set_int(int i) { val = i; }// return or change the value pointed to, so ok for const objectsint get_ptr_val() const { return *ptr; }void set_ptr_val(int val) const { *ptr = val; }private:int *ptr;int val;};1.1 默認復制/賦值與指針成員
因為 HasPtr 類沒有定義復制構造函數,所以復制一個 HasPtr 對象將復制兩個成員:
int obj = 0;HasPtr ptr1(&obj, 42); // int* member points to obj, val is 42HasPtr ptr2(ptr1); // int* member points to obj, val is 42復制之后,ptr1 和 ptr2 中的指針指向同一對象且兩個對象中的 int 值相同。
但是,因為指針的值不同于它所指對象的值,這兩個成員的行為看來非常不同。
復制之后,int 值是清楚和獨立的,而指針則糾纏在一起。
注意:具有指針成員且使用默認合成復制構造函數的類具有普通指針的所有缺陷。
?????????尤其是,類本身無法避免懸垂指針。
1.2 指針共享同一對象
復制一個算術值時,副本獨立于原版,可以改變一個副本而不改變另一個:
ptr1.set_int(0); // changes val member only in ptr1ptr2.get_int(); // returns 42ptr1.get_int(); // returns 0復制指針時,地址值是可區分的,但指針指向同一基礎對象。
如果在任一對象上調用 set_ptr_val,則二者的基礎對象都會改變:
兩個指針指向同一對象時,其中任意一個都可以改變共享對象的值。
1.3 懸垂指針(Dangling Pointers)
因為類直接復制指針,會使用戶面臨潛在的問題:HasPtr 保存著給定指針。
用戶必須保證只要 HasPtr 對象存在,該指針指向的對象就存在:
這里的問題是 ip 和 ptr 中的指針指向同一對象。
刪除了該對象時,ptr 中的指針不再指向有效對象。
然而,ptr 卻沒有辦法得知對象已經不存在了。
[2. 定義智能指針類]
智能指針除了增加功能外,其行為像普通指針一樣。
本例中讓智能指針負責刪除共享對象。
用戶將動態分配一個對象并將該對象的地址傳給新的 HasPtr 類。
用戶仍然可以通過普通指針訪問對象,但絕不能刪除指針。?
HasPtr 類將保證在撤銷指向對象的最后一個 HasPtr 對象時刪除對象。
具體而言,復制對象時,副本和原對象將指向同一基礎對象,
如果通過一個副本改變基礎對象,則通過另一對象訪問的值也會改變。?
新的 HasPtr 類需要一個析構函數來刪除指針,但是,析構函數不能無條件地刪除指針。
如果兩個 HasPtr 對象指向同一基礎對象,
那么在兩個對象都撤銷之前,我們并不希望刪除基礎對象。
為了編寫析構函數,需要知道這個 HasPtr 對象是否為指向給定對象的最后一個。
2.1 引入使用計數
定義智能指針的通用技術是采用一個使用計數(或引用計數)。
智能指針類將一個計數器與類指向的對象相關聯。
使用計數跟蹤該類有多少個對象共享同一指針。
使用計數為 0 時,刪除對象。每次創建類的新對象時,初始化指針并將使用計數置為 1。
當對象作為另一對象的副本而創建時,復制構造函數復制指針并增加與之相應的使用計數的值。
對一個對象進行賦值時,
賦值操作符減少左操作數所指對象的使用計數的值(如果使用計數減至 0,則刪除對象),
并增加右操作數所指對象的使用計數的值。
最后,調用析構函數時,析構函數減少使用計數的值,如果計數減至 0,則刪除基礎對象。
唯一的創新在于決定將使用計數放在哪里。
計數器不能直接放在 HasPtr 對象中,為什么呢?考慮下面的情況:
如果使用計數保存在 HasPtr 對象中,創建 p3 時怎樣更新它?
可以在 p1 中將計數增量并復制到 p3,但怎樣更新 p2 中的計數?
2.2 使用計數類
實現使用計數有兩種經典策略,另一種方法將在后續章節中講述。
這里所用的方法中,需要定義一個單獨的具體類用以封閉使用計數和相關指針:
這個類的所有成員均為 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* 值。
先看看構造函數和復制控制成員:
private:U_Ptr *ptr; // points to use-counted U_Ptr classint 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 firstif (--ptr->use == 0)delete ptr; // if use count goes to 0 on this object, delete itptr = rhs.ptr; // copy the U_Ptr objectval = rhs.val; // copy the int memberreturn *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 objectint *get_ptr() const { return ptr->ip; }int get_int() const { return val; }// change the appropriate data membervoid 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 classint val;};那些使用指針操作的函數必須對 U_Ptr 解引用,以便獲取 int* 基礎對象。
復制 HasPtr 對象時,int 成員的行為與第一個類中一樣。
所復制的是 int 成員的值,各成員是獨立的,副本和原對象中的指針仍指向同一基礎對象,
對基礎對象的改變將影響通過任一 HasPtr 對象所看到的值。
然而,HasPtr 的用戶無須擔心懸垂指針,只要他們讓 HasPtr 類負責釋放對象,
HasPtr 類將保證只要有指向基礎對象的 HasPtr 對象存在,基礎對象就存在。
為了管理具有指針成員的類,
必須定義復制構造函數、賦值操作符和析構函數這三個復制控制成員。
值型類將指針成員所指基礎值的副本給每個對象。
復制構造函數分配新元素并從被復制對象處復制值,
賦值操作符撤銷所保存的原對象并從右操作數向左操作數復制值,析構函數撤銷對象。?
“智能指針”的類在對象間共享同一基礎值,從而提供了指針型行為。
使用計數是管理智能指針類的通用技術。
同一基礎值的每個副本都有一個使用計數。
復制構造函數將指針從舊對象復制到新對象時,會將使用計數加 1。
賦值操作符將左操作數的使用計數減 1 并將右操作數的使用計數加 1,
如果左操作數的使用計數減至 0,賦值操作符必須刪除它所指向的對象,
最后,賦值操作符將指針從右操作數復制到左操作數。
析構函數將使用計數減 1,并且,如果使用計數減至 0,就刪除基礎對象。
?[3. 定義值型類]
處理指針成員的另一個方法是給指針成員提供值語義。
具有值語義的類所定義的對象,其行為很像算術類型的對象:
復制值型對象時,會得到一個不同的新副本。
對副本所做的改變不會反映在原有對象上,反之亦然。string 類是值型類的一個例子。
要使指針成員表現得像一個值,復制 HasPtr 對象時必須復制指針所指向的對象:
復制構造函數不再復制指針,它將分配一個新的 int 對象,并初始化該對象以保存與被復制對象相同的值。
每個對象都保存屬于自己的 int 值的不同副本,所以析構函數將無條件刪除指針。
賦值操作符不需要分配新對象,
它只是必須記得給其指針所指向的對象賦新值,而不是給指針本身賦值:
return *this;}
換句話說,改變的是指針所指向的值,而不是指針。
即使要將一個對象賦值給它本身,賦值操作符也必須總是保證正確。
本例中,即使左右操作數相同,操作本質上也是安全的,因此,不需要顯式檢查自身賦值。
轉載于:https://www.cnblogs.com/yshl-dragon/p/3227707.html
總結
- 上一篇: DRBD+HeartBeat+NFS 架
- 下一篇: 常用的web安全处理