Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1799232
  • 博文数量: 600
  • 博客积分: 10581
  • 博客等级: 上将
  • 技术积分: 6205
  • 用 户 组: 普通用户
  • 注册时间: 2008-11-06 10:13
文章分类
文章存档

2016年(2)

2015年(9)

2014年(8)

2013年(5)

2012年(8)

2011年(36)

2010年(34)

2009年(451)

2008年(47)

分类: C/C++

2009-08-26 15:27:45

l        带引用计数的基类

引用计数不只用在字符串类上,只要是多个对象具有相同值的类都可以使用引用计数。改写一个类以获得引用计数需要大量的工作,而我们已经有太的工作需要做了。这样不好吗:如果我们将引用计数的代码写成与运行环境无关的,并能在需要时将它嫁接到其它类上?当然很好。很幸运,有一个方法可以实现它(至少完成了绝大部分必须的工作)。

第一步是构建一个基类RCObject,任何需要引用计数的类都必须从它继承。RCObject封装了引用计数功能,如增加和减少引用计数的函数。它还包含了当这个值不再被需要时摧毁值对象的代码(也就是引用计数为0时)。最后,它包含了一个字段以跟踪这个值对象是否可共享,并提供查询这个值和将它设为false的函数。不需将可共享标志设为true的函数,因为所有的值对象默认都是可共享的。如上面说过的,一旦一个对象变成了不可共享,将没有办法使它再次成为可共享。

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;

};

RCObjcet可以被构造(作为派生类的基类部分)和析构;可以有新的引用加在上面以及移除当前引用;其可共享性可以被查询以及被禁止;它们可以报告当前是否被共享了。这就是它所提供的功能。对于想有引用计数的类,这确实就是我们所期望它们完成的东西。注意虚析构函数,它明确表明这个类是被设计了作基类使用的(见Item E14)。同时要注意这个析构函数是纯虚的,它明确表明这个类只能作基类使用。

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 also

                                       // Item 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; }

可能很奇怪,我们在所有的构造函数中都将refCount设为了0。这看起来违反直觉。确实,最少,构造这个RCObject对象的对象引用它!在它构造后,只需构造它的对象简单地将refCount设为1就可以了,所以我们没有将这个工作放入RCObject内部。这使得最终的代码看起来很简短。

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

RCObject的赋值运算看起来完全出乎意料:它没有做任何事情。这个函数不太可能被调用的。RCObject是基于引用计数来共享的值对象的基类,它不该被从一个赋给另外一个,而应该是拥有这个值的对象被从一个赋给另外一个。在我们这个设计里,我们不期望StringValue对象被从一个赋给另外一个,我们期望在赋值过程中只有String对象被涉及。在String参与的赋值语句中,StringValue的值没有发生变化,只是它的引用计数被修改了。

不过,可以想象,一些还没有写出来的类在将来某天可能从RCObject派生出来,并希望允许被引用计数的值被赋值(见Item M23Item E16)。如果这样的话,RCObject的赋值操作应该做正确的事情,而这个正确的事情就是什么都不做。想清楚了吗?假设我们希望允许在StringValue对象间赋值。对于给定的StringValue对象sv1sv2,在赋值过程中,它们的引用计数值上发生什么?

sv1 = sv2;                    // how are sv1's and sv2's reference

                              // counts affected?

在赋值之前,已经有一定数目的String对象指向sv1。这个值在赋值过程中没有被改变,因为只是sv1的值被改变了。同样的,一定数目的String对象在赋值之前指向前v2,在赋值后,同样数目的对象指向sv2sv2的引用计数同样没有改变。当RCObject在赋值过程中被涉及时,指向它的对象的数目没有受影响,因此RCObject::operator=不应该改变引用计数值。上面的实现是正确的。违反直觉?可能吧,但它是正确的。

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

为了使用我们新写的引用计数基类,我们将StringValue修改为是从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) + 1];

  strcpy(data, initValue);

}

String::StringValue::~StringValue()

{

  delete [] data;

}

这个版本的StringValue和前面的几乎一样,唯一改变的就是StringValue的成员函数不再处理refCount字段。RCObject现在接管了这个工作。

不用感觉不舒服,如果你注意到嵌套类(StringValue)从一个与包容类(String)无关的类(RCObject)继承而来的话。它第一眼看上去是有些古怪,但完全合理。嵌套类和其它类是完全相同的,所以它有自由从它喜欢的任何其它类继承。以后,你不用第二次思考这种继承关系了。   

l        自动的引用计数处理

RCObject类给了我们一个存储引用计数的地方,并提供了成员函数供我们操作引用计数,但调用这些函数的动作还必须被手工加入其它类中。仍然需要在String的拷贝构造函数和赋值运算函数中调用StringValueaddReference removeReference函数。这很笨拙。我们想将这些调用也移入一个可重用的类中,以使得String这样的类的作者不用再担心引用计数的任何细节。能实现吗?C++支持这样的重用吗?

能。没有一个简单的方法将所有引用计数方面的工作从所有的类中移出来;但有一个方法可以从大部分类中将大部分工作移出来。(在一些类中,你可以消除所有引用计数方面的代码,但我们的String类不是其中之一。有一个成员函数搞坏了这件事,我希望你别吃惊,它是我们的老对头:非const版本的operator[]。别放心上,我们最终制服了这家伙。)

每个String对象包含一个指针指向StringValue对象:

class String {

private:

  struct StringValue: public RCObject { ... };

  StringValue *value;                // value of this String

  ...

};

我们必须操作StringValue对象的refCount字段,只要任何时候任一个指向它的指针身上发生了任何有趣的事件。“有趣的事件”包括拷贝指针、给指针赋值和销毁指针。如果我们能够让指针自己检测这些事件并自动地执行对refCount字段的必须操作,那么我们就自由了。不幸的是,指针功能很弱,对任何事情作检测并作出反应都是不可能的。还好,有一个办法来增强它们:用行为类似指针的对象替代它们,但那样要多做很多工作了。

这样的对象叫灵巧指针,你可以在Item M28这看到它的更多细节。就我们这儿的用途,只要知道这些就足够了:灵巧指针对象支持成员选择(->)和反引用(*)这两个操作符,就象真的指针一样,并和内建指针一样是强类型的:你不能将一个指向T的灵巧指针指向一个非T类型的对象。

这儿是供引用计数对象使用的灵巧指针模板:

// template class for smart pointers-to-T objects. T must

// support the RCObject interface, typically by inheriting

// from RCObject

template

class RCPtr {

public:

  RCPtr(T* realPtr = 0);

  RCPtr(const RCPtr& rhs);

  ~RCPtr();

  RCPtr& operator=(const RCPtr& rhs);

  T* operator->() const;            // see Item 28

  T& operator*() const;             // see Item 28

private:

  T *pointee;                       // dumb pointer this

                                    // object is emulating

  void init();                      // common initialization

}; 

这个模板让灵巧指针对象控制在构造、赋值、析构时作什么操作。当这些事件发生时,这些对象可以自动地执行正确的操作来处理它们指向的对象的refCount字段。

例如,当一个RCPtr构建时,它指向的对象需要增加引用计数值。现在不需要程序员手工处理这些细节了,因为RCPtr的构造函数自己处理它。两个构造函数几乎相同,除了初始化列表上的不同,为了不写两遍,我们将它放入一个名为init的私有成员函数中供二者调用:

template

RCPtr::RCPtr(T* realPtr): pointee(realPtr)

{

  init();

}

template

RCPtr::RCPtr(const RCPtr& rhs): pointee(rhs.pointee)

{

  init();

}

template

void RCPtr::init()

{

  if (pointee == 0) {                // if the dumb pointer is

    return;                          // null, so is the smart one

  }

if (pointee->isShareable() == false) {           // if the value

    pointee = new T(*pointee);                   // isn't shareable,

  }                                              // copy it

pointee->addReference();             // note that there is now a

}                                    // new reference to the value

将相同的代码移入诸如init这样的一个独立函数是很值得效仿的,但它现在暗淡无光,因为在此处,这个函数的行为不正确。

问题是这个:当init需要创建value的一个新拷贝时(因为已存在的拷贝处于不可共享状态),它执行下面的代码:

pointee = new T(*pointee);

pointee的类型是指向T的指针,所以这一语句构建了一个新的T对象,并用拷贝构造函数进行了初始化。由于RCPtr是在String类内部,T将是String::StringValue,所以上面的语句将调用String::StringValue的拷贝构造函数。我们没有为这个类申明拷贝构造函数,所以编译器将为我们生成一个。这个生成的拷贝构造函数遵守C++的自动生成拷贝构造函数的原则,只拷贝了StringValue的数据pointer,而没有拷贝所指向的char *字符串。这样的行为对几乎任何类(而不光是引用计数类)都是灾难,这就是为什么你应该养成为所有含有指针的类提供拷贝构造函数(和赋值运算)的习惯(见Item E11)。

RCPtr模板的正确行为取决于T含有正确的值拷贝行为(如深拷贝)的拷贝构造函数。我们必须在StringValue中增加这样的一个构造函数:

class String {

private:

  struct StringValue: public RCObject {

    StringValue(const StringValue& rhs);

    ...

  };

  ...

};

String::StringValue::StringValue(const StringValue& rhs)

{

  data = new char[strlen(rhs.data) + 1];

  strcpy(data, rhs.data);

}

深拷贝的构造函数的存在不是RCPtr的唯一假设。它还要求TRCObject继承,或至少提供了RCObject的所提供的函数。事实上由于RCPtr对象只是被设计了指向引用计数对象的,这个假设并不过分。不过,这个假设必须被明确写入文档。

RCPtr的最后一个假设是它所指向的对象类型为T。这似乎是显然的。毕竟,pointee的类型被申明为T*。但pointee可能实际上指向T的一个派生类。例如,如果我们有一个类SpecialStringValue是从String::StringValue继承的:

class String {

private:

  struct StringValue: public RCObject { ... };

  struct SpecialStringValue: public StringValue { ... };

  ...

};

我们可以生成一个String,包容的RCPtr指向一个SpecialStringValue对象。这时,我们希望init的这句:

pointee = new T(*pointee);                // T is StringValue, but

                                          // pointee really points to

                                          // a SpecialStringValue

调用的是SpecialStringValue的拷贝构造函数,而不是StringValue的拷贝构造函数。我们可以提供使用虚拷贝构造函数(见Item M25)来实现这一点。对于我们的String类,我们不期望从StringValue派生子类,所以我们忽略这个问题。

用这种方式实现了RCPtr的构造函数后,类的其它函数实现得很轻快。赋值运算很简洁明了,虽然“需要测试源对象的可共享状态”将问题稍微复杂化了。幸好,同样的问题已经在我们为构造函数写的init函数中处理了。我们可以爽快地再度使用它:

template

RCPtr& RCPtr::operator=(const RCPtr& rhs)

{

  if (pointee != rhs.pointee) {          // skip assignments

                                         // where the value

                                         // doesn't change

    if (pointee) {

      pointee->removeReference();        // remove reference to

    }                                    // current value

    pointee = rhs.pointee;               // point to new value

    init();                              // if possible, share it

  }                                      // else make own copy

  return *this;

}

析构函数很容易。当一个RCPtr被析构时,它只是简单地将它对引用计数对象的引用移除:

template

RCPtr::~RCPtr()

{

  if (pointee)pointee->removeReference();

}

如果这个RCPtr是最后一个引用它的对象,这个对象将在RCObject的成员函数removeReference中被析构。因此,RCPtr对象无需关心销毁它们指向的值的问题。

最后,RCPtr的模拟指针的操作就是你在Item M28中看到的灵巧指针的部分:

template

T* RCPtr::operator->() const { return pointee; }

template

T& RCPtr::operator*() const { return *pointee; }

l        合在一起

够了!完结!最后,我们将各个部分放在一起,构造一个基于可重用的RCObjectRCPtr类的带引用计数的String类。或许,你还没有忘记这是我们的最初目标。

每个带引用计数的Sting对象被实现为这样的数据结构:

类的定义是:

template                       // template class for smart

class RCPtr {                           // pointers-to-T objects; T

public:                                 // must inherit from RCObject

  RCPtr(T* realPtr = 0);

  RCPtr(const RCPtr& rhs);

  ~RCPtr();

  RCPtr& operator=(const RCPtr& rhs);

  T* operator->() const;

  T& operator*() const;

private:

  T *pointee;

  void init();

};

class RCObject {                       // base class for reference-

public:                                // counted objects

  void addReference();

  void removeReference();

  void markUnshareable();

  bool isShareable() const;

  bool isShared() const;

protected:

  RCObject();

  RCObject(const RCObject& rhs);

  RCObject& operator=(const RCObject& rhs);

  virtual ~RCObject() = 0;

private:

  int refCount;

  bool shareable;

};

class String {                           // class to be used by

public:                                  // application developers

  String(const char *value = "");

  const char& operator[](int index) const;

  char& operator[](int index);

private:

  // class representing string values

  struct StringValue: public RCObject {

    char *data;

    StringValue(const char *initValue);

    StringValue(const StringValue& rhs);

    void init(const char *initValue);

    ~StringValue();

  };

  RCPtr value;

};

绝大部分都是我们前面写的代码的翻新,没什么奇特之处。仔细检查后发现,我们在String::StringValue中增加了一个init函数,但,如我们下面将看到的,它的目的和RCPtr中的相同:消除构造函数中的重复代码。

这里有一个重大的不同:这个String类的公有接口和本条款开始处我们使用的版本不同。拷贝构造函数在哪里?赋值运算在哪里?析构函数在哪里?这儿明显有问题。

实际上,没问题。它工作得很好。如果你没看出为什么,需要重学C++了(prepare yourself for a C++ epiphany)。

我们不再需要那些函数了!确实,String对象的拷贝仍然被支持,并且,这个拷贝将正确处理藏在后面的被引用计数的StringValue对象,但String类不需要写下哪怕一行代码来让它发生。因为编译器为String自动生成的拷贝构造函数将自动调用其RCPtr成员的拷贝构造函数,而这个拷贝构造函数完成所有必须的对StringValue对象的操作,包括它的引用计数。RCPtr是一个灵巧指针,所以这是它将完成的工作。它同样处理赋值和析构,所以String类同样不需要写出这些函数。我们的最初目的是将不可重用的引用计数代码从我们自己写的String类中移到一个与运行环境无关的类中以供任何其它类使用。现在,我们完成了这一点(用RCObjectRCPtr两个类),所以当它突然开始工作时别惊奇。它本来就应该能工作的。

将所以东西放在一起,这儿是RCObject的实现:

RCObject::RCObject()

: refCount(0), shareable(true) {}

RCObject::RCObject(const RCObject&)

: refCount(0), shareable(true) {}

RCObject& RCObject::operator=(const RCObject&)

{ return *this; }

RCObject::~RCObject() {}

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; }

这是RCPtr的实现:

template

void RCPtr::init()

{

  if (pointee == 0) return;

  if (pointee->isShareable() == false) {

    pointee = new T(*pointee);

  }

  pointee->addReference();

}

template

RCPtr::RCPtr(T* realPtr)

: pointee(realPtr)

{ init(); }

template

RCPtr::RCPtr(const RCPtr& rhs)

: pointee(rhs.pointee)

{ init(); }

template

RCPtr::~RCPtr()

{ if (pointee)pointee->removeReference(); }

template

RCPtr& RCPtr::operator=(const RCPtr& rhs)

{

  if (pointee != rhs.pointee) {

    if (pointee) pointee->removeReference();

    pointee = rhs.pointee;

    init();

  }

return *this;

}

template

T* RCPtr::operator->() const { return pointee; }

template

T& RCPtr::operator*() const { return *pointee; }

    这是String::StringValue的实现:

void String::StringValue::init(const char *initValue)

{

  data = new char[strlen(initValue) + 1];

  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::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内部完成的处理引用计数的担子。第二,剩下的代码几乎没有变化:灵巧指针无缝替换了内建指针。实际上,唯一的变化是在operator[]里,我们用调用isShared函数代替了直接检查refCount的值,并用灵巧指针RCPtr对象消除了写时拷贝时手工维护引用计数值的工作。

这当然全都很漂亮。谁能反对减少代码?谁能反对成功的封装?然而,这个全新的String类本身对用户的冲击远胜过它的实现细节,这才是真正的闪光点。如果没有什么消息是好消息的话,这本身就是最好的消息。String的接口没有改变!我们增加了引用计数,我们增加了标记某个String的值为不可共享的能力,我们将引用计数功能移入一个新类,我们增加了灵巧指针来自动处理引用计数,但用户的一行代码都不需要修改。当然,我们改变了String类的定义,所以用户需要重新编译和链接,但他们在自己代码上的投资受到了完全的保护。你看到了吗?封装确实是个很好的东西。
阅读(815) | 评论(0) | 转发(0) |
0

上一篇:引用计数---1

下一篇:引用计数---3

给主人留下些什么吧!~~