- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
13.1 一个简单的基类
从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。为说明继承,首先需要一个基类。Webtown 俱乐部决定跟踪乒乓球会会员。作为俱乐部的首席程序员,需要设计一个简单的 TableTennisPlayer 类,如程序清单 13.1 和 13.2 所示。
程序清单 13.1 tabtenn0.h
程序清单 13.2 tabtenn0.cpp
TableTennisPlayer 类只是记录会员的姓名以及是否有球桌。有两点需要说明。首先,这个类使用标准 string 类来存储姓名,相比于使用字符数组,这更方便、更灵活、更安全,而与第 12 章的 String 类相比,这更专业。其次,构造函数使用了第 12 章介绍的成员初始化列表语法,但也可以像下面这样做:
这将首先为 firstname 调用 string 的默认构造函数,再调用 string 的赋值运算符将 firstname 设置为 fn,但初始化列表语法可减少一个步骤,它直接使用 string 的复制构造函数将 firstname 初始化为 fn。
程序清单 13.3 使用了这个类。
程序清单 13.3 usett0.cpp
下面是程序清单 13.1.-13.3 组成的程序的输出:
注意到该程序实例化对象时将 C-风格字符串作为参数:
但构造函数的形参类型被声明为 const string &。这导致类型不匹配,但与第 12 章创建的 String 类一样,string 类有一个将 const char *作为参数的构造函数,使用 C-风格字符串初始化 string 对象时,将自动调用这个构造函数。总之,可将 string 对象或 C-风格字符串作为构造函数 TableTennisPlayer 的参数;将前者作为参数时,将调用接受 const string &作为参数的 string 构造函数,而将后者作为参数时,将调用接受 const char *作为参数的 string 构造函数。
13.1.1 派生一个类
Webtown 俱乐部的一些成员曾经参加过当地的乒乓球锦标赛,需要这样一个类,它能包括成员在比赛中的比分。与其从零开始,不如从 TableTennisClass 类派生出一个类。首先将 RatedPlayer 类声明为从 TableTennisClass 类派生而来:
冒号指出 RatedPlayer 类的基类是 TableTennisplayer 类。上述特殊的声明头表明 TableTennisPlayer 是一个公有基类,这被称为公有派生。派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问(稍后将介绍保护成员)。
上述代码完成了哪些工作呢?Ratedplayer 对象将具有以下特征:
- 派生类对象存储了基类的数据成员(派生类继承了基类的实现);
- 派生类对象可以使用基类的方法(派生类继承了基类的接口)。
因此,RatedPlayer 对象可以存储运动员的姓名及其是否有球桌。另外,RatedPlayer 对象还可以使用 TableTennisPlayer 类的 Name( )、hasTable( ) 和 ResetTable( ) 方法(参见图 13.1)。
图 13.1 基类对象和派生类对象
需要在继承特性中添加什么呢?
- 派生类需要自己的构造函数。
- 派生类可以根据需要添加额外的数据成员和成员函数。
在这个例子中,派生类需要另一个数据成员来存储比分,还应包含检索比分的方法和重置比分的方法。因此,类声明与下面类似:
构造函数必须给新成员(如果有的话)和继承的成员提供数据。在第一个 RatedPlayer 构造函数中,每个成员对应一个形参;而第二个 Ratedplayer 构造函数使用一个 TableTennisPlayer 参数,该参数包括 firstname、lastname 和 hasTable。
13.1.2 构造函数:访问权限的考虑
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。例如,RatedPlayer 构造函数不能直接设置继承的成员(firstname、lastname 和 hasTable),而必须使用基类的公有方法来访问私有的基类成员。具体地说,派生类构造函数必须使用基类构造函数。
创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作。例如,下面是第一个 RatedPlayer 构造函数的代码:
其中:TableTennisPlayer(fn,ln,ht) 是成员初始化列表。它是可执行的代码,调用 TableTennisPlayer 构造函数。例如,假设程序包含如下声明:
则 RealPlayer 构造函数将把实参“Mallory”、“Duck”和 true 赋给形参 fn、In 和 ht,然后将这些参数作为实参传递给 TableTennisPlayer 构造函数,后者将创建一个嵌套 TableTennisPlayer 对象,并将数据“Mallory”、“Duck”和 true 存储在该对象中。然后,程序进入 RealPlayer 构造函数体,完成 RealPlayer 对象的创建,并将参数 r 的值(即 1140)赋给 rating 成员(参见图 13.2)。
图 13.2 将参数传递给基类构造函数
如果省略成员初始化列表,情况将如何呢?
必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数,因此上述代码与下面等效:
除非要使用默认构造函数,否则应显式调用正确的基类构造函数。
下面来看第二个构造函数的代码:
这里也将 TableTennisPlayer 的信息传递给了 TableTennisPlayer 构造函数:
由于 tp 的类型为 TableTennisPlayer &,因此将调用基类的复制构造函数。基类没有定义复制构造函数,但第 12 章介绍过,如果需要使用复制构造函数但又没有定义,编译器将自动生成一个。在这种情况下,执行成员复制的隐式复制构造函数是合适的,因为这个类没有使用动态内存分配(string 成员确实使用了动态内存分配,但本书前面说过,成员复制将使用 string 类的复制构造函数来复制 string 成员)。
如果愿意,也可以对派生类成员使用成员初始化列表语法。在这种情况下,应在列表中使用成员名,而不是类名。所以,第二个构造函数可以按照下述方式编写:
有关派生类构造函数的要点如下:
- 首先创建基类对象;
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
- 派生类构造函数应初始化派生类新增的数据成员。
这个例子没有提供显式构造函数,因此将使用隐式构造函数。释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。
注意:
创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数。可以使用初始化器列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。
成员初始化列表
派生类构造函数可以使用初始化器列表机制将值传递给基类构造函数。请看下面的例子:
其中 derived 是派生类,base 是基类,x 和 y 是基类构造函数使用的变量。例如,如果派生类构造函数接收到参数 10 和 12,则这种机制将把 10 和 12 传递给被定义为接受这些类型的参数的基类构造函数。除虚基类外(参见第 14 章),类只能将值传递回相邻的基类,但后者可以使用相同的机制将信息传递给相邻的基类,依此类推。如果没有在成员初始化列表中提供基类构造函数,程序将使用默认的基类构造函数。成员初始化列表只能用于构造函数。
13.1.3 使用派生类
要使用派生类,程序必须要能够访问基类声明。程序清单 13.4 将这两种类的声明置于同一个头文件中。也可以将每个类放在独立的头文件中,但由于这两个类是相关的,所以把其类声明放在一起更合适。
程序清单 13.4 tabtenn1.h
程序清单 13.5 是这两个类的方法定义。同样,也可以使用不同的文件,但将定义放在一起更简单。
程序清单 13.5 tabtenn1.cpp
程序清单 13.6 创建了 TableTennisPlayer 类和 RatedPlayer 类的对象。请注意这两个类对象是如何使用 TableTennisPlayer 类的 Name( ) 和 HasTable( ) 方法的。
程序清单 13.6 usett1.cpp
下面是程序清单 13.4~程序清单 13.6 组成的程序的输出:
13.1.4 派生类和基类之间的特殊关系
派生类与基类之间有一些特殊关系。其中之一是派生类对象可以使用基类的方法,条件是方法不是私有的:
另外两个重要的关系是:基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象:
然而,基类指针或引用只能用于调用基类方法,因此,不能使用 rt 或 pt 来调用派生类的 ResetRanking 方法。
通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。然而,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针:
上述规则是有道理的。例如,如果允许基类引用隐式地引用派生类对象,则可以使用基类引用为派生类对象调用基类的方法。因为派生类继承了基类的方法,所以这样做不会出现问题。如果可以将基类对象赋给派生类引用,将发生什么情况呢?派生类引用能够为基对象调用派生类方法,这样做将出现问题。例如,将 RatedPlayer::Rating( ) 方法用于 TableTennisPlayer 对象是没有意义的,因为 TableTennisPlayer 对象没有 rating 成员。
如果基类引用和指针可以指向派生类对象,将出现一些很有趣的结果。其中之一是基类引用定义的函数或指针参数可用于基类对象或派生类对象。例如,在下面的函数中:
形参 rt 是一个基类引用,它可以指向基类对象或派生类对象,所以可以在 Show( ) 中使用 TableTennis 参数或 Ratedplayer 参数:
对于形参为指向基类的指针的函数,也存在相似的关系。它可以使用基类对象的地址或派生类对象的地址作为实参:
引用兼容性属性也让您能够将基类对象初始化为派生类对象,尽管不那么直接。假设有这样的代码:
要初始化 olaf2,匹配的构造函数的原型如下:
类定义中没有这样的构造函数,但存在隐式复制构造函数:
形参是基类引用,因此它可以引用派生类。这样,将 olaf2 初始化为 olaf1 时,将要使用该构造函数,它复制 firstname、lastname 和 hasTable 成员。换句话来说,它将 olaf2 初始化为嵌套在 RatedPlayer 对象 olaf1 中的 TableTennisPlayer 对象。
同样,也可以将派生对象赋给基类对象:
在这种情况下,程序将使用隐式重载赋值运算符:
基类引用指向的也是派生类对象,因此 olaf1 的基类部分被复制给 winner。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论