1. 程式人生 > >筆記十二:智慧指標(二)

筆記十二:智慧指標(二)

導語:

智慧指標(一) 中講解了智慧指標的實現方式一,即僱傭一個使用計數類記錄共享物件。現在講解智慧指標的另一種實現方式,控制代碼形式的智慧指標。在介紹控制代碼形式的智慧指標之前,先介紹代理類

代理類:

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()存在的原因這一節是有出入的。