導語:
在智能指針(一) 中講解了智能指針的實現方式一,即僱傭一個使用計數類記錄共享對象。現在講解智能指針的另一種實現方式,句柄形式的智能指針。在介紹句柄形式的智能指針之前,先介紹代理類。
代理類:
1、現假設存在一個基類和它的派生類,設計如下:
基類:
class Animal
{
public:
Animal () {cout << "Create Animal..."<<endl;}
virtual void Name() const {cout <<"This is Animal..."<<endl;}
virtual ~Animal(){};
};
派生類:
class Cat: public Animal
{
public:
Cat () {cout << "Create Cat..."<<endl;}
void Name() const {cout <<"This is Cat..."<<endl;}
~Cat() {}
};
class Dog: public Animal
{
public:
Dog(){cout<<"Create Dog..."<<endl;}
void Name() const {cout <<"This is Dog..."<<endl;}
~Dog(){}
};
2、若希望用容器或內置數組保存因繼承而相關聯的對象,例如動物園是一個容器,一個動物園是其中的一個元素,一個動物園的標誌就是動物,劃分到具體動物就是貓,狗…等。
若定義multiset保存Animal基類類型的對象:
vector<Animal> Zoo;
存在一個Animal標籤和一個Cat標籤:
Animal ani;
Cat cat;
Zoo.push_back(ani);
Zoo.push_back(cat);
Zoo[0].Name();
Zoo[1].Name();
則加入派生類型Cat的對象時,只將對象的基類部分保存在容器中,派生類對象將被切掉。所以, 容器與通過繼承相關的類型不能很好的融合。若派生類屬性沒有被切掉,Zoo[1].Name()的結果應該是This is Cat… 而實際派生類屬性被切掉之後,結果爲:
從圖中可以看出cat的Name()函數的屬性被“切掉”。
3、容器無法保存派生類對象的屬性,那麼通過容器保存指針是否可行呢?
修改代碼如下:
vector<Animal*> Zoo;
Animal ani;
Cat cat;
Zoo.push_back(&ani);
Zoo.push_back(&cat);
Zoo[0]->Name();
Zoo[1]->Name();
打印結果:
結果證明派生類的屬性得以保留。原因是採用了基類類型的指針或引用實現了動態綁定的作用。
該方法的弊端是什麼呢?——用戶管理對象和指針的問題。
1) 假設ani或cat是動態分配的,即Cat *cat = new Cat;
那麼,用戶必須保證在容器消失時適當地釋放對象。
2)假設Zoo[1]存在,那麼Zoo[1]所指對象就必須存在,若cat在程序其他位置被釋放掉,那麼Zoo[1]在不知情的情況下就成爲了野指針,導致其後該指針的使用會引發錯誤。
4、指針不可取,那麼採用對象的複製操作呢?
代碼如下:
Cat *cat = new Cat;
vector<Animal*> Zoo;
Zoo.push_back(new Cat(*cat));
Zoo[0]->Name();
delete cat; cat = NULL;
cout <<"Cat對象被銷燬後,對Zoo[1]的影響:";
Zoo[0]->Name();
此時,存在一個cat對象,但是容器的指針並不是指向這個cat對象,而是在建立一個容器指針時同時動態生成一個cat對象的副本,該容器內指針指向這個副本,那麼cat對象的銷燬或修改與否,都不會影響Zoo[1]。
OK,那麼假設現在有一個對象obj,需要Zoo[1]指向obj的副本,但obj的數據類型是Animal*, Cat* 還是Dog* 目前尚不清楚,如下:
假設,我們猜想obj是Dog*類型的,發現代碼有誤,這表明對於不知道對象的確切類型時分配已知對象的新副本,採用new 數據類型(值)
不可取。
5、直接複製不可取,那麼定義虛操作進行復制又是否可行呢?
基類:
virtual Animal* clone() const {return new Animal(*this);}
派生類:
Cat* clone() const {
return new Cat(*this);}
Dog* clone() const {return new Dog(*this);}
這裏涉及到一個重要的知識點:如果虛函數的基類實例返回類類型的指針或引用,則派生類可以返回派生類類型的指針或引用 。例如基類中返回Animal* clone()
,派生類中返回Cat* clone()
。
此時,不再需要已知obj的類型,直接調用clone()函數即可。
現在,假設有2種情況:
1)現有Zoo要存儲100只Cat,那麼相應操作即爲:
Zoo.push_back(cat1->clone());
Zoo.push_back(cat2->clone());
...
Zoo.push_back(cat100->clone());
100只Cat的具體操作細節都暴露在外面,例cat1->clone()
。
2)爲了解決1)中問題,則需將對象的實際操作進行隱藏,只需要提供給用戶一個使用接口即可,這就是所謂的封裝。c++的三大特性:繼承、封裝、多態能夠通過類進行直接的反映。
6、代理類Agent
現設計一個代理類AnimalSmartHandle:
class AnimalSmartHandle
{
public:
AnimalSmartHandle(): ap(0) {} // 1
AnimalSmartHandle(const Animal& ani): ap(
ani.clone()){} // 2
AnimalSmartHandle(const AnimalSmartHandle& ash) : ap( ash.ap ? ash.ap : 0) {} // 3
AnimalSmartHandle &operator= (const AnimalSmartHandle& ash){ap = ash.ap; return *this;} // 4
const Animal *operator->() const {if (ap) return ap;} // 5
const Animal &operator*() const {if(ap) return *ap;} //6
~AnimalSmartHandle(){delete ap;} // 7
private:
Animal *ap; // 8
};
上述代碼解釋:
// 8 —— Animal* 類型的指針變量,指向目標基類或派生類。
// 1 —— 默認構造函數,當採用該默認構造函數時,基類或派生類對象並沒有建立,故ap初始化爲0。(PS: 這裏ap初始化爲NULL是否可行?通過測試,發現可以正常運行,結果輸出一致,所以這裏初始化0只是可以直觀的告訴用戶還沒有存在的Animal對象而已,並不表示這是唯一的初始化值)
// 2 —— 將該代理類與抽象基類或派生類進行關聯,原因之前也提到過通過基類類型的指針或引用實現動態綁定。
// 3,4,7 —— 類中存在指針,則必然涉及到複製控制:複製構造函數,賦值操作符重載,析構函數。
// 5 ——重載箭頭操作符。
AnimalSmartHandle ash2(cat);
ash2->Name();
(*ash2).Name();
這裏ash2->
即ash2.operator->()
返回Animal *
的指針變量,那麼對指針變量指向的對象擁有成員的操作即爲(ahs2.operator->())->Name()
等價於ahs2->Name()
。
若繼續有100只Cat進行復制,那麼通過代理類封裝後的形式如下:
Cat cat1;
AnimalSmartHandle ash1(cat1);
ash1->Name();
cout<<"\n"<<endl;
Cat cat2;
AnimalSmartHandle ash2(cat2);
(*ash2).Name();
cout<<"\n"<<endl;
//...
Cat cat100;
AnimalSmartHandle ash100(cat100);
運行結果爲:
最後,思考一個問題,若有多個代理類共同指向一個目標對象,該如何處理?
句柄型智能指針:
當有多個類共享一個基礎對象時,爲了減少內存開銷,通過智能指針的引用計數功能可以實現共享。
這裏添加一個使用計數指針:size_ t *use
,修改句柄類如下:
class AnimalSmartHandle
{
public:
AnimalSmartHandle(): ap(0),use(new size_t(1)) {} // 1
AnimalSmartHandle(const Animal& ani): ap(
ani.clone()), use(new size_t(1)){} // 2
AnimalSmartHandle(const AnimalSmartHandle& ash) : ap( ash.ap ? ash.ap : 0),use(ash.use) {++*use;} // 3
AnimalSmartHandle &operator= (const AnimalSmartHandle& ash){
++(*(ash.use));
if(--*use == 0)
{delete ap;ap=NULL; delete use; use=NULL;}
ap = ash.ap;
use = ash.use;
return *this;} // 4
const Animal *operator->() const {if (ap) return ap;} // 5
const Animal &operator*() const {if(ap) return *ap;} //6
~AnimalSmartHandle(){
if(--*use == 0)
{delete ap;ap=NULL; delete use; use=NULL;}} // 7
private:
Animal *ap; // 8
size_t *use; //使用計數
};
測試代碼如下:
Cat cat;
AnimalSmartHandle ash(*cat.clone());
ash->Name();
Dog dog;
AnimalSmartHandle ash1(*dog.clone());
ash1->Name();
cout<<"******************"<<endl;
ash1 = ash;
ash->Name();
結果如下:
調試使用計數的變化情況如下:
總結:
1)寫到這裏,回顧一下智能指針的前一種方式,是使用了額外的計數類進行使用計數判斷,而這裏直接是句柄指針的數據成員,是否可以對第一種方式進行修改,也變成SmartPtr的數據成員呢?
修改前一種方式的代碼如下:
template<class T>
class SmartPtr
{
public:
SmartPtr() : ptr(0),use(new size_t(1)) { }
SmartPtr(T& val) : ptr(&val),use(new size_t(1)) {}
SmartPtr(const SmartPtr &orig) : ptr(orig.ptr), use(orig.use) { ++*use; } //複製構造函數,使用計數加1
SmartPtr<T>& operator=(const SmartPtr& rhs);
const T *operator->() const {if (ptr) return ptr;}
const T &operator*() const {if(ptr) return *ptr;}
~SmartPtr() {
if (--*use == 0)
{
delete ptr;
delete use;
}
}
private:
T *ptr; //指向U_ptr對象的指針
size_t *use; //使用計數
};
測試代碼:
int *p = new int(7);
int *q = new int(8);
SmartPtr<int> SP(*p);
cout<<"SP指向p的時候:"<<*SP<<endl;
SmartPtr<int> SP1(*q);
cout<<"SP1指向q的時候:"<<*SP1<<endl;
SP = SP1;
cout<<"SP = SP1的時候:"<<*SP<<endl;
SmartPtr<int> SP2(SP);
cout<<"SP2 = SP的時候:"<<*SP<<endl;
SmartPtr<int> SP3(SP);
cout<<"SP3 = SP的時候:"<<*SP<<endl;
SmartPtr<int> SP4(SP);
cout<<"SP4 = SP的時候:"<<*SP<<endl;
結果:
通過修改代碼,我個人覺得也可以實現智能指針的功能(如有不妥,懇請指教~~)。
2)句柄類智能指針,我認爲共享的不是原始基礎目標對象,而是基礎目標對象的副本而已,給你一個基礎對象,句柄智能指針中的指針並不是指向該對象,而是先生成一個副本,然後再指向該副本。而前者使用額外的計數類則直接指向的是原始基礎對象。使用副本的原因,我個人理解是爲了方便基礎類與派生類的運行時識別(動態綁定)。
後記:
1)本文中Animal、Cat、Dog的使用參考自網上一篇也是介紹智能指針的文章,具體網址不太清楚了, O(∩_∩)O~
2)本文參考了2篇很優秀的關於智能指針,代理,句柄的文章:
http://rangercyh.blog.51cto.com/1444712/1293679
http://rangercyh.blog.51cto.com/1444712/1291958
但是,對於句柄類那一篇有疑問。比如句柄類是爲了實現“不去複製對象來實現運行時綁定” ,但文中給出的class Point
沒有虛函數,則不會有多態性或動態綁定這一說法了。其二,爲了解決copy函數多餘且複雜的問題,Handle::Handle(const Point &p0) : u(new int(1)), p(new Point(p0)) { }
感覺與代理類一文中copy()存在的原因這一節是有出入的。