我的C++实践(16):引用计数实现

    我们在做设计时,将接口与实现相分离是一个基本的策略。分离接口与实现主要有两种技术:
    (1)抽象基类。这个大家比较熟悉,在C++中是声明了纯虚函数的类,在Java、C#等语言中有现成的关键字。设计时将接口部分放在abstract base class中,它们是一个virtual析构函数和一组pure virtual函数。实现部分由各子类担当。这种类称为Interface Class,通过继承的方式来实现。
    (2)句柄类。即pimpl手法,把隶属对象的数据(即实现)从原对象中抽离出来,封装成一个称为impl的实现对象,在原对象中用一个指针成员指向它。pimpl即pointer to implementation,这种impl类称为Handle Class,它通过组合方式的来实现,主类的数据被抽离成一个独立的类(称为数据类,或值类),通过组合一个它的指针来完成工作。我们知道,在设计时应该优先使用组合而不继承,因为组合的耦合性更低。pimpl手法在C++的程序库设计中到处都是。
    用组合的方式来做设计时,如果多个主类对象的数据相同,我们经常可以让它们共享这一个值对象,以提高性能,这就需要对共享一个值对象的多个主对象进行引用计数。例如对字符串类String,它里面会有一个char* data的数据,我们可以把这个数据抽离出来设计成一个独立的值类StringValue,在String类中用一个StringValue*型指针指向数据。当进行拷贝或赋值时,比如s1="abcd",s2=s1,我们并不把s1的数据"abcd"深拷贝(对指针所指向的数据进行拷贝)给s2,而是让s2与s1共享这一份数据,这样可以大大提高性能。因此,我们需要对共享值对象"abcd"的主对象个数进行引用计数,要跟踪有多少个主对象引用了这一个值对象,引用计数一旦变成0就删除这个值对象。当我们需要写入时,比如s2[2]='x',这时就要执行真正的拷贝,把s1的"abcd"拷贝给s2,然后把s2的"abcd"修改为"abxd",这就是写时拷贝技术。从这可以看出,任何的组合方式都可以看作是一种pimpl手法,被组合的对象可以认为是主对象的值对象。智能指针类就是这样的,它所指向的对象可以认为是智能指针对象的数据。
    1、引用计数类。 在前面的“智能指针”介绍中我们实际上已经实现了引用计数,simplerefcount.hpp中的SimpleReferenceCount就是一个引用计数类,不过它是通过成员函数模板来对任意的指针类型进行计数,这实际上是相当于用模板的方式来实现引用计数的。现在我们需要对一般类型的对象进行引用计数,我们改用继承的方式来实现,把引用计数设计成抽象基类RCObject,值对象需要计数,它必须继承这个抽象基类,因此引用计数基类成为值对象的一部分

    解释:
    (1)我们声明析构函数为纯虚的,因此RCObject是一个抽象类,不能实例化,只能由值类来继承。同时,RCObject类中不单单只是接口的声明,它也有计数功能的实现,它们是值类共性(都需要计数)的抽离,这样的类一般称为抽象混合基类。注意当把析构函数声明为纯虚的时,必须同时要有定义,因为子类一定会调用基类的析构函数,这样它就必须有定义,而不只是声明。
    (2)这是一个侵入式的计数器类,因为值对象继承自RCObject,计数器就成为了值对象的一部分,要占用值对象的内存空间。通过继承,值对象自身有了计数功能和共享开关。
    (3)这里增加了一个共享标志shareable,表示值对象能不能共享。如果值对象不想让多个主对象共享,就可以用markUnshareable()关闭这个标志。isShared()表示对象是否已经被共享。引入共享标志后就可以实现写时拷贝。比如对String类,访问可能会是读操作cout<<s2[2],也可能是写操作s2[2]='x'。如果是写操作,则在opeartor[]函数中可先判断值对象是否已经被共享,如果已经被共享,则必须拷贝一份出来进行写操作。
    (4)引用计数为0时,删除对象用delete this,这就要求值对象必须分配在堆上,可见,引用计数的抽象基类实现是有限制的。当然,我们用模板并且通过组合的方式来实现引用计数可以更完善,也更高效。这里主要是为了演示Interface Class和Handle Class的设计策略。
    2、封装值对象的智能指针。 由于我们需要在主类中组合一个值类(有引用计数功能)的指针,通过这个指针调用各个计数操作来完成值对象的计数功能,例如在String类中通过StringValue* data指针来调用各个计数操作,这会在主类中增加很多函数调用代码。为此,我们可以把这个值类指针data封装成智能指针,由智能指针类来完成所有对计数函数的调用,只要在主类中组合一个指向值类的智能指针对象(而不组合原始的值类指针)即可。当然,我们可以直接复用在“智能指针”介绍中开发的通用型智能指针,但这里的智能指针对所指类型有要求(是值类,有计数功能),具有特殊性,并不能直接用通用型的智能指针。因此,我们独立实现了一个简单的智能指针RCPtr:

    解释:
    (1)RCPtr的构造。在根据传来的值对象指针构造RCPtr对象时,需要判断这个值对象是否可以共享,如果它设为不可共享,那就不能简单地增加引用计数了,而是只能只能对它进行拷贝,这样RCPtr就指向了一个独立值对象,而没有共享原来的那个值对象。因为RCPtr被组合在主对象中,说明这个独立值对象有一个主对象引用了它,因此引用计数要加1。如果可以共享,则直接增加引用计数即可。
    (2)RCPtr的拷贝也一样。把主对象a拷贝给主对象b时,a中的RCPtr成员也会拷贝给b。我们知道a和b需要共享a中的值对象,而值对象被封装在RCPtr中(让RCPtr成为主对象的一个成员,而不是赤裸裸的值对象),因此RCPtr的拷贝只要增加值对象的引用计数即可,并没有拷贝RCPtr指向的值对象(除非它不可共享,这时就必须深拷贝指向的值对象,使a和b拥有各自独立的值对象)。
    (3)RCPtr的赋值。主对象赋值a=b时,会导致里面的RCPtr成员的赋值。因为赋值时a可以共享b内部的值对象以提高性能,这需要先保存a中的RCPtr,然后把b的RCPtr指针(即内部的哑指针)赋给a,这样a就要使用b内部的值对象了,但接着要调用init(),或者是直接增加b内部的那个值对象的引用计数,或者是因为它不可共享,需要拷贝一份出来给a。最后a原来引用的值对象的计数要减1(因为主对象a已经不再引用它了)。可见,值对象拥有了共享否决权后使得引用计数的实现变得复杂了。因为值对象可以声明它不想在各个主对象之间被共享,因此我们需要增加额外的代码来判断它是否可共享,而不是在主对象拷贝或赋值时直接简单地增加值对象的引用计数。
    (4)解引用操作符operator*必须返回T&型引用,不能返回T型对象,因为哑指针pointee可能指向了派生类对象,如果返回T型对象,会导致对象切割问题。同理,箭头操作符opeator->也必须返回T*型指针。
    3、使用了引用计数的String类实现。 我们使用pimpl手法,把String内的char*型数据封装成独立的StringValue值类,在String类中用一个指针成员(这里使用封装StringValue的智能指针RCPtr<StringValue>)指向String的数据。StringValue类有引用计数功能,因此要继承自RCObject,多个String对象在 拷贝或赋值时可以共享一个StringValue值对象。

    解释:
    (1)这里值类StringValue封装了String的数据和实现细节,可见我们把Stirng的接口与实现细节分离了。它被实现为内嵌类,继承自RCObject,有引用计数功能。它表示是专为String设计的,只在String内部使用。
    (2)这里使用了智能指针RCPtr来封装StringValue值对象,这样String的实现就非常简洁,没有任何的计数操作的调用代码。当String对象进行拷贝或赋值时,需要对里面的StringValue值对象进行引用计数操作,所有的这些操作都委托给了智能指针RCPtr<StringValue>对象,它既含有实际的StringValue值对象,又能调用StringValue的计数函数完成实际的计数操作。String里面没有了指针成员,都是对象成员,因此无需实现拷贝构造函数、赋值运算符、析构函数等,使用默认的就够了,一切都非常的简洁。
    (3)const版本的operator[]只能是读操作,因此不需要实现写时拷贝。而non-const版的operator[]可能是读cout<<s2[2],也可能是写s2[2]='x'。如果是写,而对象已经被共享了,则必须进行拷贝了,我们对拷贝出来的一份值对象进行写操作,就需要把它标记为不可共享,这种情况下使得这个拷贝出来的值对象永远不可共享了。另一方面,我们也没有区分出读和写操作,如果这个operator[]调用是读操作(比如cout<<s2[2]),那我们同样也进行了拷贝,而实际上在读的时候并不需要拷贝,可见这样的写时拷贝实现并不是完美的,它导致有时在读的时候也进行了拷贝。事实上,opeator[]运算并不能区分是读cout<<s2[2],还是写s2[2]='x',因为opeartor[]函数里只是返回一个值的引用,实际的写操作并不是在函数里面完成的。要区分读和写,我们需要使用代理类技术(即代理模式)。
    4、为既有类添加引用计数。 前面我们设计StringValue时,让它继承RCObject,使它有了引用计数功能。可见引用计数是一种通用的功能,任何类(不一定要作为值类)只要继承RCObject,就拥有了引用计数功能。现在如果程序库中已经存在一个类Widget,它不能被修改,但客户端又需要它具有引用计数功能,该怎么办呢?唯一的办法就是把引用计数功能委托给其他类的完成。我们可以对Widget进行包装,比如通过组合把Widget包装成RCWidget类,让RCWidget类组合一个指向Widget对象的RCPtr智能指针来完成Widget的所有功能,同时增加引用计数功能。我们把RCWidget提供给客户端使用就可以了。最直接的实现是让RCWidget继承RCObject,但这样的话RCWidget对象就只能创建在堆上了。其实我们可以让RCPtr来完成对Widget的引用计数。这就需要修改RCPtr的设计,其实并不难。只要在RCPtr中引入一个内嵌类,让它继承RCObject,用这个内嵌类来对作为模板实参传过去的Widget类进行引用计数,修改后的智能指针为RCIPtr类。

    解释:
    (1)RCIPtr与RCPtr相比,只有两点不同,一是RCIPtr直接指向值对象,现在RCIPtr引入了一个中间层CountHolder类,CountHolder代理对象指向实际的值对象,同时还能对值对象进行引用计数(继承了RCObject)。二是提供一个友好的getRCObject()来返回指向的值对象,当然operator*也是直接返回值对象的,但getRCObject()对客户端而言更友好,因为客户端会经常使用这个函数。
    (2)RCIPtr中的CountHolder对象必须分配在堆上(因此用counter指针),它在计数值变成0时会自己销毁自己(delete this),因此并不需要在RCIPtr的析构函数中delete counter,只需要减少引用计数即可,这一点要特别注意。
    (3)RCWidget包装了Widget,具有Widget的功能,通过RCIPtr它还具有了对Widget的引用计数功能,因此多个RCWidget可以共享一个Widget。当调用非const操作时,说明有可能修改Widget对象的内容,需要写时拷贝。先用getRCObject()获得值对象,看看它是否被共享,若被共享了,则需要拷贝一份出来才能进行修改,然后调用实际的可能做修改动作的Widget操作。当调用const操作时,不会修改Widget对象内容,直接转发调用。RCWidget的实现非常简洁,没有指针成员,因此无需拷贝构造函数、赋值操作符、析构函数等,使用默认就可以了。
    这其实就是Decorator模式的应用。Decorator模式用于动态给一个对象添加一些额外的职责,就扩展功能而言,Decorator模式比生成子类方式更为灵活。我们用RCWidget对Widget进行装饰,动态地给它增加了引用计数功能。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值