← 学习库 C++程序设计教程 本册目录

第13章 继承与派生

原书第 208 页

第 13 章 事物的继承和派生——继承、派生及面向对象的程序设计方法

就像人类在不断衍生和繁殖一样,动物和其他事物也有相应的继承与派生的特性。在大千世界中,各种各样的事物均有这种继承与被继承的关系。从某一类事物派生出的另一类事物,是其从属类或细分类。就拿一个家庭来说,祖宗衍生和繁殖了今日的后代,这就是派生;而今日的后代与祖宗之间存在着继承和被继承的属性,这些属性是事物(即对象)之间所包含的特性。所以,在面向对象的程序设计中,如果能考虑到这些特性,则更符合实际的事物,编写出的程序的可读性、可维护性和可移植性会更好,程序也更简洁明了。

13.1 计算平均分及打印学生成绩清单的例题详解

【例 13-1】请设计一个程序,计算学生某科的平均分,并打印学生成绩清单和平均分。

1. 用心分析人想通,机随人行写提纲

按照面向对象的程序设计方法,先画“顺瓜摸根”(类的继承层次)图,如图13-1所示(“顺瓜摸根”法的详细介绍见13.3.2节)。

Image
图 13-1 计算平均分及打印学生成绩清单的类的多种继承层次图

初步筛选一下,方案四明显是本末倒置,初选就删除此方案,剩下的方案基本合理,但还捉摸不定,那就填写在方案比较表13-1中,进一步加以比较、分析,以便进行方案选优。

表13-1 方案比较表
方案继承与派生的合理性、有利性可读性可移植性可维护性编程方便性备注
方案一
方案二一般一般不太好一般
方案三差(没继承)不好不好不好一般
原书第 209 页

从表 13-1 中可以明显看出,方案一为优胜者,所以选择它。该方案的意思是:要摘到这个瓜(打印成绩及平均分),就要有打印类,打印又要有平均分类和成绩类(这些可通过继承平均分得到),平均分又要从成绩类中取到成绩来计算。方案一将这几个项目(类)分得很细,很利于移植。例如,有一个学生成绩存档软件,这时就可以把成绩类移植过去;又例如,有一个学生成绩查询系统,则把成绩类和打印类移植过去,编程人员就能省很多力气。其中,方案三最不好,因为若只需学生成绩类,则这三者混在一起的类中就分不出成绩类来,所以,移植性不好,其他方面的性能也较差。

根据此最优方案,画出各个类的架构表,如表13-2~表13-4所示。在这几个表中具体分配了功能和任务,头脑跟踪运行,通过一层层继承与派生的打印→平均分→成绩的关系,调用各个函数成员,最后可知能实现题目要求的功能,即打印成绩和平均分,由此证明,这些类的继承是合理的。

表13-2 基类架构表
基 类
成 绩 类
功能(函数成员)原材料(数据成员)
输入成绩成绩
表13-3 第一派生类架构表
第一派生类
平均分类
功能(函数成员)原材料(数据成员)
计算平均分平均分
表 13-4 第二派生类架构表
第__派生类
打印类
功能(函数成员)原材料(数据成员)
打印平均分

写出各个函数成员的提纲和主函数的提纲如下。

基类:循环输入学生成绩。

第一派生类:循环调出学生成绩→计算平均分。

第二派生类:循环打印学生成绩→打印平均分。

主函数:开始→循环输入学生成绩→循环调出学生成绩→计算平均分→循环打印学生成绩→打印平均分。

2. 对号入座写文章,头脑运行改文章

根据上述提纲对号入座编写各个类的声明、定义及主函数,其源程序如下。

//ex13-1-1.cpp
#include<iostream.h>
#define NUM3
class Sco1//声明成绩类
{

protected:

float scoa[NUM];

原书第 210 页

public:

void dr2()//输入考试成绩函数成员
{
    cout<<"请输入"<<NUM<<"个学生的考试成绩"<<endl;
    for(int i=0;i<NUM;i++)
    {
        cin>>scoa[i];
        cout<<scoa[i]<<endl;//最后,此句可注释掉
    }
    //dis();
    //usedis();
}

class Avel1:protected Scol1//声明平均分类,并保护继承成绩类
{
    protected:
        float avesco;

    public:
        Avel1()//平均分的构造函数
    {
        avesco=0;//将放平均分的数据成员初始化为0,因为avesco要放累加和,
        //清0是累加和的初始化处理
        cout<<avesco<<'n';//最后,此句可注释掉
    }
    void a2()//float scoa[]//计算平均分
    {
        dr2();//调用输入考试成绩的函数成员
        //dis();
        cout<<avesco<<'n';//最后,此句可注释掉
        for(int i=0;i<NUM;i++)
        {
            cout<<scoa[i]<<'t'<<avesco<<endl;//最后,此句可注释掉
            avesco=avesco+scoa[i];
            cout<<scoa[i]<<endl;//最后,此句可注释掉
        }
        cout<<avesco<<endl;//最后,此句可注释掉
    }
};

class Pri1:protected Avel1//声明打印类,并保护继承平均分类
{
    protected:
        //float scoa[3];
    }
}
原书第 211 页

int i,j,k;

public:

    void dis()
    {
        a2();//调用计算平均分的函数成员
        cout<<"考试成绩:"<<endl;
        for(int i=0;i<NUM;i++)
            cout<<scoa[i]<<endl;
        cout<<"平均分:"<<endl;
        cout<<avesco<<endl;
    }
};

void main()
{
    Pri1 d1;//定义打印类的对象 d1
    d1.dis();
}

因为平均分类继承了成绩类,打印类继承了平均分类,所以,平均分类的函数成员可以大大方方地直接拿成绩类的函数成员来用,同样,打印类的函数成员也可以大大方方地直接拿平均分类的函数成员来用,如“dr2();//调用输入考试成绩的函数成员”和“a2();//调用计算平均分的函数成员”,而不用再声明各个对象,又用对象名来调用。

注意:程序中凡在句前有“//”的语句,就是要去掉的调试用的语句或错句。在句末有“//最后,此句可注释掉”的语句,就是最后完善时可去掉的语句。之所以要留下这些景观,就是为了让读者能更好地理解调试过程,并学习程序如何做到更加完善。

3. C++帮助且裁判,人定胜机凯歌扬

程序调试记录如下。

(1)数据成员的初始化不正确

出现错误如下:

error C2252: 'avesco': pure specifier can only be specified for functions

因为 “float avesco=0;” 这个保护数据成员的初始化是在声明这个数据成员时进行的,这是错误的,应在特定的函数中初始化,应把此句放在 a2 函数中。

出现错误如下:

error C2065: 'avesco': undeclared identifier

执行 cl.exe 时出错.

双击错误信息行,光标指向打印类 Pri1 的 “cout<<avesco<<endl;” 处。

因为在成员函数 a2 中声明了 “float avesco=0;”,则 avesco 是局部变量,而不是数据成员,Pri1 继承不到,所以,还是要用构造函数为 avesco 赋初始值。

原书第 212 页

出现错误如下:

error C2380: type(s) preceding 'Ave1' (constructor with return type, or illegal redefinition of current class-name?)

因为构造函数是没有返回值的,而下面这个构造函数加了 void:

void Ave1()
{
    float avesco=0;
}

去掉上面这个构造函数 $ ^{1} $的void,此错纠正。

(2) 结果不正确

第一次运行结果为:

100

90

80

考试成绩:

-1.07374e+008

-1.07374e+008

-1.07374e+008

平均分:

-3.57913e+007

结果不正确。

在 dr2()函数成员定义中的 “cin>>scoa[i];” 之后加入一句 “cout<<scoa[i]<<endl;”,然后再一次运行程序。

第二次运行结果为:

100

100

90

90

80

80

考试成绩:

-1.07374e+008

-1.07374e+008

-1.07374e+008

平均分:

-3.57913e+007

结果还是不正确。

这说明输入时的成绩是正确的,但是到了计算平均分和打印时,不论输入什么数,结果均一样,都是随机数,即没有取到scoa数组中的数据。

原书第 213 页

依照上述方法在多处添加语句 “cout<<scoa[i]<<endl;” 和 “cout<<avesco<<n;”(这是调试技巧),可知问题如下:

在求平均分类的构造函数中声明 “float avesco=0;”,只是把 avesco 这个构造函数中的局部变量初始化为 0,而打印类继承的数据成员 avesco 并没有被初始化为 0,所以结果当然是随机数了。

另外,在打印类中又声明了“float scoa[3];”,则scoa是打印类本身的局部变量,而不是继承自基类的数据成员scoa,又因为没有给它赋初值,所以当然也是随机数了。

注意:在构造函数中赋初值时,不要再声明了,若再声明,则变成构造函数时局部变量了,此处即不要在 avesco=0 之前再写 float 了。还有,如果继承基类或上一层派生类的代码(包括数据成员和函数成员),就不要再声明一个同样的代码(包括数据成员和函数成员)了,此处即不要在打印类中再写一遍 “float scoa[3];” 了。

调试后的最终运行结果为:

Image

13.2 勤奋实践,总结评分

13.2.1 习题

1.请用类的继承与派生编写计算圆面积和圆柱体体积的程序。用编写的程序计算几组圆面积和圆柱体体积并显示。

原书第 214 页

2.编写计算每月工资的程序,工资由基本工资、补贴和水电费组成,试用类的继承与派生来设计。输入几个人的工资分项,计算出每月工资。

3.用类的继承与派生编写一个计算所购房屋的面积及其总金额的程序。计算几组房屋的面积及其总金额。

4.以某学生的姓名为基类,他的各科学分为子类,编写一个程序,计算出他所得的总学分。

5.在第4章习题的第7题中,若将产品的基⼚数据信息作为基类,将判别产品是合格品还是次品作为派⽣类,分别继承基类,请编程完成该题的工作。

6.在第8章习题的第6题中,将图书数据结构中的“状态”作为派生类,将其余信息作为基类,请用面向对象的方法编程完成该题的工作。

  1. 在第8章习题的第7题中,将英语单词作为基类,游戏作为派生类,请用面向对象的方法编程完成该题的工作。
  1. 在第8章习题的第8题中,将数学题作为基类,游戏作为派生类,请用面向对象的方法编程完成该题的工作。

9.在第8章习题的第9题中,将学生信息作为基类,插入作为派生类,请用面向对象的方法编程完成该题的工作。

  1. 在第4章习题的第8题中,将商品信息作为基类,打印价格条作为派生类,请用面向对象的方法编程完成该题的工作。
13.2.2 思考题
  1. 试用继承与派生管理图书馆的图书。
  1. 请用继承与派生解决一个实际对象的问题。
13.2.3 总结评分,继续提高

浏览、体验了例题、习题和思考题之后,说说自己的体会和收获,并认真总结,按表13-5给自己评判一下可以得多少分。

表 13-5 总结评分表
成 绩ABCD不足的原因改进的方法备 注
C++知识完全理解和会用基本理解和会用肤浅理解和会用基本不理解和不会用
程序原创(独立编程)的比例(%)≥9080~8960~79<60

不论得分多少,大家都要总结经验,扬长避短,勤奋努力,不断提高。

最后浏览一下本章的第4节,可以先考考自己,然后再浏览,以便使重点知识掌握得更牢固。

13.3 继承、派生及面向对象的程序设计方法知识

继承与派生是绝大多数事物都含有的特性,你承认它也好,不承认它也罢,它都是存在

原书第 215 页

的,这是事物的客观规律。遵循了事物的客观规律,发现问题就会更容易些,分析问题就会更正确些,解决问题的成功率也会更高一些。

那么,在面向对象的程序设计中,如何将继承与派生体现出来并加以利用呢?这需要进一步地进行学习。

13.3.1 在什么情况下用到继承、派生及面向对象的程序设计方法

当需要解决问题的事物(对象)中包含继承与派生的关系时,就应该用继承、派生及面向对象的程序设计方法。

13.3.2 关于继承、派生及面向对象的程序设计方法

1. 关于继承与派生

(1) 什么是继承与派生

从某一个类衍生出另一个类,这就是派生,派生类的某些特性继承于派生它的类。就像父亲生了儿子,即父亲派生出儿子,儿子继承了父亲的某些特性一样。父亲即是父类,儿子即是子类,孙子又是子子类,其关系如图13-2所示。

Image

虚箭头线:多继承 实箭头线:单继承

图 13-2 继承与派生的关系示意图

一般将父类称为基类,将子类称为派生类,子子类又是子类的派生类。

而且 $ \underline{\text{基}} $类不一定是一个,可能是两个以上,这种情况称为多重继承法。例如父亲是基类1,母亲是基类2,儿子是派生类,则儿子同时继承了基类1和基类2。

(2) 继承与派生的分类

继承与派生可分为公有继承(public)、保护继承(protected)和私有继承(private)3类。例如,父亲留给独生子的遗产当中,电视机是公有继承,全家人都可以继承、可以看;电脑是保护继承(因为只有懂电脑的人可以用);银行存款是私有继承(因为只有自己才能管自己私有的存款)。

继承与派生的分类(即继承方式)与第12章讲的访问属性之间有一定的关系,如表13-6所示。

原书第 216 页
表 13-6 3 种继承方式下,基类成员在派生类中的访问属性
派生类的继承方式\n基类成员公有继承保护继承私有继承
公有成员(公有成员)(保护成员)(私有成员)
保护成员(保护成员)(保护成员)(私有成员)
私有成员(不能访问)(不能访问)(不能访问)

从表 13-6 中可以看出,基类成员(除了私有成员外)在派生类中的访问属性是:公有继承不改变基类原有的访问属性,其他两种继承都相应地改变了基类原有的访问属性,使得外部不能访问这些派生类了。反过来说,使得外部能按基类原有的访问属性去访问派生类的继承方式是公有继承。即一般来说,若想用基类的程序代码(包括数据成员和函数成员),就选用公有继承。

(3) 怎样确定基类和派生类

凡具有通用性或可以被其他程序利用的类,就把它设为基类,反之,一般设为派生类。

具体来说,应先通过“顺瓜摸根”法画出类的继承层次图,拟定一些类,然后按上述原则初定基类和派生类,画出类架构图,再检查是否能完成所要做的任务,继承关系是否合理。如不能,再重复上述几步,直到认为合理后再进行后续工作。

当然,可能会在后续画流程图、编写程序及调试程序时发现基类或派生类设置得不合理,不方便编程,那也可按上述几步重新选择基类和派生类,直到设置得较为合理为止。

(4) 怎样声明基类和派生类

基类的声明与定义和第12章所讲的类的声明与定义的方法完全一样。因为如果没有派生类,一个类或多个类都是基类。

派生类的声明与定义和基类的声明与定义的方法基本相同,只是在类名后要加上“:”,后跟继承方式及被继承的基类名。

【例13-2】

class Ustu:public Stu//派生类 Ustu 公有继承了基类 Stu

如果是多重继承,则用逗号间隔,将继承方式和被继承的基类名分别写在“:”后。

【例13-3】

class Student:protected English,protected Computer,protected Chinese //派生类 Student 分别保护继承了基类 English、Computer 和 Chinese

(5) 构造函数的执行顺序

每次创建对象时,构造函数的执行顺序均要从基类开始,运行基类构造函数→第一派生类构造函数→第二派生类构造函数→……→创建这个对象的派生类构造函数。

可参见13.3.3节的例13-4。

(6) 继承方式与继承范围

继承范围即派生类可以使用的被继承类成员的范围,也称为访问属性。

访问属性也有3种:公有成员(public)、保护成员(protected)和私有成员(private)。随着基类和派生类的建立,基类和被继承类的成员在派生类中的访问属性有些就发生了

原书第 217 页

变化,用表13-6来说明。表中圆括号内的内容是在3种继承方式下,基类成员在派生类中的访问属性(即访问范围,该访问范围的含义与第12章的表12-3一致),即继承了之后,被继承类的成员在派生类中可以被使用(访问)的范围,但被继承类的成员在其原有类中的属性不变。注意:表中的访问属性与派生类的原有成员属性无关,原有成员属性已在声明定义派生类时就定义好了。

例如,表中的第3列第2行的圆括号中的内容(保护成员)是指:基类的公有成员被派生类保护继承后,在该派生类中的访问属性变成了保护成员,但在基类中还是公有成员。

从表13-6中可看出:

1)公有继承和保护继承可以访问基类或上一层派生类的公有或保护成员。

2)无论何种继承方式,基类或上一层派生类的私有成员都不能被访问。

继承就是想使用(访问)被继承类里面的东西(数据成员和函数成员),故一般应选择公有继承。

(7) 继承方式的用途

继承方式的用途如表13-7所示。

表 13-7 继承方式的用途
继 承 方 式用 途
公有继承当被继承类的成员(除了私有成员外)需要保持原访问属性时,选择此方式
保护继承当被继承类的成员(除了私有成员外)均需要保护时,选择此方式
私有继承当被继承类的成员(除了私有成员外)均需要变成私有成员时,选择此方式

注:被继承类是指基类或上一层派生类。

(8)怎样运用继承与派生的方法使用基类和派生类

具体请参见例13-1。

2. 关于面向对象的程序设计方法

面向对象的程序设计方法的初步知识已在第12章中有所介绍,该设计方法还是按如图2-3所示的C++程序的开发流程来进行,下面来详细介绍。

(1)用心分析人想通,机随人行写提纲

1)还是按编程三部曲想通人怎么做。

但这时的人怎么做,不是单单想做事情的过程了(这是面向过程的程序设计),而首先要想设置什么对象,通过什么对象去完成题目要求的功能,这才是面向对象的程序设计。

2)设置什么对象。

对象是从属于某个类的,即设置什么对象的关键在于选择类及确定继承与派生的问题,这是基本前提。

3)怎样设置类、继承、派生,以及设计方案选优。

应该采用 “顺瓜摸根” 的办法,因为编程的最终目的是要完成题目要求的功能,所以顺瓜(功能)摸根,就肯定能完成题目要求的功能,不会白做。又因为我们只看到瓜(功能),并不知道这个瓜是从哪里结出来的,即不知道这个瓜的根(基类)在哪里。例如,要求功能是搞个南瓜,采用 “顺瓜摸根” 法是这样进行的(注意,摸的是实体,如支藤、主藤

原书第 218 页

和根,而不是想过程,思想上要从以前面向过程编程时的那种思维方式上扭转过来):

摘到南瓜→在支藤上结着南瓜→顺着支藤找到了主藤→顺着主藤找到了南瓜根

$$ ( 功能 )\quad( 第二派生类 )\quad( 第一派生类 )\quad( 基类 ) $$

这个示意图就是类的继承层次图,继承方向按箭头方向进行。

然后将最通用、最有共性的类作为基类,其他按共性的减少依次排列为第一派生类、第二派生类……最后一层派生类,再与类的继承层次图进行对证,对证合理后,就可以继续进行下一步了。

结合图13-1a所示的方案来细细品味:

$$ 瓜(打印成绩及平均分)\quad\rightarrow\quad 打印 \quad\rightarrow\quad 平均分 \quad\rightarrow\quad 成绩(根) $$

$$ ( 功能 )\quad( 第二派生类 )( 第一派生类 )\quad( 基类 ) $$

从上可知,要摘到瓜(打印成绩及平均分),就要有打印类(支藤);打印什么内容,则要继承平均分类(主藤);平均分从何而来,是继承了成绩类(根),因为有了成绩才能计算平均分。这样就从瓜一直摸到根了。再从根到瓜逆序验证一遍,确认合理后,就可以进行下一步了。

在 “顺瓜摸根” 中,要用大胆创新、灵活多变、开放进取的思想去多方思考,就会得到多种方案,将这些方案对应的 “顺瓜摸根” 图(类的继承层次图)都画出来,先不考虑方案好或不好。然后,将这多个 “顺瓜摸根” 图编上方案号,并列在方案比较表中,方案比较表如表 13-8 所示。

表13-8 方案比较表
方案号继承与派生的合理性、有利性可读性可移植性可维护性编程方便性备注
方案一
方案二
...
方案N

对于方案比较表中列出的多个方案,按照人做事情的方法(即编程三部曲)去想。想什么?想把这些事情(题目要求的功能)放在哪些类里面更合理且更有利于编程,有利于继承和派生,对程序的可读性、可维护性、可移植性较好。根据想的结果填“好、较好、中、一般、不好”,然后进行比较、分析和筛选,从中挑选出最优方案。

4)根据初步选出的最优方案,给每个类详细分配功能和任务,即给每个类画个类架构表,看是否可以完成总体功能及任务,如表13-9所示的基类架构表。

表 13-9 基类架构表
基 类
成 绩 类
功能(函数成员)原材料(数据成员)
输入成绩成绩

5)重新考虑类的继承层次图是否合理,能否优质地完成任务。可以按照“顺瓜摸根”

原书第 219 页

的方法,头脑跟踪运行各个类架构表中具体分配的功能任务,即通过一层层继承与派生的关系,调用各函数成员,并结合数据成员考虑,看是否能完成题目要求的功能。若经过分析,认为类的继承层次图不合理,就重复上述几步;若合理,则转入写提纲或画流程图的步骤。

6)根据按编程三部曲想通的人的做法,分别写出提纲或画出流程图,并头脑跟踪运行,认为正确后再转入下一步。

(2)对号入座写文章,头脑运行改文章

根据选择的最优方案、类的继承层次图、类的架构表,以及提纲或流程图编写程序。在编写程序时,一般将各个类的声明写成相应的头文件,最好一个头文件对应一个类的声明,并对应一个源文件定义类,也就是各个源文件就对应其相应的头文件进行定义,而且,头文件和相应的源文件最好同名(注意:扩展名不同),如学生类的头文件名为 student.h,源文件名为 student.cpp,最后,再加个主文件(写主函数)。

再经过头脑运行改文章,认为正确后转入下一步。

(3)C++帮助且裁判,人定胜机凯歌扬

这一步与以前一样,只不过在输入和调试时从最小的单元(即最少的程序代码)开始,调试通过一个程序单元后,再加下一个程序单元。

13.3.3 如何运用继承、派生及面向对象的程序设计方法

按 13.3.2 节中所述的方法来运用继承、派生及进行面向对象的程序设计。

【例 13-4】请设计一个继承、派生及面向对象的程序,其中,有一个基类,两个派生类,一个继承一个,继承方式自行决定,以能继承为准,每个类均有一个字符串(自行设计),要求程序能在构造函数中打印自己类中的字符串,以演示多继承的构造函数构造(执行)过程。

根据前面所讲的知识,编写程序如下。

//ex13-10.cpp
#include<iostream>
#include<string>
using namespace std;

class Sco1//声明基类
{

protected:

string str1;

public:

Sco1()

{
    str1="正在运行基类的构造函数";
    cout<<str1<<endl;
}
};

class Ave1:public Sco1//声明第一派生类,并公有继承基类
原书第 220 页

protected:

string str2;

public:

    Ave1()
{
        str2="接着运行第一派生类的构造函数";
        cout<<str2<<endl;
    }
};

class Pri1:protected Avel1//声明第二派生类,并保护继承第一派生类
{
    protected:
        string str3;
        public:
            Pri1()
        {
            str3="最后运行定义该对象类的构造函数";
            cout<<str3<<endl;
        }
    };

    void main()
{
        Pri1 d1;//定义打印类的对象,构造函数的过程也体现了继承与派生的关系
    }
}

行结果:

运行结果:

正在运行基类的构造函数

接着运行第一派生类的构造函数

最后运行定义该对象类的构造函数

结果正确。

请读者对照程序跟踪运行结果,细细品味构造函数的构造(执行)过程。还可参考第20章的“仓库材料管理软件”综合设计实例。

13.4 继承、派生及面向对象的程序设计方法的亮点

1. 在什么情况下用继承、派生及面向对象的程序设计方法的亮点

当需要解决问题的事物(对象)中包含继承与派生的关系时,就应该用继承、派生及面向对象的程序设计方法。

  1. 继承、派生及面向对象的程序设计方法具体知识的亮点

1)从某一个类衍生出另一个类,这就是派生,派生类的某些特性继承于派生它的类。

原书第 221 页

2)继承与派生可分为公有继承(public)、保护继承(protected)和私有继承(private)3类。

3)凡具有通用性或可以被其他程序利用的类,就把它设为基类,反之,一般设为派生类。

4)派生类的声明与定义和基类的声明与定义的方法基本相同,只是在类名后要加上“:”,后跟继承方式及被继承的基类名。

5)注意继承方式与继承范围。

3. 运用继承、派生及面向对象的程序设计方法的亮点

真正面向对象的程序设计方法是要考虑继承与派生的,其关键在于设计基类和派生类。运用“顺瓜摸根”法,画出正确合理的类的继承层次图,设计基类及派生类,其后的方法与单个类的设计和使用方法基本相同。

← 第12章 类、对象与面向对象程序设计第14章 运算符重载 →