more effective c++--引用计数

引用计数是这样一个技巧,它允许多个有相同值的对象共享这个值的实现。这个技巧 有两个常用动机。第一个是简化跟踪堆中的对象的过程。一旦一个对象通过调用 new 被分配 出来,最要紧的就是记录谁拥有这个对象,因为其所有者--并且只有其所有者--负责对 这个对象调用 delete。但是,所有权可以被从一个对象传递到另外一个对象(例如通过传 递指针型参数),所以跟踪一个对象的所有权是很困难的。象 auto_ptr(见 Item M9)这样 的类可以帮助我们,但经验显示大部分程序还不能正确地得到这样的类。引用计数可以免除 跟踪对象所有权的担子,因为当使用引用计数后,对象自己拥有自己。当没人再使用它时, 它自己自动销毁自己。因此,引用计数是个简单的垃圾回收体系。 第二个动机是由于一个简单的常识。如果很多对象有相同的值,将这个值存储多次是 很无聊的。更好的办法是让所有的对象共享这个值的实现。这么做不但节省内存,而且可以 使得程序运行更快,因为不需要构造和析构这个值的拷贝。 和大部分看似简单的主意一样,这个动机也有一个曲折而有趣的细节。在其中必须有 一个正确实现的引用计数体系。在开始钻研细节前,让我们掌握一些基础。一个好主意是先 着眼于我们将可能如何遇到多个对象有相同的值。这儿有一个: class String { // the standard string type may public: // employ the techniques in this // Item, but that is not required String(const char *value = “”); String& operator=(const String& rhs); … private: char *data; }; String a, b, c, d, e; a = b = c = d = e = “Hello”; 看起来,对象 a 到 e 都有相同的值“Hello”。其值的形态取决于 String 类是怎么实现 的,但通常的实现是每个 string 对象有一个这个值的拷贝。例如,String 的赋值操作可能 实现为这样: String& String::operator=(const String& rhs) { if (this == &rhs) return *this; // see Item E17 delete [] data; data = new char[strlen(rhs.data) + 1]; strcpy(data, rhs.data); return *this; // see Item E15 } 根据这个实现,我们可以推测,这 5 个对象及其值如下: 其冗余是显然的。在一个理想的世界中,我们希望将上图改为这样: 这里,只存储了一个“Hello”的拷贝,所有具有此值的 String 对象共享其实现。 实际世界中,实现这个主意是不可能的,因为我们需要跟踪多少对象共享同一个值。 如果上面的对象 a 被赋了“Hello”以外的另外一个值,我们不能摧毁值“Hello”,因为还 有四个对象需要它。另一方面,如果只有一个对象有“Hello”这个值,当其超出生存空间 时,没有对象具有这个值了,我们必须销毁这个值以避免资源泄漏。 保存当前共享/引用同一个值的对象数目的需求意味着我们的那张图必须增加一个计 数值(引用计数): (有些人将其叫作 use count,但我不是其中之一。C++有很多它自己的特性,最后需 要的一个是专业名词的派别之争。) z 实现引用计数 创建一个带引用计数的 String 类并不困难,但需要注意一些细节,所以我们将略述这 样一个类的大部分常用成员函数的实现。然而,在开始之前,认识到“我们需要一个地方来 存储这个计数值”是很重要的。这个地方不能在 String 对象内部,因为需要的是每个 String 值一个引用计数值,而不是每个 String 对象一个引用计数。这意味着 String 值和引用计数 间是一一对应的关系,所以我们将创建一个类来保存引用计数及其跟踪的值。我们叫这个类 StringValue,又因为它唯一的用处就是帮助我们实现 String 类,所以我们将它嵌套在 String 类的私有区内。另外,为了便于 Sting 的所有成员函数读取其数据区,我们将 StringValue 申明为 struct。需要知道的是:将一个 struct 内嵌在类的私有区内,能便于 这个类的所有成员访问这个结构,但阻止了其它任何人对它的访问(当然,除了友元)。 基本设计是这样的: class String { public: … // the usual String member // functions go here private: struct StringValue { … }; // holds a reference count // and a string value StringValue *value; // value of this String }; 我们可以给这个类起个其它名字(如 RCString)以强调它使用了引用计数,但类的实 现不该是类的用户必须关心的东西,用户只关心类的公有接口。而我们带引用计数的 String 版本与不带引用计数的版本,其接口完全相同,所以为什么要用类的名字来把问题搅混呢? 真的需要吗?所以我们没有这么做。 这是 StringValue 的实现: class String { private: struct StringValue { int refCount; char *data; StringValue(const char *initValue); ~TStringValue(); }; … }; String::StringValue::StringValue(const char *initValue)
refCount(1)
{
data = new char[strlen(initValue) + 1];
strcpy(data, initValue);
}
String::StringValue::~StringValue()
{
delete [] data;
}
这是其所有的一切,很清楚,这不足以实现带引用计数的 String 类。一则,没有拷贝
构造函数和赋值运算(见 Item E11);二则,没有提供对 refCount 的操作。别担心,少掉
的功能将由 String 类提供。StringValue 的主要目的是提供一个空间将一个特别的值和共
享此值的对象的数目联系起来。StringValue 给了我们这个,这就足够了。
我们现在开始处理 String 的成员函数。首先是构造函数:
class String {
public:
String(const char *initValue = “”);
String(const String& rhs);

};
第一个构造函数被实现得尽可能简单。我们用传入的 char *字符串创建了一个新的
StringValue 对象,并将我们正在构造的 string 对象指向这个新生成的 StringValue:
String::String(const char *initValue)
value(new StringValue(initValue))
{}
这样的用户代码:
String s(“More Effective C++”);
生成的数据结构是这样的:
String 对象是独立构造的,有同样初始化值的对象并不共享数据,所以,这样的用户
代码:
String s1(“More Effective C++”);
String s2(“More Effective C++”);
产生这样的数据结构:
消除这样的副本是可能的:通过让 String(或 StringValue)对象跟踪已存在的
StringValue 对象,并只在是不同串时才创建新的对象。但这样的改进有些偏离目标。于是,
我将它作为习题留给读者。
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 时),String 的析构函
数才摧毁 StringValue 对象:
class String {
public:
~String();

};
String::~String()
{
if (–value->refCount == 0) delete value;
}
和没有引用计数的版本比较一下效率。那样的函数总调用 delete,当然会有一个相当
程度的运行时间的代价。现在提供的 String 对象们实际上有时具有相同的值,上面的这个
实现在此时只需要做一下减少引用计数并与 0 进行比较。
如果在这个问题上引用计数没有向外界表现出来,你就根本不需要花注意力。
这就是 String 的构造和析构,我们现在转到赋值操作:
class String {
public:
String& operator=(const String& rhs);

};
当用户写下这样的代码:
s1 = s2; // s1 and s2 are both String objects
其结果应该是 s1 和 s2 指向相同的 StringValue 对象。对象的引用计数应该在赋值时
被增加。并且,s1 原来指向的 StringValue 对象的引用计数应该减少,因为 s1 不再具有这
个值了。如果 s1 是拥有原来的值的唯一对象,这个值应该被销毁。在 C++中,其实现看起
来是这样的:
String& String::operator=(const String& rhs)
{
if (value == rhs.value) { // do nothing if the values
return *this; // are already the same; this
} // subsumes the usual test of
// this against &rhs (see Item E17)
if (–value->refCount == 0) { // destroy *this’s value if
delete value; // no one else is using it
}
value = rhs.value; // have *this share rhs’s
++value->refCount; // value
return *this;
}
z 写时拷贝
围绕我们的带引用计数的 String 类,考虑一下数组下标操作([]),它允许字符串中
的单个字符被读或写:
class String {
public:
const char&
operator[](int index) const; // for const Strings
char& operator[](int index); // for non-const Strings

};
这个函数的 const 版本的实现很容易,因为它是一个只读操作,String 对象的值不受
影响:
const char& String::operator[](int index) const
{
return value->data[index];
}
(这个函数实现了 C++传统意义上的下标索引(根本不会说“不”)。如果你想加上参数
检查,这是非常容易的。)
非 const 的 operator[]版本就是一个完全不同的故事了。它可能是被调用了来读一个
字符,也可能被调用了来写一个字符:
String s;

cout << s[3]; // this is a read
s[5] = ‘x’; // this is a write
我们希望以不同的方式处理读和写。简单的读操作,可以用与 const 的 operator[]类
似的方式实现,而写操作必须用完全不同的方式来实现。
当我们修改一个 String 对象的值时,必须小心防止修改了与它共享相同 StringValue
对象的其它 String 对象的值。不幸的是,C++编译器没有办法告诉我们一个特定的 operator[]
是用作读的还是写的,所以我们必须保守地假设“所有”调用非 const operator[]的行为
都是为了写操作。(Proxy 类可以帮助我们区分读还是写,见 Item M30。)
为了安全地实现非 const 的 operator[],我们必须确保没有其它 String 对象在共享这
个可能被修改的 StringValue 对象。简而言之,当我们返回 StringValue 对象中的一个字符
的引用时,必须确保这个 StringValue 的引用计数是 1。这儿是我们的实现:
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];
}
这个“与其它对象共享一个值直到写操作时才拥有自己的拷贝”的想法在计算机科学
中已经有了悠久而著名的历史了,尤其是在操作系统中:进程共享内存页直到它们想在自己
的页拷贝中修改数据为止。这个技巧如此常用,以至于有一个名字:写时拷贝。它是提高效
率的一个更通用方法--Lazy 原则--的特例。
z 指针、引用与写时拷贝
大部分情况下,写时拷贝可以同时保证效率和正确性。只有一个挥之不去的问题。看
一下这样的代码:
String s1 = “Hello”;
char *p = &s1[1];
数据结构是这样的:
现在看增加一条语句:
String s2 = s1;
String 的拷贝构造函数使得 s2 共享 s1 的 StringValue 对象,所以数据结构将是:
下面这样的语句将有不受欢迎的结果:
*p = ‘x’; // modifies both s1 and s2!
String 的拷贝构造函数没有办法检测这样的问题,因为它不知道指向 s1 拥有的
StringValue 对象的指针的存在。并且,这个问题不局限于指针:它同样存在于有人保存了
一个 String 的非 const operator[]的返回值的引用的情况下。
至少有三种方法来应付这个问题。第一个是忽略它,假装它不存在。这是实现带引用
计数的 String 类的类库中令人痛苦的常见问题。如果你有带引用计数的 String 类,试一下
上面的例子,看你是否很痛苦。即使你不能确定你操作的是否是带引用计数的 String 类,
也无论如何应该试一下这个例子。由于封装,你可能使用了一个这样的类型而不自知。
不是所以的实现都忽略这个问题。稍微好些的方法是明确说明它的存在。通常是将它
写入文档,或多或少地说明“别这么做。如果你这么做了,结果为未定义。”无论你以哪种
方式这么做了(有意地或无意地),并抱怨其结果时,他们辩解道:“好了,我们告诉过你别
这么做的。”这样的实现通常很方便,但它们在可用性方面留下了太多的期望。
第三个方法是排除这个问题。它不难实现,但它将降低一个值共享于对象间的次数。
它的本质是这样的:在每个 StringValue 对象中增加一个标志以指出它是否为可共享的。在
最初(对象可共享时)将标志打开,在非 const 的 operator[]被调用时将它关闭。一旦标
志被设为 false,它将永远保持在这个状态(注 10)。
这是增加了共享标志的修改版本:
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 对象的数目。
z 带引用计数的基类
引用计数不只用在字符串类上,只要是多个对象具有相同值的类都可以使用引用计数。
改写一个类以获得引用计数需要大量的工作,而我们已经有太的工作需要做了。这样不好吗:
如果我们将引用计数的代码写成与运行环境无关的,并能在需要时将它嫁接到其它类上?当
然很好。很幸运,有一个方法可以实现它(至少完成了绝大部分必须的工作)。
第一步是构建一个基类 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)继承而来的话。它第一眼看上去是有些古怪,但完全合理。嵌套类和其
它类是完全相同的,所以它有自由从它喜欢的任何其它类继承。以后,你不用第二次思考这
种继承关系了。
z 自动的引用计数处理
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::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的唯一假设。它还要求 T 从 RCObject 继承,或
至少提供了 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; }
z 合在一起
够了!完结!最后,我们将各个部分放在一起,构造一个基于可重用的 RCObject 和 RCPtr
类的带引用计数的 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 referencepublic:
// 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 类中移到一个与运行环境无关的类中以供任何其它类使用。现在,我们
完成了这一点(用 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::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 类的定义,所以用户需要重新编译和链接,但他们在自己代码上的投资受到了
完全的保护。你看到了吗?封装确实是个很好的东西。
z 在现存类上增加引用计数
到现在为止,我们所讨论的都假设我们能够访问有关类的源码。但如果我们想让一个
位于支撑库中而无法修改的类获得引用计数的好处呢?不可能让它们从 RCObject 继承的,
所以也不能对它们使用灵巧指针 RCPtr。我们运气不好吗?
不是的。只要对我们的设计作小小的修改,我们就可以将引用计数加到任意类型上。
首先考虑如果从 RCObject 继承的话,我们的设计看起来将是什么样子。在这种情况下,
我们需要增加一个类 RCWidget 以供用户使用,而所有的事情都和 String/StringValue 的例
子一样,RCWidget 和 String 相同,Widget 和 StringValue 相同。设计看起来是这样的:
我们现在可以应用这句格言:计算机科学中的绝大部分问题都可以通过增加一个中间
层次来解决。我们增加一个新类 CountHolder 以处理引用计数,它从 RCObject 继承。我们
让 CountHolder 包含一个指针指向 Widget。然后用等价的灵巧指针 RCIPter 模板替代 RCPtr
模板,它知道 CountHolder 类的存在。(名字中的“i”表示间接“indirect”。)修改后的设
计为:
如同 StringValue 一样,CountHolder 对用户而言,是 RCWidget 的实现细节。实际上,
它是 RCIPtr 的实现细节,所以它嵌套在这个类中。RCIPtr 的实现如下:
template
class RCIPtr {
public:
RCIPtr(T* realPtr = 0);
RCIPtr(const RCIPtr& rhs);
~RCIPtr();
RCIPtr& operator=(const RCIPtr& rhs);
const T* operator->() const; // see below for an
T* operator->(); // explanation of why
const T& operator*() const; // these functions are
T& operator*(); // declared this way
private:
struct CountHolder: public RCObject {
~CountHolder() { delete pointee; }
T *pointee;
};
CountHolder *counter;
void init();
void makeCopy(); // see below
};
template
void RCIPtr::init()
{
if (counter->isShareable() == false) {
T *oldValue = counter->pointee;
counter = new CountHolder;
counter->pointee = new T(*oldValue);
}
counter->addReference();
}
template
RCIPtr::RCIPtr(T* realPtr)
counter(new CountHolder)
{
counter->pointee = realPtr;
init();
}
template
RCIPtr::RCIPtr(const RCIPtr& rhs)
counter(rhs.counter)
{ init(); }
template
RCIPtr::~RCIPtr()
{ counter->removeReference(); }
template
RCIPtr& RCIPtr::operator=(const RCIPtr& rhs)
{
if (counter != rhs.counter) {
counter->removeReference();
counter = rhs.counter;
init();
}
return *this;
}
template // implement the copy
void RCIPtr::makeCopy() // part of copy-on-
{ // write (COW)
if (counter->isShared()) {
T *oldValue = counter->pointee;
counter->removeReference();
counter = new CountHolder;
counter->pointee = new T(*oldValue);
counter->addReference();
}
}
template // const access;
const T* RCIPtr::operator->() const // no COW needed
{ return counter->pointee; }
template // non-const
T* RCIPtr::operator->() // access; COW
{ makeCopy(); return counter->pointee; } // needed
template // const access;
const T& RCIPtr::operator*() const // no COW needed
{ return *(counter->pointee); }
template // non-const
T& RCIPtr::operator*() // access; do the
{ makeCopy(); return *(counter->pointee); } // COW thing
RCIPtr 与 RCPtr 只两处不同。第一,RCPtr 对象直接指向值对象,而 RCIptr 对象通过
中间层的 CountHolder 对象指向值对象。第二,RCIPtr 重载了 operator->和 operator*,
当有对被指向的对象的非 const 的操作时,写时拷贝自动被执行。
有了 RCIPtr,很容易实现 RCWidget,因为 RCWidget 的每个函数都是将调用传递给
RCIPtr 以操作 Widget 对象。举个例子,如果 Widget 是这样的:
class Widget {
public:
Widget(int size);
Widget(const Widget& rhs);
~Widget();
Widget& operator=(const Widget& rhs);
void doThis();
int showThat() const;
};
那么 RCWidget 将被定义为这样:
class RCWidget {
public:
RCWidget(int size): value(new Widget(size)) {}
void doThis() { value->doThis(); }
int showThat() const { return value->showThat(); }
private:
RCIPtr value;
};
注意 RCWidget 的构造函数是怎么用它被传入的参数调用 Widget 的构造函数的(通过
new 操作符,见 Item M8);RCWidget 的 doThis 怎么调用 Widget 的 doThis 函数的;以及
RCWidget 的 showThat 怎么返回 Widget 的 showThat 的返回值的。同样要注意 RCWidget 没
有申明拷贝构造函数和赋值操作函数,也没有析构函数。如同 String 类一样,它不需要这
些函数。感谢于 RCIPtr 的行为,RCWidget 的默认版本将完成正确的事情。
如果认为生成 RCWidget 的行为很机械,它应该自动进行,那么你是对的。不难写个小
程序接受如 Widget 这样的类而输出 RCWidget 这样的类。如果你写了一个这样的程序,请让
我知道。
z 评述
让我们从 Widget、String、值、灵巧指针和引用计数基类中摆脱一下。给个机会回顾
一下,在更广阔的环境下看一下引用计数。在更大的环境下,我们必须处理一个更高层次的
问题,也就是什么时候使用引用计数?
实现引用计数不是没有代价的。每个被引用的值带一个引用计数,其大部分操作都需
要以某种形式检查或操作引用计数。对象的值需要更多的内存,而我们在处理它们时需要执
行更多的代码。此外,就内部的源代码而言,带引用计数的类的复杂度比不带的版本高。没
有引用计数的 String 类只依赖于自己,而我们最终的 String 类如果没有三个辅助类
(StringValue、RCObject 和 RCPtr)就无法使用。确实,我们这个更复杂的设计确保在值
可共享时的更高的效率;免除了跟踪对象所有权的需要,提高了引用计数的想法和实现的可
重用性。但,这四个类必须写出来、被测试、文档化、和被维护,比单个类要多做更多的工
作。即使是管理人员也能看出这点。
引用计数是基于对象通常共享相同的值的假设的优化技巧(参见 Item M18)。如果假设
不成立的话,引用计数将比通常的方法使用更多的内存和执行更多的代码。另一方面,如果
你的对象确实有具体相同值的趋势,那么引用计数将同时节省时间和空间。共享的值所占内
存越大,同时共享的对象数目越多,节省的内存也就越大。创建和销毁这个值的代价越大,
你节省的时间也越多。总之,引用计数在下列情况下对提高效率很有用:
少量的值被大量的对象共享。这样的共享通常通过调用赋值操作和拷贝构造而发生。
对象/值的比例越高,越是适宜使用引用计数。
对象的值的创建和销毁代价很高昂,或它们占用大量的内存。即使这样,如果不是多
个对象共享相同的值,引用计数仍然帮不了你任何东西。
只有一个方法来确认这些条件是否满足,而这个方法不是猜测或依赖直觉(见 Item
M16)。这个方法是使用 profiler 或其它工具来分析。使用这种方法,你可以发现是否创建
和销毁值的行为是性能瓶颈,并能得出对象/值的比例。只有当你手里有了这些数据,你才
能得出是否从引用计数上得到的好处超过其缺点。
即使上面的条件满足了,使用引用计数仍然可能是不合适的。有些数据结构(如有向
图)将导致自我引用或环状结构。这样的数据结构可能导致孤立的自引用对象,它没有被别
人使用,而其引用计数又绝不会降到零。因为这个无用的结构中的每个对象被同结构中的至
少一个对象所引用。商用化的垃圾收集体系使用特别的技术来查找这样的结构并消除它们,
但我们现在使用的这个简单的引用计数技术不是那么容易扩充出这个功能的。
即使效率不是主要问题,引用计数仍然很吸引人。如果你不放心谁应该去执行删除动
作,那么引用计数正是这种让你放下担子的技巧。很多程序员只因为这个原因就使用引用计
数。
让我们用最后一个问题结束讨论。当 RCObject::removeReference 减少对象的引用计
数时,它检查新值是否为 0。如果是,removeReference 通过调用 delete this 销毁对象。
这个操作只在对象是通过调用 new 生成时才安全,所以我们需要一些方法以确保 RCObject
只能用这种方法产生。
此处,我们用习惯方法来解决。RCObject 被设计为只作被引用计数的值对象的基类使
用,而这些值对象应该只通过灵巧指针 RCPtr 引用。此外,值对象应该只能由值会共享的对
象来实例化;它们不能被按通常的方法使用。在我们的例子中,值对象的类是 StringValue,
我们通过将它申明为 String 的私有而限制其使用。只有 String 可以创建 StringValue 对象,
所以 String 类的作者应该确保这些值对象都是通过 new 操作产成的。
于是,我们限制 RCObject 只能在堆上创建的方法就是指定一组满足这个要求的类,并
确保只有这些类能创建 RCObject 对象。用户不可能无意地(或有意地)用一种不恰当的方
法创建 RCObject 对象。我们限制了创建被引用计数对象的权力,当我们交出这个权力时,
必须明确其附带条件是满足创建对象的限制条件。
z 注 10
标准 C++运行库中的 string 类型(见 Item E49 和 Item M35)同时使用了方法 2 和方
法 3。从非 const 的 operator[]中返回的引用直到下一次的可能修改这个 string 的函数的
调用为止都是有效的。在此之后,使用这个引用(或它指向的字符),其结果未定义。这样
就它允许了:string 的可共享标志在调用可能修改 string 的函数时被重设为 true。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值