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. 新闻内页 上一篇写一篇问题,ID不连续,不用链表

    y要什么链表? 用sql查询上一篇 SELECT id,title FROM t_article WHERE id<10 ORDER BY id DESC LIMIT 1; 用sql查下一篇 S ...

  2. hibernate一对多关系 在一方查询会获得重复数据,重复数量就是多端数据数量用@Fetch(FetchMode.SUBSELECT)解决

    先来看数据表 版块表只有两个数据 板块1是推荐,下边没有子栏目 板块2下边有14个子栏目 在1的一端来查询,发现结果有16条 也就是板块1+版块2+版块2和他的14个子集都列出来了,这明显不对 板块对 ...

  3. Activiti实战02_环境搭建

    1:下载Activiti 访问:https://www.activiti.org/download-bpm 可以下载Activiti相关文档和历史版本压缩包,在 https://www.activit ...

  4. IO流16 --- 对象流操作字符串 --- 技术搬运工(尚硅谷)

    序列化 @Test public void test12() throws IOException { ObjectOutputStream oos = new ObjectOutputStream( ...

  5. java mybatis 参数问题

  6. 浓缩版 《C和指针》基础篇(Chpt.1~Chpt.9)

    导语 近日,笔者在课业之余阅读了<C和指针(Pointers on C)> (by Kenneth A.Reek)一书,从中记录了关于C语言的诸多知识点,包括在C语言基础特性的学习过程中没 ...

  7. 入门servlet:request获取请求参数通用方式

    一.获取请求参数通用方式 1. String getParameter(String name):根据参数名称获取参数值 例:username=flypig&password=2343 2. ...

  8. workbench使用

    1.你是指默认的mysql目录下data里面的'mysql'这个schema没有在workbench里面看到吧?点击菜单-Edit->Preferences里面的SQL Editor,然后把&q ...

  9. Visual Studio 2013打开Orchard1.8.1版本源码,出现无法打开解决方法

    当使用Visual Studio打开项目时,如果遇到如上情况,可能是缓存问题,删除解决方案缓存(suo后缀文件)后重新打开sln解决方案即可,如下图

  10. Javascript-new Date() 与 Date() 的区别

    var today1 = Date() //返回一个字符串(string),没有getDate等日期对象方法,内容为当前时间 var today2 = new Date() //返回一日期对象,内容为 ...