《C++ Primer Plus(中文第6版)》第十三章 类继承

        本章内容包括:is-a 关系的继承、如何以公有方式从一个类派生出另一个类、保护访问类型、构造函数成员初始化列表、向上和向下强制类型转换、虚函数成员、早期(静态)联编与晚期(动态)联编、抽象基类、纯虚函数、何时以及如何使用公有继承。

        继承完成的工作:

  • 可以在已有类的基础上添加功能。例如对于数组类,可以添加数字运算。
  • 可以给类添加数据。例如、对于字符串类,可以派生出一个类,并添加指定字符串显式颜色的数据成员。
  • 可以修改类方法的行为。

        当然可以通过复制原始代码,并对其进行修改来完成上述工作,但继承机制只需提供新特性,甚至不需要访问源代码就可以派生出类。

13.1 一个简单的基类

        从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。为说明继承,首先需要一个基类。

13.1.1 派生一个类

派生类的声明:

上述代码中的 public 表明 TableTennisPlayer 是一个公有基类,这被称为公有派生。派生类对象包含基类对象。使用公有派生,基类的公有成员将称为派生类的公有成员;基类的私有部分也将称为派生类的一部分,但只能通过基类的公有和保护方法访问。

  • 派生类对象存储了基类的数据成员(派生类继承了基类的实现);
  • 派生类对象可以使用基类的方法(派生类继承了基类的接口)。

需要在继承特性中添加的:

  • 派生类需要自己的构造函数。
  • 派生类可以根据需要添加额外的数据成员和成员函数。

构造函数必须给新成员(如果有的话)和继承的成员提供数据。

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

        派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。派生类的构造函数不能直接设置继承的成员,必须使用基类的公有方法来访问私有的基类成员。具体来说,派生类构造函数必须使用基类构造函数。

        创建派生类对象时,程序首先创建基类对象。从概念上说,基类对象应当在程序进入派生类构造函数之前被创建。必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数(除非要使用默认构造函数,否则应显式调用正确的基类构造函数)。

        有关派生类构造函数的要点:

  • 首先创建基类对象;
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
  • 派生类构造函数应初始化派生类新增的数据成员。

释放对象时,首先执行派生类的析构函数,然后再执行基类的析构函数。

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

        派生类对象可以使用基类的非私有方法。另外两个重要的关系是:基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。然而,基类指针和引用只能用于调用基类方法,而不行调用派生类的方法。

13.2 继承:is-a 关系

        C++ 有 3 种继承方式:公有继承、保护继承和私有继承。公有继承是最常用的方式,它建立一种 is-a 关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作也可以对派生类对象执行。

13.3 多态公有继承

        若需要实现同一个方法在派生类和基类中的行为是不同的,那么可以选择多态公有继承,实现多态公有继承的两种方法:一是:在派生类中重新定义基类的方法。二是使用虚方法。

        在基类中声明虚方法后,它在派生类中将自动生成虚方法。(也能够在派生类声明中使用关键字 virtual 来指出哪些函数是虚函数(注意:virtual 只用于类声明的方法原型中,而不用于方法定义)。

注意:如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚方法。这样,程序将工具对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。

记住派生类并不能直接访问基类的私有数据,必须使用基类的公有方法才能进行访问私有数据。派生类构造函数在初始化基类私有数据时,采用的是成员初始化列表语法。

可以创建一个指向基类的指针数组,这样数组中的指针既可以指向基类对象,也可以指向派生类对象,从而实现使用一个数组来表示多种类型的对象(多态性)。

13.4 静态联编和动态联编

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

        动态联编与通过指针和引用调用的方法相关,它们由继承控制。C++ 不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型,然而,指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换。

double x = 2.5;
int *pb = &x;    // invalid assignment, mismatched pointer types
long &r1 = x;    // invalid assignment, mismatched reference types

// 指向基类的引用或指针可以引用派生类对象
BrassPlus dilly("tmp", 111, 222);
Brass *pb = &dilly;
Brass *rb = dilly;

        将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使得公有继承不需要进行显式类型转换。相反,将基类指针或引用转换为派生类指针或引用称为向下强制转换(如果不适用显式类型转换,则向下强制转换是不允许的)。隐式向上类型转换使得基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++ 使用虚函数来满足这种需求。

13.4.2 虚成员函数和动态联编

1、为什么有两种类型的联编以及为什么默认为静态联编

        为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销(稍后将介绍一种动态联编方法)。例如如果类不会用作基类,则不需要动态联编。同样,如果派生类不重新定义基类的任何方法,也不需要使用动态联编。在这些情况下,使用静态联编更合理,效率也更高。由于静态联编的效率更高,因此被设置为C++默认的选择。

注意:如果要在派生类中程序定义基类的方法,则将它设置为虚方法;否则设置为非虚方法。

2、虚函数的工作原理

        通常,编译器处理虚函数的方法是:给对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该虚函数表将保存函数元素版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中。注意,无论类中包含的虚函数是1个还是10个,都只需在对象中添加1个地址成员,只是表的大小不同而已。

        调用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表。如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数。如果使用类声明中的第三个虚函数,程序将使用地址为数组中第三个元素的函数。

        使用虚函数时,在内存和执行速度上有一定的成本包括:

  • 每个对象都将增大,增大量为存储地址的空间(即虚函数表所占据的空间大小);
  • 对于每个类,编译器都将创建一个虚函数表(数组);
  • 对于每个函数调用,都需要执行一项额外操作,即到表中查找地址。

        虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。

13.4.3 有关虚函数的注意事项

  • 在基类方法的声明中使用关键字 virtual 可以使得该方法在基类以及所有派生类(包括派生类派生出来的类)中都是虚方法的。
  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这样基类指针或引用可以指向派生类对象
  • 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。

1、构造函数

        构造函数不能说虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然而,派生类的构造函数使用基类的一个构造函数,这种顺序不同于继承机制。因而,派生类不继承基类的构造函数,所以将类构造函数声明为虚函数没有意义。

2、析构函数

        析构函数应当是虚函数,除非类不用做基类。这意味着:即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而应提供虚析构函数,计算它不执行任何操作!

3、友元

        友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。

4、没有重新定义

        如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的

5、重新定义将隐藏方法

       ,重新定义继承的方法并不是重载。如果派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。

注意:

第一、如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化。

第二、如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如果自定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。如果不需要修改可以直接调用基类版本,例如:

void Person::virFunc()    const {Based::virFunc();}

13.5 访问控制:protected

        访问类别除了 public 和 private 外,还存在一个访问类别:protected,在类外只能用公有类成员来访问 protected 部分中的类成员。private 和 protected 之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问保护成员(派生类的成员不能直接访问私有成员,即不能直接使用对象加“.”来进行访问)。

13.6 抽象基类(Abstract base class)

        抽象基类描述的是至少使用一个虚函数接口,从 ABC 派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口。

        C++ 通过使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为 =0 。当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。要成为真正的 ABC(抽象基类),必须至少包含一个纯虚函数。原型中的 “=0” 使虚函数称为纯虚函数。但 C++ 甚至允许纯虚函数有定义。在原型中使用 “=0” 指出类是一个抽象基类,在类中可以不定义该函数,但在类外的实现文件中提供方法的定义

13.6.1 应用 ABC 概念 和 理念 (P516)

        设计 ABC 之前,首先应开发一个模型——指出编程问题所需的类以及它们之间相互关系。可以将 ABC 看作是一种必须实施的接口。ABC 要求具体派生类覆盖其纯虚函数——迫使派生类遵循 ABC 设置的接口规则。

13.7 继承和动态内存分配(基类使用 new)

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

                是否需要为派生类定义显式析构函数、复制构造函数和赋值运算符呢?不需要!

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

                在这种情况下必须为派生类定义显式析构函数、复制构造函数和赋值运算符。

        13.7.3 使用动态内存分配和友元的继承示例(P522)

13.8 (待续、CY)

  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值