5. 技术 - 上
条款 25:将 constructor 和 non-member function 虚化
假设你设计一个软件,用来处理时事新闻,其内容由文字和图形构成:
class NLComponent { ... }; // 抽象基类,用于时事消息的组件,其中内含至少一个纯虚函数
class TextBlock : public NLComponent { ... }; // 没有内含任何纯虚函数
class Graphic : public NLComponent { ... }; // 没有内含任何纯虚函数
class NewsLetter {
public:
...
private:
list<NLComponent*> components;
};
virtual constructor
我们让 NewsLetter 拥有一个 constructor 并用 istream 作为自变量,这个 constructor 将读取的数据并产生必要的核心数据结构:
class NewsLetter {
public:
NewsLetter(istream& str);
private:
// 从 str 读取下一个 NLComponent 的数据,产生组件,并返回一个指针指向它
static NLComponent* readComponent(istream& str);
list<NLComponent*> components;
};
NewsLetter::NewsLetter(istream& str) {
while (str) {
// 将 readComponent 返回的指针添加到 component list 尾端
component.push_back(readComponent(str));
}
}
由于 readComponent 产生一个(TextBlock 或 Graphic)对象,所以其行为类似 constructor,但它能产生不同类型的对象,所以我们称它为一个 virtual constructor。所谓 virtual constructor 是某种函数,视其获得的输入,可产生不同类型的对象。
virtual copy constructor
virtual copy constructor 会返回一个指针,指向其调用者(某对象)的一个新副本。基于这种行为,virtual copy constructor 通常以 copySelf 或 cloneSelf 或 clone 命名:
class NLComponent {
public:
virtual NLComponent* clone() = 0;
};
class TextBlock : public NLComponent {
public:
virtual TextBlock* clone() const {
return new TextBlock(*this);
}
};
virtual copy constructor 就只是调用真正的 copy constructor 而已。copy 的意义对于这两个函数而言是一致的。但是请注意,虚函数的返回类型,derived class 重新定义其 base class 的一个虚函数时,不再需要一定得声明与原本相同的返回类型。
将 non-member function 的行为虚化
就像 constructor 无法真正被虚化一样,non-member function 也是。我们认为应该可以让 non-member function 行为视其参数的动态类型而不同。
可以参考下面的例子:
class NLComponent {
public:
virtual ostream& print(ostream& s) const = 0;
};
class TextBlock : public NLComponent {
public:
virtual ostream& print(ostream& s) const;
...
};
inline ostream& operator<<(ostream& s, const NLComponent& c) {
return c.print(s);
}
non-member function 的虚化十分容易:写一个虚函数做实际工作,再写一个什么都不做的非虚函数,只负责调用虚函数。为了避免蒙受函数调用的成本,可以将非虚函数设置为 inline。
条款 26:限制某个 class 所能产生的对象数量
举个需要限制的例子,你的系统只有一台打印机,所以你希望打印机的对象个数限制为 1。
允许零个或一个对象
每当产生一个对象,都会有一个 constructor 被调用。阻止某个 class 产出对象的最简单方法就是将其 constructor 声明为 private。这样就移除了每个人产出对象的权力,但是我们也可以选择性地解除这项约束。
第一种方法就是,设置一个友元函数:
class Printer {
public:
...
friend Printer& thePrinter();
private:
Printer();
};
Printer& thePrinter() {
static Printer p; // 唯一的打印机对象
return p;
}
这个设计有三个成分。第一,Printer class 的 constructor 是 private 的,可以压制对象的诞生;第二,全局函数 thePrinter 被声明为此 class 的友元函数,这样就不受 private 的约束;第三,thePrivate 内含一个 static Printer 对象,意思是只有一个 Printer 对象会被产生出来。
一旦 client 需要使用打印机,就调用 thePrinter。由于此函数返回一个 reference,代表一个 Printer 对象,所以 thePrinter 可以用在任何需要 Printer 对象的地方:
thePrinter().reset();
thePrinter().submitJob(buffer);
第二种方法是设置一个 static member function:这样这个函数就不像全局变量了,同时也消除了 friend 的必要性:
class Printer {
public:
static Printer& thePrinter();
};
但是现在 client 使用打印机时会显得有点冗长:
Printer::thePrinter().reset();
Printer::thePrinter().submitJob(buffer);
第三种方法是把 Printer 和 thePrinter 从全局空间中移走,放入一个 namespace 内:
namespace PrintingStuff {
// 现在这个 class 位于 PrintingStuff 命名空间内
class Printer {
public:
...
friend Printer& thePrinter();
private:
Printer();
};
Printer& thePrinter() {
static Printer p; // 唯一的打印机对象
return p;
}
} // 命名空间结尾
现在 client 使用这个函数就需要使用完全限定名:
PrintingStuff::thePrinter().reset();
PrintingStuff::thePrinter().submitJob(buffer);
这个 thePrinter 的实现代码中,有两个精细的地方值得探讨。
第一,形成唯一一个 Printer 对象的,是函数中的 static 对象而非 class 中的 static 对象。class 拥有一个 static 对象的意思是:即使从未被用到,它也会被构造(及析构)。函数拥有一个 static 对象的意思是:此对象在函数第一次被调用时才产生。如果该函数从未被调用,这个对象也绝不会诞生。而且我们可以确切直到 function static 在什么时候被初始化,但是我们无法知道一个 class static 会在什么时候被初始化。
第二,是函数的 static 对象与 inline 的互动。回忆这个代码:
Printer& thePrinter() {
static Printer p; // 唯一的打印机对象
return p;
}
除了第一次被调用,其余都是只有一行的函数,但是依旧没有被声明为 inline。这是因为对于 non-member function,inline 不仅意味着将每一个调用动作以函数本身取代,还意味着这个函数有内部连接。函数如果带有内部连接,可能会在程序中被复制,也就是说程序的目标代码可能会对带有内部连接的函数复制一份以上的代码,而此复制性也为包括函数内的 static 对象。所以,千万不要产生 内含 local static 对象的 inline non-member function。
不同的对象构造状态
也有一种简单的方法,就是使用一个 static data member 来记录对象个数,在 constructor 累加,destructor 种递减,如果外界企图创建多个对象,可以抛出一个异常。
但是这种策略也有问题。假设我们有一台特别的打印机,譬如说彩色打印机,其 class 和一般的打印机 class 有许多共同点,所以我们会使用继承机制。假设我们系统安装了一台普通打印机和一台彩色彩印机:
Printer p;
ColorPrinter cp;
本应该是一个 Printer 对象,一个 ColorPrinter 对象,但实际上是有两个 Printer 对象,一旦 cp 的 base class 成本被构造时,就会抛出一个异常。
当其他对象内含 Printer 对象时,类似的问题依旧会发生:
class CPFMachine {
private:
Printer p; // 针对打印功能
FaxMachine f; // 针对传真功能
CopyMachine c; // 针对影印功能
};
问题出在 Printer 对象可于 3 种不同状态下生存:1、他自己,2、派生的 base class 成分,3、内嵌于较大对象之中。这些不同状态的呈现,也导致了你心里所想的目前存在的对象个数与编译器所想的不同。
事实上采用原先的 Printer class 策略,很容易达成这种约束。带有 private constructor 的 class 不得被继承,这也导致了禁止派生的性质。
上述性质不必一定和有限个对象联想在一起。举个例子,你希望允许任意数量的 FSA 对象产生,但是也希望没有任何 class 继承自 FSA:
class FSA {
public:
// 伪构造函数
static FSA* makeFSA();
private:
FSA();
};
FSA* makeFSA() {
return new FSA();
}
这里每个 makeFSA 都返回一个指针,指向独一无二的对象,即允许产生无数个 FSA 对象。
允许对象生生灭灭
参考这样的例子:
create Printer object p1;
use p1;
delete p1;
...
create Printer object p2;
use p2;
delete p2;
上述动作是被允许的,但是它在程序的不同地点使用了不同的 Printer 对象。解决办法就是:将稍早的对象计数码和之前的伪构造函数结合起来:
class Printer {
public:
static Printer* makePrinter();
private:
static size_t numObjects;
Printer();
};
size_t Printer::numObject = 0;
Printer::Priter() {
if (numObject >= 1) throw exception;
++numObject;
}
Printer* Printer::makePrinter() {
return new Priner;
}
一个用来计算对象个数的 base class
我们可以设计一个 base class,作为对象计数之用,并让诸如 Printer 之类的 class 继承它。我们可以用某种方法封装整个计数工具:
template<class BeingCounted>
class Counted {
protected:
Counted();
~Counted() { --numObjects; }
private:
static int numObjects;
static const size_t maxObjects;
void init;
};
template<class BeingCounted>
void Counted<BeingCounted>::init() {
if (numObject >= 1) throw exception;
++numObject;
}
条款 27: 要求(或禁止)对象产生于 heap 之中
要求对象产生于 heap 之中(heap-base object)
要实现这种限制,就必须阻止 client 使用 new 以外的方法产生对象。non-heap object 会在其定义点自动构造,并在其寿命结束时自动析构,所以只要让那些被隐式调用的构造动作和析构动作不合法即可。
最直接的方法是将 constructor 和 desturctor 声明为 private。但是没有理由这么做。比较好的方法是让 destructor 成为 private,而 constructor 成为 public。可以导入一个伪析构函数,来调用真正的 desturctor。
class UPNumber {
public:
UPNumber();
...
// 伪析构函数
void destroy() const { delete this; }
private:
~UPNumber();
};
client 需要这么写:
UPNumber n; // 错误
UPNumber *p = new UPNumber; // 正确
...
delete p; // 错误 企图调用 private destructor
p->destroy(); // 正确
而将所有 constructor 都声明为 private,虽然也可以,但是 class 通常由很多 construct,class 的作者必须将它们所有都声明为 private。如果这些函数由编译器产生,则产生的函数为 public。所以比较容易的办法还是只声明 destructor 为 private。
但是上述的方法也妨碍了继承和内含。解决这个问题只需要将 UPNumber 的 destructor 成为 protected(并仍保持其 constructor 为 public),便可以解决。至于必须内含 UPNumber 对象的 class,可以修改为内含一个指向 UPNumber 对象的指针。
class UPNumber {
protected:
~UPNumber();
};
// 继承
class NonNegativeUPNumber : public UPNumber { ... };
// 内含
class Asset {
public:
Asset(int initValue) : value(new UPNumber(initValue)) { }
~Asset() { value->destroy(); }
private:
UPNumber *value;
}
判断某个对象是否位于 heap 内
没有什么办法可以让 UPNumber constructor 侦测出以下状态有什么不同:
NonNegativeUPNumber *n1 = new NonNegativeUPNumber; // 在 heap 内
NonNegativeUPNumber n2; // 不在 heap 内
通过 new 操作符判断
class UPNumber {
public:
// 如果建立一个 non-heap objuct,抛出一个异常
class HeapConstraintViolation {};
static void * operator new(size_t size);
UPNumber();
...
private:
static bool onTheHeap; // 在构造函数内,指示对象是否被构造在堆上
};
bool UPNumber::onTheHeap = false;
void *UPNumber::operator new(size_t size) {
onTheHeap = true;
return ::operator new(size);
}
UPNumber::UPNumber() {
if (!onTheHeap) {
throw HeapConstraintViolation();
}
proceed with normal construction here;
onTheHeap = false; // 清楚 flag,供下一个对象使用对象
}
这里利用率一个观念:当对象被分配于 heap 内,operator new 会被调用起来分配原始内存,然后会有一个 constructor 被调用将对象初始化于该内存。更明确的说,operator new 将 onTheHeap 设为 true,而每一个 constructor 会检查该值,看看构造中的对象内存是否由 operator new 分配而来。
先思考下面代码:
UPNumber *numberArray = new UPNumber[100];
第一个问题是:数组内存由 operator new[] 分配,这里会调用 100 次构造函数,但是只在最开始的时候分配了一次内存,所以只有第一次调用构造函数前把 onTheHeap 设置为 true。当调用第二个构造函数时,会抛出一个异常;
第二个问题是:即使没有数组,前述的位设立代码也可能失败,考虑下面代码:
UPNumber *pn = new UPNumber(*new UPNumber);
这里含有两个 new operator 调用动作,会产生两个 operator new 和两个 UPNumber constructor 调用动作。但是它们的顺序并不能保证,如果两个 operator new 在两个 construtor 之前被调用(这对编译器而言没有错误),前两次设立的 onTheHeap 会被第一次 constructor 清除。
通过地址空间判断
在很多系统都有的一个事实,程序的地址空间以线性序列组织而成,程序的栈从高地址往低地址扩展,堆则从低地址往高地址扩展。你可能会想能够借由下面这个函数来判断某个特定的地址是否在堆中:
// 不正确的尝试,来判断一个地址是否在堆中
bool onHeap(const void *address) {
char onTheStack; // 局部栈变量
return address < &onTheStack;
}
这个函数背后的观点是,在 onHeap 函数中 onTheSatck 是一个局部变量,所以它在 stack 内。当调用 onHeap 时,其 stack frame(activation record)被放在程序栈的顶端,因为栈是自顶向下扩展的,onTheStack 的地址肯定比低于其他任何一个位于 stack 中的变量。如果参数 address 的地址小于 onTheStack 的地址,它就不会在栈上,肯定在堆上。
虽然逻辑是正确的,但是对象可能会被分配与 3 个地方,而不是两个。stack 和 heap 可以持有对象,但是还有 static 对象(也包括 global scope 和 namespace scope 内的对象)。它们的位置,视系统而定,但是在很多系统中它们被置于 heap 之下。所以 onHeap 无法区分 heap 对象和 static 对象:
char *pc = new char; // 返回 true
char c; // 返回 false
static char sc; // 返回 true
通过 abstract mixin base class(抽象混合基类)判断
如果你要判断某个地址是否位于 heap 中,就一定得走入不可移植的、因系统而已的阴暗角落。事实上,要判断对象是否位于 heap 内,可能是因为你想知道为它调用 delete 是否安全,即 delete this 是否安全。然而,此动作是否安全,和指针是否指向一个位于 heap 内的对象是两回事。因为,并非所有指向 heap 内的指针都可以被安全的删除。参考下面例子:
class Asset {
private:
UPNumber value;
...
};
Asset *pa = new Asset;
显然 *pa(及其成员)位于 heap 内。但是对着一个指向 pa->value 的指针进行删除动作是不安全的,因为没有一个这样的指针是以 new 获得的。判断指针的删除动作是否安全,比判断指针是否指向 heap 内的对象简单,因为前者的判断依据就只是:此指针是否由 new 返回。
abstract base class 是一个不能够被实例化的 base class,也就是说它至少有一个纯虚函数。所谓 mixin class 则是提供一组定义完好的能力,能够与其 derived class 所可能提供的其他任何能力兼容。这样的 class 几乎总是 abstract。
现在来实现这个抽象混合基类,其基本思想是,operator new 函数负责分配内存并将条目加入 list 内;operator delete 负责释放内存并从 list 身上移除条目;isOnHeap 决定某对象的地址是否在 list 内,如果在,则认为是存在于 heap 中:
class HeapTracked { // 混合类; 追踪并记录被 operator new 返回的指针
public:
class MissingAddress {}; // 异常类
virtual ~HeapTracked() = 0; // 纯虚函数
static void *operator new(size_t size);
static void operator delete(void *ptr);
bool isOnHeap() const;
private:
typedef const void* RawAddress;
static list<RawAddress> addresses;
};
// static class member 的义务性定义
list<RawAddress> HeapTracked::addresses;
// HeapTracked 的析构函数是纯虚函数,使得该类变为抽象类。但析构函数必须被定义,所以我们做了一个空定义。.
HeapTracked::~HeapTracked() { }
void * HeapTracked::operator new(size_t size) {
void *memPtr = ::operator new(size); // 获得内存
addresses.push_front(memPtr); // 把地址放到 list 的前端
return memPtr;
}
void HeapTracked::operator delete(void *ptr) {
// 得到一个 "iterator",用来找出哪一笔 list 元素内含 ptr
list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), ptr);
if (it != addresses.end()) { // 如果发现一个元素
addresses.erase(it); // 则删除该元素
::operator delete(ptr); // 释放内存
}
else { // 否则
throw MissingAddress(); // ptr 就不是用 operator new 分配的,所以抛出一个异常
}
}
bool HeapTracked::isOnHeap() const {
// 得到一个指针,指向 *this 占据的内存空间的起始处,
const void *rawAddress = dynamic_cast<const void*>(this);
// 在 operator new 返回的地址 list 中查到指针
list<RawAddress>::iterator it =
find(addresses.begin(), addresses.end(), rawAddress);
return it != addresses.end(); // 返回 it 是否被找到
}
唯一比较难理解的是这句话:
const void *rawAddress = dynamic_cast<const void*>(this);
凡涉及多重或虚拟基类的对象,会拥有多个地址,因此会比较复杂。这个问题也在 isOnHeap 中,但由于 isOnHeap 只施行于 HeapTracked 对象身上,所以我们可以利用 dynamic_cast 将指针动态转换为 void*,获得一个指向原指针所指对象的内存起始处的指针。不过,dynamic_cast 只适用于那种所指对象至少有一个虚函数的指针身上。
有了这样的 class 便可以为任何 class 加上追踪指针(指向 heap 分配所得)的能力。他们唯一需要做的就是令 class 继承 HeapTracked。例如,我们希望能够判断某个 Asset 对象指针是否指向一个 heap-based object:
class Asset : public HeapTracked {
private:
UPNumber value;
...
};
然后我们既可以查询 Asset* 指针如下:
void inventoryAsset(const Asset *ap) {
if (ap->isOnHeap()) {
ap is a heap - based asset — inventory it as such;
}
else {
ap is a non - heap - based asset — record it that way;
}
}
不过需要注意,像 HeapTracked 这样的 mixin class 有个缺点,那就是它不能够使用于内置类型上。
禁止对象产生于 heap 中
阻止对象被分配于 heap 中,一般而言有三种可能:1、对象被直接实例化,2、对象被实例化为 derived class object 内的 base class 成分,3、对象被内嵌于其他对象之中。
阻止用户直接将对象实例化于 heap
可以让客户无法调用 new,虽然你不能影响 new operator 的能力(语言内置的),但你可以利用一个事实:new operaot 总是调用 operator new:
class UPNumber {
private:
static void *operator new(size_t size);
static void operator delete(void *ptr);
};
现在用户只能做某些被允许的事情:
UPNumber n1; // 正确
static UPNumber n2; // 正确
UPNumber *p = new UPNumber; // 错误,企图调用 private operator new
如果你想禁止数组相关的,可以将 operator new[] 和 operator delete[] 设置为 private。
阻止派生类的基类被实例化
如果将 operator new 在基类中被声明为 private,往往也会妨碍派生类中基类部分被实例化,因为 operator new 和 operator delete 都会被继承,所以如果这些函数不在派生类内声明为 public,派生类继承的便是其基类所声明的 private 版本:
class UPNumber { ... }; // 如上
// 假设此类未声明 operator new
class NonNegativeUPNumber : public UPNumber { ... };
NonNegativeUPNumber n1; // 正确
static NonNegativeUPNumber n2; // 正确
NonNegativeUPNumber *p = new NonNegativeUPNumber; // 错误 试图调用 private operator new
如果 derived class 声明了一个数组自己的 operator new(且为 public),当用户将 derived class object 分配于 heap 内时,该 operator new 会被调用,此时就需要别的方法来阻止。
阻止对象被嵌入到其它对象时被实例化
UPNumbe r的 operator new 是 private 这一点,不会对包含 UPNumber 成员对象的对象的分配产生任何影响:
class Asset {
public:
Asset(int initValue);
...
private:
UPNumber value;
};
Asset *pa = new Asset(100); // 正确, 调用 Asset::operator new 或 ::operator new, 不是 UPNumber::operator new
对使用目的而言,我们曾希望如果一个 NUPumber 对象被构造于 heap 以外,则在 constructor 抛出一个 exception,这次是希望如果对象被产生于 heap 之内的化,就抛出一个 exception。既然我们做不到前者,那么我们也做不到后者。
条款 28:smart pointer(智能指针)
智能指针类似于内置指针,但是提供了更多的功能。当你用智能指针取代 C++ 内置指针,你可以获得以下各种指针行为的控制权:
- 构造和析构:可以决定智能指针什么时候产生,什么时候销毁。
- 复制和赋值:当一个智能指针被赋值或复制时,你可以控制发生什么事。
- 解引用:当客户解引用智能指针时,你有权决定发生什么事情。
智能指针由 template 产生出来。由于与内置指针类似,所以必须是 strongly typed 的;用户可利用 template 参数表明其所指对象的类型。大多数智能指针模板看起来都像这样:
template<class T>
class SmartPtr {
public:
SmartPtr(T* realPtr = 0); // 建立一个智能指针
SmartPtr(const SmartPtr& rhs); // 复制
~SmartPtr(); // 销毁
SmartPtr& operator=(const SmartPtr& rhs); // 赋值
T* operator->() const; // 解引用
T& operator*() const; // 解引用
private:
T *pointee;
};
每个 smart pointer-to-T 都内含有一个 dumb pointer-to-T,后者才是时间指针行为的真正主角。
智能指针的构造、赋值、析构
智能指针的构造行为:确定一个目标物(通常利用智能指针的 constructor 自变量),然后让智能指针内部的 dumb pointer 指向它。如果尚未决定目标物,就将内部指针设为 0,或是发出一个错误消息(可能是抛出 exception)。
如果一个智能指针拥有它所指的对象,它就有责任在本身即将被销毁时删除该对象 —— 前提是这个智能指针所指对象时动态分配而得。
对于复制或赋值时,如果采用常规方法,会导致多个 auto_ptr 指向同一个对象的可能,这样会造成多次销毁同一对象。如果使用 new 操作符为所指对象产生一个新副本,这样可能会使性能遭受冲击,此外还不一定能明确知道产生了什么类型对象,因为一个 auto_ptr<T> 不一定必须指向一个类型为 T 的对象,可能是 T 的派生类。事实上 auto_ptr 采用一个更富有弹性的方法:当 auto_ptr 被复制或赋值时,auto_ptr 会将其对象拥有权转移:
// 复制
template<class T>
auto_ptr<T>::auto_ptr(auto_ptr<T>& rhs) {
pointee = rhs.pointee; // 把 *pointee 的所有权传递到 *this
rhs.pointee = 0; // rhs 不再拥有任何东西
}
// 赋值
template<class T>
auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs) {
if (this == &rhs) // 如果这个对象自我赋值
return *this; // 什么也不要做
delete pointee; // 删除现在拥有的对象
pointee = rhs.pointee; // 把 *pointee 的所有权传递到 *this
rhs.pointee = 0; // rhs 不再拥有任何东西
return *this;
}
注意,assignment 操作符在掌握一个新对象的拥有权之前,必须先删除它所拥有的对象。由于 auto_ptr 会转移对象拥有权,所以通过 by value 的方式传递参数并不是一个好方法,pass-by-reference-to-const 才是适当的途径。
实现解引用操作符
现在思考 operator* 和 operator-> 函数。前者返回所指对象:
template<class T>
T& SmartPtr<T>::operator*() const {
perform "smart pointer" processing;
return *pointee;
}
注意返回值是 reference 而非对象,因为 pointee 不需非得指向类型为 T 的对象,也可以指向一个 T 派生类的对象。如果此时返回的是一个对象,则会发生切割问题,这样如果调用虚函数则使用的是 base class 版本的。
对于 operator-> 来说,假如有这么一条语句:
pt->displayEditDialog();
会被编译器解释为:
(pr.operator->())->displayEditDialog();
这意味着不论 operator-> 返回什么,在该返回值身上施行 -> 操作符都必须是合法的。因此 operator-> 只可能返回两种东西:一个 dumb pointer(指向某对象),或是一个 smart pointer。通常情况下返回一个普通的 dumb pointer,其实现如下:
template<class T>
T* SmartPtr<T>::operator->() const {
perform "smart pointer" processing;
return pointee;
}
测试智能指针是否为 NULL
对于智能指针,我们可以产生、销毁、赋值、复制、解引用,但是我们无法判断智能指针是否为 NULL:
SmartPtr<TreeNode> ptn;
...
if (ptn == 0) // 错误
if (ptn) // 错误
if (!ptn) // 错误
虽然说为我们的智能指针类加上一个 isNull 函数很容易,但是这样就不如 dumb pointer 那样自然的测试是否为 NULL。另一种做法是,提供要给隐式类型转换操作符,这类转换的传统目标是 void*:
template<class T>
class SmartPtr {
public:
...
operator void*(); // 如果智能指针为 NULL,返回 0
...
};
SmartPtr<TreeNode> ptn;
...
if (ptn == 0) // 正确
if (ptn) // 正确
if (!ptn) // 正确
但是,这样就允许把智能指针拿来和不同类型进行比较:
SmartPtr<Apple> pa;
SmartPtr<Orange> po;
...
if (pa == po) // 编译成功
虽然没有编写以这两个类型为参数的 operator== 函数,但是由于两个智能指针都可以被隐式转换为 void* 指针,而针对内置指针存在比较函数,所以上式可以通过编译。
这种做法允许你提供测试 nullness 的合理语法,但是却会引起不同类型的智能指针的比较。
将智能指针转换为哑指针
参考下面的例子:
class Tuple { ... }; // 同上
void normalize(Tuple *pt); // 注意使用的是 dumb pointer
DBPtr<Tuple> pt;
normalize(pt); // 错误
这个调用动作之所以失败,是因为目前没有办法可以将一个 DBPtr<Tuple> 转换为一个 Tuple*。
我们可以为 smart pointer-to-T template 添加一个隐式转换操作符:
template<class T>
class DBPtr {
public:
operator T*() { return pointee; }
};
DBPtr<Tuple> pt;
...
normalize(pt); // 能够运行
此时也可以满足 nullness 测试问题:
if (pt == 0) // 正确 将 pt 转变成 Tuple*
if (pt) // 同上
if (!pt) // 同上
但是这样,就可以使 client 得以轻易的直接对 dumb pointer 操作,这也违反了之鞥你指针当初的设计目的:
void processTuple(DBPtr<Tuple>& pt) {
Tuple *rawTuplePtr = pt; // 把 DBPtr<Tuple> 转变成 Tuple*
use rawTuplePtr to modify the tuple;
}
而且,即使你提供了一个隐式转换操作符,可以将智能指针转换为其内部的哑指针,你的智能指针还是无法完全取代 dumb pointer。因为从智能指针转换为哑指针是一种用户定制的转换行为,但是编译器禁止一次施行一个以上的这类转换。
而且如果 智能指针类提供了隐式转换到 dumb pointer 的方法,就会出现下面这种情况:
DBPtr<Tuple> pt = new Tuple;
...
delete pt;
这应该无法编译,毕竟 pt 不是指针,是对象,你不能删除一个对象。但是上述的 delete 语句,它会暗自将 pt 转换为一个 Tuple*,然后删除。
总之,重点很简单:不要提供对 dumb pointer 的隐式转换操作符。
智能指针和与继承有关的类型转换
参考下面的 public inheritance 继承体系:
class MusicProduct { ... };
class Cassette : public MusicProduct { ... };
class CD : public MusicProduct { ... };
现在假设有一个函数,给它一个 MusicProduct 对象,他就会显示标题并播放:
void displayAndPlay(const MusicProduct* pmp, int numTimes);
我们可能这样使用这个函数:
Cassette *funMusic = new Cassette("Alapalooza");
CD *nightmareMusic = new CD("Disco Hits of the 70s");
displayAndPlay(funMusic, 10);
displayAndPlay(nightmareMusic, 0);
这很正常,但是如果我们将 dumb pointer 换成相应的 smart pointer:
SmartPtr<Cassette> funMusic(new Cassette("Alapalooza"));
SmartPtr<CD> nightmareMusic(new CD("Disco Hits of the 70s"));
displayAndPlay(funMusic, 10); // 错误
displayAndPlay(nightmareMusic, 0); // 错误
之所以无法通过编译,是因为没办法将 SmartPtr<Cassette> 或 SmartPtr<CD> 转换为 SmartPtr<MusicProduct>。编译器认为这三个是互不相干的 class。
有一个方法可以解除这个束缚,就是让每一个 smart pointer class 有一个隐式类型转换操作符(见条款 05),用来转换成另一个 smart pointer class。
但是这种做法有两个缺点。第一,你必须为每一个 SmartPtr class 加入这种特殊的式子;因为你所指的对象可能位于继承体系的底层,而你必须为对象直接继承或间接继承的每一个 base class 提供一个转换操作符。由于编译器禁止一次执行一个以上的用户定制的类型转换函数,所以无法将一个 smart poinger-to-T 转换为一个 smart pointer-to-indirect-vase-class-of-T —— 除非他们能够在单一步骤内完成。
当然也可以通过将 non-virtual member function 声明为 template 的方法来生成上述转换代码:
operator SmartPtr<newType>() { // 用于实现隐式类型转换
return SmartPtr<newType>(pointee);
}
现在再来看这个代码:
displayAndPlay(funMusic, 10);
funMusic 类型是 SmartPtr<Cassette>,函数 displayAndPlay 期望得到 SmartPtr<MusicProduct> 对象。编译器侦测到类型吻合,于是寻找某种方法,将 funMusic 转换成 SmartPtr<MusicProduct>。
首先它在 SmartPtr 类里寻找带有 SmartPtr 类型参数的单参数构造函数(见条款 05),但是没有找到;于是接着寻找一个隐式类型转换操作符,但是也失败了;接着编译器在试图寻找一个“可实例化以导出合适的函数”的 member function template。最后它们在 SmartPtr<Cassette> 找到了这样的东西,把 newType 绑定到 MusicProduct 上,生成了所需函数。于是生成这样的代码:
SmartPtr<Cassette>:: operator SmartPtr<MusicProduct>() {
return SmartPtr<MusicProduct>(pointee);
}
这种方法只适用于继承体系的向上转型。事实上这种方法对于指针类型之间的任何合法隐式转换都能成功。这就可能会造成二义性错误,参考下面代码:
class Cassette : public MusicProduct { ... };
class CasSingle : public Cassette { ... };
void displayAndPlay(const SmartPtr<MusicProduct>& pmp, int howMany);
void displayAndPlay(const SmartPtr<Cassette>& pc, int howMany);
SmartPtr<CasSingle> dumbMusic(new CasSingle("Achy Breaky Heart"));
displayAndPlay(dumbMusic, 1); // 错误
此例中 displayAndPlay 被重载,一种接受 SmartPtr<MusicProduct> 对象,另一种接受 SmartPtr<Cassette> 对象。当我们给予其一个 SmartPtr<CasSingle> 对象时,如果是 dumb pointer 会选择其直接继承的 Cassette 版本。但是我们的 smart pointer 却认为这两个版本一样好,所以会导致二义性错误。
smart pointer 虽然 smart,却不是 pointer。
智能指针与 const
对于 dunb pointer,const 可用来修饰被指之物,或是指针本身,或者两者兼具:
CD goodCD("Flood");
const CD *p; // p 是一个 non-const 指针,指向 const CD 对象
CD* const p = &goodCD; // p 是一个 const 指针,指向 non-const CD 对象; 因为 p 是 const, 它必须被初始化
const CD * const p = &goodCD; // p 是一个 const 指针,指向一个 const CD 对象
但是智能指针只能用在指针身上,不能用于其所指对象:
const SmartPtr<CD> p = &goodCD; // p 是一个 const 智能指针,指向 non-const CD 对象
在 dumb pointer 指针上,我们可以使用 non-const 指针作为 const 指针的初值:
CD *pCD = nwe CD("Famous Movie Themes");
const CD* pConstCD = pCD; // 正确
但是下述语句:
SmartPtr<CD> pCD = new CD("Famous Movie Themes");
SmartPtr<const CD> pConstCD = pCD; // 错误
SmartPtr<CD> 和 SmartPtr<const CD> 是完全不同的类型。