第9章继承机制.ppt
第9章 继承机制,1,本讲主要内容,继承与派生的基本概念类的继承方式单继承多重继承虚基类(重复继承),2,单派生,多重派生,一 继承的基本概念,1.继承与“is a”关系 继承的一个常见用途是表达不同类型对象的“is a”关系。如一个圆“is a”形状,一个矩形“is a”形状,则circle_shape和rectangle_shape类可以从shape类中继承,这种属性称为“is a”关系。在C+中,称circle_shape和rectangle_shape类是从基类shape中派生的。下图可以说明这一概念。,3,可以说,类的继承和派生的层次结构,是人们对自然界中的事物进行分类、分析和认识的过程在程序设计中的体现。是个性与共性的关系,4,2、继承机制,所谓继承(inheritance)就是利用已有的数据类型定义出新的数据类型。利用类的“继承”,就可以将原来的程序代码重复使用,从而减少了程序代码的冗余度,符合软件重用的目标。所以说,继承是面向对象程序设计的一个重要机制。在继承关系中,称被继承的类为基类(base class)(或父类),而把通过继承关系定义出来的新类称为派生类(derived class)(子类)。由此可见,派生类既可以对基类的性质进行扩展,又可以进行限制,从而得到更加灵活、更加适用的可重用模块,大大缩短程序的开发时间。,5,3、继承的目的,假如一个工作要求修改(或增强)原有程序的功能,并且原先程序是以OOP类方式实现的,那么该任务的实现有两种选择:(1)直接修改原有类,但采用这种方式时,修改过程中很可能会发生小错误,这样不仅无法增加原先类的功能,反而使原先程序无法恢复。(2)为原先类定义一个派生类,由于派生类可继承基类的特性,因此,原先程序的功能可以保留,而将要扩充的功能放在派生类内,这样在调试阶段,可以只针对新设计的派生类进行调试,因此,即可保留原先的类继续使用,又可大大缩短扩充原有程序的时间。,6,4 派生类的声明和对象定义,设若干基类已经适当的构建,则新类追加于这些基类的声明格式分为两种形式:一种是单继承,一种是多继承 class 派生类名:继承方式 基类名1,继承方式 基类名2,继承方式 基类名n 派生类的成员声明;继承方式由关键字private,public,protected给出声明中的“基类名”是已经存在的类的名称,“派生类名”是在已存在的类的基础上通过添加成员而得到的新类。当派生类只有一个直接基类的继承情况,称为单继承。当派生类同时有多个基类的继承情况,称为多继承。,7,5 派生类的生成过程,派生类生成过程中需要指定派生类的类名,继承方式和新增成员。派生类的生成过程可分为三个步骤:吸收基类成员改造基类成员和添加新的成员。吸收基类成员:将基类除构造函数和析构函数以外的成员全部接受。改造基类成员:根据继承方式决定基类成员在派生类中的访问控制问题。添加新的成员:派生类新增成员是派生类不同于基类的地方,也是继承和派生机制的关键所在。,8,二、类的三种继承方式,(1)对于一个类的成员有三种访问控制方式,分别为:private、public、protected(2)对于类的继承有三种继承方式:(私有继承)private、(公有继承)public、(保护继承)protected对于派生类成员的访问控制属性由上面两条决定,9,P243表7-2-2的解释,下面是关于编译器继承方式的描述:1).对于任意继承方式,基类的私有成员对于派生类是“不可访问的”。2).对于公共继承方式,基类的公共成员为派生类的公共成员,基类的保护成员为派生类的保护成员。3).对于保护继承方式,基类的公共的和保护的成员变为派生类的保护成员。4).对于私有继承方式,基类的公共的和保护的成员变为派生类的私有成员。,10,公有继承,11,private,protected,public,private,protected,public,原基类私有,原基类私有,公有继承时的属性变化,基类原访问属性,派生类访问属性,保护继承,12,private,protected,public,private,protected,public,原基类私有,原基类私有,保护继承时的属性变化,基类原访问属性,派生类访问属性,私有继承,13,private,protected,public,private,protected,public,原基类私有,原基类私有,私有继承时的属性变化,基类原访问属性,派生类访问属性,几点声明,1 私有成员与不可访问成员是两个不同的概念。某个类的私有成员只能被该类的成员函数所访问,而类的不可访问成员甚至不能被该类自身的成员函数所访问(如:在私有继承中,派生类成员不能访问private部分)。类的不可访问成员总是从某个基类派生来的,它要么是基类的私有成员,要么是基类的不可访问成员。,14,实例,15,class A private:int s;public:void set(int n)s=n;int gets()return s;class B:public A/类B以公有继承的方式继承类A的成员 private:int t;public:void sett(int n)t=n;int gett()return t*gets();/通过基类的成员函数访问基类的私有成员;,main()B ob;ob.set(12);/通过类外的对象访问基类的公有成员 ob.sett(5);coutob.gett()endl;return 0;运算结果:60这里应注意两个问题:虽然派生类以公有的方式继承了基类,但并不是说派生类就可以访问基类的私有成员,基类无论怎样被继承,其私有成员对基类而言仍然保持私有性。在派生类中声明的名字如果与基类中声明的名字相同,则派生类中的名字起支配作用。也就是说,若在派生类的成员函数中直接使用该名字的话,该名字是指在派生类中声明的名字。如果要使用基类中的名字,则应使用作用与运算符加以限定,即在该名字前加“基类名”。,16,假设类B以私有方式继承A,class B:private A/类B以私有继承的方式继承类A的成员 private:int t;public:void sett(int n,int m)set(n);t=m;/可直接访问从基类中私有继承的成员 int gett()return t*gets();/通过基类的成员函数间接访问基类的私有成员;main()B ob;ob.set(12);/非法,不能通过类外对象访问从基类私有继承来的成员 ob.sett(5,7);coutob.gett()endl;return 0;,17,如前所述,不论是公有派生还是私有派生,派生类都不能访问他的基类的私有成员,要想访问,只能通过调用基类成员函数的方式来实现,也就是使用基类提供的接口来访问。这对于频繁访问基类私有成员的派生类而言,很不方便。为此,C+提供了具有另一种访问特性的成员即保护(protected)成员。,18,class A private:int s;protected:int r;/声明变量r为保护成员 public:int t;void setst(int n,int m)s=n;t=m int gets()return s;class B:protected A/类B以保护继承的方式继承类A的成员 private:int p;public:void setsrp(int n,int m,int l)setst(n,m);r=m;p=l;/可直接访问从基类中保护继承的成员 int getp()p=p+t*gets();/通过基类的成员函数间接访问基类的私有成员 return p;,19,三、派生类的构造函数,1.派生类构造函数和析构函数构建的原则:(1)基类的构造函数和析构函数不能被派生类继承。(2)如果基类没有定义构造函数,派生类也可以不定义构造函数,全都采用缺省的构造函数,此时,派生类新增成员的初始化工作可用其他公有函数来完成。(3)如果基类定义了带有形参表的构造函数,派生类就必须定义新的构造函数,提供一个将参数传递给基类构造函数的途径,以便保证在基类进行初始化时能获得必需的数据。(4)如果派生类的基类也是派生类,则每个派生类只需负责其直接基类的构造,不负责自己的间接基类的构造。(5)派生类是否要定义析构函数与所属的基类无关,如果派生类对象在撤销时需要做清理善后工作,就需要定义新的析构函数。,20,2 派生类构造函数的构建,(1)派生类的构造函数的定义格式如下:派生类名:派生类构造函数名(参数表):基类构造函数名(参数表)/派生类新增成员的初始化语句 派生类的构造函数除初始化其自身定义的数据成员外,还必须对基类中的数据成员进行初始化,也就是说,派生类的构造函数要负责调用基类的构造函数。(2)虽然派生类可以直接访问基类的数据成员,甚至在构造时初始化它们,但是一般不这么做,而是通过基类的接口(成员函数)去访问它们,初始化也是通过基类的构造函数。这样,避免了类与类之间的相互干扰。,21,3 派生类析构函数的构建,派生类析构函数的功能与基类析构函数的功能一样,也是在对象撤销时进行必需的清理善后工作。析构函数不能被继承,如果需要,则要在派生类中重新定义。跟基类的析构函数一样,派生类的析构函数也没有数据类型和参数。派生类析构函数的定义的方法与基类的析构函数的定义方法完全相同,而函数体只需完成对新增成员的清理和善后就行了,基类和对象成员的清理善后工作系统会自动调用他们各自的析构函数来完成。,22,派生类构造函数、析构函数的执行顺序在定义派生类的对象时,系统首先执行基类的构造函数,然后执行派生类的构造函数。而系统执行析构函数的顺序恰恰相反,即先执行派生类的析构函数,再执行基类的析构函数。若在基类中没有定义任何构造函数,这时在派生类的构造函数的定义中可以省略对基类构造函数的调用,此时系统将去调用基类的默认构造函数。实例:继承与构造、析构函数,23,构造函数,class GraduateStudent:public Studentprotected:int gCount;public:GraduateStudent(int gN,float gG,int gC):Student(gN,gG)coutConstructing GraduateStudentn;gCount=gC;,24,析构函数,class GraduateStudent:public Studentprotected:int gCount;public:GraduateStudent()coutDestructing GraduateStudentn;,25,四 赋值兼容规则,赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。赋值兼容规则替代的几种情况:派生类的对象可以赋值给基类对象。派生类的对象可以初始化基类的引用。派生类的对象的地址可以赋给指向基类的指针。,26,赋值兼容规则应用举例,class LOCATION;class POINT:public LOCATION;class CIRCLE:public POINT;main()CIRCLE circle(100,200,50);circle.show();POINT point=circle;point.show();/派生类对象可以直接给基类对象赋值 LOCATION location(circle);/派生类对象可以直接初始化基类对象 LOCATION/派生类对象可以直接赋给基类的指针;,27,四 多重继承,当一个派生类具有多个基类时,称这种派生为多重继承或多基派生。如下图:,28,定义多重继承类的方式如下:class 派生类名:访问方式 基类名,访问方式 基类名;其中:访问方式为public、protected或private。如在程序中大量使用的iostream提供的服务,这个库中的多个类就是应用了多重继承,输入/输出流同时具有输入流和输出流的特征,因而iostream同时继承了istream和ostream两个类。(可查看系统库中的文件iostream.h),29,多种继承中的主要问题是标识不唯一。比如,在派生类继承的这多个基类中有同名成员时,派生类中就会出现来自不同基类的同名成员,就出现了标识不唯一或二义性的情况,这在程序中是不允许的。例如:,30,31,class base1 public:int x;int a();int b();int b(int);int c();,class base2 int x;int a();public:float b();int c();,class derived:base1,base2;void d(derived/错误,有二义性,解决这个问题的办法有三种,一是使用作用域运算符“”,二是使用同名覆盖的原则,三是使用虚函数。,1.使用作用域运算符 如果派生类的基类之间没有继承关系,同时又没有共同的基类,则在引用同名成员时,可在成员名前加上类名和作用域运算符“”,来区别来自不同基类的成员。例如,将例9.8中的函数d(derived,32,2.使用同名覆盖的原则在派生类中重新定义与基类中同名的成员(如果是成员函数,则参数表也要相同,参数不同的情况为重载)以隐蔽掉基类的同名成员,在引用这些同名的成员时,使用的就是使用派生类中的函数,也就不会出现二义性的问题了。例如:,33,class base public:int x;void show()cout”This is base,x=”xendl;,class derived:base public:int x;/同名数据成员 void show()/同名成员函数 cout”This is derived,x=”xendl;,void main()derived ob;ob.x=5;/用同名覆盖原则引用派生类数据成员 ob.show();/用同名覆盖原则引用派生类成员函数 ob.base:x=12;/用作用域运算符访问基类成员 ob.base:show();/用作用域运算符访问基类成员,2、多重继承的构造函数和析构函数,声明多继承构造函数的一般形式为:派生类名:(参数总表):基类名1(参数表1),基类名n(参数表n),对象成员名1(对象成员参数表1),对象成员名m(对象成员参数表m)/派生类新增成员的初始化语句,34,其中:派生类的构造函数名与派生类名相同。参数总表列出初始化基类成员数据、新增对象成员数据和派生类新增成员数据所需要的全部参数。冒号后列出需要使用参数进行初始化的所有基类的名字和所有对象成员的名字及各自的参数表,之间用逗号分开。对于使用缺省构造函数的基类或对象成员,可以不给出类名或对象名以及参数表。(4)系统首先执行各基类的构造函数,然后再执行派生类的构造函数,处于同一层次的各基类构造函数的执行顺序与声明派生类时所指定的各基类顺序一致,而与派生类的构造函数定义中所调用基类构造函数的顺序无关。,【例】测试多重继承关系下,基类和派生类的构造函数的执行顺序。,#include“iostream.h”class B1protected:int b1;public:B1(int val1)b1=val1;cout”base1 is called“endl;,35,class B2 protected:int b2;public:B2(int val2)b2=val2;cout”base2 is called”endl;,class D:public B1,public B2protected:int d;public:D(int val1,int val2,int val3);D:D(int val1,int val2,int val3):B1(val1),B2(val2)/如改为D:D(int val1,int val2,int val3):B2(val2),B1(val1)效果一样/原因:处于同一层次的各基类构造函数的执行顺序与声明派生类时所指定的各基类顺序一致,而与派生类的构造函数定义中所调用基类构造函数的顺序无关。d=val3;cout”derived class is called“;void main()D dobj(1,2,3);,36,该程序的执行结果是:base1 is called base2 is calledderived class is called,五 重复继承(虚基类),多重继承下,一个派生类可从多个基类派生出来,又由于一个基类可派生出多个派生类,因此可能会产生一个类是通过多条路径从一个给定的类中派生出来的,这种现象称为“重复继承”,简而言之,就是指一个派生类多次继承同一个基类,如右图所示。,37,1、重复继承的应用背景,注意C+不允许:一个派生类直接继承同一个基类两次(如下左图);一个基类既是直接基类又是间接基类(如下右图),即不能同时使用基类和扩展类。,38,B,B,D,2、虚基类的定义与格式,虚基类是这样的一个基类:它虽然被一个派生类间接地多次继承,但派生类却只继承一份该基类的成员,这样,避免了在派生类中访问这些成员时产生二义性。将一个基类声明为虚基类必须在各派生类定义时,在基类的继承访问控制保留字前面加上关键字virtual,格式如下:class 派生类名:virtual public 基类名/声明派生类成员;,39,使用虚基类时,要特别注意派生类的构造函数。对于普通基类,派生类的构造函数负责调用其直接基类的构造函数以初始化其直接基类的数据成员;而对于虚基类的任何派生类,其构造函数不仅负责调用直接基类的构造函数,还需调用虚基类的构造函数。调用顺序:1)虚基类的构造函数,2)按声明时的顺序调用直接基类的构造函数,40,创建一个派生类对象时,构造函数与析构函数的调用次序:,构造函数的调用次序:最先调用虚基类的构造函数;调用普通基类的构造函数,多个基类则按派生类声明是的次序,从左到右调用;调用对象成员的构造函数,按类声明中对象成员出现的次序调用;调用派生类的构造函数。析构函数的调用次序与构造函数的正好相反,41,练习1,42,练习2,43,44,