筆記十二:智慧指標(二)
導語:
在智慧指標(一) 中講解了智慧指標的實現方式一,即僱傭一個使用計數類記錄共享物件。現在講解智慧指標的另一種實現方式,控制代碼形式的智慧指標。在介紹控制代碼形式的智慧指標之前,先介紹代理類。
代理類:
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~
但是,對於控制代碼類那一篇有疑問。比如控制代碼類是為了實現“不去複製物件來實現執行時繫結” ,但文中給出的class Point
沒有虛擬函式,則不會有多型性或動態繫結這一說法了。其二,為了解決copy函式多餘且複雜的問題,Handle::Handle(const Point &p0) : u(new int(1)), p(new Point(p0)) { }
感覺與代理類一文中copy()存在的原因這一節是有出入的。