《C++ Primier Plus》笔记:类的进阶部分(ch13 类继承 ch14 C++中的代码重用 ch15 友元、异常和其他)

Chapter13 类继承

  • C++提供了比修改代码更好的方法来扩展和修改类:类继承

13.1.1 派生一个类

  • 下面的声明代表是一个公有基类,这称为公有派生
    class RatedPlayer: public TableTennisPlayer
    {
        ...
    }
    
  • 派生类对象包含基类对象
  • 使用公有派生,基类的公有成员将成为派生类的公有成员
  • 基类的私有部分也成为派生类的一部分,但只能通过基类的公有和保护方法访问
  • 派生类对象存储了基类的数据成员(继承了基类的实现),可以使用基类的方法(继承了基类的接口)

13.1.2 构造函数:访问权限的考虑

  • 派生类构造函数必须使用基类构造函数:创建派生类对象时,程序首先创建基类对象

    RatedPlayer::RatedPlayer(unsigned int r, const string & fn, 
            const string & ln, bool ht): TableTennisPlayer(fn, ln, ht)
            {
                rating = r;
            }
    
    
  • 必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数

    RatedPlayer::RatedPlayer(unsigned int r, const string & fn, 
            const string & ln, bool ht)
            {
                rating = r;
            }
    
    //等效代码
    RatedPlayer::RatedPlayer(unsigned int r, const string & fn, 
            const string & ln, bool ht): TableTennisPlayer()
            {
                rating = r;
            }
    
  • 也可以对成员变量使用成员初始化列表

    RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp): TableTennisPlayer(tp), rating(r)
        {
        }
    
  • 释放对象的顺序和创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数

  • 除了虚基类,类只能将值传递回相邻的(上一个)基类

  • 成员初始化列表只能用于构造函数

13.1.3 使用派生类

  • 要使用派生类,程序必须要能够访问基类声明。比较合适的是将两类的声明放在一个文件中

13.1.4 派生类和基类之间的特殊关系

  • 基类指针可以在不进行显式类型转换的情况下指向派生类对象
  • 基类引用可以再不进行显式类型转换的情况下引用派生类对象
  • 基类指针或引用只能用于调用基类方法,不能调用派生类方法
  • 不能将基类对象的地址赋给派生类引用和指针。因为如果允许,将可能访问派生类内定义而基类未定义的方法。
  • 引用兼容性属性能将基类对象初始化为派生类对象
    RatedPlayer olaf1(1840, "Olaf", Loaf, true);
    TableTennisPlayer olaf2(olaf1);
    
    它将olaf2初始化为嵌套在RatedPlayer对象olaf1中的TableTennisPlayer对象
  • 也可以将派生对象赋给基类对象
    RatedPlayer olaf1(1840, "Olaf", Loaf, true);
    TableTennisPlayer winner;
    winner = olaf1;             //olaf1的基类部分被复制给winner
    
    程序将使用隐式重载赋值运算符:
    TableTennisPlayer & operator= (const TableTennisPlayer &) const;

13.2 继承:is-a 关系

  • C++有三种继承关系:公有继承、保护继承和私有继承
  • 公有继承建立一种is-a关系
  • 剩下的关系有:has-a关系、is-like-a关系、is-implemented-as-a关系、use-a关系等

13.3 多态公有继承

  • 有两种机制可用于实现多态公有继承:
    • 在派生类中重新定义基类的方法
    • 使用虚方法
class Brass
{
    public:
        virtual void Withdraw(double amt);
        virtual void ViewAcct() const;
}

class BrassPlus: public Brass
{
    public:
        virtual void ViewAcct() const;
        virtual void Withdraw(double amt);
}

  • 使用了关键字virtual的方法被称为虚方法
  • 如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法:
    • 如果没有用virtual,将根据引用类型或指针类型选择方法
    • 如果使用了virtual,将根据引用或指针指向的对象类型来选择方法
  • 方法在基类中被声明为虚的后,在派生类自动成为虚方法;然而不妨在派生类中用virtual指出那些是虚函数
  • 基类声明了一个虚析构函数,这是为了确保释放派生对象时,按正确的顺序调用析构函数。如果析构函数不是虚的,将调用基类的析构函数,顺序错误
  • 在基类中将派生类对象会重新定义的方法声明为虚方法,这能确保按照指针指向的类型而非指针类型调用方法
  • 关键字virtual只用于类声明的方法原型中,而没有用于方法定义中
  • 在派生类方法中,应使用作用域解析运算符来调用基类方法
  • 可以使用一个基类指针数组,来表示多种类型的对象,这就是多态性

13.4 静态联编和动态联编

  • 将源代码中的函数调用解释为执行特定的函数代码块成为函数名联编
  • 在编译过程中进行联编成为静态联编(static binding),又称为早期联编
  • 编译器生成能够在程序运行时选择正确的虚方法的代码,这称为动态联编(dynamic binding),又称为晚期联编(late binding)

13.4.1 指针和引用类型的兼容性

  • 动态联编与通过指针和引用调用方法相关,是由继承控制的
  • 将派生类引用或指针转换为基类或指针被称为向上强制转换(upcasting)
  • 将基类指针或引用转换为派生类指针或引用被称为乡下强制转换(downcasting)
  • 隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编

13.4.2 虚成员函数和动态联编

  • 编译器对非虚方法使用静态联编,对虚方法使用动态联编

  • 动态联编必须采用一种方法(虚函数表)来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销

  • 通常编译器处理虚函数的方法是,给每个对象添加一个隐藏成员,其中保存了一个指向函数地址数组的指针,该数组被称为虚函数表。

  • 派生类对象将包含一个指向独立地址表的指针。

  • 如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,将保存函数原始版本的地址

  • 调用虚函数时,将查看存储在对象中的虚函数地址,然后转向相应的函数地址表
    在这里插入图片描述

  • 虚函数表的成本是:

    • 每个对象将增大,增大量为存储地址的空间
    • 对于每个函数调用,都需要执行一项额外的操作,到表中查找地址

13.4.3 有关虚函数注意事项

  • 在基类方法的声明中使用关键字virtual可使该方法在基类及所有的派生类(包括派生类派生出来的类)中是虚的
  • 构造函数不能是虚函数
  • 析构函数应当是虚函数。除非类不用做基类
  • 给类定义一个虚析构函数并非错误,即使这个类不用做基类
  • 友元不能是虚函数,因为友元不是类成员,而只有类成员才能是虚函数
  • 如果派生类位于派生链中,则将使用最新的虚函数版本
  • 重新定义继承的方法不会生成重载版本,而是隐藏了接受一个int参数的基类版本
    class Dwelling
    {
        public:
            virtual void shoperks(int a);
    }
    
    class Hovel: public Dwelling
    {
        public:
            virtual void shoperks();
    }
    
    如果重新定义继承的方法,应确保与原来的原型完全相同。但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针,这种特性被称为返回类型协变(covariance of return type)
  • 如果基类声明被重载了,则应该在派生类中重新定义所有的基类版本

13.5 访问控制:protected

  • 关键字protected和private相似,在类外只能用公有类成员来访问protected部分中的类成员
  • 派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员
  • 对于成员函数来说,保护访问控制使派生类能够访问公众不能使用的内部函数

13.6 抽象基类

  • Circle是一种Ellipse,但Circle的属性要少很多。可以从Ellipse和Circle类中抽象出它们的共性,放到一个抽象基类ABC中
  • C++通过使用纯虚函数提供未实现的函数,纯虚函数声明的结尾处为=0
    class BaseEllipse
    {
        public:
            virtual double Area() const = 0;
    }
    
  • 当类声明中包含纯虚函数时,不能创建该类的对象:包含纯虚函数的类只能用作基类。
  • 要成为真正的抽象基类,必须至少包含一个纯虚函数
  • 在原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数

13.6.1 应用ABC概念

  • 首先定义名为AcctABC的抽象基类,这个类包含Brass和BrassPlus共有的所有方法和数据成员,而在两个类中行为不同的方法应被定义为虚函数
  • 可以将ABC看作一种必须实施的接口,ABC要求具体派生类覆盖其纯虚函数:迫使派生类遵循ABC设置的接口规则。

13.7 继承和动态内存分配

13.7.1 第一种情况:派生类不使用new

class DMA
{
    private:
        char* label
        ...
    public:
        baseDMA(...)
        baseDMA(...)
        virtual ~baseDMA(...)
        baseDMA & operator=(const baseDMA& rs);
}

class lacksDMA: public baseDMA
{
    ...
}
  • 派生类不使用new,则不需要为lackDMA类定义显式析构函数、复制构造函数和赋值运算符

13.7.2 第二种情况:派生类使用new

  • 在这种情况下必须为派生类定义显式析构函数、复制构造函数和赋值运算符都必须使用基类的方法来处理基类元素:
    • 析构函数是自动完成的
    • 构造函数是通过在初始化成员列表中调用基类的复制构造函数完成的
    • 赋值运算符是通过作用域解析运算符显式调用基类赋值运算符完成的
  • 派生类析构函数自动调用基类析构函数,故其自身的职责是对派生类构造函数执行清理
  • 这种情况下的复制构造函数只能访问派生类的数据,因此必须调用baseDMA的复制构造函数来处理共享的baseDMA数据
  • 派生类的显式赋值运算符必须负责所有继承的baseDMA基类对象的赋值,可以用过显式调用基类赋值运算符来做

13.8.4 类函数小结

在这里插入图片描述

Chapter14. C++中的代码重用

代码重用的机制有:

  • 公有继承
  • 包含(container)、组合(competition)、或层次化(layering):包含这样的类成员:本身是另一个类的对象
  • 使用私有或保护继承,通常包含和私有、保护继承用于实现has-a关系

14.1.2 Student类的设计

  • 用于建立has-a关系的C++技术是组合
  • 使用公有继承时,类可以继承接口,可能还有实现:获得接口是is-a关系的组成部分
  • 使用组合,类可以获得实现,但不能获得接口:不继承接口时has-a关系的组成部分

14.1.3 Student类示例

  • 将该typedef放在类定义的私有部分意味着可以在Student类的实现中使用,但不能在类外使用
  • 当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序
    Student(const char* str, const double* pd, int n): scores(pd, n), name(str)()
    
    name成员仍将首先被初始化,因为在类定义中它首先被声明

14.2 私有继承

  • 私有继承中,基类的公有成员和保护成员都将成为派生类的私有成员;基类方法将不会称为派生对象共有接口的一部分,但可以在派生类的成员函数中使用它们
  • 使用公有继承,基类的公有方法将成为派生类的公有方法:派生类将继承基类的接口
  • 使用私有继承,基类的公有方法将称为派生类的私有方法:派生类不继承基类的接口
  • 使用私有继承,类将继承实现:派生类将有基类成员和基类方法
  • 私有继承将对象作为一个未被命名的继承对象添加到类中
  • 使用术语子对象(subobject)表示通过继承或包含添加的对象

14.2.1 Student类示例(新版本)

  • 使用私有继承,要使用关键字private:
    class Student: private std::string, private std::valarray<double>
    {
        public:
            ...
    }
    
    
    私有继承提供了两个无名称的子对象成员
  • 使用多个基类的继承被称为多重继承(multiple inheritance, MI)
  • 私有继承的构造函数的成员初始化列表使用类名(std::string)而不是变量名来初始化
    Student(const char* str, const double* pd, int n):
        std::string(str), ArrayDn(pd, n)
    
  • 私有继承是的能够使用类名和作用域解析运算符来调用基类的方法
    double Student::Average() const
    {
        if(ArrayDb::size()>0)
            return ArrayDb::sum()
        else:
            return 0;
    }
    
  • 可以通过强制类型转换获得内部的无名称基类
  • 用类名显式限定函数名并不适合友元函数,可以通过显式转换为基类来调用正确的函数
  • 私有继承中,未进行显式类型转换的派生类引用或指针无法赋值给基类的引用或指针

14.2.2 使用包含还是使用私有继承

  • 大多数C++程序员倾向于使用包含:
    1. 更容易理解
    2. 继承会引起很多问题,尤其是MI
    3. 包含能包括多个同类的子对象
  • 然而私有继承能提供的特性比包含多:
    1. 私有继承能访问基类的保护成员
    2. 派生类可用重新定义虚函数,但包含类不能

14.2.3 保护继承

  • 保护继承在列出基类时使用protected
class Student: protected std::string,
               protected std::valarray<double>
  • 基类的公有成员和保护成员都将成为派生类的保护乘员
  • 使用私有继承时,第三代类将不能使用基类的接口,因为基类的公有方法在派生类中称为私有方法;
  • 使用保护继承时,第三代类将可以使用基类的公有方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ootdzmPn-1658153320245)(Table14_1.png)]

隐式向上转换意味着无需进行显式类型转换就可以将基类指针或引用指向派生类对象

14.2.4 使用using重新定义访问权限

  • 用一个using声明来指出派生类可以使用特定的基类成员,即使采用的时私有派生
    class Student: private std::string, private std::valarray<double>
    {
        public:
            using std::valarray<double>::min;
    }
    
  • using声明只是用成员名,没有括号,函数参数列表和返回值
  • using声明只适用于继承,而不适用于包含

14.3 多重继承

  • 和单继承一样,公有MI表示的也是is-a关系
  • 必须用关键字public来限定每一个基类,因为除非特别指出,编译器将认为是私有派生
  • MI可能带来的问题是:
    1. 从两个不同的基类继承同名方法
    2. 从两个或更多相关基类继承用一个类的多个实例

14.3.1 有多少Worker

  • 从两个或更多相关基类继承用一个类的多个实例,将是的用基类指针来引用不同的对象复杂化:C++引入新技术————虚基类(virtual base calss)
  • 虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象

在这里插入图片描述
在这里插入图片描述

class Singer: virtual public Worker{...};
class Waiter: public virtual Worker{...};
class SingingWaiter: public Singer, public Waiter{...};
  • SingingWaiter对象将只包含Worker对象的一个副本
  • C++在基类是虚的时候,禁止信息通过中间类自动传递给基类
  • 如果类有间接虚基类,则除非只需要使用该虚基类的默认构造函数,否则必须显式调用该虚基类的某个方法

14.3.2 哪个方法

  • 多重继承中可能导致同名函数调用的二义性,可以使用作用域解析运算符来指定,也可以在派生类中重新定义函数来指定
  • 在类通过多条虚途径和非虚途径继承某个特定基类时,该类将包含一个表示所有虚途径的基类子对象和分别表示个非虚途径的多个基类子对象
    C: virtual B
    D: virtual B
    X: B
    Y: B
    M: C,D,X,Y      //将包含3个B
    
  • 如果类从不同的类哪里继承了两个或更多的同名成员(函数或变量)如果没有用类名限定将导致二义性;但如果使用虚基类,当某个名称优先于(dominates)其他所有名称,将不会导致二义性
  • 派生类中的名称优先于直接或间接祖先类中的相同名称
  • 虚二义性规则与访问规则无关:即使E::omg()是私有的,不能在F类中直接访问,但使用omg仍将导致二义性

14.4.1 定义类模板

  • 模板不是类和成员函数定义,是C++编译器指令,说明如何生成类和成员函数定义
  • 模板的具体实现被称为实例化(instantiation)或具体化(specialization)
  • 不能将模板成员函数放在独立的实现文件中,由于模板不是函数,不能单独定义
  • 模板必须和特定的模板实例化请求一起使用:最简单的方法是把模板放在一个头文件中,并在需要使用的地方包含头文件
template <class Type>
class Stack
{
    private:
        ...
    public:
        ...
}
template <class Type>
Stack<Type>::Stack(){
    ...
}

14.4.2 使用模板类

  • 仅在程序包含模板并不能生成模板类,必须请求实例化:需要声明一个类型为模板类的对象
    Stack<int> kernels;
    Stack<string> cols;
    
    编译器将按Stack模板生成两个独立的类声明和两组独立的类方法
  • 泛型标识符Type称为类型参数,只能用类型赋值
  • 必须显式提供类型,这和函数模板不同,模板函数可以根据实参确定类型

14.4.4 数组模板示例和非类型参数

  • 可以使用非类型参数或表达式参数:
    template<class T, int n>
    ...
    ArrayTP<double, 12>     //用double替换T,用12替换n
    ArrayTP<double, 13>     //两个不同的声明
    
  • 表达式参数可以是整型、枚举、引用或指针
  • 模板代码不能修改参数的值,也不能使用参数的地址(不能n++或&n)
  • 实例化模板时,用作表达式参数的值必须为常量表达式

14.4.5 模板多功能性

  • 模板类可以用作基类,也可用作组件类,还可用作其他模板的类型参数
  • 可以递归调用模板
    Array<Stack<int>> asi;
    
  • 可以为模板参数提供默认值
    template<class T1, class T2=int>
    

14.4.6 模板的具体化

  • 类模板和函数模板相似,有隐式实例化、显式实例化和显式具体化,统称为具体化(specialization):使用具体的类型生成类声明
    • 隐式实例化:
      ArrayTP<double, 30> *pt;        //还不需要对象
      pt = new ArrayTP<double, 30>;   //现在需要对象
      
      编译器在需要对象之前,不会生成类的隐式实例化。第二条语句将导致编译器生成类定义,并根据该定义创建一个对象
    • 显式实例化
      template class ArrayTP<string, 100>;
      
      虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)
    • 显式具体化
      有时可能要在位特殊类型实例化时对模板进行修改,这时可以使用显式具体化
      template <> class ClassName<specialized-type-name> {...};
      
      早期编译器可能只识别这种格式:
      class ClassName<specialized-type-name> {...};
      
    • 部分具体化
      C++允许部分具体化,给类型参数之一制定具体的类型:
      template <class T1, class T2> class Pair {...}  //通用模板
      template <class T1> class Pair<T1, int> 
      
      如果指定了所有类型,前面的<>将为空,退化为显式具体化
  • 如果有多个模板可以选择,编译器将使用具体化程度最高的模板
  • 也可以通过为指针提供特殊版本来部分具体化现有的模板
    template <class T> class Feeb {...};    //Feeb<char> gb1;
    template <class T*> class Feeb {...};   //Feeb<char*> gb2;
    
  • 部分具体化能够为模板设置限制条件
    template <class T1, class T2, class T3> class Trio{...};
    template <class T1, class T2> class Trio<T1, T2, T2> {...};
    template <class T1> class Trio<T1*, T1*, T1*>{...};
    

14.4.7 成员模板

  • 模板可以用作结构、类或模板类的成员
    template<class T>
    class beta
    {
        private:
            template<class V>
            class hold
            {
                ...
            }
            hold<T> ...
    }
    
    //定义
    template<class T>
        template<class U>
         U beta<T>::blab(U u, T t){
            ...
         }
    

14.4.9 模板类和友元

  • 模板类声明也可以有友元,模板的友元分三类:
    • 非模板友元
      在模板中将一个常规函数声明为友元
      template<class T>
      class HasFriend
      {
          public:
              friend void counts();
          
          //或者:
              friend void report(HasFriend<T> &);
      }
      
    • 约束(bound)模板友元
      友元的类型取决于类被实例化时的类型。首先在类定义的前面声明每个模板函数
      template<class T> void counts();
      template<calss T> void report(T &);
      
      随后在模板做再次将函数声明为友元
      template<class TT>
      class HasFriendT
      {
          friend void counts<TT>
          friend void report<> (HasFriend<TT> &)
      }
      
    • 非约束(bound)模板友元
      友元的所有具体化都是雷达每一个具体化的友元。对于非约束友元,友元模板类型参数和模板类型参数不同:
      template <class T>
      class ManyFriend
      {
          template <class C, class D> friend void show2(C&, D&);
      }
      

14.4.10 模板别名(C++11)

  • C++11新增功能为模板提供别名:
    template<class T>
    using arrtype = std::array<T,12>
    
    //使用方法
    arrtype<double> gallons;
    
    //另外用法
    using pc2 = const char*;
    

Chapter15 友元、异常和其他

15.1 友元

  • 类并非只能拥有友元函数,也可以将类作为友元类
    friend class Remote;
    
    • 友元类的所有方法都可以访问原始类的私有成员和保护成员
    • 也可以做更严格的限制,只能将特定的成员函数指定为另一个类的友元
  • 那些函数、成员函数或类为友元是由类定义的,而不能从外部强加友元关系

15.1.1 友元类

  • 例如:遥控器可以改变电视机的状态,遥控器可以作为电视机的一个友元
  • 友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要
  • 如果不使用友元类,则必须将Tv类的私有部分设置为公有的,或者创建一个笨拙的、大型类来包含电视机和遥控器

15.1.2 友元成员函数

  • 需要在Remote类定义的前面插入:
    class Tv;       //forward declaration
    
    class Remote
    {
        set_chan(Tv & t, int c);    
    }
    
    class Tv
    {
        friend class Remote;                          //Remote的所有方法都可影响Tv的私有成员
        friend Remote::set_chan(Tv & c, int c);       //Tv类决定谁是它的友元
    }
    
  • 以下的顺序是不对的:当编译器在Tv类的声明中看到Remote的一个方法(set_chan)被声明为Tv的友元之前,应该先看到Remote类的声明和该方法(set_chan)的声明
    class Remote{...};
    class Tv{...};
    class Remote{...};
    

15.1.3 其他友元关系

  • 一些Tv类方法也能影响Remote类对象,这可以通过让类彼此成为对方的友元实现
  • 对于使用Remote对象的Tv方法,其原型可在Remote类声明之前声明,但必须在Remote类声明之后定义

15.1.4 共同的友元

  • 需要友元的另一种情况是,函数需要访问两个类的私有数据,可以将函数设为两个类的友元

15.2 嵌套类

  • 在另一个类中声明的类称为嵌套类(nested class):定义了一种类型,仅在包含嵌套类声明的类中有效
  • 包含类的成员函数可以创建和使用被嵌套类的对象
  • 仅当声明位于公有部分,才能在包含类的外部使用嵌套类,且必须使用作用域解析运算符
  • 假设想在方法文件中定义构造函数,则定义必须指出Node类是在Queue类中定义的
    class Queue
    {
        class Node
        {
            ...
        }
    }
    
    //构造函数
    Queue::Node::Node(const Item &): item(i), next(0) ()
    

15.2.1 嵌套类和访问权限

  • 对于从Queue派生来的类,Node也是不可见的,因为派生类不能直接访问基类的私有部分
  • 如果嵌套类是在另一个类保护部分声明的,则它对于后者来说是可见,但对于外部世界则是不可见的;此时派生类将知道嵌套类,并可以直接创建这种类型的对象
  • 由于嵌套类的作用域为包含它的类,因此在外部使用时必须使用类限定符
声明位置包含它的类是否可以使用它从包含它的类派生而来的类是否可以使用它在外部是否可使用
私有部分
保护部分
公有部分是,通过类限定符
  • Queue类对象只能显式调用Node对象的公有成员

15.3 异常

  • 异常是相对较新的C++功能,一些老编译器可能没有实现
  • 对于被零除的情况,很多新式编译器通过生成一个表示无穷大的特殊浮点数值处理,cout将这种值显示为Inf,inf,INF或类似的东西;其他编译器可能生成在发生被零除时崩溃的程序:最好在编写在所有系统上都能以相同的受控方式运行的代码

15.3.1 调用abort()

  • Abort()函数的原型位于头文件cstdlib中,其经典实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination,然后终止程序
  • 它还返回一个随实现而异的值,告诉操作系统处理失败

15.3.3 异常机制

  • C++异常是对程序运行过程中发生的异常情况的一种响应,通常有三个组成部分:
    • 使用try块
    • 引发异常
    • 使用处理程序捕获异常
try
{
    ...     //可能引发错误的代码
    throw "Bad not allowed";
}
catch (const char* a)
{
    ...
    //处理错误
}

  • throw关键字表示引发异常:throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数
  • 处理程序以catch开头:异常类型可以是字符串或其他C++类型
  • 程序查找与异常类型匹配的catch块
  • 如果没有引发任何异常,则程序跳过try块后面的catch块,直接执行处理程序后面的第一条语句

15.3.5 异常规范和C++11

  • C++98使用异常规范指出函数可能引发的异常:C++11将其摒弃了
  • C++11确实支持一种特殊的异常规范,指出函数不会引发异常
    double marm() noexcept;
    

15.3.6 栈解退

  • 假设函数由于出现异常而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址
  • 随后控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句

15.3.7 其他异常特性

  • throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合
  • 引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用
    try
    {
        ...
        problem no;
        throw no
    }
    catch(problem & p)
    {
        //p将指向no的副本而不是no本身
    }
    
    
  • 基类引用能够捕获任何从基类派生的错误类
  • 引发的异常对象将与第一个与之匹配的catch块捕获:这意味着catch块的排列顺序应该和派生顺序相反,即基类放在最下面
  • 可以使用省略号来表示捕获任何异常
    catch {...}
    
    与基类类似,可以将该语句放在最后,类似于switch语句的default
  • 可以创建捕获对象而不是引用的处理程序,在catch语句中使用基类对象时,将捕获所有派生类对象,但派生特性将被剥去,因此将使用虚方法的基类版本

15.3.8 exception类

  • exception头文件定义了exception类,可以将它作为其他异常类的基类
  • 有一个名为what()的虚拟成员函数,它返回一个字符串:由于是虚方法,可以在exception的派生类中重新定义它
  • C++库定义了很多基于exception的异常类型
    • stdexcept类:定义了logic_error和runtime_error类,都是以公有方式从exception类派生而来的;这些类的构造函数接受一个string对象作为参数
  • logic_error可以派生四种其他的错误类型,每一种都有类似于logic_error的构造函数,能提供一个供what()返回的字符串
    • domain_error:定义域错误
    • invalid_argument:给函数传递了意料之外的值
    • length_error:没有足够的空间
    • out_of_bounds: 引用错误
  • runtime_error可以派生三种错误
    • range_error
    • overflow_error
    • underflow_error
  • 如果上述错误不能满足需求,应该自己派生一个
  • 头文件new包含bad_alloc类的声明,它是从expection类公有派生而来的
  • C++标准提供了一种在失败时返回空指针的new
    int* pt = new (std::nothrow) int;
    int* pt = new (std::nowthrow) int[500];
    

15.3.9 异常、类和继承

  • 可以从一个异常类派生出另一个异常类
  • 可以在类定义中嵌套异常类声明来组合异常
  • 这种嵌套声明本身可以被继承,还可以用作基类

15.3.10 异常何时会迷失方向

异常被引发时,在两种情况下会导致问题

  • 如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配,否则成为意外异常(unexpected exception)
    • 如果发生意外异常,将调用unexpected()函数,这个函数将调用terminate()
    • 可以用set_unexpected()函数来指定unexpected调用的函数
    • unexpected_handler函数可以:
      • 通过调用terminate(), abort(), exit()来终止程序
      • 引发异常
    • 如果新引发的异常和原来的异常规范匹配,将从那里开始处理
    • 如果新引发的异常和原来的异常规范不匹配,且异常规范没有包括std::bad_exception类型,将调用terminate()
    • 如果新引发的异常和原来的异常规范不匹配,异常规范包括std::bad_exception类型,则不必配的异常将被std::bad_exception异常所取代:这种机制可以捕获所有的异常,通过设计一个替代函数将意外异常转换为bad_exception异常
  • 如果异常不是在函数中引发的,则必须捕获它,如果没有被捕获,则异常被称为未捕获异常(uncaught exception)
    • 未捕获异常不会导致程序立刻异常终止:程序将首先调用函数terminate()
    • 默认情况下terminate()调用abort(): 也可以使用set_terminate()指定调用的函数
    • set_terminate()函数将不带任何参数且返回类型尾void的函数的名称作为参数,并返回该函数的地址
    • 如果调用了set_terminate()多次,将使用最后一次调用设置的函数

15.3.11 有关异常的注意事项

  • 使用异常会增加程序代码,降低程序的运行速度
  • 异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异
  • 异常和动态内存分配并非总能配合工作

15.4.1 RTTI的用途

  • 可以让基类指针指向任何一个派生类对象,通过RTTI来知道指针指向的是哪种对象

15.4.2 RTTI的工作原理

  • RTTI只适用于包含虚函数的类
  • C++有3个支持RTTI的元素:
    • dynamic_cast 运算符使用一个指向基类的指针来生成一个指向派生类的指针,否则返回空指针0
    • typeid运算符返回一个指出对象类型的值
    • type_info结构存储了有关特定类型的信息
  • 只能将RTTI用于包含虚函数的类层次结构,因为只有对于这种类层次结构,才应该将派生类对象的地址赋给基类指针
  • 如果指向的对象(*pt)的类型尾Type或从Type直接或间接派生来的类型,则下面的表达式将指针pt转换为Type类型的指针
    dynamic_cast<Type *>(pt)
    
  • 也可以将dynamic_cast用于引用。没有与空指针对应的引用值,因此无法使用特殊的引用之来指示失败;当请求不正确时,dynamic_cast将引发类型为bad_cast的异常,这种异常是从exception类派生而来的:
    #include<typeinfo>
    try
    {
        Superb & rs = dynamic_cast<Superb &>(rg);
    }
    
    catch(bad_cast &)
    {
        ...
    };
    
  • typeid运算符使得能够确定两个对象是否为同种类型
  • typeid运算符接受两种参数:
    1. 类名
    2. 结果为对象的表达式
  • typeid运算符返回一个对type_info对象的引用。type_info类重载了==和!=运算符:
    typeid(Magnificant)==typeid(*pg)    //如果pg是一个空指针,程序将引发异常
    
  • type_info类的实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串;通常是类的名称
  • 使用if(type_id(类)==某类),将会对新加的类不友好。而以下语句适用于所有从Superb派生来的类
    if(ps = dynamic_cast<Superb* >(pg))
        pg -> Say();
    

15.5 类型转换运算符

  • 添加了四个类型转换运算符,使转换过程更规范:
    • dynamic_cast:使得能在类层次结构中进行向上转换,而不允许其他转换
      dynamic_cast<type-name> (expression)
      
    • const_cast:改变值为const或volatile
      const_cast <type-name> (expression)
      
      除了const和volatile特征可以不同之外,type-name和expression的类型必须相同。用于这样的类,有时是常量但有时需要修改
    • static_cast:仅当type-name可被隐式转换成expression所属的类型或expression可被隐式转换成type_name所属的类,下面的转换才是合法的
      static_cast <type-name> (expression)
      
      • 比如派生类和基类、基类和派生类之间是合法的,与其他类之间的转换是不合法的
      • 合法的又如枚举值-整数、double-int、float-long
    • reinterpret_cast:用于天生危险的类型转换
      reinterpret_cast<type-name> (expression)
      
      struct dat {short a; short b}
      long value = 0xA224B118;
      dat* pd = reinterpret_cast<dat* >(&value);
      cout << hex << pd->a;
      
      • 它不允许删除const
      • 依赖于底层的编程技术,是不可移植的:例如不同系统在存储多字节整型时可能以不同的顺序存储其中的字节
      • reinterpret_cast并不支持所有的类型转换,比如可以将指针类型转换为足以存储指针表示的整型,但不能将指针转换为更小的整型或浮点型;不能将函数指针转换为数据指针,反之亦然
      • 下面的类型转换在C语言中是允许的,但在C++通常不允许,因为char类型太小,不能存储指针
        char ch = char (&d)
        
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值