C++编程思想18.pdf
下载下载第1 8章运行时类型识别运行时类型识别(Run-time type identification,RT T I)是在我们只有一个指向基类的指针或引用时确定一个对象的准确类型。这可以被看作C+的第二大特征,在我们茫然不知所措时,这确实是一个很有用的工具。一般情况下,我们并不需要知道一个类的确切类型,虚函数机制可以实现那种类型的正确行为。但是有些时候,我们有指向某个对象的基类指针,确定该对象的准确类型是很有用的。这些信息让我们更高效地完成一个特定情况下的操作,防止基类的接口变得很笨拙。大多数的类库用一些虚函数来提供运行时的类型信息。当异常处理功能加入到 C+时,它要求知道有关这个对象的准确类型信息。下一步是在语言中访问这些信息,这很容易实现。这一章解释RT T I是干什么的以及怎样使用它。另外,这一章还解释了 C+新的映射语法是什么,它和RT T I很相似。18.1 例子shape这里有一个使用多态性的类层次的例子。基类为 s h a p e,三个派生类分别为c i r c l e、s q u a r e和t r i a n g l e;下面是一个典型的类继承关系图,基类在上,派生类向下生长。面向对象程序设计的一般目标就是用代码体管理指向基类的指针,所以如果想增加一个新类来扩充程序(比如从 s h a p e中派生出r h o m b o i d),代码体部分并不受影响。在本例中,s h a p e接口部分的虚函数是 d r a w(),其目的就是让用户程序员通过一个s h a p e指针来调用d r a w(),d r a w()在所有的派生类中都被重新定义。由于它是一个虚函数,所以即使是用一个 s h a p e()型的指针来调用它,它仍然会被正确调用。因此,创建一个特定的对象(c i r c l e、s q u a r e或t r i a n g l e),取它的地址并把它映射到 s h a p e*(忘掉对象的实际类型),然后在程序的其他地方用这个匿名指针。继承关系图如上所示,所以这种从多个派生类到基类的映射叫做向上映射。18.2 什么是RTTI假如在编程中遇到了特殊的问题,而只要我们知道了一个一般指针的准确类型它就会迎刃而解,我们该怎么办?比如,假设允许我们的用户将任一形状变成紫色来表示加亮。用这种方法,他们可以发现屏幕上的所有三角形都被加亮。我们可能自然地想到用虚函数,像 Tu r n C o l o r I f Yo u A r e A(),它允许一些种类颜色的枚举型参数和s h a p e:c i r c l e、s h a p e:s q u a r e或s h a p e:t r i a n g l e参数。为了解决这种问题,多数类库设计者把虚函数放在基类中,使运行时返回特定对象的类型信息。我们可能见过一些名字为 i s A()和type Of()之类的成员函数,这些就是开发商定义的RT T I函数。使用这些函数,当处理一个对象列表时就可以说:“如果这个对象是t r i a n g l e类的,图 18-1就把它变成紫色。”当C+中引进异常处理时,它的实现要求把一些运行时间类型信息放在虚函数表中。这意味着只要对语言作一点小小的扩充,程序员就能获得有关一个对象的运行时间类型信息。所有的开发商都在自己的类库中加入了RT T I,所以它已包含在C+语言中。RT T I与异常一样,依赖驻留在虚函数表中的类型信息。如果试图在一个没有虚函数的类上用RT T I,就得不到预期的结果。RTTI的两种使用方法使用RT T I有两种不同的方法。第一种就像s i z e o f(),因为它看上就像一个函数。但实际上它是由编译器实现的。t y p e i d()带有一个参数,它可以是一个对象引用或指针,返回全局t y p e i n f o类的常量对象的一个引用。可以用运算符“=”和“!=”来互相比较这些对象。也可以用n a m e()来获得类型的名称。注意,如果给t y p e i d()传递一个s h a p e*型参数,它会认为类型为s h a p e*,所以如果想知道一个指针所指对象的精确类型,我们必须逆向引用这个指针。比如,s是个s h a p e*,cout typeid(*s).name()endl;将显示出s所指向的对象类型。也可以用b e f o r e(t y p e i n f o&)查询一个t y p e i n f o对象是否在另一个t y p e i n f o对象的前面(以定义实现的排列顺序),它将返回t r u e或f a l s e。如果写:if(typeid(me).before(typeid(you)/.那么表示我们正在查询m e在排列顺序中是否在y o u之前。RT T I的第二个用法叫“安全类型向下映射”。之所以用“向下映射”这个词也是由于类继承的排列顺序。如果映射一个 c i r c l e*到s h a p e*叫向上映射的话,那么将一个 s h a p e*映射成一个c i r c l e*就叫向下映射了。当然一个 c i r c l e*也是一个s h a p e*,编译器允许任意的向上映射,但一个s h a p e*不一定就是c i r c l e*,所以编译器在没有明确的类型映射时并不允许我们完成一个向下映射任务。当然可以用原有的C风格的类型映射或C+的静态映射(s t a t i c _ c a s t,将在本章末介绍)来强制执行,这等于在说:“我希望它实际上是一个c i r c l e*,而且我打算要求它是。”由于并没有明确地知道它实际上是 c i r c l e,因此这样做是很危险的。在开发商制定的 RT T I中一般的方法是:创建一个函数来试着将s h a p e*指派为一个c i r c l e*(在本例中),检查执行过程中的数据类型。如果这个函数返回一个地址,则成功;如果返回n u l l,说明我们并没有一个c i r c l e*对象。C+的RT T I的“安全类型向下映射”就是按照这种“试探映射”函数的格式,但它(非常合理地)用模板语法来产生这个特殊的动态映射函数(d y n a m i c _ c a s t),所以本例变成:动态映射的模板参数是我们想要该函数创建的数据类型,也就是这个函数的返回值。函数参数是我们试图映射的源数据类型。通常只要对一种类型作这种工作(比如将三角型变成紫色),但如果想算出各种s h a p e的数目,可以用面下例子的框架:当然这是人为的我们可能已经在各个类型中放了一个静态数据成员并在构造函数中对第18章 运行时类型识别383下载它自增。如果可以控制类的源代码并可以修改它,当然可以这样做。下面这个例子用来计算s h a p e的个数,它用了静态数据成员和动态映射两种方法:384C+编程思想下载第18章 运行时类型识别385下载对于这个例子,两种方法都是可行的,但静态数据成员方法只能用于我们拥有源代码并已安装了静态数据成员和成员函数时(或者开发商已为我们提供了这些),另外RT T I可能在不同的类中用法不同。18.3 语法细节本节详细介绍RT T I的两种形式是如何运行的以及两者之间的不同。18.3.1 对于内部类型的typeid()为了保持一致性,t y p e i d()也可以运用于内部类型,所以下面的表达式结果为t r u e:18.3.2 产生合适的类型名字t y p e i d()必须在所有的状况下都可以运行,比方说,下面的类中包含了一个嵌套类:386C+编程思想下载t y p e i n f o:n a m e()成员函数还是提供了适当的类名,其结果为o n e:n e s t e d。18.3.3 非多态类型虽然typeid()可以运用于非多态类型(基类中没有虚函数),但我们用这种方法获得的信息是值得怀疑的。假设类层次如下:如果创建一个派生类对象并向上映射它:t y p e i d()运算符将返回一个结果,但可能不是我们想要的。因为没有非多态机制,所以可以使用静态类型信息:一般希望RT T I用于多态类。18.3.4 映射到中间级动态映射不仅可用来确定准确的类型,也可用于多层次继承关系中的中间类型。如下例:第18章 运行时类型识别387下载由于多重继承,问题变得更复杂了。如果我们创建了一个 m i 2并将向上映射到根类(在这种情况下,从两种可能的根类中选出一种),然后成功地动态映射回派生类m i或m i 2。我们甚至可以从一个根类映射到另一个:这可以成功地映射,因为D 2实际上指向一个m i 2对象,它包含了类型d 1的一个子对象。映射到中间级在d y n a m i c _ c a s t与t y p e i d()之间产生了一个有趣的差异。t y p e i d()总是产生一个typeinfo 对象的引用来描述一个对象的准确类型,因此它不会给出中间层次的信息。在下面的表达式中(它的值是t r u e),t y p e i d()并不像d y n a m i c _ c a s t那样把D 2看作一个指向派生类的指针:D 2的类型就是指针的准确类型:18.3.5 void指针运行时类型的识别对一个v o i d型指针不起作用:v o i d*确实意味着“根本没有类型信息”。18.3.6 用模板来使用RTTI模板产生许多不同类的名字,而有时希望指出有关下面使用的对象是哪个类的。RT T I提供388C+编程思想下载了一个实现此功能的方便方法。下面的例子修改了第 1 3章的代码,它没有用预处理宏显示了构造函数和析构函数调用的顺序。T Y P E I N F O.H头文件必须被包含,以便于调用t y p e i d()返回的t y p e i n f o对象的任何成员函数。这个模板用了一个整型常量来区分两个类,但用类参数也行。在构造函数与析构函数的内部,RT T I的信息用来产生类的名字并显示,类X既用了继承又用了组合来创建一个类,这里构造函数与析构函数调用的顺序很有趣。这种技术有助于我们理解C+语言是如何工作的。18.4 引用RT T I必须能与引用一起工作。指针与引用存在明显不同,因为引用总是由编译器逆向引用,而一个指针的类型或它指向的类型可能要检测。请看下例:第18章 运行时类型识别389下载t y p e i d()看到的指针类型是基类而不是派生类,而它看到的引用类型则是派生类:与此相反,指针指向的类型在 t y p e i d()看来是派生类而不是基类,而用一个引用的地址时产生的是基类而不是派生类。表达式也可以用t y p e i d()运算符,因为它们也有一个类型:异常对一个引用完成了一个动态映射,其结果还必须被指定到一个引用上。但如果映射失败则会产生什么呢?因为不能有空的引用,所以这里是抛出一个异常的合适地方。在标准 C+中异常类型为b a d-c a s t,但在下面的例子中,用一个处理块来捕获所有异常:失败的原因当然是因为D 1实际上并不指向一个X对象,如果这里没有抛出一个异常,x r就会没有边界,所有被创建的对象或引用的安全保障都可能被打破。如果在调用t y p e i d()时试图去除一个空指针的引用,也会引起一个异常,在标准 C+中,这个异常叫b a d-t y p e i d:这里可以在t y p e i d操作之前检查指针是否为空来避免异常的产生(不像上面的那个引用的例子),这是最好的方法。18.5 多重继承当然,RT T I机制必须适用于任何复杂的多重继承,包括v i r t u a l基类:390C+编程思想下载即使只提供一个v i r t u a l基类指针,t y p e i d()也能准确地检测出实际对象的名字。用动态映射同样也会工作得很好,但编译器将不允许我们试图用原来的方法强制映射:编译器知道这不可能正确,所以它要求我们用动态映射。18.6 合理使用RTTI因为RT T I可以让我们用一个匿名的多态指针来发现类型信息,所以它常常被初学者滥用,因为它可能在虚函数完成之前就有意义了。对于许多来自过程编程背景的人来说,要他们不把程序组织成为一组 s w i t c h语句是非常困难的。他们可能会用 RT T I完成这些,但这样会在代码开发维护阶段丢失多态性的非常重要的价值。C+的意图是:尽可能地使用虚函数,必要时才使用RT T I。当然,要想以我们所想的那样使用虚函数,我们必须控制基类的定义,因为随着程序的不断扩大,有时我们可能发现基类并没有我们想要的虚函数,如果基类来自类库或其他由别人控制的来源,就可以用RT T I作为一种解决办法:我们可以继承一个新类并加上我们的成员函数。在代码的其他地方我们可以检测到我们的新增类型和调用的那个成员函数。这不会破坏多态性和程序逻辑的可扩展性,因为加一个新类并不要求我们寻找 s w i t c h语句。当然如果在主程序中增加新的代码时用到了这个新类,我们就必须检测这个特定类型。把一个特征放在一个基类中可能意味着为了某个特定类的利益,所有从该类派生出的类都保留了一些无意义的虚函数的残留。这使得接口变得不清晰,使那些必须重新定义纯虚函数的人当他们从这个类派生新类时感到很不方便。比方说,假设在第1 4章(1 4.6)节的W I N D S.C P P程序中,我们想清除管弦乐队中所有乐器的无用值。一种方法是在基类 i n s t r u m e n t第18章 运行时类型识别391下载中放一个虚函数C l e a r S p i t v a l v e(),但这就会引起混乱,因为它暗示p e r c u s s i o n和e l e c t r o n i c乐器也有无用值。RT T I提供了一个更合理的方法,因为可以把函数放在一个合适的特定类中(这里是w i n d)。最后,RT T I有时可以解决效率问题。如果代码用一种好的方法来用多态机制,但结果是这种通用代码对某个对象起反作用,使其运行效率低下。我们可以用RT T I将这种类型找出来,并写出针对特定情况的代码以提高效率。回顾垃圾再生器例子下面是第1 5章垃圾再生例子(trash recycling)的一个相似的版本,这里我们没有在类层次中建立类信息,而是采用了RT T I:392C+编程思想下载第18章 运行时类型识别393下载这个问题的本质是这些垃圾被扔进了一个没有分类的单一的垃圾箱中,所以特定的类型信息被丢失了。但之后特定类型信息必须恢复以便对垃圾准确分类,所以 RT T I被用上了。在第1 5章中,一个RT T I系统插入到类的继承关系中,但正如我们在这里看到的那样,用 C+预定义的RT T I更方便。18.7 RTTI的机制及花费典型的RT T I是通过在V TA B L E中放一个额外的指针来实现的。这个指针指向一个描述该特定类型的t y p e i n f o结构(每个新类只产生一个t y p e i n f o的实例),所以t y p e i d()表达式的作用实际上很简单。V P T R用来取t y p e i n f o的指针,然后产生一个结果 t y p e i n f o结构的一个引用这是一个决定性的步骤我们已经知道它要花多少时间。对于d y n a m i c _ c a s t ,多数情况下是很容易的,先恢复源指针的 RT T I信息再取出目标*的类型RT T I信息,然后调用库中的一个例程判断源指针是否与目标*相同或者是目标*类型的基类。它可能对返回的指针做了一点小的改动,因为目的指针类可能存在多重继承的情况,而源指针类型并不是派生类的第一个基类。在多重继承时情况会变得复杂些,因为394C+编程思想下载一个基类在继承层次中可能出现一次以上,并且可能有虚基类。用于动态映射的库例程必须检查一串长长的基类列表,所以动态映射的开销比 t y p e i d()要大(当然我们得到的信息也不同,这对于我们的问题来说可能很关键),并且这是非确定性的,因为查找一个基类要比查找一个派生类花更多的时间。另外动态映射允许我们比较任何类型,不限于在同一个继承层次中比较两个类。这使得动态映射调用的库例程开销更高了。18.8 创建我们自己的RTTI如果编译器还不支持RT T I,可以在类库中很容易地建立自己的RT T I。这是很有意义的事情,因为在人们发现所有的类库实际上都有要用到某种形式的 RT T I之后才在C+引入RT T I。(在异常处理被加入到C+后,人感觉“自由”一些了,因为异常处理要求有关类的准确信息)。从本质上说,RT T I只要两个函数就行了,一个用来指明类的准确类型的虚函数,一个取得基类的指针并将它向下映射成派生类,这个函数必须产生一个指向更加派生类的指针(我们可能希望也能处理引用)。有许多方法来实现我们自己的RT T I,但都要求每个类有一个唯一的标识符和一个能产生类型信息的虚函数。下例用了一个叫 d y n a c a s t()的静态成员函数,它调用一个类型信息函数d y n a m i c _ t y p e(),这两个函数都必须在每个新派生类中重新定义:第18章 运行时类型识别395下载396C+编程思想下载每个子类必须创建它自己的t y p e I D,重新定义虚函数d y n a m i c _ t y p e()来返回这个t y p e I D,并定义一个静态成员调用 d y n a c a s t(),它用一个基类指针作为参数(或继承层次中任意层上的一个指针在这种情况下,指针被简单地向上映射)。在从s e c u r i t y派生出的类中,可以看到每个类都定义了自己的 t y p e I D,并加到b a s e I D中。b a s e I D可以从派生类中直接访问,这一点是关键所在,因为e n u m必须在编译时计算出值的大小,所以采用内联函数的方法来读一个私有数据成员的方法不会成功。这是一个需要 p r o t e c t e d成员的一个典型事例。enum baseID为所有从s e c u r i t y派生出的类建立了一个基本的标识符,这样如果一个 I D值与已有I D值发生冲突,可能改变一下基值就可以改变所有的I D值(因为这个例子中并不比较不同的继承树,所以不可能发生 I D冲突)。在所有的类中,类的I D值都是p r o t e c t e d,所以它可以被派生类访问,但终端用户则不能访问它们。这个例子说明了创建RT T I需要处理哪些事情。我们不仅要确定对象的准确类型,还要能判断这个类是不是从我们要找的类中派生出来的。比如:m e t a l是从c o m m o d i t y派生出来的,c o m m o d i t y有一个叫s p e c i a l()的函数,所以如果有一个m e t a l类的对象,就可以调用s p e c i a l()。如果d y n a m i c _ t y p e()只告诉我们这个对象的准确类型,当我们问它一个m e t a l对象是否是c o m m o d i t y对象时,它会说“不是”,而这实际上是不正确的。所以 RT T I还必须能合理地在继承层次中将一种类型映射到某一中间类型上和准确类型上。d y n a c a s t()函数通过调用虚函数d y n a m i c _ t y p e()来确定类型信息。这个函数用一个我们正试图映射到的类的 t y p e I D为参数。它是一个虚函数,所以函数体在对象的准确类型中。每个d y n a m i c _ t y p e()函数首先检查传入的 t y p e I D是否与自己的类型匹配,它检查是否与基类匹配,第18章 运行时类型识别397下载这只要调用基类的d y n a m i c _ t y p e()函数就行了。就像一个循环函数调用,每个 d y n a m i c _ t y p e()都检查传入的参数是否与自己的 I D值相等,如不匹配,它调用基类的 d y n a m i c _ t y p e(),并将它结果返回。当一直找到继承树的根部时,它将返回零,表示没有匹配的类。如果d y n a m i c _ t y p e()返回1(t r u e),则指针指向的对象要么就是我们要找的类型,要么是这个类的派生类,然后d y n a c a s t()用s e c u r i t y指针作参数,并把它映射成想要的类型,如果返回值是f a l s e,,d y n a c a s t()返回零,表示映射不成功,用这种方法使它看上去就像C+中的d y n a m i c _ c a s t运算符一样。C+的动态映射运算符比上面的例子多一项功能:它可以比较两个继承层次中的类型,这两个继承层次可以是完全分开的。这就增加了系统的通用性,使它适用于跨层次体系的类型比较,当然这也增加了系统的复杂性。现在我们很容易想像出怎样创建一个使用上面方案并允许更容易转换成内置 d y n a m i c _ c a s t运算符的D Y N A M I C-C A S T宏来。18.9 新的映射语法无论什么时候用类型映射,都是在打破类型系统 1,这实际上是在告诉编译器,即使知道一个对象的确切类型,还是可以假定认为它是另外一种类型。这本身就是一种很危险的事情,也是一个容易发生错误的地方。不幸的是,每一种类型映射都是不同的:它是用括号括起来的目标类型的名字。所以如果我们的一段代码不能正确工作,而我们知道应该检查所有的类型映射看它们是否是产生错误的原因。我们怎么保证可以找出所有的类型映射呢?在一个 C程序中无法做到这一点。因为 C编译器并不总是要求类型映射(它可以用一个 v o i d指针指向不同的类型而不必强迫使用映射),而映射表现不同,所以我们不知道我们是不是已经找出所有的映射了。为了解决这个问题,C+用保留字 d y n a m i c _ c a s t(本章第一部分的主题)、c o n s t _ c a s t、s t a t i c _ c a s t和r e i n t e r p r e t _ c a s t来提供了一个统一的类型映射语法。当需要进行动态映射时,这就提供了一个解决问题的可能。这意味着那些已有的映射语法已经被重载得太多了,不能再支持任何其他的功能了。通过使用这些映射来代替原有的(n e w t y p e)语法,我们可以在任何程序中很容易地找出所有的映射。为了支持已有的代码,大多数编译器都可以产生不同级别的错误或警告,并可由用户对错误或警告产生选择打开或关闭。如果把新类型映射的全部错误打开的话,就可以确保我们找出项目中所有的类型映射,这使得查找错误变得很容易。下表指出了四个不同形式的映射的含义:s t a t i c _ c a s t为了“行为良好”和“行为较好”而使用的映射,包括一些我们可能现在不用的映射(如向上映射和自动类型转换)c o n s t _ c a s t用于映射常量和变量(c o n s t和v o l a t i l e)d y n a m i c _ c a s t为了安全类型的向下映射(本章前面已经介绍)r e i n t e r p r e t _ c a s t为了映射到一个完全不同的意思。这个关键词在我们需要把类型映射回原有类型时要用到它。我们映射到的类型仅仅是为了故弄玄虚和其他目的。这是所有映射中最危险的三个新映射将在后面小节中完整地介绍。398C+编程思想下载1 参看Jose Lajoie“The new cast notation and the bool data type”,C+报告,1 9 9 4年9月,p p.4 6-5 1.18.9.1 static_casts t a t i c _ c a s t可以用于所有良定义转换。这些包括“安全”转换与次安全转换,“安全”转换是编译器允许我们不用映射就能完成的转换。次安全转换也是良定义的。由 s t a t i c _ c a s t覆盖的变换的类型包括典型的无映射变换、窄化变换(丢失信息)、用v o i d*的强制变换、隐式类型变换和类层次的静态导航。第18章 运行时类型识别399下载在第(1)段中,我们看到了在C语言中使用过的各种类型的转换,用映射的或不用映射的。从一个i n t到一个l o n g或f l o a t当然不成问题,因为后者可以存放 i n t包含的全部值,可以用一个s t a t i c _ c a s t来使这些转换变得醒目一些,虽然并不是必须要这样做。在第(2)段中,我们可以看到转换回原有类型的方法。这里可能丢失部分数据,因为一个i n t没有一个l o n g或f l o a t那么“宽”。因此这些被称为“窄化转换”,编译器仍可以完成这些转换,但会给我们一个警告信息。可以去掉这些警告,并用一个映射指出我们确实需要映射。在C+中,从v o i d*向外赋值是不允许的(这与C中不同),请看第(3)段。这样做是很危险的,它要求程序员清楚地知道他正在干什么。当我们查找错误时,s t a t i c _ c a s t比老式的标准映射更容易定位。第(4)段显示了几种隐式类型转换,这些通常是由编译器自动完成的。它们是自动的和不要求映射的,用s t a t i c _ c a s t会使它变得醒目,以便于我们以后需要查找它或明确它们的含义。如果在一个类层次中没有虚函数或者如果我们有其他允许我们安全地向下映射的信息,则用静态向下映射比 d y n a m i c _ c a s t稍微快一些,就像在第(5)段中显示的那样。另外,s t a t i c _ c a s t不允许我们映射到类层次之外,就像传统的映射一样,所以它更安全。然而静态地导航类层次总是冒险的,因此我们应该用d y n a m i c _ c a s t,除非特殊情况。18.9.2 const_cast如果想把一个 const 转换为非c o n s t,或把一个 v o l a t i l e转换成一个非 v o l a t i l e,就要用到c o n s t _ c a s t。这是可以用c o n s t _ c a s t的唯一转换。如果还有其他的转换牵涉进来,它必须分开来指定,否则会有一个编译错误。400C+编程思想下载如果用了一个c o n s t对象的地址创建一个指针指向一个 c o n s t,在没有一个映射时不能把它赋给一个非c o n s t的指针中。老式风格的映射可以完成这个,但使用 c o n s t _ c a s t更合适。这同样适用于v o l a t i l e。如果想在一个c o n s t成员函数内部改变一个类成员,传统的方法就是用(X*)t h i s映射掉常量性质。现在也可以使用较好的 c o n s t _ c a s t来映射掉常量性质,但一个更好的方法是使那些特殊的数据成员成为m u t a b l e,这样在类定义时它更清楚,不会在成员函数定义中被隐藏掉,而且这些成员可以在c o n s t成员函数中改变。18.9.3 reinterpret_cast这是一种不太安全的类型映射机制,也是最容易引起错误的一种。一般情况下,编译器都包含了一组开关,允许我们强制使用 c o n s t _ c a s t和r e i n t e r p r e t _ c a s t,它们可以定位那些不安全的类型映射。r e i n t e r p r e t _ c a s t假设一个对象仅仅是一个比特模式,它可以被当作完全不同的对象对待(为了某种模糊的目的)。这是低层处理,在C中已经很不好了。实际上在我们用它做别的事情之前,总是要用r e i n t e r p r e t _ c a s t将其映射回原来的类型。第18章 运行时类型识别401下载类X包含一些数据和一个虚函数,在m a i n()中,一个X的对象被打印出以显示出它已被初始化为零了,然后它的地址用r e i n t e r p r e t _ c a s t映射为一个i n t*,假设它是一个i n t*,这个对象被索引成像一个数组,并且成员1被置为4 7(理论上),但在这里输出结果1 却是:0 0 0 0 047 0 0 0 0很明显,认为对象的第一个数据存放在对象的起始地址处这一假定是不安全的。事实上,这个编译器把V P T R放在对象的开始处,所以如果用x p 0 而不是用x p 1 ,就会使V P T R变得毫无价值。为了更正这个错误,可以用对象的大小减去数据成员的大小算出 V P T R的大小,然后对象的地址被映射为一个 l o n g型(用r e i n t e r p r e t _ c a s t)。假定V P T R是放在对象的开始处的,这样,实际数据的开始地址就被计算出来了。结果数字映被射回 i n t*,现在索引值可以产生想要的结果了:0 47 0 0 0402C+编程思想下载1 对于特定的编译器,结果可能不同。当然这种方法不值得推荐,而且可移植性差。这是一个 r e i n t e r p r e t _ c a s t指示器能做的事情之一,但当我们决定我们必须要用它时,它是可用的。18.10 小结RT T I是一个很方便的额外特征,就像蛋糕上加了一层糖衣。虽然一般都是把一个指针向上映射为一个基类指针,然后使用基类的接口(通过虚函数),但是偶尔需要知道一个基类指针指向的对象的确切类型来提高程序的效率,这时如果我们束手无策,就可使用 RT T I。因为基于虚函数的RT T I已经出现在几乎所有的类库中,所以这是一个非常有用的特征,因为它意味着:1)我们并不需要把它建在其他类库中。2)我们不用担心它是否将建在其他库中。3)在继承过程中我们不需要有额外的编程花费用来管理RT T I配置。4)语法是一致的,我们不需要为每个新库来重新配置。因为RT T I使用很方便,像C+的多数特征一样,所以它可能被滥用,包括那些天真的或有决心的程序员。最常见的滥用可能来自那些不理解虚函数的程序员,他们用 RT T I去做类型检查编码。C+的哲学似乎是提供强有力的工具并维护类型的完整和防止类型的违规,但我们如果有意滥用某一个特征的话,没有什么可以阻止我们。有时走点小弯路是获取经验的最快途径。新的类型映射语法在调试阶段对我们有很大的帮助,因为这种类型映射在我们的类型系统中开了一个小洞,并允许错误流进去。而这种新的语法使我们更容易定位这些错误入口通道。18.11 练习1.用RT T I帮助程序调试,即打印一个使用了 t y p e i d()的模板的确切名称。用不同的类型将这个模板实例化,然后看看结果是什么。2.用RT T I实现本章前面讲的Tu r n C o l o r I f Yo u A r e A()函数。3.将第1 4章的W I N D 5.C P P拷贝到一个新的位置,然后修改其中的 i n s t r u m e n t的层次。在w i n d类中加一个虚函数C l e a r S p i t Va l v e()并在所有的w i n d的派生类中重新定义它。让 t s t a s h的一个实例拥有一些i n s t r u m e n t指针,把用n e w创建的各类i n s t r u m e n t对象赋给它们。现在用RT T I巡视这个包容器,在所有的对象中找类w i n d或它的派生类的对象,为这些对象调用C l e a r S p i t Va l v e()函数。注意,如果i n s t r u m e n t基类中已经包含了C l e a r S p i t Va l v e()函数,它可能会引起混乱。第18章 运行时类型识别403下载