第16章 多重继承
d1和d2都包含base的子对象,所以mi包含基的两个子对象。从继承图形状上看,有时该继承层次结构称为“菱形”。没有菱形情况时,多重继承相当简单,但是只要菱形一出现,由于新类中存在重叠的子对象,麻烦就开始了。重叠的子对象增加了存储空间,这种额外开销是否成为一个问题取决于我们的设计,但它同时又引入了二义性。
首先,由于d1和d2分别对vf ()定义这会导致一个冲突,所以不能生成m i类。其次,在对b [ ]的数组定义中试图创建一个new mi并将类型转化为base*,由于没有办法搞清我们打算使用d 1子对象的base还是d2子对象的base作为结果地址,所以编译器将不会受理。
为了解决第一个问题,必须对类mi中的函数vf()进行重新定义以消除二义性。对于第二个问题的解决应着眼于语言扩展,这就是对virtual赋予新的含义。假若以virtual的方式继承一个基类,则仅仅会出现一个基类子对象。虚基类由编译器的指针法术( pointer magic)来实现,该方法使人想起普通虚函数的实现。
d1和d2在它们的构造函数中都必须对base初始化,这和我们想象的一样。但mi和x也都如此,尽管它们和base相隔好几个层次。这是因为每一个都能成为最晚辈派生类。编译器是无法知道使用d1初始化base还是使用d2的,因而我们总是被迫在最晚辈派生类中具体指出。注意,仅仅这个被选中的虚基构造函数被调用。
第17章异常处理(讨论在MoreEff)
一、C语言的出错处理
1) 出错信息可通过函数的返回值获得。如果函数返回值不能用,则可设置一全局错误判断标志(标准C语言中errno( )和perror( )函数支持这一方法)。由于对每个函数调用都进行错误检查,这十分繁琐并增加了程序的混乱度。程序设计者可能简单地忽略这些出错信息,因为乏味而迷乱的错误检查必须随着每个函数调用而出现。另外,来自偶然出现异常的函数的返回值可能并不反映什么问题。
2) 可使用C标准库中一般不太熟悉的信号处理系统,利用signal()函数(判断事件发生的类型)和raise()函数(产生事件)。由于信号产生库的使用者必须理解和安装合适的信号处理系统,所以应紧密结合各信号产生库,但对于大型项目,不同库之间的信号可能会产生冲突。
3) 使用C标准库中非局部的跳转函数:setjmp( ) 和longjmp( )。setjmp( ) 函数可在程序中存储一典型的正常状态,如果进入错误状态, longjmp( )可恢复setjmp( ) 函数的设定状态,并且状态被恢复时的存储地点与错误的发生地点紧密联系。#include <iostream.h>
#include <setjmp.h>
jmp_buf kkk;
void o(void)
{
cout<<"oooo"<<endl;
longjmp(kkk,100);
}
void main()
{
cout<<"main"<<endl;
if (setjmp(kkk)==0)
{ cout<<"setjmp()"<<endl;
o(); }
else
{ cout<<"else"<<endl; }
}
二、抛出异常
关键字throw的引入引起了一系列重要的相关事件发生。首先是throw调用构造函数创建一个原执行程序中并不存在的对象。 其次,实际上这个对象正是throw函数的返回值,即使这个对象的类型不是函数设计的正常返回类型。
三、异常规格说明
异常规格说明再次使用了关键字throw,函数的所有潜在异常类型均随着关键字throw而插入函数说明中。所以函数说明可以带有异常说明如下:
void f ( ) throw ( toobig, toosmall, divzero);
而传统函数声明:
void f ( );
意味着函数可能抛出任何一种异常。
如果是:
void f ( ) throw ( );
这意味着函数不会有异常抛出。
为了得到好的程序方案和文件,为了方便函数调用者,每当写一个有异常抛出的函数时都应当加入异常规格说明。
1. unexpected( )
如果函数实际抛出的异常类型与我们的异常规格说明不一致,这时会调用特殊函数unexpected( )。
2. set_unexpected( )
unexpected( )是使用指向函数的指针而实现的,所以我们可通过改变指针的指向地址来改变相对应的运算。这些可通过类似于set_new_handler( ) 的函数set_unexpected( ) 来实现,set_unexpected( )函数可获取不带输入和输出参数的函数地址和void返回值。它还返回unexpected指针的先前值,这样我们可存储unexpected( )函数的原先指针值,并在后面恢复它。为了使用set_unexpected( )函数,我们必须包含头文件EXCEPT.H。虽然new_handler( )函数的指针可为空,但unexpected( )函数的指针却不能为空。它的缺省值指向terminate( )(后面将会介绍)函数,但是,只要我们使用异常抛出和异常规格说明,我们就应该编写自己的unexpected( )函数,用于记录或者再次抛出异常及抛出新的异常或终止程序运行。
假如任意层的处理器都没有捕获到这个异常,那么这个异常就是“未捕获的”或“未处理的”。如果已存在的异常在被捕获之前又有一个新的异常产生将造成异常不能被获取,最常见的这种情况的产生原因是异常对象的构造函数自身会导致新的异常。
1. terminate( )
如果异常未能被捕获,特殊函数terminate( )将自动被调用。如同函数unexception( )终止函数一样,它实际上也是一个指向函数的指针。在C标准库中它的缺省值为指向函数abort( )的指针,abort( )函数可以不用调用正常的终止函数而直接从程序中退出(这意味着静态全局函数的析构函数不用被调)。如果一个异常未被捕获,析构函数不会被调用,则异常对象将不会被清除。含有未捕获的异常将被认为是程序错误。我们可将程序(如果有必要,包括main( )的所有代码)封装在一个测试块中,这个测试块由各异常处理器按序组成,并可以捕获任意异常的缺省处理器(catch(. . .))结束。如果我们不将程序按上述方法封装,将使我们的程序十分臃肿。一个未能被捕获的异常可看成是一个程序错误。
2. set_terminate( )
我们可以使用标准函数set_terminate()来安装自己的终止函数terminate(),set_ terminate()返回被替代的terminate()函数的指针,这样就可存贮该指针并在需要时进行恢复。定做的终止函数terminate()必须不含有输入参,其返回值为void。另外所安装的任何终止处理器terminate( )必须不返回或抛出异常,但是作为替换将调用一些程序终止函数。在实际中如果函数terminate( )被调用就意味着问题将无法被恢复。如同函数unexpected( )一样,函数terminate( )的指针不能为零。
四、构造函数的异常处理情况
如果构造函数中出现异常,这将产生问题:如果异常抛出发生在构造函数创建对象时,对象的析构函数将无法调用其相应的对象。这意味着在编写构造函数的程序时必须十分谨慎。构造函数进行存储资源分配时存在普遍的困难。如果构造函数在运行时有异常抛出,析构函数将无法收回这些存储资源。这些问题大多数发生在未加保护的指针上。
五、标准异常 exception是所有标准C++库异常的基类。我们可以调用what()以获得其特性的显示说明
logic_erro是由exception派生的。它报告程序的逻辑错误,这些错误在程序执行前可以被检测到
runtime_error是由exception派生的它报告程序运行时错误,这些错误仅在程序运行时可以被检测到
由l o g i c _ e r r o r派生的异常 domain_error 报告违反了前置条件
invalid_argument 指出函数的一个无效参数
length_error 指出有一个产生超过NPOS长度的对象的企图(NPOS:类型size_t的最大可表现)
out_of_range 报告参数越界
bad_cast 在运行时类型识别中有一个无效的dynamic_cast表达式
bad_typeid 报告在表达式typeid( * p )中有一个空指针P(运行时类型识别的特性)
由r u n t i m e _ e r r o r派生的异常 range_error 报告违反了后置条件
overflow_error 报告一个算术溢出
bad-alloc 报告一个存储分配错误
六、异常的典型使用
使用异常便于:1) 使问题固定下来和重新调用这个(导致异常的)函数。2) 把事情修补好而继续运行,不去重试函数。3) 计算一些选择结果用于代替函数假定产生的结果。4) 在当前上下文环境尽其所能并且再把同样的异常弹向更高的上下文中。5) 在当前上下文环境尽其所能并且把一个不同的异常弹向更高的上下文中。6) 终止程序。7) 包装使用普通错误方案的函数(尤其是C的库函数),以便产生异常替代。8) 简化,假若我们的异常方案建造得过于复杂,使用时会令人懊恼。9) 使我们的库和程序更安全。这是短期投资(为了调试)和长期投资(为了应用的健壮性)问题。
1. 随时使用异常规格说明
异常的规格说明像一个函数原型:它告诉用户书写异常处理代码以及处理什么异常。它告诉编译器异常可能出现在这个函数中。当然,我们不能总是通过检查代码而预见什么异常会发生在特定的函数中。有时这个特定函数所调用的函数产生了一个出乎意料的异常,有时一个不会抛出异常的老函数被一个会抛出异常的新函数替换了,这样我们将产生对unexpect( )的调用。无论何时,只要使用异常规格说明或者调用含有异常的函数,都应该创建自己的unexpected( )函数,该函数记录信息而且重新抛出同样的异常。
2. 起始于标准异常
在创建我们自己的异常前应检查标准C++异常库。假若标准异常正合所需,则这样会使我们的用户更易于理解和处理。假若所需要的异常类型不是标准库的一部分,则尽量从某个已存在标准expection中派生形成。假若在expection的类接口中总是存在what( )函数的期望定义,这会使用户受益匪浅。
3. 套装我们自己的异常
如果为我们的特定类创建异常,在我们的类中套装异常类是一个很好的主意,这为读者提供了一个清晰的消息—这些异常仅为我们的类所使用。另外,它可防止命名域混乱。
4. 使用异常层次 异常层次为不同类型的重要错误的分类提供了一个有价值的方法,这些错误可能会与我们的类或库冲突。该层次可为用户提供有帮助的信息,帮助他们组织自己的代码,让他们可以选择是忽略所有异常的特定类型还是正确地捕获基类类型。而且在以后,任何异常可通过对相同基类的继承而追加,而不会被迫改写所有的已生成代码—基类处理器将捕获新的异常。当然,标准C + +异常是一个异常层次的优秀例子,通过使用可进一步增强和丰富它。
5. 多重继承
多重继承最必要做的地方就是需要把一个指向对象的指针向上映射到两个不同的基类,也就是需要两个基类的多态行为的地方。这样,异常层次对于多重继承是有用的,因为多重继承异常类的任一根的基类处理器都可处理异常。
6. 用“引用”而非“值”去捕获
如果抛出一个派生类对象而且该对象被基类的对象处理器通过值捕获到,对象会被“切片”,这就是说,随着向基类对象的传递,派生类元素会依次被割下,直到传递完成。这样的偶然性并不是所要的,因为对象的行为像基类而不象它本来就是的派生类对象(实际就是“切片”以前)。
当对象通过值被捕获时,因为它被转化成一个base对象(由构造函数完成),而且在所有的情况下表现出base对象的行为;然而当对象通过引用被捕获时,仅仅地址被传递而对象不会被切片,所以它的行为反映了它处于派生中的真实情况。虽然也可以抛出和捕获指针,但这样做会引入更多的耦合——抛出器和捕获器必须为怎样分配和清理异常对象而达成一致。这是一个问题,因为异常本身可能会由于堆的耗尽而产生。如果抛出异常对象,异常处理系统会关注所有的存储。
7. 在构造函数中抛出异常
由于构造函数没有返回值,因此在先前我们可以有两个选择以报告在构造期间的错误:
1) 设置一个非局部标志并且希望用户检查它。
2) 返回一个不完全被创建的对象并且希望用户检查它。
这是一个严重的问题,因为C程序员必须依赖一个隐含的保证:对象总是成功地被创建,这在类型如此粗糙的C中是不合理的。但是在C + +程序中,构造失败后继续执行是注定的灾难,于是构造函数成为抛出异常最重要的地方之一。现在有一个安全有效的方法去处理构造函数错误。然而我们还必须把注意力集中在对象内部的指针上和构造函数异常抛出时的清除方法上。
8. 不要在析构函数中导致异常
由于析构函数会在抛出其他异常时被调用,所以永远不要打算在析构函数中抛出一个异常,或者通过执行在析构函数中的相同动作导致其他异常的抛出。如果这些发生了,这意味着在已存在的异常到达引起捕获之前抛出了一个新的异常,这会导致对terminate( )的调用。这里的意思是:假若调用一个析构函数中的任何函数都有可能会抛出异常,这些调用应该写在析构函数中的一个try块中,而且析构函数必须自己处理所有自身的异常。这里的异常都不应逃离析构函数。
9. 避免无保护的指针
假若资源分配给无保护的指针,那么意味着在构造函数中存在一个缺点。由于该指针不拥有析构函数,所以当在构造函数中抛出异常时那些资源将不能被释放。
七、 开销
为了使用新特性必然有所开销。当异常被抛出时有相当的运行时间方面的开销,这就是从来不想把异常用于普通流控制的一部分的原因,而不管它多么令人心动。异常的发生应当是很少的,所以开销聚集在异常上而不是在普通的执行代码上。设计异常处理的重要目标之一是:在异常处理实现中,当异常不发生时应不影响运行速度。这就是说,只要不抛出异常,代码的运行速度如同没有加载异常处理时一样。无论与否,异常处理都依赖于使用的特定编译器。异常处理也会引出额外信息,这些信息被编译器置于栈上。除了能作为特定的“异常范围” (它可能恰恰是全局范围)的对象传进送出外,异常对象可以像其他对象一样被正确地在周围传递。当异常处理器工作完成时,异常对象也被相应地销毁。
一、C语言的出错处理
1) 出错信息可通过函数的返回值获得。如果函数返回值不能用,则可设置一全局错误判断标志(标准C语言中errno( )和perror( )函数支持这一方法)。由于对每个函数调用都进行错误检查,这十分繁琐并增加了程序的混乱度。程序设计者可能简单地忽略这些出错信息,因为乏味而迷乱的错误检查必须随着每个函数调用而出现。另外,来自偶然出现异常的函数的返回值可能并不反映什么问题。
2) 可使用C标准库中一般不太熟悉的信号处理系统,利用signal()函数(判断事件发生的类型)和raise()函数(产生事件)。由于信号产生库的使用者必须理解和安装合适的信号处理系统,所以应紧密结合各信号产生库,但对于大型项目,不同库之间的信号可能会产生冲突。
3) 使用C标准库中非局部的跳转函数:setjmp( ) 和longjmp( )。setjmp( ) 函数可在程序中存储一典型的正常状态,如果进入错误状态, longjmp( )可恢复setjmp( ) 函数的设定状态,并且状态被恢复时的存储地点与错误的发生地点紧密联系。#include <iostream.h>
#include <setjmp.h>
jmp_buf kkk;
void o(void)
{
cout<<"oooo"<<endl;
longjmp(kkk,100);
}
void main()
{
cout<<"main"<<endl;
if (setjmp(kkk)==0)
{ cout<<"setjmp()"<<endl;
o(); }
else
{ cout<<"else"<<endl; }
}
二、抛出异常
关键字throw的引入引起了一系列重要的相关事件发生。首先是throw调用构造函数创建一个原执行程序中并不存在的对象。 其次,实际上这个对象正是throw函数的返回值,即使这个对象的类型不是函数设计的正常返回类型。
三、异常规格说明
异常规格说明再次使用了关键字throw,函数的所有潜在异常类型均随着关键字throw而插入函数说明中。所以函数说明可以带有异常说明如下:
void f ( ) throw ( toobig, toosmall, divzero);
而传统函数声明:
void f ( );
意味着函数可能抛出任何一种异常。
如果是:
void f ( ) throw ( );
这意味着函数不会有异常抛出。
为了得到好的程序方案和文件,为了方便函数调用者,每当写一个有异常抛出的函数时都应当加入异常规格说明。
1. unexpected( )
如果函数实际抛出的异常类型与我们的异常规格说明不一致,这时会调用特殊函数unexpected( )。
2. set_unexpected( )
unexpected( )是使用指向函数的指针而实现的,所以我们可通过改变指针的指向地址来改变相对应的运算。这些可通过类似于set_new_handler( ) 的函数set_unexpected( ) 来实现,set_unexpected( )函数可获取不带输入和输出参数的函数地址和void返回值。它还返回unexpected指针的先前值,这样我们可存储unexpected( )函数的原先指针值,并在后面恢复它。为了使用set_unexpected( )函数,我们必须包含头文件EXCEPT.H。虽然new_handler( )函数的指针可为空,但unexpected( )函数的指针却不能为空。它的缺省值指向terminate( )(后面将会介绍)函数,但是,只要我们使用异常抛出和异常规格说明,我们就应该编写自己的unexpected( )函数,用于记录或者再次抛出异常及抛出新的异常或终止程序运行。
假如任意层的处理器都没有捕获到这个异常,那么这个异常就是“未捕获的”或“未处理的”。如果已存在的异常在被捕获之前又有一个新的异常产生将造成异常不能被获取,最常见的这种情况的产生原因是异常对象的构造函数自身会导致新的异常。
1. terminate( )
如果异常未能被捕获,特殊函数terminate( )将自动被调用。如同函数unexception( )终止函数一样,它实际上也是一个指向函数的指针。在C标准库中它的缺省值为指向函数abort( )的指针,abort( )函数可以不用调用正常的终止函数而直接从程序中退出(这意味着静态全局函数的析构函数不用被调)。如果一个异常未被捕获,析构函数不会被调用,则异常对象将不会被清除。含有未捕获的异常将被认为是程序错误。我们可将程序(如果有必要,包括main( )的所有代码)封装在一个测试块中,这个测试块由各异常处理器按序组成,并可以捕获任意异常的缺省处理器(catch(. . .))结束。如果我们不将程序按上述方法封装,将使我们的程序十分臃肿。一个未能被捕获的异常可看成是一个程序错误。
2. set_terminate( )
我们可以使用标准函数set_terminate()来安装自己的终止函数terminate(),set_ terminate()返回被替代的terminate()函数的指针,这样就可存贮该指针并在需要时进行恢复。定做的终止函数terminate()必须不含有输入参,其返回值为void。另外所安装的任何终止处理器terminate( )必须不返回或抛出异常,但是作为替换将调用一些程序终止函数。在实际中如果函数terminate( )被调用就意味着问题将无法被恢复。如同函数unexpected( )一样,函数terminate( )的指针不能为零。
四、构造函数的异常处理情况
如果构造函数中出现异常,这将产生问题:如果异常抛出发生在构造函数创建对象时,对象的析构函数将无法调用其相应的对象。这意味着在编写构造函数的程序时必须十分谨慎。构造函数进行存储资源分配时存在普遍的困难。如果构造函数在运行时有异常抛出,析构函数将无法收回这些存储资源。这些问题大多数发生在未加保护的指针上。
五、标准异常 exception是所有标准C++库异常的基类。我们可以调用what()以获得其特性的显示说明
logic_erro是由exception派生的。它报告程序的逻辑错误,这些错误在程序执行前可以被检测到
runtime_error是由exception派生的它报告程序运行时错误,这些错误仅在程序运行时可以被检测到
由l o g i c _ e r r o r派生的异常 domain_error 报告违反了前置条件
invalid_argument 指出函数的一个无效参数
length_error 指出有一个产生超过NPOS长度的对象的企图(NPOS:类型size_t的最大可表现)
out_of_range 报告参数越界
bad_cast 在运行时类型识别中有一个无效的dynamic_cast表达式
bad_typeid 报告在表达式typeid( * p )中有一个空指针P(运行时类型识别的特性)
由r u n t i m e _ e r r o r派生的异常 range_error 报告违反了后置条件
overflow_error 报告一个算术溢出
bad-alloc 报告一个存储分配错误
六、异常的典型使用
使用异常便于:1) 使问题固定下来和重新调用这个(导致异常的)函数。2) 把事情修补好而继续运行,不去重试函数。3) 计算一些选择结果用于代替函数假定产生的结果。4) 在当前上下文环境尽其所能并且再把同样的异常弹向更高的上下文中。5) 在当前上下文环境尽其所能并且把一个不同的异常弹向更高的上下文中。6) 终止程序。7) 包装使用普通错误方案的函数(尤其是C的库函数),以便产生异常替代。8) 简化,假若我们的异常方案建造得过于复杂,使用时会令人懊恼。9) 使我们的库和程序更安全。这是短期投资(为了调试)和长期投资(为了应用的健壮性)问题。
1. 随时使用异常规格说明
异常的规格说明像一个函数原型:它告诉用户书写异常处理代码以及处理什么异常。它告诉编译器异常可能出现在这个函数中。当然,我们不能总是通过检查代码而预见什么异常会发生在特定的函数中。有时这个特定函数所调用的函数产生了一个出乎意料的异常,有时一个不会抛出异常的老函数被一个会抛出异常的新函数替换了,这样我们将产生对unexpect( )的调用。无论何时,只要使用异常规格说明或者调用含有异常的函数,都应该创建自己的unexpected( )函数,该函数记录信息而且重新抛出同样的异常。
2. 起始于标准异常
在创建我们自己的异常前应检查标准C++异常库。假若标准异常正合所需,则这样会使我们的用户更易于理解和处理。假若所需要的异常类型不是标准库的一部分,则尽量从某个已存在标准expection中派生形成。假若在expection的类接口中总是存在what( )函数的期望定义,这会使用户受益匪浅。
3. 套装我们自己的异常
如果为我们的特定类创建异常,在我们的类中套装异常类是一个很好的主意,这为读者提供了一个清晰的消息—这些异常仅为我们的类所使用。另外,它可防止命名域混乱。
4. 使用异常层次 异常层次为不同类型的重要错误的分类提供了一个有价值的方法,这些错误可能会与我们的类或库冲突。该层次可为用户提供有帮助的信息,帮助他们组织自己的代码,让他们可以选择是忽略所有异常的特定类型还是正确地捕获基类类型。而且在以后,任何异常可通过对相同基类的继承而追加,而不会被迫改写所有的已生成代码—基类处理器将捕获新的异常。当然,标准C + +异常是一个异常层次的优秀例子,通过使用可进一步增强和丰富它。
5. 多重继承
多重继承最必要做的地方就是需要把一个指向对象的指针向上映射到两个不同的基类,也就是需要两个基类的多态行为的地方。这样,异常层次对于多重继承是有用的,因为多重继承异常类的任一根的基类处理器都可处理异常。
6. 用“引用”而非“值”去捕获
如果抛出一个派生类对象而且该对象被基类的对象处理器通过值捕获到,对象会被“切片”,这就是说,随着向基类对象的传递,派生类元素会依次被割下,直到传递完成。这样的偶然性并不是所要的,因为对象的行为像基类而不象它本来就是的派生类对象(实际就是“切片”以前)。
当对象通过值被捕获时,因为它被转化成一个base对象(由构造函数完成),而且在所有的情况下表现出base对象的行为;然而当对象通过引用被捕获时,仅仅地址被传递而对象不会被切片,所以它的行为反映了它处于派生中的真实情况。虽然也可以抛出和捕获指针,但这样做会引入更多的耦合——抛出器和捕获器必须为怎样分配和清理异常对象而达成一致。这是一个问题,因为异常本身可能会由于堆的耗尽而产生。如果抛出异常对象,异常处理系统会关注所有的存储。
7. 在构造函数中抛出异常
由于构造函数没有返回值,因此在先前我们可以有两个选择以报告在构造期间的错误:
1) 设置一个非局部标志并且希望用户检查它。
2) 返回一个不完全被创建的对象并且希望用户检查它。
这是一个严重的问题,因为C程序员必须依赖一个隐含的保证:对象总是成功地被创建,这在类型如此粗糙的C中是不合理的。但是在C + +程序中,构造失败后继续执行是注定的灾难,于是构造函数成为抛出异常最重要的地方之一。现在有一个安全有效的方法去处理构造函数错误。然而我们还必须把注意力集中在对象内部的指针上和构造函数异常抛出时的清除方法上。
8. 不要在析构函数中导致异常
由于析构函数会在抛出其他异常时被调用,所以永远不要打算在析构函数中抛出一个异常,或者通过执行在析构函数中的相同动作导致其他异常的抛出。如果这些发生了,这意味着在已存在的异常到达引起捕获之前抛出了一个新的异常,这会导致对terminate( )的调用。这里的意思是:假若调用一个析构函数中的任何函数都有可能会抛出异常,这些调用应该写在析构函数中的一个try块中,而且析构函数必须自己处理所有自身的异常。这里的异常都不应逃离析构函数。
9. 避免无保护的指针
假若资源分配给无保护的指针,那么意味着在构造函数中存在一个缺点。由于该指针不拥有析构函数,所以当在构造函数中抛出异常时那些资源将不能被释放。
七、 开销
为了使用新特性必然有所开销。当异常被抛出时有相当的运行时间方面的开销,这就是从来不想把异常用于普通流控制的一部分的原因,而不管它多么令人心动。异常的发生应当是很少的,所以开销聚集在异常上而不是在普通的执行代码上。设计异常处理的重要目标之一是:在异常处理实现中,当异常不发生时应不影响运行速度。这就是说,只要不抛出异常,代码的运行速度如同没有加载异常处理时一样。无论与否,异常处理都依赖于使用的特定编译器。异常处理也会引出额外信息,这些信息被编译器置于栈上。除了能作为特定的“异常范围” (它可能恰恰是全局范围)的对象传进送出外,异常对象可以像其他对象一样被正确地在周围传递。当异常处理器工作完成时,异常对象也被相应地销毁。
第18章运行时类型识别
当C++中引进异常处理时,它的实现要求把一些运行时间类型信息放在虚函数表中。这意味着只要对语言作一点小小的扩充,程序员就能获得有关一个对象的运行时间类型信息。所有的开发商都在自己的类库中加入了RTTI,所以它已包含在C++语言中。RTTI与异常一样,依赖驻留在虚函数表中的类型信息。如果试图在一个没有虚函数的类上用RTTI,就得不到预期的结果。
RTTI的两种使用方法
使用RTTI有两种不同的方法。第一种就像sizeof( ),因为它看上就像一个函数。但实际上它是由编译器实现的。typeid( )带有一个参数,它可以是一个对象引用或指针,返回全局typeinfo类的常量对象的一个引用。可以用运算符“==”和“!=”来互相比较这些对象。也可以用name( )来获得类型的名称。注意,如果给typeid( )传递一个shape*型参数,它会认为类型为shape*,所以如果想知道一个指针所指对象的精确类型,我们必须逆向引用这个指针。比如,s是个shape* ,
cout << typeid(*s).name()<<endl; 将显示出s所指向的对象类型。
RTTI的第二个用法叫“安全类型向下映射”。之所以用“向下映射”这个词也是由于类继承的排列顺序。如果映射一个circle*到shape*叫向上映射的话,那么将一个shape*映射成一个circle*就叫向下映射了。当然一个circle*也是一个shape*,编译器允许任意的向上映射,但一个shape*不一定就是circle*,所以编译器在没有明确的类型映射时并不允许我们完成一个向下映射任务。当然可以用原有的C风格的类型映射或C++的静态映射(static_cast)来强制执行,这等于在说:“我希望它实际上是一个circle*,而且我打算要求它是。”由于并没有明确地知道它实际上是circle,因此这样做是很危险的。在开发商制定的RTTI中一般的方法
是:创建一个函数来试着将shape*指派为一个circle*,检查执行过程中的数据类型。如果这个函数返回一个地址,则成功;如果返回null,说明我们并没有一个circle*对象.
C++的RTTI的“安全类型向下映射”就是按照这种“试探映射”函数的格式,但它(非常合理地)用模板语法来产生这个特殊的动态映射函数(dynamic_cast),所以本例变成:
shape *s=new circle;
circle *c=dynamic_cast<circle*> s;
if (c) then cout<<"succesful"<<endl;
动态映射的模板参数是我们想要该函数创建的数据类型,也就是这个函数的返回值。函数参数是我们试图映射的源数据类型。
典型的RTTI是通过在VTABLE中放一个额外的指针来实现的。这个指针指向一个描述该特定类型的typeinfo结构(每个新类只产生一个typeinfo的实例),所以typeid()表达式的作用实际上很简单。VPTR用来取typeinfo的指针,然后产生一个结果typeinfo结构的一个引用—这是一个决定性的步骤—我们已经知道它要花多少时间。对于dynamic_cast <目标* > <源指针>,多数情况下是很容易的,先恢复源指针的RTTI信息再取出目标*的类型RTTI信息,然后调用库中的一个例程判断源指针是否与目标*相同或者是目标*类型的基类。它可能对返回的指针做了一点小的改动,因为目的指针类可能存在多重继承的情况,而源指针类型并不是派生类的第一个基类。在多重继承时情况会变得复杂些,因为一个基类在继承层次中可能出现一次以上,并且可能有虚基类。