从代理类走进句柄类(理解“引用计数”和“写时拷贝”是一波什么操作以及如何使用?)

时间:2021-10-24 19:53:57

1.代理类

代理类是用来干什么的?
解决将继承体系中的对象放到同一个容器中的问题,这就必须解决两个子问题:1.编译时类型未知类型的绑定;2.合理的分配内存。
用交通工具类来举列子,现在有这样的一个类:

//交通工具类1
class Vehicle {
public:
    double weight() const; 
    void start();
    // ......
};
//各种类型的交通工具
class RoadVehicle : public Vehicle { /* ...... */ };
class AutoVehicle : public RoadVehicle { /* ...... */ };
class Aircraft    : public Vehicle { /* ...... */ };
class Helicopter  : public Aircraft { /* ...... */ };

现在我们想定义一个类,可以存放各种交通工具类的指针

Vehicle *parking_lot[1000];    // 指针数组
Helicopter  x;
parking_lot[num_vehicles++]=new Helicopter(x);

但是假如我们不知道x是什么类型,可以这样解决,修改Vehicle 类如下:

//交通工具类2
class Vehicle {
public:
    double weight() const; 
    void start();
    Vehicle *copy() const;
    // ......
};

这样每个具体的交通工具类实现一个copy()接口,以Helicopter类为例,说明copy()的作用

Vehicle *Helicopter::copy() const
{
    //为避免悬垂指针危险,这里应该拷贝一份Helicopter类
    return new Helicopter(*this); 
}

接下来这样操作,将未知类型的x放入parking_lot中

parking_lot[num_vehicles++] = x.copy();

这样,是不是就实现了一个对象指针数组(首先是一个数组,数组元素是对象指针,指向各种交通工具),存放各种交通工具(无论是否已知该交通工具的类型,因为每个交通工具类中都必须实现一个copy()接口,来拷贝自己,然后将这个对象的指针返回)

那么,问题又来了,我们知道,定义一个交通工具类的时候,通常是将其中的方法定义为纯虚函数,形成一个抽象类,但是,抽象类是不能实例化的;此外,还有一个问题,就是内存管理,因为我需要释放parking_lot数组中所指向的每个类。
此时就出现了代理类这个概念,先看代码,在做解析:

class Vehicle {
public:
    virtual double weight() const = 0; 
    virtual void start() = 0;
    virtual Vehicle *copy() const = 0;
    // ......
};

class VechicleProxy {
public:
    VechicleProxy();
    VechicleProxy(const Vehicle &);
    ~VechicleProxy();
    VechicleProxy(const VechicleProxy &);
    VechicleProxy &operator=(const VechicleProxy &);
private:
    Vehicle *p;
};

VechicleProxy::VechicleProxy(): p(0) { }
VechicleProxy::VechicleProxy(const Vehicle &BigStar): p(BigStar.copy()) {}
VechicleProxy::~VechicleProxy() { delete p; }
VechicleProxy::VechicleProxy(const VechicleProxy &v): p(v.p ? v.p->copy() : 0) {}
VechicleProxy::operator=(const VechicleProxy &v)
{
    if (this != &v)
    {
        delete p;
        p = (v.p ? v.p->copy() : 0);
    }
    return *this;
}

在代理类VechicleProxy中,只有一个Vehicle *类型的成员变量,这样可以实现指向不同的交通工具,并且实现了内存清理工作,又解决了抽象类Vechicle不能实例化的问题。
现在,可以这样使用代理类

VehicleProxy parking_lot[1000]; Helicopter x; parking_lot[num_vehicles++] = x;

总结:
代理类解决了这样的问题:内存的分配和编译时类型未知对象的绑定。

2.句柄类

在代理类中存在两个问题,第一,通常是不允许对类添加一个copy函数,因为这样就修改了这个类,第二,当一个类非常大时,copy函数中实现对这个类的拷贝开销是非常大的。
句柄类是用来干什么的?
1.能够在运行时绑定未知类型的类及其继承类
2.管理内存分配和释放问题
3.避免代理类每次复制时都要拷贝对象的操作
现在,以平面坐标系上的点作为基类来理解句柄类的概念

class Point {
public:
    Point() : xval(0), yval(0) {  }
    Point(int x, int y) : xval(x), yval(y) {  }
    int x() const { return xval; }
    int y() const { return yval; }
    Point &x(int xv) { xval = xv; return *this; }
    Point &y(int yv) { yval = yv; return *this; }
private:
    int xval, yval;
}

同时,像代理类那样实现对这个类的管理(构造函数,拷贝构造函数,赋值运算符的重载等)

class handle {
public:
    Handle();
    Handle(int, int);
    Handle(const Point &);
    Handle(const Handle &);
    Handle &operator=(const Handle &);
    Handle &x(int);
    ~Handle();
private:
    Point *p;
}

现在,我们的目的是要设计出一种方法,能够让我不必去复制对象,同时能够达到运行时绑定对象的方法,通俗的说也就是让AA、BB、CC等类都指向基类Point。解决的方法就是定义句柄(也称 智能指针),上面的handle就是句柄的一个雏形,让多个句柄指向同一个基类副本,只有当第一次给handle中p赋值时才会产生Point的唯一副本。此时,出现了一个问题,当有许多句柄指向一个基类副本的时候,怎样释放这个副本的内存呢?
此时就需要用到“引用计数”,记录一共有几个句柄绑定到了基类Point上,只有当数目为0的时候,才能删除掉这个副本,现在完善Handle代码如下:

//句柄类
class handle {
public:
    Handle();
    Handle(int, int);
    Handle(const Point &);
    Handle(const Handle &);
    Handle &operator=(const Handle &);
    Handle &x(int);
    ~Handle();
private:
    Point *p;
    int *u;
}
//当第一次实例化句柄类的时候,产生Point的副本,引用计数值为1
Handle::Handle() : u(new int(1)), p(new Point) {  }
Handle::Handle(int x, int y) : u(new int(1)), p(new Point(x, y)) {  }
Handle::Handle(const Point &p0) : u(new int(1)), p(new Point(p0)) {  }
//当实例化多个句柄的时候,仅仅对引用计数值进行++操作
Handle::Handle(const Handle &h) : u(h.u), p(h.p) { ++*u; }
Handle & Handle::operator=(const handle &h)
{
    ++*h.u;
    if (--*u == 0)
    {
        delete u;
        delete p;
    }
    u = h.u;
    p = h.p;
    return *this;
}
Handle::~Handle()
{
     if (--*u == 0)//只有当引用计数为0的时候,删除副本
     {
          delete u;
          delete p;               
     }
}

细心的读者会发现,我并没有实现Handle &x(int);的函数体,因为这里涉及到一个问题,如下:

// 新的句柄h绑定到Point对象,产生Point的副本,xval为3,yval为4
Handle h(3, 4);
// 调用拷贝构造函数,h2也绑定到Point对象
Handle h2 = h;
//这行代码就会产生歧义,这个5是赋值给哪个句柄的xval?
h2.x(5);
// 这行代码也会产生歧义,n的值为3还是5?
int n = h.(x);

所以,句柄应该是值语义还是指针语义?如果是指针语义,就不能对对象的内容进行修改,如果修改了,那修改者应该清楚自己是想对原副本进行修改。如果是值语义,那就需要引入“写时拷贝”技术,只有当需要对该句柄值修改时,才会重新拷贝一份副本保存在自己的内存中,而不是去修改原副本。针对两种语义,分别实现Handle &x(int);

//指针语义
Handle &Handle::x(int x0)
{
    p->x(x0);
    return *this;
}

Handle &Handle::x(int x0)
{
//因为是值语义,需要将引用计数减1后,产生一个新的副本
//若此时刚好只有一个引用计数,那就可以直接修改原副本了
    if (*u != 1)
    {
        --*u;
        p = new Point(*p);
    }
    p->x(x0);
    return *this;
}

此外,还可以在handle类中实现->等操作符的重载,使handle像指针一样使用去调用Point的成员函数。
以上只是说明了句柄类中的第2和第3个问题,至于第1个问题,和代理类类似,具体可以阅读这篇博客https://www.cnblogs.com/monicalee/p/3861336.html