在第六章,为了避免复制,我们设计了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 #includeclass 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类来完成。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)