笔记十二:智能指针(二)

导语:

智能指针(一) 中讲解了智能指针的实现方式一,即雇佣一个使用计数类记录共享对象。现在讲解智能指针的另一种实现方式,句柄形式的智能指针。在介绍句柄形式的智能指针之前,先介绍代理类


代理类:

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

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