分类: 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 M23和Item E16)。如果这样的话,RCObject的赋值操作应该做正确的事情,而这个正确的事情就是什么都不做。想清楚了吗?假设我们希望允许在StringValue对象间赋值。对于给定的StringValue对象sv1和sv2,在赋值过程中,它们的引用计数值上发生什么?
sv1 = sv2; // how are sv1's and sv2's reference
// counts affected?
在赋值之前,已经有一定数目的String对象指向sv1。这个值在赋值过程中没有被改变,因为只是sv1的值被改变了。同样的,一定数目的String对象在赋值之前指向前v2,在赋值后,同样数目的对象指向sv2。sv2的引用计数同样没有改变。当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的拷贝构造函数和赋值运算函数中调用StringValue的addReference和 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
{
init();
}
template
RCPtr
{
init();
}
template
void RCPtr
{
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
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
RCPtr
class String {
private:
struct StringValue: public RCObject { ... };
struct SpecialStringValue: public StringValue { ... };
...
};
我们可以生成一个String,包容的RCPtr
pointee = new T(*pointee); // T is StringValue, but
// pointee really points to
// a SpecialStringValue
调用的是SpecialStringValue的拷贝构造函数,而不是StringValue的拷贝构造函数。我们可以提供使用虚拷贝构造函数(见Item M25)来实现这一点。对于我们的String类,我们不期望从StringValue派生子类,所以我们忽略这个问题。
用这种方式实现了RCPtr的构造函数后,类的其它函数实现得很轻快。赋值运算很简洁明了,虽然“需要测试源对象的可共享状态”将问题稍微复杂化了。幸好,同样的问题已经在我们为构造函数写的init函数中处理了。我们可以爽快地再度使用它:
template
RCPtr
{
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
{
if (pointee)pointee->removeReference();
}
如果这个RCPtr是最后一个引用它的对象,这个对象将在RCObject的成员函数removeReference中被析构。因此,RCPtr对象无需关心销毁它们指向的值的问题。
最后,RCPtr的模拟指针的操作就是你在Item M28中看到的灵巧指针的部分:
template
T* RCPtr
template
T& RCPtr
l 合在一起
够了!完结!最后,我们将各个部分放在一起,构造一个基于可重用的RCObject和RCPtr类的带引用计数的String类。或许,你还没有忘记这是我们的最初目标。
每个带引用计数的Sting对象被实现为这样的数据结构:
类的定义是:
template
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
};
绝大部分都是我们前面写的代码的翻新,没什么奇特之处。仔细检查后发现,我们在String::StringValue中增加了一个init函数,但,如我们下面将看到的,它的目的和RCPtr中的相同:消除构造函数中的重复代码。
这里有一个重大的不同:这个String类的公有接口和本条款开始处我们使用的版本不同。拷贝构造函数在哪里?赋值运算在哪里?析构函数在哪里?这儿明显有问题。
实际上,没问题。它工作得很好。如果你没看出为什么,需要重学C++了(prepare yourself for a C++ epiphany)。
我们不再需要那些函数了!确实,String对象的拷贝仍然被支持,并且,这个拷贝将正确处理藏在后面的被引用计数的StringValue对象,但String类不需要写下哪怕一行代码来让它发生。因为编译器为String自动生成的拷贝构造函数将自动调用其RCPtr成员的拷贝构造函数,而这个拷贝构造函数完成所有必须的对StringValue对象的操作,包括它的引用计数。RCPtr是一个灵巧指针,所以这是它将完成的工作。它同样处理赋值和析构,所以String类同样不需要写出这些函数。我们的最初目的是将不可重用的引用计数代码从我们自己写的String类中移到一个与运行环境无关的类中以供任何其它类使用。现在,我们完成了这一点(用RCObject和RCPtr两个类),所以当它突然开始工作时别惊奇。它本来就应该能工作的。
将所以东西放在一起,这儿是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
{
if (pointee == 0) return;
if (pointee->isShareable() == false) {
pointee = new T(*pointee);
}
pointee->addReference();
}
template
RCPtr
: pointee(realPtr)
{ init(); }
template
RCPtr
: pointee(rhs.pointee)
{ init(); }
template
RCPtr
{ if (pointee)pointee->removeReference(); }
template
RCPtr
{
if (pointee != rhs.pointee) {
if (pointee) pointee->removeReference();
pointee = rhs.pointee;
init();
}
return *this;
}
template
T* RCPtr
template
T& RCPtr
这是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对象消除了写时拷贝时手工维护引用计数值的工作。