筆記十二:智能指針(二)

導語:

智能指針(一) 中講解了智能指針的實現方式一,即僱傭一個使用計數類記錄共享對象。現在講解智能指針的另一種實現方式,句柄形式的智能指針。在介紹句柄形式的智能指針之前,先介紹代理類


代理類:

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章