C++沉思录 第七章:改进的Handle类

C++沉思录 第七章:改进的Handle类,第1张

C++沉思录 第七章:改进的Handle类 一 温故而知新

        在第六章,为了避免复制,我们设计了Handle类。对于某个特定的类(我们是以Point类为例),它工作的很好,既能体现指针语义,也能提供写时复制。回想类的设计过程,我们为Point类设计了一个中间类UsePoint,用来控制引用计数,并在类内放了一个Point 成员:

class UsePoint{

    //...

    Point p;

};

        同时在Handle类内放了一个私有数据成员:

class Point_Handle{

    //...

private:

        UsePoint* up;
};

        你应该发现了一些不方便的地方:每当我们想为一个特定类设计Handle,就要为其写一个新的中间类。

        再回到第五章,我们用代理类来解决在容器中存储处于继承体系中的类。

        当我们想为继承体系设计Handle时,记住我们想这样使用:

Handle arr[ 10 ] = {  };

        Handle现在没法工作了,因为我们的Handle只为特定类服务,没有动态绑定的功能,现在就只能在容器中存储某个特定类。

二 问题出在哪里

        为了触发C++的动态绑定,我们需要使用基类的引用或指针,很显然目前的Handle类并没有:因为使用了中间类UsePoint,在Handle类内存放的是UsePoint*。为了实现我们的目标,我们应该在Handle类内存放基类的指针:

class Point_Handle{

    //...

private:

    Point* p;

};

        这样,我们可以为Handle传递任意派生类的对象。

三 分离引用计数

        接下来,由于丢弃了中间类,我们需要再次考虑引用计数。我们当然不能将引用计数放在Point中,因为不想修改Point,而且Point现在是一个继承体系。不过我们可以在将引用计数放在Handle类中,并以指针的形式。

        还记得第六章我们的讨论吗?Handle类具有指针语义,也就是多个Handle可能指向同一个Point。那么引用计数能否自动同步?我们看看修改后Handle类的拷贝构造函数:

Point_Handle( const Point_Handle& ph ):up( ph.up ),uc(ph.uc){

        ++*uc; 
    }

        复制的都是一份指针的拷贝,析构时,也可以根据引用计数的值,删除指针,完全满足我们的要求。

        简而言之,我们需要丢弃中间类,将引用计数分离,并修改Handle类:

class Point_Handle{
public:
    //...
private:
    Point* p;    //基类指针
    int* uc;     //引用计数
};

        我们看看修改后Handle类的完整定义:

class Point_Handle{
public:
    Point_Handle(  ):up( new Use_Point( ) ),uc(new int(1)){  }
    Point_Handle( int x,int y ):up( new Use_Point( x,y ),uc(new int(1)) ){  }
    ~Point_Handle(  )
    {
        --*uc;
        if(*uc == 0)
        {
            delete up;
            delete uc;
        }
    }
    Point_Handle( const Point_Handle& ph ):up( ph.up ),uc(ph.uc){

        ++*uc; 
    }
    Point_Handle( const Point& p ):up( new Use_Point(p) ),uc(new int(1)){  }
    Point_Handle& operator=( const Point_Handle& ph ){
        ++*ph.uc;;                             //右侧对象引用计数+1
        if( --*uc == 0 )                       //左侧对象将指向新对象
        {                                      //原引用计数-1
            delete up;                         //如果为0,就析构原来指向的内存
            delete uc;
        }
        up = ph.up;
        uc = ph.uc;                            //现在指向新的副本了
        return *this;
    }
private:
    Point* p;
    int* uc;
};
四 封装引用计数

         到目前为止,我们可以用Handle保存处于继承体系的对象了。但是引用计数毫无封装性可言,更别谈之后想要扩充引用计数的功能。

        我们打算封装引用计数为一个类,那么,它的功能至少应该是这样:

class UseCount{
public:
    UseCount():p(new int(1)){}
    UseCount( const UseCount& uc):p(uc.p){
        ++*p;
    }
    UseCount& operator=(const UseCount& uc){
        //...
    }
    ~UseCount(){
        if(--*p == 0)
        {
            delete p;
        }
    }

private:
    int* p;
};

        现在重写Handle类。

        由于引用计数可以依靠默认构造函数,因此Handle类的构造函数变的异常简单:

class Point_Handle{
public:
    //默认构造函数
    Point_Handle():p(new Point()){}
    //构造函数
    Point_Handle(int x,int y):p(new Point(x,y)){}
    Point_Handle(const Point& p0):p(new Point(p0)){}
    //拷贝构造函数
    Point_Handle(const Point_Handle& ph):p(ph.p),uc(ph.uc){}
    //析构函数
    ~Point_Handle(){
        //?
    }
    //拷贝赋值运算符
    Point_Handle& operator=(const Point_Handle& ph){
        //?
    }
private:
    Point* p;        //基类指针
    UseCount uc;     //引用计数
}

        析构函数和拷贝赋值运算符稍微复杂点。

        为了析构 Point* p ,我们需要知道UseCount是否是1。因此我们需要为UseCount添加一个成员函数,用来判断UseCount是否为1:

class UseCount{
public:
    //...
    bool only(){
        return *p == 1;
    }
};

        为了执行Handle类的赋值 *** 作,我们换一种思路来考虑:Handle类的赋值 *** 作包括了两部分:Point* 和 UseCount。Point*依赖于UseCount。

        赋值 *** 作符左边的UseCount始终被改写。

        如果左边的类引用计数目前为1,那么被改写后,引用计数(int* uc;)应该被析构,我们就把右边的引用计数赋值给左边;

        如果左边的引用计数大于1,那么被改写后,仅仅自减,引用计数不需要析构,我们再把右边的引用计数赋值给左边。

        如果左边的引用计数被析构,意味着左边的Handle类是唯一的控制类,此时我们也应该析构Point*;否则,就不需要析构左边的Point*,做简单的赋值 *** 作就可以。

        我们可以在UseCount内新增一个成员函数,用来判断当前的引用计数是否被析构:

        与此同时,UseCount的拷贝赋值运算符应该私有化,因为我们不希望为外界提供该接口,以防止外界随意赋值,修改计数器的值:

class UseCount{
public:
    //...

    bool destruct(const UseCount& u){
        ++*u.p;                //右边先自加
        if(--*p == 0)          //左边自减,如果此时为0,应该析构  
        {
                delete p;      //析构计数器
                p = u.p;       //把右边赋值给左边
                return true;   //告诉外界,析构了
        }
        p = u.p;               //没有析构,直接赋值
        return false;          //未析构        
    }
private:
    UseCount& operator=(UseCount& u){}    //私有化,不允许赋值 *** 作
};

        有了这些,我们来重写Handle类的析构函数和赋值 *** 作符:

    //析构函数
    ~Point_Handle(){
        if(uc.only()){        //如果计数器为1
            delete p;         //就析构当前对象
        }
    }
    //拷贝赋值运算符
    Point_Handle& operator=(const Point_Handle& ph){
        if(uc.destruct(ph.uc)){    //当前的计数器对象是否将被析构;
            delete p;              //如果是的话,也应该析构Point* p;
        }
        p = ph.p;                  //不是的话,就直接赋值即可,因为左边的Handle类对象仍然存在

        return *this;
    }
五 存取和写入

        对于写 *** 作,我们仍然使用写时复制实现Handle类的值语义。先看看最开始的实现方法:

        当我们执行了这样的 *** 作,并进行写值的时候:

Point_Handle h(3,4);
Point_Handle h1;

//复制
h1 = h;

//写对象。为了体现值语义,我们在这里新建一个Point,并让h1控制这个新的Point
//所以h.x() == 3    h1.x() == 5
h1.x(5);

        所以,我们是这样做的:

    //...其他同...
    Point_Handle& xw(int x0) {    
		if (up->use_count != 1)            //如果引用计数不为1
		{
			--up->use_count;                //递减引用计数
			up = new Use_Point(up->p);      //创建新Point
		}
		up->p.xw(x0);                       //写新Point
		return *this;
	}
    Point_Handle& yw(int y0) {
		if (up->use_count != 1)
		{
			--up->use_count;
			up = new Use_Point(up->p);
		}
		up->p.yw(y0);
		return *this;
	}

        我们需要判断当前的引用计数是否为1。如果是,那么就直接修改值;如果不是,就自减引用计数,并新建Point,让Handle类控制这个新Point。

        应用到现在的UseCount类,我们需要一个成员函数,来判断是不是需要创建新的Point:

class UseCount{
public:
    //...

    bool createNewPoint()
    {
        if(*p == 1)            //判断引用计数是否为1
        {
            return false;      //是的话就不用创建
        }
        --*p;                  //不是先自减
        p = new int(1);        //创建新的引用计数,并置1
        return true;           //需要创建新的Point
    }
};

        那么写 *** 作就应该是这样:

    Point_Handle& xw(int x0) {
		if (uc.createNewPoint())    //如果需要创建新Point
		{
			p = new Point(*p);      //用当前Point的值创建新Point
		}
		p->xw(x0);                  //写新Point对象
		return *this;
	}
    Point_Handle& yw(int y0) {
		if (uc.createNewPoint())
		{
			p = new Point(*p);
		}
		p->yw(y0);
		return *this;
	}

        对于取 *** 作,就很简单了:

class Point_Handle()
{
public:
    //...

    int xr() const {
        return p->x();
    }

    int yr() const {
        return p->y();
    }
};
六 使用Handle

        有这样一个继承体系:

#ifndef TEST1_H
#define TEST1_H

#include 

//base class
class A {
public:
	A() {}
	A(int x0, int y0) :x(x0), y(y0) {}
    virtual A* copy() = 0;
	virtual void print() {
		std::cout << "x = " << x << "y = " << y << std::endl;
	}
	//虚析构函数
	virtual ~A() {
	}
private:
	int x, y;
};

class B : public A {
public:
	//默认构造
	B() {}
	B(int x0,int y0,std::string na):A(x0,y0),name(na){
	}
	//拷贝构造
	B(const B& b):name(b.name) {
	}
    
    A* copy() override{
        return new B(*this);
    }
	void print() {
		std::cout << name << std::endl;
	}
	//析构函数
	virtual ~B() {
	}
private:
	std::string name;
};

class C : public A {
public:
	//默认构造
	C() {
	}
	C(int x0, int y0, int age) :A(x0, y0), age(age) {
	}
	//拷贝构造
	C(const C& c) :age(c.age) {
	}
    A* copy() override{
        return new C(*this);
    }    
	void print() {
		std::cout << age << std::endl;
	}
	//析构函数
	virtual ~C() {
	}
private:
	int age;
};

class D : public A {
public:
	//默认构造
	D() {
	}
	D(int x0, int y0, std::string ss) :A(x0, y0), s(ss) {
	}
	//拷贝构造
	D(const D& d) :s(d.s) {
	}
    A* copy() override{
        return new D(*this);
    }
	void print() {
		std::cout << s << std::endl;
	}
	//析构函数
	virtual ~D() {
	}
private:
	std::string s;
};


#endif //TEST1_H

        UseCount类:

#ifndef USECOUNT_H
#define USECOUNT_H
#include 

class UseCount {
public:
	UseCount() :p(new int(1)) {}
	UseCount(const UseCount& uc) :p(uc.p) {}
	~UseCount() {
		if (-- * p == 0) {
			delete p;
		}
	}
	bool only() {
		return *p == 1;
	}

	bool destructor(const UseCount& uc) {
		++* uc.p;
		if (-- * p == 0) {
			delete p;
			p = uc.p;
			return true;
		}
		p = uc.p;
		return false;
	}

	bool createNew() {
		if (*p == 1)
		{
			return false;
		}

		--* p;
		p = new int(1);
		return true;
	}
private:
	int* p;
	UseCount& operator=(UseCount& uc) {}
};
#endif //USECOUNT_H

        我们希望在容器中存储这个继承的所有派生类,为此提供这个类专属的Handle类:

#ifndef HANDLE_H
#define HANDLE_H

#include 
#include "UseCount.h"
#include "test1.h"

class Handle {
public:
	Handle():ap(0) {}
	Handle(const A& a):ap(a.copy()) {
	}
	Handle(const Handle& h):ap(h.ap), u(h.u) {}
	~Handle() {
		if (u.only())
		{
			//delete ap;
		}
	}
	Handle& operator=(const Handle& h) {
		if (u.destructor(h.u)) {
			delete ap;
		}
		ap = h.ap;
		return *this;
	}
	void sprint() {
		ap->print();
	}
private:
	A* ap;
	UseCount u;
};

#endif //HANDLE_H

         注意,这几处有所修改:

~Handle() {
		if (u.only())
		{
			//delete ap; //由于存在虚基类,我们为其传入对象指针,因此不需要delete,将由对象负责
		}
	}

         现在,我们可以这样使用了:

#include 
#include "Handle.h"

void htest() {
	A a(5,7);
	B b(1,2,"yxl");
	C c(3,4,20);
	D d(5,6,"hw");

	Handle arr[4]{a,b,c,d};
	arr[0].sprint();
	arr[1].sprint();
	arr[2].sprint();
	arr[3].sprint();
}
int main()
{
	htest();
	return 0;
}

        输出:

x = 5y = 7
yxl
20
hw

F:yxlwindows_programProject2DebugProject2.exe (进程 19932)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

 七 总结

        改进的Handle类相比于第六章,完全可以应付处于继承体系下的对象了;并且,不需要再为特定的类创建引用计数类,也不需要考虑内存管理方面的问题。所有的修改 *** 作现在都集中在Handle类来完成。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5690554.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存