MoreEffectiveC++Item35 条款29 引用计数

一 什么是引用计数

引用计数就是一个简单的垃圾回收体系,使用引用计数后,对象会自己拥有自己,当没人在使用它时,它就会自行销毁.

使用引用计数有两大动机

1.简化跟踪堆对象的过程:一旦一个对象new出来,我们就要紧紧的跟踪其拥有者,只有拥有者才能调用其delete.其跟踪的最难处就是传递所有权

2.如果很多对象有用相同的值,我们可以为其实现共享原则,以减少内存的占用

举个简单的例子

class String {                     // the standard string type may
public:                            // employ the techniques in this 
                                   // Item, but that is not required   String(const char *value = ""); 
String(const char * value = "");
String &opreator= (const String& rhs);
...
private:
char * data;
};
String a,b,c,d,e;
a = b = c = d = e = "Hello;
//其赋值操作实现可能如下
String& String::operator=(const String& rhs) 
{ 
  if (this == &rhs) return *this;      
  delete [] data; 
  data =   new char[ strlen(rhs.data) + 1]; 
  strcpy(data, rhs.data); 
  return *this;           
} 

 其对象所有权见下图

在实际中我们更希望其是这个样子

但是这个时候问题来了:

上面的对象 a 被赋了“Hello”以外的另外一个值,我们不能摧毁值“Hello”,因为还有四个对象需要它。另一方面,如果只有一个对象有“Hello”这个值,当其超出生存空间时,没有对象具有这个值了,我们必须销毁这个值以避免资源泄漏。 

保存当前共享/引用同一个值的对象数目的需求意味着我们的那张图必须增加一个计数值,也就是本Item的中心,引用计数


 

二 实现引用计数

我们需要一个地方来存储这个计数值”是很重要的。这个地方不能在 String 对象内部,因为需要的是每个 String值一个引用计数值,而不是每个 String 对象一个引用计数。这意味着 String值和引用计数间是一一对应的关系,所以我们将创建一个类来保存引用计数及其跟踪的值。我们叫这个类StringValue,又因为它唯一的用处就是帮助我们实现 String 类,所以我们将它嵌套在String 类的私有区内

class String {
public:
    ...
private:
    struct StringValue { ... }; // 包含引用计数和字符串值
    StringValue *value; // value of this String
};

 

StringValue 的实现的实现如下

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

StringValue 的主要目的是提供一个空间将一个特别的值和共 享此值的对象的数目联系起来

String类的构造函数如下

String::String(const char *initValue) 
: value(new StringValue(initValue)) {} 

 String 对象是独立构造的,有同样初始化值的对象并不共享数据,所以

String s1("More Effective C++"); 
String s2("More Effective C++"); 

产生这样的数据结构:

消除这种问题很简单,我们就不在这里赘述了 

 

String 的拷贝构造函数很高效:因为新生成的 String 对象与被拷贝的对象共享相同的 StringValue对象: 

String::String(const String& rhs) 
: value(rhs.value) 
{ 
  ++value->refCount; 
} 

现在像这样的代码

String s1("More Effective C++"); 
String s2 = s1; 

产生这样的数据结构:

这肯定比通常的(不带引用计数的)string 类高效,因为不需要为新生成的 string 值分配内存、释放内存以及将内容拷贝入这块内存。现在,我们只不过是拷贝了一个指针并 增加了一次引用计数

 

String 类的析构函数

//只要引用计数值不是 0,也就是至少有一个 String 对象使用这个值,这个值就不可以被销毁,如果调用析够函数前,引用计数已经为1了,那么delete
class String { 
public: 
  ~String(); 
  ... 
}; 
String::~String() 
{ 
  if (--value->refCount == 0) delete value; 
} 

 

String的赋值操作

String& String::operator=(const String& rhs)
{
    if (value == rhs.value) { //处理自身赋值
        return *this; 
    } 
    if (--value->refCount == 0) { 
        delete value; 
    }
    value = rhs.value; 
    ++value->refCount;
    return *this;
}

 三 写时拷贝

现在我们考虑一下数组下标操作([]),它允许字符串中 的单个字符被读或写

class String { 
public: 
  const char& 
    operator[](int index) const;       // for const Strings 
  char& operator[](int index);           // for non-const Strings 
... 
}; 

这个函数的 const 版本的实现很容易,因为它是一个只读操作,String 对象的值不受影响

 

非 const 的 operator[]版本就是一个完全不同的故事了。它可能是被调用了来读一个字符,也可能被调用了来写一个字符

String s; 
...
cout << s[3];                        //
s[5] = 'x';                          //

必须小心防止修改了与它共享相同 StringValue 对象的其它String对象的值。不幸的是,C++编译器没有办法告诉我们一个特定的operator[] 是用作读的还是写的,所以我们必须保守地假设“所有”调用非 const operator[]的行为 都是为了写操作。(Proxy 类可以帮助我们区分读还是写,见 Item M30。)

char& String::operator[](int index) 
{ 
  // if we're sharing a value with other String objects, 
  // break off a separate copy of the value for ourselves 
  if (value->refCount > 1) { 
    --value->refCount;                    // decrement current value's 
                                          // refCount, because we won't 
                                          // be using that value any more     
      value =                             // make a copy of the 
      new StringValue(value->data);       // value for ourselves 
  } 
  // return a reference to a character inside our 
  // unshared StringValue object 
  return value->data[index]; 
} 

  


 

四 指针、引用与写时拷贝

看这样的代码

String s1 = "Hello"; 
char *p = &s1[1]; 

它的数据结构像这样

现在我们增加一句

String s2 = s1; 

其数据结构

那么此时我们该变p所指向的值

*p = 'x';                     // modifies both s1 and s2! 

 

这个将是我们极为不想看到的,因为我只改变了p所指向的值,却改变了s2的值 

这种现象是我们在使用引用计数时不想看到的,我们避免它的发生,我们只需要控制器 非const得赋值函数即可,为了区分const 和 非const得赋值函数,我们增加一个bool值

//这是增加了共享标志的修改版本: 
class String { 
private: 
  struct StringValue { 
    int refCount; 
    bool shareable;                // add this 
    char *data; 
    StringValue(const char *initValue); 
    ~StringValue(); 
  }; 
... 
}; 
String::StringValue::StringValue(const char *initValue) 
:   refCount(1), 
    shareable(true)                // add this 
{ 
  data = new char[strlen(initValue) + 1]; 
  strcpy(data, initValue); 
} 
String::StringValue::~StringValue() 
{ 
  delete [] data; 
} 
//如你所见,并不需要太多的改变;需要修改的两行都有注释。当然,String 的成员函 数也必须被修改以处理这个共享标志。这里是拷贝构造函数的实现: 
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 > 1) { 
    --value->refCount; 
    value = new StringValue(value->data); 
  } 
  value->shareable = false;           // add this 
  return value->data[index]; 
} 
//如果使用 Item M30 中的 proxy 类的技巧以区分读写操作,你通常可以降低必须被设为 不可共享的 StringValue对象的数目。 

 

 


 

五 带引用计数的基数RCObject

下面我们来介绍一个C++自带的引用计数基类RCObject

class RCObject { 
public: 
  RCObject(); 
  RCObject(const RCObject& rhs); 
  RCObject& operator=(const RCObject& rhs); 
  virtual ~RCObject() = 0; 
  void addReference(); 
  void removeReference(); 
  void markUnshareable(); 
  bool isShareable() const; 
  bool isShared() const; 
  private: 
  int refCount; 
  bool shareable; 
}; 

 

它的实现代码如下

RCOject 的实现代码: 
RCObject::RCObject() 
: refCount(0), shareable(true) {} 
RCObject::RCObject(const RCObject&) 
: refCount(0), shareable(true) {} 
>RCObject& RCObject::operator=(const RCObject&) 
{ return *this; } 
RCObject::~RCObject() {}        // virtual dtors must always be implemented, even if they are pure virtual and do nothing (see alsoItem M33 and Item E14) 
void RCObject::addReference() { ++refCount; } 
void RCObject::removeReference() 
{   if (--refCount == 0) delete this; } 
void RCObject::markUnshareable() 
{ shareable = false; } 
bool RCObject::isShareable() const 
{ return shareable; } 
bool RCObject::isShared() const 
{ return refCount > 1; } 

 

1.构造函数中都将 refCount 设为了 0,只需构造它的对象简单地将 refCount 设为1 就可以了

2.拷贝构造函数也将 refCount设为 0,这是因为我们正在构造新的值对象,而这个新的值对象总是未被共享的, 只被它的构造者引用。再一次,构造者负责将 refCount 设为正确的值

下面我们来实现我们的String类

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) + 1]; 
  strcpy(data, initValue); 
} 
String::StringValue::~StringValue() 
{ 
  delete [] data; 
} 

 

六 自动的引用计数处理

 

posted @ 2017-07-16 20:15  WangZijian  阅读(249)  评论(0)    收藏  举报