1、第第2章章 继承与派生继承与派生n本章学习重点掌握内容:本章学习重点掌握内容:n继承的概念继承的概念n派生类的建立及继承的方式派生类的建立及继承的方式n各种继承方式下基类成员的访问机制各种继承方式下基类成员的访问机制n派生类的构造函数和析构函数派生类的构造函数和析构函数n多重继承多重继承n虚基类虚基类11/14/20221第第2章章 继承与派生继承与派生n2.1 继承与派生的基础知识继承与派生的基础知识 n2.2 类的继承方式类的继承方式 n2.3 派生类的构造函数与析构函数派生类的构造函数与析构函数 n2.4 基类与派生类的转换基类与派生类的转换 n2.5 多重继承多重继承 n2.6 虚基类
2、虚基类n2.7 综合应用实例综合应用实例11/14/202222.1 继承与派生的基础知识继承与派生的基础知识2.1.1 继承与派生的基本概念继承与派生的基本概念 现实世界中,许多事物之间的并不是孤立存在的,现实世界中,许多事物之间的并不是孤立存在的,它们存在共同的特性,有细微的差别,可以使用层次它们存在共同的特性,有细微的差别,可以使用层次结构描述它们之间的关系。例如交通工具的层次结构结构描述它们之间的关系。例如交通工具的层次结构如图如图2.1所示:所示:11/14/202232.1.1 继承与派生的基本概念继承与派生的基本概念 C+通过类派生(通过类派生(Class Derivation)
3、的机制支)的机制支持继承(持继承(Inheritance)。允许程序员在保持原有类)。允许程序员在保持原有类特性的基础上进行扩展,增加功能,派生出新类。继特性的基础上进行扩展,增加功能,派生出新类。继承是面向对象程序设计中的代码复用的最重要的手段承是面向对象程序设计中的代码复用的最重要的手段之一。被继承的类称为之一。被继承的类称为基类基类(Base Class)、父类或)、父类或超类(超类(Superclass),而新产生的类称为),而新产生的类称为派生类派生类(Derived Class)或子类()或子类(Subclass)。)。基类和派基类和派生类的集合称作类继承层次结构(生类的集合称作类
4、继承层次结构(Hierarchy),继),继承呈现了面向对象程序设计的层次结构。承呈现了面向对象程序设计的层次结构。11/14/202242.1.1 继承与派生的基本概念继承与派生的基本概念 一个新类从已有的类获得其已有的特性称为继承一个新类从已有的类获得其已有的特性称为继承。通过继承,新类获得了父类的所有数据成员和成员函通过继承,新类获得了父类的所有数据成员和成员函数,并可以添加自己的数据成员和成员函数。一个基数,并可以添加自己的数据成员和成员函数。一个基类可以派生出很多的子类,一个子类也可以作为另一类可以派生出很多的子类,一个子类也可以作为另一个新类的基类,因此基类和子类是相对而言的。继承
5、个新类的基类,因此基类和子类是相对而言的。继承的方式有以下的方式有以下2种:种:单一继承和多重继承单一继承和多重继承。11/14/202252.1.1 继承与派生的基本概念继承与派生的基本概念单一继承和多重继承单一继承和多重继承请注意图中箭头的方向,本书约定,箭头表示继承的方向,由子类指请注意图中箭头的方向,本书约定,箭头表示继承的方向,由子类指向基类。向基类。11/14/202262.1.2 派生类的定义派生类的定义定义派生类的一般格式为:定义派生类的一般格式为:class 派生类名:继承方式派生类名:继承方式 基类名基类名private:成员表成员表1;/派生类增加或重写的私有成员派生类增
6、加或重写的私有成员protected:成员表成员表2;/派生类增加或重写的保护成员派生类增加或重写的保护成员public:成员表成员表3;/派生类增加或重写的公有成员派生类增加或重写的公有成员;11/14/202272.1.2 派生类的定义派生类的定义其中:基类名是已声明的类,派生类名是新生成的类名;其中:基类名是已声明的类,派生类名是新生成的类名;继承方式规定了如何访问从基类继承的成员。继承的方式包继承方式规定了如何访问从基类继承的成员。继承的方式包括:私有继承(括:私有继承(private)、保护继承()、保护继承(protected)、)、公有继公有继承(承(public)。不同的继承方
7、式下,派生类继承的父类成员的访)。不同的继承方式下,派生类继承的父类成员的访问权限是不同的。继承方式可以省略不写,默认的继承方式为私问权限是不同的。继承方式可以省略不写,默认的继承方式为私有继承(有继承(private););派生类的定义中包括子类新增加的成员和继承父类需要重写派生类的定义中包括子类新增加的成员和继承父类需要重写的成员。新添加的成员是派生类对基类的发展,说明派生类新的的成员。新添加的成员是派生类对基类的发展,说明派生类新的属性和方法;派生类继承了父类的数据成员和成员函数,有时继属性和方法;派生类继承了父类的数据成员和成员函数,有时继承来的成员函数需要改进,以满足新类的实际需要。
8、承来的成员函数需要改进,以满足新类的实际需要。C+允许在允许在派生类中重新声明和定义这些成员函数,使这些函数具有新的功派生类中重新声明和定义这些成员函数,使这些函数具有新的功能,称之为重写或覆盖。重写函数起屏蔽、更新作用,取代基类能,称之为重写或覆盖。重写函数起屏蔽、更新作用,取代基类成员,完成新功能。成员,完成新功能。11/14/202282.1.2 派生类的定义派生类的定义【例例2.1】已知盒子已知盒子CBox类,用继承与非继承两种不同的方法定类,用继承与非继承两种不同的方法定义彩色盒子义彩色盒子CColorbox类。类。分析:盒子类(分析:盒子类(Cbox)具有长、宽和高,成员函数)具有
9、长、宽和高,成员函数SetLength()、()、SetWidth()和()和SetHeight()分别设置盒子的长、宽()分别设置盒子的长、宽和高,成员函数和高,成员函数Volume()计算盒子的体积。彩色盒子除具有以计算盒子的体积。彩色盒子除具有以上特性外,还有一个数据成员上特性外,还有一个数据成员color表示盒子的颜色,相应的成表示盒子的颜色,相应的成员函数员函数SetColor()用于设置彩色盒子的颜色。()用于设置彩色盒子的颜色。11/14/202292.1.2 派生类的定义派生类的定义非继承的方式,分别定义非继承的方式,分别定义CBox类和类和CColorbox类类盒子类的定义:
10、盒子类的定义:代码见备注代码见备注彩色盒子类的定义:代码见备注彩色盒子类的定义:代码见备注11/14/2022102.1.2 派生类的定义派生类的定义使用派生类定义:使用派生类定义:class CColorbox:class CColorbox:public CBoxpublic CBox /公有继承公有继承/新增的私有数据成员新增的私有数据成员public:public:void SetColor(int c)/void SetColor(int c)/新增的成员函数新增的成员函数 color=c;color=c;private:private:int color;int color;利用继
11、承机制产生类比第一种简单多了,但功能一样。派生类利用继承机制产生类比第一种简单多了,但功能一样。派生类CColorboxCColorbox公有继承公有继承CboxCbox类,它包括基类类,它包括基类CBoxCBox类的全部数据成员类的全部数据成员(lengthlength,widthwidth,heightheight)和成员函数()和成员函数(SetWidth SetWidth、SetHeighSetHeigh和和SetWidthSetWidth),但访问权限发生了变化。并且添加自己的新成员),但访问权限发生了变化。并且添加自己的新成员数据成员数据成员colorcolor和成员函数和成员函数
12、SetColor()SetColor()。11/14/2022112.1.3 派生类的生成派生类的生成仔细分析派生新类这个过程,实际是经历了以下步骤:仔细分析派生新类这个过程,实际是经历了以下步骤:首先首先继承基类的成员,不论是数据成员,还是成员函数,除构造继承基类的成员,不论是数据成员,还是成员函数,除构造函数与析构函数外全部接收,全部成为派生类的成员。函数与析构函数外全部接收,全部成为派生类的成员。第二步第二步是重写基类成员。当基类成员在派生类的应用中不合适时,是重写基类成员。当基类成员在派生类的应用中不合适时,可以对继承的成员加以重写。如果派生类声明了一个与基类成员可以对继承的成员加以重
13、写。如果派生类声明了一个与基类成员函数相同的成员函数时,派生类中的新成员则屏蔽了基类同名成函数相同的成员函数时,派生类中的新成员则屏蔽了基类同名成员,类似函数中的局部变量屏蔽全局变量。称为同名覆盖员,类似函数中的局部变量屏蔽全局变量。称为同名覆盖(Override)。)。第三步第三步定义新成员。新成员必须与基类成员不同名,是派生类自定义新成员。新成员必须与基类成员不同名,是派生类自己的新特性。派生类新成员的加入使得派生类在功能上有所发展。己的新特性。派生类新成员的加入使得派生类在功能上有所发展。这一步是继承与派生的核心特征。这一步是继承与派生的核心特征。第四步第四步是重写构造函数与析构函数。因
14、为派生类不继承基类的构是重写构造函数与析构函数。因为派生类不继承基类的构造函数与析构函数,并且派生类的需要对新添加的数据成员进行造函数与析构函数,并且派生类的需要对新添加的数据成员进行必要的初始化,所以构造函数与析构函数需要重写。必要的初始化,所以构造函数与析构函数需要重写。11/14/2022122.2 类的继承方式类的继承方式 派生类中包含基类的成员和派生类自己增加的成派生类中包含基类的成员和派生类自己增加的成员,那么这两部分的成员关系和访问权限该如何确定员,那么这两部分的成员关系和访问权限该如何确定呢?在继承机制中,并不是简单的把基类的私有成员呢?在继承机制中,并不是简单的把基类的私有成
15、员直接作为派生类的私有成员,把基类的公有成员直接直接作为派生类的私有成员,把基类的公有成员直接作为派生类的公有成员。派生类继承的基类成员访问作为派生类的公有成员。派生类继承的基类成员访问权限由继承方式来控制。权限由继承方式来控制。继承方式有三种:继承方式有三种:public(公有)继承(公有)继承、protected(保护)继承保护)继承和和private(私有)(私有)继承。不同的继承继承。不同的继承方式,决定了从基类继承来的成员的访问权限。下面方式,决定了从基类继承来的成员的访问权限。下面分别介绍不同继承方式下,派生类成员的访问权限。分别介绍不同继承方式下,派生类成员的访问权限。11/14
16、/2022132.2.1 公有继承公有继承当定义一个派生类时,将基类前的继承方式指定为当定义一个派生类时,将基类前的继承方式指定为public,则称为则称为公有派生(或公有继承)。公有派生(或公有继承)。采用公有继承方式时,基类采用公有继承方式时,基类的公有成员和保护成员的访问权限在派生类中不变。而基类的的公有成员和保护成员的访问权限在派生类中不变。而基类的私有成员在派生类中是不可访问。但它仍然是基类的私有成员,私有成员在派生类中是不可访问。但它仍然是基类的私有成员,如果需要在派生类中引用继承基类的私有成员,那么需要通过如果需要在派生类中引用继承基类的私有成员,那么需要通过基类的公有或保护的成
17、员函数访问。基类的公有或保护的成员函数访问。【例例2.3】演示公有继承方式下,不同成员的访问权限。程序代演示公有继承方式下,不同成员的访问权限。程序代码见备注:码见备注:11/14/2022142.2.1 公有继承公有继承【例例2.4】公有派生方式下如何访问继承的基类原有私有数据成公有派生方式下如何访问继承的基类原有私有数据成员。程序代码见备注:员。程序代码见备注:11/14/2022152.2.2 私有继承私有继承 当定义一个派生类时,将基类前的继承方式指定为当定义一个派生类时,将基类前的继承方式指定为privateprivate,则称为私有继承。则称为私有继承。用私有继承方式建立的派生类称
18、为私有派生类,用私有继承方式建立的派生类称为私有派生类,其基类成为私有基类。采用私有继承方式时,私有基类的公有成其基类成为私有基类。采用私有继承方式时,私有基类的公有成员和保护成员在私有派生类中成为私有成员。即派生类成员可访员和保护成员在私有派生类中成为私有成员。即派生类成员可访问它们,而派生类外不可访问它们。基类的私有成员在派生类中问它们,而派生类外不可访问它们。基类的私有成员在派生类中成为不可访问的成员。私有继承基类成员的访问权限如表成为不可访问的成员。私有继承基类成员的访问权限如表2-22-2所所示。私有继承的意义是将基类中原来能被外部访问的成员隐藏起示。私有继承的意义是将基类中原来能被
19、外部访问的成员隐藏起来,不让外界引用。来,不让外界引用。【例例2.5】私有继承演示。私有继承演示。11/14/2022162.2.2 私有继承私有继承由上例可以看到私有继承方式:由上例可以看到私有继承方式:(1 1)不能通过派生类对象()不能通过派生类对象(box1box1)引用从私有继承过来的任何)引用从私有继承过来的任何成员。如成员。如box1.set(3,5,6);box1.set(3,5,6);或或box1.length=100box1.length=100。(2 2)在派生类内部(如派生类的成员函数),不可以访问基类)在派生类内部(如派生类的成员函数),不可以访问基类的私有成员(如的
20、私有成员(如length=lenlength=len,lengthlength为基类的私有成员),但可为基类的私有成员),但可以访问基类的公有和保护成员(如以访问基类的公有和保护成员(如height=hheight=h,heightheight为基类的保为基类的保护成员)。护成员)。(3 3)如果派生类需要访问基类的私有成员,可以通过派生类的)如果派生类需要访问基类的私有成员,可以通过派生类的成员函数调用基类的公有成员函数实现如:成员函数调用基类的公有成员函数实现如:void set_1(double len,double w,double h,int c)void set_1(double
21、len,double w,double h,int c)set(len,w,h);/set(len,w,h);/基类的公有成员函数基类的公有成员函数 由上可以看出,私有派生的限制太多,一般不经常使用。由上可以看出,私有派生的限制太多,一般不经常使用。11/14/2022172.2.3 保护继承保护继承当定义一个派生类时,将基类前的继承方式指定为当定义一个派生类时,将基类前的继承方式指定为protectedprotected,则称为保护继承。则称为保护继承。在保护继承中,基类的公有成员和保护成员成在保护继承中,基类的公有成员和保护成员成为派生类的保护成员,在派生类中可以直接访问,但在派生类外为派
22、生类的保护成员,在派生类中可以直接访问,但在派生类外不能直接访问任何基类成员的。基类中的私有成员成为派生类的不能直接访问任何基类成员的。基类中的私有成员成为派生类的不可访问成员,在派生类中不可直接访问。保护继承基类成员的不可访问成员,在派生类中不可直接访问。保护继承基类成员的访问权限如表访问权限如表2-32-3所示。保护继承的意义是将基类的公有成员也所示。保护继承的意义是将基类的公有成员也保护起来,不让类外部任意访问。保护起来,不让类外部任意访问。11/14/202218继承的方式有三种,使用不同继承方式,基类的成员在派生类中继承的方式有三种,使用不同继承方式,基类的成员在派生类中的访问权限也
23、不同。不同继承方式下基类成员在派生类的访问权的访问权限也不同。不同继承方式下基类成员在派生类的访问权限总结如表限总结如表2-42-4所示。所示。2.2.4 继承方式的总结和比较继承方式的总结和比较11/14/2022192.3 派生类的构造函数与析构函数派生类的构造函数与析构函数 派生类的成员是由派生类的成员是由基类中的数据成员基类中的数据成员和和派生类中新增的数据派生类中新增的数据成员成员共同构成。而在继承机制下,构造函数不能够被继承。因此,共同构成。而在继承机制下,构造函数不能够被继承。因此,对继承过来的基类成员的初始化工作也得由派生类的构造函数完对继承过来的基类成员的初始化工作也得由派生
24、类的构造函数完成。也就是说在定义派生类的构造函数时,既要初始化派生类新成。也就是说在定义派生类的构造函数时,既要初始化派生类新增数据,又要初始化基类的成员。所以,在定义派生类的构造函增数据,又要初始化基类的成员。所以,在定义派生类的构造函数时,有两步需要做:数时,有两步需要做:编写代码完成自己的数据成员进行初始化编写代码完成自己的数据成员进行初始化调用基类构造函数使基类数据成员得以初始化。调用基类构造函数使基类数据成员得以初始化。11/14/2022202.3.1 简单派生类的构造函数简单派生类的构造函数 单一继承的构造函数的定义形式为:单一继承的构造函数的定义形式为:派生类名派生类名:派生类
25、构造函数名派生类构造函数名(参数总表参数总表):):基类构造函数名基类构造函数名 (参数名表参数名表)派生类新增成员的初始化语句派生类新增成员的初始化语句;定义派生类的构造函数时,在构造函数的参数总表中包括定义派生类的构造函数时,在构造函数的参数总表中包括基基类构造函数所需的参数和派生类新增的数据成员初始化所需的参类构造函数所需的参数和派生类新增的数据成员初始化所需的参数数。冒号后面基类构造函数名。冒号后面基类构造函数名 (参数名表参数名表),表示要调用基类的,表示要调用基类的构造函数。构造函数。【例例2.62.6】演示派生类的构造函数执行顺序。演示派生类的构造函数执行顺序。11/14/202
26、2212.3.2 析构函数析构函数 析构函数析构函数的功能是做善后工作,析构函数无返回类型也没有的功能是做善后工作,析构函数无返回类型也没有参数,情况比较简单。在派生过程中,基类的析构函数不能继承,参数,情况比较简单。在派生过程中,基类的析构函数不能继承,如果需要析构函数的话,要在派生类中重新定义。派生类析构函如果需要析构函数的话,要在派生类中重新定义。派生类析构函数定义格式与非派生类无任何差异,只要在函数体内把派生类新数定义格式与非派生类无任何差异,只要在函数体内把派生类新增一般成员处理好就可以了。而对基类成员的善后工作,系统会增一般成员处理好就可以了。而对基类成员的善后工作,系统会自己调用
27、基类的析构函数来完成。如果没有显示的定义析构函数,自己调用基类的析构函数来完成。如果没有显示的定义析构函数,系统会自动生成一个默认的析构函数,清理工作就是靠它们来完系统会自动生成一个默认的析构函数,清理工作就是靠它们来完成的。成的。析构函数各部分执行次序与构造函数相反,首先对派生类新析构函数各部分执行次序与构造函数相反,首先对派生类新增成员析构,然后对基类成员析构。增成员析构,然后对基类成员析构。11/14/2022222.3.3 复杂派生类的构造函数和析构函数复杂派生类的构造函数和析构函数 一个派生类中新增加的成员可以是简单的数据成员,也可以一个派生类中新增加的成员可以是简单的数据成员,也可
28、以是类对象。派生类可以是单一继承,也可以是多重继承。假如派是类对象。派生类可以是单一继承,也可以是多重继承。假如派生类是多重继承,并且新增数据成员有一个或多个类对象,那么生类是多重继承,并且新增数据成员有一个或多个类对象,那么派生类需要初始化的数据有三部分:继承的成员、新增类对象的派生类需要初始化的数据有三部分:继承的成员、新增类对象的成员和新增普通成员。这种复杂派生类的构造函数定义如下:成员和新增普通成员。这种复杂派生类的构造函数定义如下:派生类名派生类名:派生类构造函数名派生类构造函数名(总参数表总参数表):基类构造函数名基类构造函数名1(1(参数表参数表1)1),基类构造函数名基类构造函
29、数名2(2(参数表参数表2)2),子对象名子对象名1 1(参数表(参数表n n),),子对象名子对象名2 2(数表(数表n+1n+1)派生类新增普通数据成员的初始化;派生类新增普通数据成员的初始化;11/14/2022232.3.3 复杂派生类的构造函数和析构函数复杂派生类的构造函数和析构函数派生类构造函数的调用顺序派生类构造函数的调用顺序如下:如下:1.1.基类构造函数基类构造函数。按它们在派生类定义中的先后顺序,依次调用。按它们在派生类定义中的先后顺序,依次调用。2.2.子对象的构造函数子对象的构造函数。按它们在派生类定义中的先后顺序,依次。按它们在派生类定义中的先后顺序,依次调用。调用。
30、3.3.派生类的构造函数派生类的构造函数。复杂派生类的析构函数,只需要编写对新增普通成员的善后处复杂派生类的析构函数,只需要编写对新增普通成员的善后处理,而对类对象和基类的善后工作是由类对象和基类的析构函数理,而对类对象和基类的善后工作是由类对象和基类的析构函数完成的。析构函数的调用顺序与构造函数相反。完成的。析构函数的调用顺序与构造函数相反。【例例2.7】复杂继承举例。复杂继承举例。11/14/2022242.3.3 复杂派生类的构造函数和析构函数复杂派生类的构造函数和析构函数【例例2.7】复杂复杂继承举例。继承举例。11/14/2022252.3.3 复杂派生类的构造函数和析构函数复杂派生
31、类的构造函数和析构函数在派生类构造函数使用中应注意以下问题:在派生类构造函数使用中应注意以下问题:(1)派生类构造函数的定义中可以省略对基类构造函数的调用,其派生类构造函数的定义中可以省略对基类构造函数的调用,其条件是在基类中必须有缺省的构造函数或者根本没有定义构造条件是在基类中必须有缺省的构造函数或者根本没有定义构造函数。当然,基类中没有定义构造函数,派生类调用基类的缺函数。当然,基类中没有定义构造函数,派生类调用基类的缺省构造函数。省构造函数。(2)当基类的构造函数使用一个或多个参数时,则派生类必须定义当基类的构造函数使用一个或多个参数时,则派生类必须定义构造函数,提供将参数传递给基类构造
32、函数途径。在有的情况构造函数,提供将参数传递给基类构造函数途径。在有的情况下,派生类构造函数的函数体可能为空,仅起到参数传递作用。下,派生类构造函数的函数体可能为空,仅起到参数传递作用。11/14/2022262.4 基类与派生类的转换基类与派生类的转换基类与派生类对象之间有赋值兼容关系,由于派生类中包含从基类继基类与派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。具体表现在以下几个方面:的时候可以用其子类对象代替。具体表现在以下几个方面:(1)
33、派生类对象可以向基类对象赋值派生类对象可以向基类对象赋值(2)可以用子类可以用子类(即公用派生类即公用派生类)对象对其基类对象赋值。如对象对其基类对象赋值。如(3)Cbox与与CColorbox:(4)CBox box;/定义基类定义基类CBox 对象对象box(5)CColorbox colorbox;/定义类定义类Ccolorbox的对象的对象colorbox(6)box=colorbox;/用派生类用派生类B对象对象b1对基类对象对基类对象a1赋赋值值(7)在赋值时舍弃派生类自己的成员。实际上,所谓赋值只是对数据成在赋值时舍弃派生类自己的成员。实际上,所谓赋值只是对数据成员赋值,对成员函
34、数不存在赋值问题。员赋值,对成员函数不存在赋值问题。11/14/2022272.4 基类与派生类的转换基类与派生类的转换(2)派生类对象可以向基类对象的引用进行赋值或初始化派生类对象可以向基类对象的引用进行赋值或初始化CBox box;/定义基类定义基类CBox 对象对象boxCColorbox colorbox;/定义派生类定义派生类Ccolorbox的对象的对象colorboxCBox&r1=box;这时,引用变量这时,引用变量r1是是box的别名,的别名,r1和和box共享同一段存储单元。也可共享同一段存储单元。也可以用子类对象初始化基类的引用变量,例如:以用子类对象初始化基类的引用变量
35、,例如:CBox&r2=colorbox;或者保留上面第或者保留上面第3行行“CBox&r1=box;”,而,而对对r1重新赋值:重新赋值:r1=colorbox;注意:将派生类对象赋值给基类引用。注意:将派生类对象赋值给基类引用。此时基类引用并不是派生类对象此时基类引用并不是派生类对象的别名,也不与派生类共享同一段存储单元。它只是派生类中基类部的别名,也不与派生类共享同一段存储单元。它只是派生类中基类部分的别名,基类引用与派生类中基类部分共享同一段存储单元。例如:分的别名,基类引用与派生类中基类部分共享同一段存储单元。例如:CBox&r2=colorbox;r2与与colorbox具有相同的
36、起始地址。具有相同的起始地址。11/14/2022282.4 基类与派生类的转换基类与派生类的转换(3)派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。例如:向基类对象的指针变量也可以指向派生类对象。例如:CBox box;/定义基类定义基类CBox 对象对象boxCColorbox colorbox;/定义类定义类CBox的公用派生类的公用派生类Ccolorbox的对象的对象colorboxCBox*pt=&box;/定义基类定义基类CBox 对象指针变量对象指针变量pt并指向并
37、指向boxpt-SetHeight(10);/调用调用box.SetHeight()函数函数pt=&colorbox;/将派生类基类地址赋值给基类指针,即将派生类基类地址赋值给基类指针,即pt指向指向colorboxpt-SetHeight(10);/调用调用colorbox.SetHeight()函数函数11/14/2022292.5 多重继承多重继承 前面主要介绍了单一继承,仅仅提到多重继承的概念。在现实世前面主要介绍了单一继承,仅仅提到多重继承的概念。在现实世界中,很多时候一个类会有两个或两个以上的基类。例如沙发床,界中,很多时候一个类会有两个或两个以上的基类。例如沙发床,既继承了床的特
38、性又继承了沙发的特性。沙发床的多重继承结构图既继承了床的特性又继承了沙发的特性。沙发床的多重继承结构图如图如图2.7所示。所示。C+中,定义派生类时,派生类有两个或多个基类中,定义派生类时,派生类有两个或多个基类称为多重继承。称为多重继承。11/14/2022302.5 多重继承多重继承2.5.1 多重继承的定义多重继承的定义多重继承可以看作是单一继承的扩展,多重继承的定义格式如下:多重继承可以看作是单一继承的扩展,多重继承的定义格式如下:class 派生类名:派生类名:继承方式继承方式1 基类名基类名1,继承方式继承方式2 基类名基类名,public:新增加的公有成员;新增加的公有成员;pr
39、otected:新增加的保护成员;新增加的保护成员;private:新增加的私有成员;新增加的私有成员;;11/14/2022312.5 多重继承多重继承多重继承派生类的构造函数多重继承派生类的构造函数格式如下:格式如下:派生类名派生类名:派生类构造函数名派生类构造函数名(总参数表总参数表):基类名基类名1(参数表参数表1),基类名基类名2(参数表参数表2)派生类构造函数体派生类构造函数体其中,总参数表中各个参数包含了其后基类的各个分参数表。多重继承其中,总参数表中各个参数包含了其后基类的各个分参数表。多重继承下派生类的构造函数与单一继承下派生类构造函数相似,它必须同下派生类的构造函数与单一继
40、承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。同时,派生类的参数必时负责该派生类所有基类构造函数的调用。同时,派生类的参数必须包含完成所有基类初始化所需的参数。须包含完成所有基类初始化所需的参数。11/14/2022322.5.1 多重继承的定义多重继承的定义【例例2.8】设计沙发床类。设计沙发床类。分析:床类可以用来睡觉分析:床类可以用来睡觉Sleep(),沙发类可以用来看电视,沙发类可以用来看电视WatchTV()。沙发床具有床和沙发两者的特特性,沙发床还有自。沙发床具有床和沙发两者的特特性,沙发床还有自己的特性己的特性折叠折叠FoldOut()。因此先定义床类和沙
41、发类,沙发床。因此先定义床类和沙发类,沙发床类由这两个类派生。然后添加沙发床自己的特性。程序代码如下:类由这两个类派生。然后添加沙发床自己的特性。程序代码如下:11/14/2022332.5.1 多重继承的定义多重继承的定义沙发床类继承了床类和沙发类,具有了沙发和床的特性,沙发床类继承了床类和沙发类,具有了沙发和床的特性,因此可以使用派生类对象因此可以使用派生类对象ss调用床的调用床的Sleep()成员函数,成员函数,完成睡觉功能;通过调用沙发完成睡觉功能;通过调用沙发WatchTV()成员函数完成员函数完成看电视功能;调用新增的成看电视功能;调用新增的FoldOut()可以完成折叠与可以完成
42、折叠与打开功能。派生类对象调用构造函数的顺序是先基类的打开功能。派生类对象调用构造函数的顺序是先基类的构造函数再派生类的构造函数,有两个基类(构造函数再派生类的构造函数,有两个基类(Cbed和和Csofa)时,基类构造函数调用的顺序是按照声明派生)时,基类构造函数调用的顺序是按照声明派生类时基类的排列顺序来进行。类时基类的排列顺序来进行。11/14/2022342.5.2 多重继承中的二义性问题多重继承中的二义性问题 多重继承反映了现实生活中的情况,使一些复杂的问题简单化,多重继承反映了现实生活中的情况,使一些复杂的问题简单化,提高了程序开发效率。但多重继承中存在两类二义性问题:提高了程序开发
43、效率。但多重继承中存在两类二义性问题:1.调用不同基类中相同成员时产生的二义性调用不同基类中相同成员时产生的二义性 派生类的多个基类之间出现相同的成员,则在派生类中访问此成派生类的多个基类之间出现相同的成员,则在派生类中访问此成员时会出现二义性。员时会出现二义性。11/14/2022352.5.2 多重继承中的二义性问题多重继承中的二义性问题 解决多重继承中调用不同基类中相同成员时产生的二义性可以使用以解决多重继承中调用不同基类中相同成员时产生的二义性可以使用以下方法:下方法:(1)使用域作用符)使用域作用符解决二义性问题可以使用解决二义性问题可以使用:域作用符对此成员函数加以区分:域作用符对
44、此成员函数加以区分:ob1.CBed:SetWeight(100);/调用基类调用基类Cbed的函数成员的函数成员SetWeight()()ob1.CSofa:SetWeight(200);/调用基类调用基类CSofa的函数成员的函数成员SetWeight()()使用域作用符可以消除编译时二义性。程序员需要知道类的继承层次信息,使用域作用符可以消除编译时二义性。程序员需要知道类的继承层次信息,加大程序开发的复杂度。加大程序开发的复杂度。11/14/2022362.5.2 多重继承中的二义性问题多重继承中的二义性问题(2)覆盖函数同名隐藏(覆盖)覆盖函数同名隐藏(覆盖)派生类继承了两个基类的派生
45、类继承了两个基类的print()函数,派生类在自己的构造函数中函数,派生类在自己的构造函数中又重写了又重写了print()函数。如果在主函数如下:函数。如果在主函数如下:void main()CSleeperSofa ob1;/继承了两个基类的特性继承了两个基类的特性ob1.print();/正确,正确,print()是覆盖函数不存在二义性是覆盖函数不存在二义性ob1.print();语句能通过编译,这是因为派生类新增的成员函数语句能通过编译,这是因为派生类新增的成员函数print()覆盖了基类中的同名成员,这点与局部变量屏蔽全局变量类似。()覆盖了基类中的同名成员,这点与局部变量屏蔽全局变量
46、类似。可以使用域作用符调用基类的成员函数。如可以使用域作用符调用基类的成员函数。如ob1.CBed:print(),则调用基类则调用基类Cbed的的print()。11/14/2022372.5.2 多重继承中的二义性问题多重继承中的二义性问题注意:在不是虚函数的情况下,如果派生类中新增的成员函数与基类的某一成员函数同注意:在不是虚函数的情况下,如果派生类中新增的成员函数与基类的某一成员函数同名,则该函数会隐藏基类中所有该函数的重载函数。名,则该函数会隐藏基类中所有该函数的重载函数。#includeclass CA public:void f(int x)coutthe f(int)of CA
47、!endl;void f()coutthe f()of CA!endl;class CB:public CA public:void f()coutthe f()of CB!endl;void main()CB b;b.f();b.f(0);11/14/2022382.5.2 多重继承中的二义性问题多重继承中的二义性问题2派生类中访问公共基类成员时产生的二义性派生类中访问公共基类成员时产生的二义性 派生类有多个基类,而这多个基类又从同一个基类派生,则在派派生类有多个基类,而这多个基类又从同一个基类派生,则在派生类中访问公共基类成员时会出现二义性。此时引入虚基类解决。生类中访问公共基类成员时会出
48、现二义性。此时引入虚基类解决。11/14/2022392.5.2 多重继承中的二义性问题多重继承中的二义性问题【例例2.9】一个公共基类在派生类中产生的二义性问题。一个公共基类在派生类中产生的二义性问题。类类B与类与类C由类由类A公有派生,而类公有派生,而类D由类由类B与类与类C公有派生公有派生(如图如图2.9所示所示),则类则类D中将包含类中将包含类A的两个拷贝。这种同一个基类在派生类中产生多个的两个拷贝。这种同一个基类在派生类中产生多个拷贝不仅多占用了存储空间,而且可能会造成二义性问题。拷贝不仅多占用了存储空间,而且可能会造成二义性问题。11/14/2022402.6 虚基类虚基类 在多重
49、继承中,虚基类保证从不同路径继承过来的公共基类成员在多重继承中,虚基类保证从不同路径继承过来的公共基类成员在内存中就只有一个拷贝,所以可解决同名基类成员产生的二义性在内存中就只有一个拷贝,所以可解决同名基类成员产生的二义性问题。问题。2.6.1 虚基类的定义虚基类的定义虚基类的定义是在派生类的定义过程中定义的,语法格式如下:虚基类的定义是在派生类的定义过程中定义的,语法格式如下:class 派生类名派生类名:virtual 继承方式继承方式 基类类名基类类名.;其中,其中,virtual 关键字只对紧随其后的基类名起作用。关键字只对紧随其后的基类名起作用。上述定义使得基类为派生类的虚基类,定义
50、了虚基类之后,虚基上述定义使得基类为派生类的虚基类,定义了虚基类之后,虚基类的成员在派生过程中和派生类一起维护同一个内存拷贝。类的成员在派生过程中和派生类一起维护同一个内存拷贝。【例例2.10】用虚基类的方法解决公共基类成员产生的二义性。用虚基类的方法解决公共基类成员产生的二义性。11/14/2022412.6 虚基类虚基类(1)采用虚基类后,在)采用虚基类后,在D类中只有唯一的数据成员类中只有唯一的数据成员x;所以在建立;所以在建立D类类的对象后,调用的对象后,调用Print()输出()输出x时,不产生二义性。时,不产生二义性。(2)A类为虚基类以后,公共基类类为虚基类以后,公共基类A的构造