第15章 友元、异常和其他

本章内容包括:

  • 友元类
  • 友元类方法
  • 嵌套类
  • 引发异常,try块和catch块
  • 异常类
  • 运行阶段类型识别(RTTI)
  • dynamic_cast和typeid
  • static_cast,const_cast和reiterpret_cast

RTTI是一种确定对象类型的机制.新的类型转换运算符提高了类型转换的安全性.

15.1 友元

  • 也可以将类作为友元,在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员.
  • 尽管友元被授予从外部访问类的私有部分的权限,但它们并不与面向对象的编程思想相悖;相反,它们提高了公有接口的灵活性.

15.1.1 友元类

  • 电视机和遥控器的例子
  • 程序清单15.1 tv.h 
    • 友元声明可以位于公有,私有或保护部分,其所在的位置无关紧要.
  • 程序清单15.2 tv.cpp
  • 程序清单15.3 use_tv.cpp

15.1.2 友元成员函数

  • 程序清单15.4 tvfm.h
  • 必须小心排列各种声明和定义的顺序

15.1.3 其他友元关系 
15.1.4 共同的友元

  • 需要使用友元的另一种情况是,函数需要访问两个类的私有数据.

15.2 嵌套类

  • 对类进行嵌套与包含并不同.包含意味着将类作为另一个类的成员,而对类进行嵌套不创建类成员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效.
  • 对类进行嵌套通常是为了帮助实现另一个类,并避免名称冲突.

15.2.1 嵌套类和访问权限

  1. 作用域 
    • 如果嵌套类是在另一个类的私有部分声明的,则只有后者知道它.
    • 如果嵌套类是在另一个类的保护部分声明的,则它对于后者来说是可见的,但是对于外部世界则是不可见的.然而,在这种情况中,派生类将指导嵌套类,并可以直接创建这种类型的对象.
    • 如果嵌套类是在另一个类的公有部分声明的,则允许后者,后者的派生类以及外部世界使用它,因为它是公有的.然而,由于嵌套类的作用域为包含它的类,因此在外部世界使用它时,必须使用类限定符.
  2. 访问控制 
    • 对嵌套类访问权的控制规则与对常规类相同.

15.2.2 模板中的嵌套

  • 程序清单15.5 queuetp.h
  • 程序清单15.6 nested.cpp

15.3 异常

  • 异常是相对较新的C++功能,有些老式编译器可能没有实现.另外,有些编译器默认关闭这种特性,您可能需要使用编译器选项来启用它.

15.3.1 调用abort

  • 程序清单15.7 error1.cpp

15.3.2 返回错误码

  • 一种比异常终止更灵活的方法是,使用函数的返回值来指出问题.
  • 程序清单15.8 error2.cpp
  • 传统的C语言数学库使用的就是这种方法,它使用的全局变量名为errno.当然,必须确保其他函数没有将该全局变量用于其他目的.

15.3.3 异常机制

  • 异常提供了将控制权从程序的一个部分传递到另一部分的途径.
  • 对异常的处理有3个组成部分: 
    • 引发异常
    • 使用处理程序捕获异常
    • 使用try块
  • 程序使用异常处理程序exception handler来捕获异常,异常处理程序位于要处理问题的程序中.catch关键字表示捕获异常.
  • 程序清单15.9 error3.cpp
  • 执行throw语句类似于执行返回语句,因为它也将终止函数的执行;但throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数.执行完try块中的语句后,如果没有引发任何异常,则程序跳过try块后面的catch块,直接执行处理程序后面的第一条语句.
  • 如果函数引发了异常,而没有try块或没有匹配的处理程序时,将会发生什么情况.在默认情况下,程序最终将调用abort()函数,但可以修改这种行为.

15.3.4 将对象用作异常类型

  • 程序清单15.10 exc_mean.h
  • 程序清单15.11 error4.cpp

15.3.5 异常规范和C++11

  • 异常规范,这是C++98新增的一项功能,但C++11却将其摒弃了.这意味着C++11仍然处于标准之中,但以后可能会从标准中剔除,因此不建议您使用它.
  • 然而,C++11确实支持一种特殊的异常规范:您可使用新增的关键字noexcept指出函数不会引发异常;还有运算符noexcept(),它判断其操作数是否会引发异常.

15.3.6 栈解退

  • 假设try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数调到包含try块和处理程序的函数.C++通常通过将信息放在栈中来处理函数调用.
  • 引发机制的一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用.
  • 程序清单15.12 error5.cpp
  • 异常极其重要的一点:程序进行栈解退以回到能够捕获异常的地方时,将释放栈中的自动存储型变量.如果变量是类对象,将为该对象调用析构函数.

15.3.7 其他异常特性

  • 既然throw语句将生成副本,为何代码中使用引用呢?毕竟,将引用作为返回值的通常原因是避免创建副本以提高效率.答案是,引用还有另一个重要特征:基类引用可以执行派生类对象.假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配.
  • 提示:如果有一个异常类继承层次结构,应这样排列catch块:将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面.
  • 在catch语句中使用基类对象时,将捕获所有的派生类对象,但派生特性将被剥去,因此将使用虚方法的基类版本.

15.3.8 exception类

  • stdexcept异常类 
    • 异常类系列logic_error描述了典型的逻辑错误.
    • 异常invalid_argument指出给函数传递了一个意料外的值.
    • 异常length_error用于指出没有足够的空间来执行所需的操作.
    • 异常out_of_bounds通常用于指示索引错误.
    • runtime_error异常系列描述了可能在运行期间发生但难以预计和防范的错误.
  • bad_alloc异常和new

    • 对于使用new导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常.
    • 程序清单15.13 newexcp.cpp
  • 空指针和new

    • C++标准提供了一种在失败时返回空指针的new,其用法如下: 
      int * pi = new (std::nothrow) int ;
      int * pa = new (std::nowthrow) int[500];

15.3.9 异常,类和继承

  • 异常,类和继承以三种方式相互关联.首先,可以像标准C++库所做的那样,从一个异常类派生出另一个;其次,可以在类定义中嵌套异常类声明来组合异常;第三,这种嵌套声明本身可被继承,还可用作基类.
  • 程序清单15.14 sales.h
  • 程序清单15.15 sales.cpp
  • 程序清单15.16 use_sales.pp

15.3.10 异常何时会迷失方向

  • 异常被引发后,在两种情况下,会导致问题. 
    • 首先,如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构总,类类型与这个类及其派生类的对象匹配),否则称为意外异常.
    • 如果异常不是在函数中引发的(或者函数没有异常规范),则必须捕获它.如果没被捕获(在没有try块或没有匹配的catch块时,将出现这种情况,则异常被称为未捕获异常.
  • 然而,可以修改程序对意外异常和未捕获异常的反应.

    • 未捕获异常不会导致程序立刻异常终止.相反,程序将首先调用函数terminate().在默认情况下terminate()调用abort()函数.可以指定terminate()应调用的函数(而不是abort())来修改terminate()的这种行为.为此,可调用set_terminate()函数.set_terminate()和terminate()都是在头文件exception中声明的.
    typedef void (*terminate_handler)();
    terminate_handler set_terminate(terminate_handler f) throw(); //C++98
    terminate_handler set_terminate(terminate_handler f) noexcept; //C++11
    void terminate(); //C++98
    void terminate() noexcept; //C++11
    • 其中的typedef使terminate_handler成为这样一种类型的名称:指向没有参数和返回值的函数的指针.set_terminate()函数将不带任何参数且返回类型为void的函数的名称(地址)作为参数,并返回该函数的地址.如果调用了set_terminate()函数多次,则terminate()将调用最后一次set_terminate()调用设置的函数.
    • 原则上 ,异常规范应包含函数调用的其他函数引发的异常.
    • unexpected()函数和set_unexpected()函数处理异常的情景.与提供给set_terminate()的函数的行为相比,提供给set_unexpected()的函数的行为受到更严格的限制.具体的说,unexpected_handler函数可以: 
      • 通过调用terminate()(默认行为),abort()或exit()来终止程序.
      • 引发异常.引发异常(第二种选择)的结果取决于unexpected_handler函数所引发的异常以及引发意外异常的函数的异常规范.
      • 如果新引发的异常与原来的异常规范匹配,则程序将从哪里开始进行正常处理,即寻找与新引发的异常匹配的catch块.基本上,这种方法将用预期的异常取代意外异常;
      • 如果新引发的异常与原来的异常规范不匹配,且一行规范汇总没有包括std::bad_exception类型,则程序将调用terminate().bad_exception是从exception派生而来的,其生命位于头文件exception中;
      • 如果新引发的异常与原来的异常规范不匹配,且原来的异常规范中包含了std::bad_exception类型,则不匹配额异常江北std::bad_exception异常所取代.
      • 总之,如果要补货所有的异常(不管是预期的异常还是意外异常),则可以这样做: 
        首先确保异常头文件的声明可用
#include <exception>
using namespace std;
  • 然后,设计一个替代函数将意外异常转换为bad_exception异常
void myUnexpected()
{
    throw std::bad_exception();//or just throw;
}
  • 仅使用throw,而不指定异常将导致重新引发原来的异常.然而,如果异常规范中包含了这种类型,则该异常将被bad_exception对象所取代.
  • 接下来在程序的开始位置将意外异常操作制定为调用该函数:set_unexpected(myUnexpected);
  • 最后,将bad_exception类型包括在异常规范中,并添加如下catch块序列:
double Argh(double,double) throw(out_of_bounds,bad_exception);
...
try{
    x = Argh(a, b);
}
catch(out_of_bounds & ex)
{
    ...
}
catch(bad_exception & ex)
{
    ...
}

15.3.11 有关异常的注意事项

  • 从前面关于如何使用一场的讨论可知,应在设计程序时就加入异常处理功能,而不是以后再添加.这样做有些缺点.例如:是欧诺个异常会增加程序代码,降低程序的运行速度.异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异.异常和动态内存分配并非总能协同工作.
void test2(int n)
{
    double * ar = new double(n);
    ...
    if (oh_no)
        throw exception();
    ...
    delete [] ar;
    return;
}
  • 这里有个问题.解退栈时,将删除栈中的变量ar.单函数过早的终止意味着函数末尾的delete[]语句被忽略.指针消失了,但它指向的内存块未被释放,并且不可访问.总之,这些内存被泄漏了.这些泄漏是可以避免的.例如:可以在引发异常的函数中补货该异常,在catch块中 包含一些清理代码,然后重新引发异常.
  • 虽然异常处理对于某些项目极为重要,但它也会增加编程的工作量,增大程序,降低程序的速度.另一方面,不进行错误检查的代码可能非常高.

15.4 RTTI

  • RTTI是运行阶段类型识别(Runtime Type Identification)的简称.RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方式.

15.4.1 RTTI的用途 
15.4.2 RTTI的工作原理

  • C++有3个支持RTTI的元素 
    • 如果可能的话,dynamic_cast运算符将使用一个纸箱积累的指针来生成一个指向派生类的指针;否则,该运算符返回0–空指针.
    • typeid运算符返回一个指出对象的类型的值.
    • type_info结构存储了有关特定类型的信息.
  • 只能讲RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该讲派生对象的地址赋给基类指针.
  • 警告:RTTI只适用于包含虚函数的类.
  • RTTI的3个元素 
    • dynamic_cast运算符 
      • dynamic_cast是最常用的RTTI组件,它不能回答”指针指向的是哪类对象”这样的问题,但能够回答”是否可以安全地将对象的地址赋给特定类型的指针”这样的问题.
      • 注意,与问题”指针指向的是哪种类型的对象”相比,问题”类型转换是否安全”更通用,也更有用.通常想知道类型的原因在于:知道类型后,就可以知道调用特定的方法是否安全.要调用方法,类型并不一定要完全匹配,而可以是定义了方法的虚拟版本的基类类型.
      • 注意:通常,如果指向的对象(*pt)的类型为Type或者是从Type直接或间接派生而来的类型,则下面的表达式将指针pt转换为Type类型的指针:C++ dynamic_cast<Type *>(pt)否则,结果为0,即空指针.
      • 程序清单15.17 rtti1.cpp 
        • 注意:即使编译器支持RTTI,在默认情况下,它也可能关闭该特性.如果该特性被关闭,程序可能仍能够通过编译,但将出现运行阶段错误.在这种情况下,您应查看文档或菜单选项.
        • 15.17中程序说明了重要的一点,即应尽可能使用虚函数,而只在必要时使用RTTI.
    • typeid运算符和type_info类 
      • typeid运算符使得能够确定两个对象是否为同种类型.它与sizeof有些相像,可以接受两种参数 
        • 类名;
        • 结果为对象的表达式.
      • typeid(Magnificent) == typeid(*pg),如果pg是一个空指针,程序将引发bad_typeid异常.该异常类型是从exception类派生而来的,是在头文件typeinfo中声明的.
      • 程序清单 15.18 rtti2.cpp
    • 误用RTTI的例子 
      • C++界有很多人对RTTI口诛笔伐,他们认为RTTI是多余的,是导致程序效率低下和糟糕编程方式的罪魁祸首.
      • 提示:如果发现在扩展的if else语句系列中使用了typeid,则应考虑是否应该使用虚函数和dynamic_cast.

15.5 类型转换运算符

  • 在允许C语言中的3种类型转换情况下,Stroustrop采取的措施是,更严格地限制允许的类型转换,并添加4个类型转换运算符,使转换过程更规范: 
    • dynamic_cast
    • const_cast
    • static_cast
    • reinterpret_cast
  • 可以根据目的选择一个适合的运算符,而不是使用通用的类型转换.这指出了进行类型转换的原因,并让编译器能够检查程序的行为是否与设计者想法吻合.
  • dynamic_cast < type-name > (expression) 
    • 该运算符的用途是,使得能够在类层次结构中进行向上转换(由于is-a关系,这样的类型转换是安全的),而不允许其他转换.
  • const_cast运算符用于执行只有一种用途的类型转换,即改变值为const或volatile,其语法与dynamic_cast运算符相同:const_cast (expression),由于编程时可能无意间同时改变类型和常量特征,因此使用const_cast运算符更安全.const_cast不是万能的.它可以修改指向一个值的指针,但修改const值的结果是不确定的. 
    • 程序清单15.19 constcast.cpp
  • static_cast运算符的语法与其他类型转换运算符相同:static_cast < type-name> (expression),仅当type_name可被隐式转换为expression所属的类型或exptession可被隐式转换为type_name所属的类型时,上述转换才是合法的,否则将出错.
  • reinterpret_cast运算符用于天生危险的类型转换.它不允许删除const,但会执行其他令人生厌的操作.reinterpret_cast < type-name > (expression),reinterpret_cast运算符并不支持所有的类型转换.另一个 限制是,不能将函数指针转换为数据指针,反之亦然.
  • 在C++中,普通类型转换也受到限制.基本上,可以执行其他类型转换科执行的操作,加上一些组合(如上面4种),但不能执行其他转换.这些限制是合理的,如果您觉得这种限制难以忍受,可以使用C语言.

15.6 总结

  • 友元使得能够为类开发更灵活的接口
  • 嵌套类是在其他类中声明的类.
  • C++异常机制为处理拙劣的编程事件,如不适当的值,I/O失败等,提供了一种灵活的方式.
  • RTTI(运行阶段类型信息)特性让程序能够检测对象的类型.

15.7 复习题 
15.8 编程练习

本章源代码下载地址

猜你喜欢

转载自blog.csdn.net/weixin_39345003/article/details/82110677