29:引用计数

本章首先实现一个带引用计数String,然后逐步优化,介绍引用计数的常规实现。

实现引用计数的String,首先需要考虑:引用计数在哪存储。这个地方不能在String对象内部,因为需要的是每个String值一个引用计数值,这意味着String值和引用计数间是一一对应的关系,因此需要创建一个类来保存引用计数及其跟踪的值。我们叫这个类StringValue。下面就是一个引用计数String的最简单实现:

class String {
public:
String(const char *initValue = "");
String(const String& rhs);
~String();
String& operator=(const String& rhs); const char& operator[](int index) const; // for const Strings
char& operator[](int index); // for non-const Strings private:
struct StringValue {
int refCount;
char *data;
StringValue(const char *initValue);
~StringValue();
};
StringValue *value;
}; String::StringValue::StringValue(const char *initValue):refCount(){
data = new char[strlen(initValue) + ];
strcpy(data, initValue);
}
String::StringValue::~StringValue(){
delete [] data;
} String::String(const char *initValue):value(new StringValue(initValue)){}
String::String(const String& rhs):value(rhs.value){
++value->refCount;
}
String::~String(){
if (--value->refCount == ) delete value;
}
String& String::operator=(const String& rhs){
if (value == rhs.value)
{
return *this;
} if (--value->refCount == )
{
delete value;
}
value = rhs.value;
++value->refCount;
return *this;
}

下面重点看下operator[]的实现。const版本的实现很容易,因为它是一个只读操作,String对象的值不受影响:

const char& String::operator[](int index) const{
return value->data[index];
}

非const的operator[]版本,它被调用可能用来读一个字符,也可能写一个字符。因此我们希望以不同的方式处理读和写,但C++编译器没有办法告诉我们一个特定的operator[]是用作读的还是写,所以我们必须保守地假设所有调用非const operator[]的行为都是为了写操作。为了安全地实现非const的operator[],必须确保没有其它String对象在共享这个可能被修改的StringValue对象。简而言之,当我们返回StringValue对象中的一个字符的引用时,必须确保这个StringValue的引用计数是1:

char& String::operator[](int index){
if (value->refCount > ) {
--value->refCount;
value = new StringValue(value->data);
}
return value->data[index];
}

这个"与其它对象共享一个值直到写操作时才拥有自己的拷贝"的想法在计算机科学中已经有了悠久而著名的历史了,尤其是在操作系统中:进程共享内存页直到它们想在自己的页拷贝中修改数据为止。这个技巧如此常用,以至于有一个名字:写时拷贝。它是提高效率的一个更通用方法--Lazy原则--的特例。

大部分情况下,写时拷贝可以同时保证效率和正确性。只有一个挥之不去的问题:

String s1 = "Hello";
char *p = &s1[];
String s2 = s1;
*p = 'x'; // 将同时修改s1和s2

有三种方法来应付这个问题。第一个是忽略它,假装它不存在;第二种方法稍微好些,明确说明它的存在,通常是将它写入文档,或多或少地说明“别这么做。如果你这么做了,结果为未定义”;第三个方法是排除这个问题。它不难实现,但它将降低一个值共享于对象间的次数。它的本质是这样的:在每个StringValue对象中增加一个标志以指出它是否为可共享的。在最初(对象可共享时)将标志打开,在非const的operator[]被调用时将它关闭。一旦标志被设为false,它将永远保持在这个状态:

class String {
public:
...
private:
struct StringValue {
int refCount;
bool shareable; // add this
char *data;
StringValue(const char *initValue);
~StringValue();
};
...
};
String::StringValue::StringValue(const char *initValue)
: refCount(), shareable(true)// add this
{
data = new char[strlen(initValue) + ];
strcpy(data, initValue);
} String::String(const String& rhs){
if (rhs.value->shareable) {
value = rhs.value;
++value->refCount;
}
else {
value = new StringValue(rhs.value->data);
}
}

所有其它的成员函数也都必须以类似的方法检查这个共享标志。非const的operator[]版本是唯一将共享标志设为false的地方:

char& String::operator[](int index){
if (value->refCount > ) {
--value->refCount;
value = new StringValue(value->data);
}
value->shareable = false; // add this
return value->data[index];
}

借助String/StringValue的实现,现在来考虑通用引用计数的实现。首先,将StringValue中的refCount和shareable部分独立出来:定义一个基类RCObject,任何一个类希望自动拥有引用计数能力,都必须继承该类,它的实现如下:

class RCObject {
public:
RCObject();
RCObject(const RCObject& rhs);
RCObject& operator=(const RCObject& rhs);
virtual ~RCObject() = ;
void addReference();
void removeReference();
void markUnshareable();
bool isShareable() const;
bool isShared() const;
private:
int refCount;
bool shareable;
}; RCObject::RCObject():refCount(), shareable(true) {}
RCObject::RCObject(const RCObject&):refCount(), shareable(true) {}
RCObject& RCObject::operator=(const RCObject&)
{ return *this; }
RCObject::~RCObject() {} void RCObject::addReference() { ++refCount; }
void RCObject::removeReference()
{ if (--refCount == ) delete this; }
void RCObject::markUnshareable()
{ shareable = false; }
bool RCObject::isShareable() const
{ return shareable; }
bool RCObject::isShared() const
{ return refCount > ; }

在构造函数中,将refCount置为0,RCObject的创建者负责调用addReference增加其引用计数。

operator=运算符实际上什么也没做。这个运算符不太可能被调用。RCObject是针对“实值可共享”之对象而设计的一个基类,在一个拥有引用计数能力的系统中,此等对象并不会被赋值给另一个对象。比如,StringValue对象不会被赋值,只有String对象会被赋值,这样的赋值动作中,StringValue的实值不会有任何改变,只有StringValue的引用次数会被改变。

RCObject::removeReference的代码负责减少对象的refCount值,还负责当refCount值降到0时析构对象。这通过delete this来实现的,这只当*this是一个堆对象时才安全。要让这个类正确,必须确保RCObject只能被构建在堆中。实现这一点的常用方法见条款27,但我们这次采用一个特别的方法,这将在本条款最后讨论。

下面是使用RCObject的代码:

class String {
private:
struct StringValue: public RCObject {
char *data;
StringValue(const char *initValue);
~StringValue();
};
...
};
String::StringValue::StringValue(const char *initValue){
data = new char[strlen(initValue) + ];
strcpy(data, initValue);
}
String::StringValue::~StringValue(){
delete [] data;
}

RCObject仅仅提供了操作refCount和shareable的能力,继承该类的StringValue的代码改动不大,RCObject操作refCount的动作还需要在其他类(String)中手动完成。我们希望这些调用动作也被封装起来,这样诸如String这样的类就无需操心引用计数的任何细节了。

没有什么轻松的办法可以让所有与引用计数相关的杂务都从应用性类身上移走,但是有一个办法可以为大部份类消除大部份杂务。(某些应用性类可以去除引用计数的所有相关杂务,但是本例的String不是其中一员,因为它有个non-const operator[]函数需要定义)

查看之前String/StringValue的实现,String内含一个指针指向StringValue对象,StringValue用以表示String的实值。为了能够当指针发生动作(复制、赋值、摧毁)时操作refCount字段,可以使用智能指针。下面就是一个智能指针的实现:

// T 必须支持 RCObject 接口,因此 T 通常继承自 RCObject。
template<class T>
class RCPtr {
public:
RCPtr(T* realPtr = );
RCPtr(const RCPtr& rhs);
~RCPtr();
RCPtr& operator=(const RCPtr& rhs);
T* operator->() const;
T& operator*() const;
private:
T *pointee;
void init(); //共同的初始化动作
}; template<class T>
RCPtr<T>::RCPtr(T* realPtr): pointee(realPtr){
init();
}
template<class T>
RCPtr<T>::RCPtr(const RCPtr& rhs): pointee(rhs.pointee){
init();
} template<class T>
void RCPtr<T>::init(){
if (pointee == ) {
return;
}
if (pointee->isShareable() == false) {
pointee = new T(*pointee); //如果其值不可共享,就复制一份。
}
pointee->addReference();
}

init函数中,当处于非共享状态时,需要创建value的一个新拷贝:pointee=new T(*pointee); 如果String使用RCPtr,则T将是String::StringValue,因此该语句将会调用StringValue的复制构造函数,但是我们没有定义StringValue的复制构造函数,而StringValue中又包含data数据,因此,编译器定义的默认复制构造函数不符合要求,所以需要定义StringValue的复制构造函数:

String::StringValue::StringValue(const StringValue& rhs){
data = new char[strlen(rhs.data) + ];
strcpy(data, rhs.data);
}

另外还有一个问题,pointee有可能指向T的一个派生类,比如假设SpecialStringValue继承于StringValue,RCPtr<StringValue>中的pointee实际指向一个SpecialStringValue,所以pointee = new T(*pointee);应该调用SpecialStringValue复制构造函数,而非StringValue的复制构造函数,可以使用虚复制构造函数实现这一点。对于String类而言,不期望从StringValue派生子类,所以这里忽略这个问题。

下面是剩下的代码:

template<class T>
RCPtr<T>& RCPtr<T>::operator=(const RCPtr& rhs){
if (pointee != rhs.pointee) {
if (pointee) {
pointee->removeReference();
}
pointee = rhs.pointee;
init();
}
return *this;
} template<class T>
RCPtr<T>::~RCPtr(){
if (pointee)pointee->removeReference();
} template<class T>
T* RCPtr<T>::operator->() const { return pointee; }
template<class T>
T& RCPtr<T>::operator*() const { return *pointee; } class String {
public:
String(const char *value = "");
const char& operator[](int index) const;
char& operator[](int index);
private:
struct StringValue: public RCObject {
char *data;
StringValue(const char *initValue);
StringValue(const StringValue& rhs);
void init(const char *initValue);
~StringValue();
};
RCPtr<StringValue> value;
}; void String::StringValue::init(const char *initValue){
data = new char[strlen(initValue) + ];
strcpy(data, initValue);
} String::StringValue::StringValue(const char *initValue)
{ init(initValue); } String::StringValue::StringValue(const StringValue& rhs)
{ init(rhs.data); } String::StringValue::~StringValue()
{ delete [] data; } String::String(const char *initValue):value(new StringValue(initValue)) {}
const char& String::operator[](int index) const
{ return value->data[index]; } char& String::operator[](int index){
if (value->isShared()) {
value = new StringValue(value->data);
}
value->markUnshareable();
return value->data[index];
}

有了RCPtr,String中无需再声明复制构造函数、赋值操作符和析构函数了,使用编译器默认生成的版本,调用RCPtr相应的函数就能完成引用计数的所有工作。上面的所有代码,形成的结构图如下:

上面的设计,有一个问题就是,为了实现引用计数功能的String,必须修改String的源码。如果想让引用计数施行与库中的一个Widget类,库中的代码不可更改的,这该怎么办?

计算机科学中的绝大部分问题都可以通过增加一个中间层次来解决。这里需要将Widget视为StringValue,提供给用户一个RCWidget类使用。但是无法使Widget继承RCObject,因此增加一个CountHolder,整个设计看起来如下:

代码如下:

template<class T>
class RCIPtr {
public:
RCIPtr(T* realPtr = );
RCIPtr(const RCIPtr& rhs);
~RCIPtr();
RCIPtr& operator=(const RCIPtr& rhs); const T* operator->() const;
T* operator->();
const T& operator*() const;
T& operator*();
private:
struct CountHolder: public RCObject {
~CountHolder() { delete pointee; }
T *pointee;
};
CountHolder *counter;
void init();
void makeCopy();
}; template<class T>
void RCIPtr<T>::init()
{
if (counter->isShareable() == false) {
T *oldValue = counter->pointee;
counter = new CountHolder;
counter->pointee = new T(*oldValue);
}
counter->addReference();
} template<class T>
RCIPtr<T>::RCIPtr(T* realPtr):counter(new CountHolder)
{
counter->pointee = realPtr;
init();
}
template<class T>
RCIPtr<T>::RCIPtr(const RCIPtr& rhs): counter(rhs.counter)
{ init(); } template<class T>
RCIPtr<T>::~RCIPtr()
{ counter->removeReference(); } template<class T>
RCIPtr<T>& RCIPtr<T>::operator=(const RCIPtr& rhs){
if (counter != rhs.counter) {
counter->removeReference();
counter = rhs.counter;
init();
}
return *this;
} template<class T>
const T* RCIPtr<T>::operator->() const //const访问,不需要写时复制
{ return counter->pointee; }
template<class T>
const T& RCIPtr<T>::operator*() const //const访问,不需要写时复制
{ return *(counter->pointee); } template<class T>
void RCIPtr<T>::makeCopy() //写时复制
{
if (counter->isShared()) {
T *oldValue = counter->pointee;
counter->removeReference();
counter = new CountHolder;
counter->pointee = new T(*oldValue);
counter->addReference();
}
} template<class T>
T* RCIPtr<T>::operator->() // non-const访问,需要写时复制
{ makeCopy(); return counter->pointee; } template<class T>
T& RCIPtr<T>::operator*() // non-const访问,需要写时复制
{ makeCopy(); return *(counter->pointee); } class Widget {
public:
Widget(int size);
Widget(const Widget& rhs);
~Widget();
Widget& operator=(const Widget& rhs);
void doThis();
int showThat() const;
}; class RCWidget {
public:
RCWidget(int size): value(new Widget(size)) {}
void doThis() { value->doThis(); }
int showThat() const { return value->showThat(); }
private:
RCIPtr<Widget> value;
};

关于引用计数的讨论就可以到此结束了,不过之前还有一个问题没有解决:当  RCObject::removeReference检查新的计数为0时,会以delete this的方式销毁这个对象。只有当对象是以new配置而得时,这才是一个安全的行为。所以需要某种方法确保RCObjects只以new配置的。这一次我们以公约规范来达成目标。RCObject的设计目的是用来做为有引用计数能力之“实值对象”的基类,而那些“实值对象”应该只被RCPtr智能指针取用。此外,应该只有确知“实值对象”共享性的所谓“应用对象”才能将“实值对象”实体化。描述“实值对象”的那些类不应该被外界看到。在我们的例子中,描述“实值对象”者为 StringValue,我们令它成为“应用对象”String内的私有成员,以限制其用途。只有String才能够产生StringValue对象,所以,确保所有StringValue对象皆以new配置而得,是String类作者的责任。

引用计数的设计并非不需成本,增加了引用计数机制,代码比之前复杂的多。引用计数是个优化技术,其适用前提是:对象常常共享实值。如果这个假设失败,引用计数反而会赔上更多内存,执行更多程序代码。以下是使用引用计数改善效率的最佳时机:相对多数的对象共享相对少量的实值;对象实值的产生或销毁成本很高,或是它们使用很多内存。

More Effective C++: 05技术(29)的更多相关文章

  1. More Effective C++: 05技术(25-28)

    25:将constructor 和 non-member functions 虚化 所谓 virtual constructor是某种函数,视其输入可产生不同类型的对象.比如下面的代码: class ...

  2. More Effective C++: 05技术(30-31)

    30:Proxy classes 代理类 在C++中使用变量作为数组大小是违法的,也不允许在堆上分配多维数组: int data[dim1][dim2]; int *data = new int[di ...

  3. Effective C++: 05实现

    26:尽可能延后变量定义式的出现时间 1:只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本:当这个变量离开其作用域时,你便得承受析构成 ...

  4. 05 技术内幕 T-SQL 查询读书笔记(第四章)

    第四章 子查询:在外部查询内嵌套的内部查询(按照期望值的数量分为,标量子查询 scalar subqueries,多值子查询multivalued subqueries)(按照子查询对外部查询的依赖性 ...

  5. Effective Java 05 Avoid creating unnecessary objects

    String s = new String("stringette"); // Don't do this. This will create an object each tim ...

  6. [Effective JavaScript 笔记]第29条:避免使用非标准的栈检查属性

    许多js环境都提供检查调用栈的功能.调用栈是指当前正在执行的活动函数链.在某些旧的宿主环境中,每个arguments对象含有两个额外的属性:arguments.callee和arguments.cal ...

  7. Effective C++:条款29:为“异常安全”而努力是值得的

    (一)先看以下这些代码: class PrettyMenu { public: void changeBackground(istream& imgSrc); private: Mutex m ...

  8. Effective C++ .05 一些不自动生成copy assigment操作的情况

    主要讲了 1. 一般情况下编译器会为类创建默认的构造函数,拷贝构造函数和copy assignment函数 2. 执行默认的拷贝构造/copy assignment函数时,如果成员有自己的拷贝构造/c ...

  9. Effective Java Index

    Hi guys, I am happy to tell you that I am moving to the open source world. And Java is the 1st langu ...

随机推荐

  1. 转:步步LINUX C--进程间通信(二)信号

    源地址:http://blog.csdn.net/jmy5945hh/article/details/7529651 linux间进程通信的方法在前一篇文章中已有详细介绍.http://blog.cs ...

  2. PAT甲级——A1065 A+B and C (64bit)

    Given three integers A, B and C in [−], you are supposed to tell whether A+B>C. Input Specificati ...

  3. 使用em为单位制作两列弹性布局

    一.DIV布局按照定位的方法分为:浮动方法(float),坐标定位方法(position),还有就是两者相结合的方法. 二.DIV布局按照定义单位的不同可分为:固定宽度布局.流体布局.弹性布局和混合布 ...

  4. Django REST Framework概述

    什么是REST REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”.这里说的表征性,就是指资源,通 ...

  5. Java 后端彻底解决跨域问题(CORS)

    接口调用出现跨域问题时,浏览器会报如下提示 XMLHttpRequest cannot load xxx. Request header field Authorization is not allo ...

  6. 在PyCharm中导入Numpy和Pygame模块 (win8.1)

    我用的是anaconda安装python3.6 已经在终端 pip install numpy 但是在pycharm运行程序出现错误:ImportError: No module named nump ...

  7. Redis源码解析:25集群(一)握手、心跳消息以及下线检测

    Redis集群是Redis提供的分布式数据库方案,通过分片来进行数据共享,并提供复制和故障转移功能. 一:初始化 1:数据结构 在源码中,通过server.cluster记录整个集群当前的状态,比如集 ...

  8. Java数据结构和算法(七)--AVL树

    在上篇博客中,学习了二分搜索树:Java数据结构和算法(六)--二叉树,但是二分搜索树本身存在一个问题: 如果现在插入的数据为1,2,3,4,5,6,这样有序的数据,或者是逆序 这种情况下的二分搜索树 ...

  9. Django项目:CRM(客户关系管理系统)--49--40PerfectCRM实现全局账号注册+验证码+页面刷新保留信息

    # gbacc_urls.py # ————————38PerfectCRM实现全局账号登录注销———————— from django.conf.urls import url from gbacc ...

  10. HDU5412 CRB and Queries 整体二分

    传送门 刚觉得最近写代码比较顺畅没什么Bug,cdq分治真是我的一个噩梦.. 整体二分模板题,带修改的区间第k小. vjudge不知抽什么风,用不了,hdu忘了密码了一直在那里各种试,难受.. 写得比 ...