1、第16章 继承徐素锦2本章主要内容o 16.1 继承的概念o 16.2 继承的工作方式o 16.3 派生类的构造o 16.4 继承与组合o 16.5 多态性o 16.6 多态的思考方式o 16.7 多态性如何工作o 16.8 不恰当的虚函数o 16.9 虚函数的限制p16.10 类的冗余p16.11 克服冗余带来的问题p16.12 类的分解p16.13 抽象类p16.14 由抽象类派生具体类p16.15 纯虚函数的需要性p作业316.1继承的概念o 宇宙万事万物都是分类分层的,解决问题可以从事物之间的上下关系中着手。这是继承引入程序设计的前提。o 例如:各类型的交通工具之间具有继承关系4交通工
2、具交通工具汽车汽车小汽车小汽车大卡车大卡车大客车大客车工具车工具车轿车轿车面包车面包车火车火车飞机飞机轮船轮船从上到下:派生从上到下:派生从下到上:继承从下到上:继承16.1继承的概念516.1继承的概念o 基类(父类):派生新类的类o 派生类(子类):从基类派生而成的类o 基类和派生类:构成类的层次关系o 单继承:从一个基类派生而成的类o 多继承:从多个基类派生而成的类o 继承类别:公有继承、私有继承、保护继承o 访问控制:决定子类对父类的访问权限616.2继承的工作方式1.派生类的定义o语法:class 派生类名:继承类别 基类名 ;o访问控制:private、public、protect
3、edo缺省为:private2.继承类别公有继承 派生时用“public”作访问控制私有继承 派生时用“private”作访问控制保护继承 派生时用“protected”作访问控制73.派生类对象结构 基类对象子类对象 子类对象中总是含有基类对象(即含有基类的数据成员),其空间总是不小于基类对象。cabab基类部分基类部分子类添加部分子类添加部分class A int a,b;class B:public A int c;16.2继承的工作方式816.2 继承的工作方式o 4.继承类别与访问控制:访问控制 继承类别 public 公有继承 private 私有继承 protected 保护继承
4、o 注意:如果类之间没有继承关系则保护成员和私有成员类似;但在类的继承关系中,保护成员在其子类中可以被直接访问,而私有成员在它的子类中也不能被直接访问。9o 公有继承:基类的公有段成员被继承为公有的,基类的保护段成员被继承为保护的。派生时用“public”作访问控制。o 保护继承:基类的公有段成员和保护段成员被继承为保护的,派生时用“protected”作访问控制。o 私有继承:基类的公有段成员和保护段成员被继承为私有的,派生时用“private”作访问控制。o 基类的私有成员在任何情况下都是基类所私有的,在基类以外都不能访问!16.2 继承的工作方式10o 公有成员:一个类的公有成员允许本类
5、的成员函数、本类的对象、派生类的成员函数、公有派生类的对象直接访问,不允许保护和私有派生类的对象直接访问。o 私有成员:一个类的私有成员只允许本类的成员函数直接访问。o 保护成员:允许本类的成员函数、派生类的成员函数直接访问,不允许对象直接访问。16.2 继承的工作方式11 基类访问控制符继承方式publicprotectedprivatepublicpublicprotected隔离protectedprotectedprotected隔离privateprivateprivate隔离基类成员在派生类中的访问控制属性基类成员在派生类中的访问控制属性12o 例如:一个含有不同继承方式的类的继承
6、结构。class Base int b1;protected:int b2;void fb2()b1=1;public:int b3;void fb3()b1=1;16.2 继承的工作方式13class Pri:private Basepublic:void test()b1=1;/error b2=1;/ok b3=1;/ok fb2();/ok fb3();/ok ;class FromPriclass FromPri :public Pri:public Pri public:public:void test()void test()b1=1;/error b1=1;/error b2=
7、1;/error b2=1;/error b3=1;/error b3=1;/error fb2();/error fb2();/error fb3();/error fb3();/error ;16.2 继承的工作方式14class Pro:protected Basepublic:void test()b1=1;/error b2=1;/ok b3=1;/ok fb2();/ok fb3();/ok ;class FromProclass FromPro :public Pro:public Pro public:public:void test()void test()b1=1;/err
8、or b1=1;/error b2=1;/ok b2=1;/ok b3=1;/ok b3=1;/ok fb2();/ok fb2();/ok fb3();/ok fb3();/ok ;16.2 继承的工作方式15class Pub:public Basepublic:void test()b1=1;/error b2=1;/ok b3=1;/ok fb2();/ok fb3();/ok ;class FromPubclass FromPub :public Pub:public Pub public:public:void test()void test()b1=1;/error b1=1;/
9、error b2=1;/ok b2=1;/ok b3=1;/ok b3=1;/ok fb2();/ok fb2();/ok fb3();/ok fb3();/ok ;16.2 继承的工作方式16int main()Pri priObj;priObj.b1=1;/error priObj.b2=1;/error priObj.b3=1;/error Pro proObj;proObj.b1=1;/error proObj.b2=1;/error proObj.b3=1;/error Pub pubObj;pubObj.b1=1;/error pubObj.b2=1;/error pubObj.b
10、3=1;/ok16.2 继承的工作方式17o 说明:o 私有继承时,基类中不管是公有的,还是保护的,一律在子类中变成私有成员。保护继承时,基类中公共和保护的成员在子类中变成保护的。公共继承时,基类中为公共、保护的成员在子类中仍保持为公共、保护的。o 在继承关系中,基类的private成员不但对应用程序隐藏,对派生类也隐藏。而基类的保护成员则只对应用程序隐藏,而对派生类则毫不隐瞒。16.2 继承的工作方式1816.2 继承的工作方式o 例如:有一个学生类Student,现在要增加研究生类,研究生类除了自己所特有的性质外,还具有学生类的所有性质,下面用继承的方法来重用学生类的代码。/*ch16_1
11、.cpp *#include#includeclass Advisor /导师类导师类 int noOfMeeting;class Student/学生类学生类 protected:char name40;int semesterHours;/学时学时 float average;/平均分平均分public:Student(char*pName=no name)strcpy(name,pName);average=semesterHours=0;void addCourse(int hours,float grade)average=semesterHours*average+grade;/总
12、分总分 semesterHours+=hours;/总修学时总修学时 average/=semesterHours;/平均分平均分 int getHours()return semesterHours;float getAverage()return average;void display()cout name=name ,hours=semesterHours ,average=averageendl;class GraduateStudent:public Student /研究生类研究生类(继承学生类继承学生类)protected:Advisor advisor;/导师导师 int q
13、ualifierGrade;/资格考试分资格考试分 public:int getQualifier()return qualifierGrade;int main()Student ds(Lo lee undergrade);GraduateStudent gs;ds.addCourse(3,2.5);ds.display();gs.addCourse(3,3.0);gs.display();运行结果:运行结果:name=Lo lee undergrad,hoursname=Lo lee undergrad,hours=3,average=0.833333=3,average=0.833333
14、 name=no name,hours name=no name,hours=3,average=1=3,average=1void fn(Student&s)/void main()GraduateStudent gs;fn(gs);函数fn()期望接受Student类对象作为它的参数,来自main()的调用传给它一个GraduateStudent类对象,fn()把它视作Student类对象予以接受。事实上,GraduateStudent的内存布局,也与“gs是研究生,当然也是学生”相吻合。Student部分部分GraduateStudent继承部分继承部分this指针指针GraduateS
15、tudent对象尺寸对象尺寸23o 默认构造:o 如果子类没有定义构造函数,则调用默认无参构造函数,默认构造函数首先调用父类的无参构造函数,完成父类对象部分的构造,然后构造子类自身o 如果父类的上面还有父类,则依次递归16.3 派生类的构造24o 自定义构造:o 为了规定父类构造函数的调用方式,而不是默认调用,需要自定义子类构造函数,并且,在子类构造函数定义体的始化列表中(即冒号后面)描述父类构造函数的调用形式 描述形式与对象成员构造的描述一致16.3 派生类的构造o 例如,下面的代码定义了研究生类,其中的构造函数实现对基类数据成员的构造:class GraduateStudent:publi
16、c Student public:GraduateStudent(char*pName,Advisor&adv):Student(pName),advisor(adv)qualifierGrade=0;/其余见ch16_1.cpp;void fn(Advisor&advisor)GraduateStudent gs(Yen Kay Doodle,advisor);gs.display();void main()Advisor da;fn(da);调用基类的构造函数产调用基类的构造函数产生一个无名对象生一个无名对象26o 说明:o 1)派生类对象构造时,先构造基类,再对象成员,最后是派生类自身o
17、 2)派生类构造函数的初始化列表中,直接调用基类的构造函数o 3)如果派生类中定义了与基类名字相同的成员,捆绑子类对象访问此成员时,则首先匹配子类,然后父类,再父类的父类,依此类推o gs.display();/call GraduateStudent:display()16.3 派生类的构造27o 拷贝构造:o 拷贝构造函数与构造函数的方式类似:子类若没有定义拷贝构造函数,则子类对象在拷贝创建时先调用父类的拷贝构造函数,再调用自己的默认拷贝构造函数完成自己的位对位拷贝。若父类没有定义拷贝构造函数,则子类对象在拷贝创建中调用父类默认的拷贝构造函数o 同样,如果子类申请了资源,需要定义拷贝构造函
18、数o GraduateStudent gs2(gs);16.3 派生类的构造2816.3 派生类的构造o 继承中的构造函数和析构函数的说明:1.派生类的构造函数的初始化列表中列出的均是直接基类的构造函数。2.构造函数不能被继承,因此派生类的构造函数只能通过调用基类的某个构造函数(如果有定义的话)来初始化基类子对象。3.派生类的构造函数只负责初始化自己定义的数据成员。2916.3 派生类的构造 4.先调用基类的构造函数,再调用派生类的数据成员所属类的构造函数,最后调用派生类自己的构造函数;派生类的数据成员的构造函数被调用的顺序取决于在类中声明的顺序。5.派生类的对象的生存期结束时调用派生类的析构
19、函数,在该析构函数结束之前再调用基类的析构函数;所以,析构函数的被调用次序与构造函数相反。6.构造函数不可以继承,不可以被重载。3016.4 继承与组合 o 继承:子类继承了父类,称为子类对象对父类对象的继承式包含。o 组合(也称“聚集”):新的、复杂的对象由已存在的、相对简单的对象组成。n 即:类的数据成员是另一个类的对象。n 类中含有对象成员,称为组合式包含。o 继承和组合都重用了类设计。o 继承和组合两者在使用上不同。31o GraduateStudent组合了Advisor称GraduateStudent 有一个Advisoro GraduateStudent类继承了Student类称
20、GraduateStudent 是一个Student继承部分继承部分派生部分派生部分其他数据成员其他数据成员Student对象对象Advisor对象对象研究生对象研究生对象组合式组合式包含包含继承式继承式包含包含16.4 继承与组合32o 继承与组合两者在使用上不同n 继承重用场合,子类就是一种父类,所以,无须捆绑父类对象便能直接对其公有和保护成员操作n 组合重用场合,使用对象成员的操作,只能捆绑对象成员对其公有成员进行操作。o void GraduateStudent:print()display();/继承得到 coutGraduateStudentn;advisor.print();/组
21、合方式 16.4 继承与组合class Vehicle/车辆类 /;class Motor /马达类 /;class Car:public Vehicle public:Motor motor;void vehicleFn(Vehicle&v);void motorFn(Motor&m);int main()Car c;VehicleFn(c);/ok motorFn(c);/error motorFn(c.motor);/ok3416.5多态性 o 多态性:面向对象系统中的又一重要概念,三大特性之一。o 多态性的概念:当不同的对象收到相同的消息时产生不同的动作。o 本书的多态性概念:运行时,
22、根据绑定的对象类型的不同,确定调用哪个成员函数的能力称为多态性。o 多态是针对具有继承关系的不同的类对象,执行同一名称的操作,而能做出不同的选择。3516.5多态性o 面向对象系统的两种编译方式:n 先期联编系统在编译时确定实现某一动作,提供了执行速度快的优点n 滞后联编系统在运行时动态实现某一动作,提供了灵活和高度抽象的优点o 先期联编和滞后联编都支持多态性。o C+系统支持两种多态性:n 静态多态性编译时的多态性n 动态多态性运行时的多态性3616.5多态性1.静态多态性(先期联编)通过函数重载实现o函数重载的两种方式:n在同一个类中重载,同名的成员函数,根据参数等不同,自动予以区别。n基
23、类成员在派生类中的重载。class A public:void show()/;class B:public Apublic:void show()void show(int xx)/;int main()A a1;B b1;a1.show();/调用A类的show()函数,操作数据是对象a1的 b1.show();/调用B类的show()函数,操作数据是对象b1的 b1.show(555);/调用B类的show(int xx)函数,操作数据是对象b1的 b1.A:show();/调用A类的show()函数,操作数据是对象b1的3816.5多态性2.动态多态性(迟后联编、滞后联编)o 在运行时
24、,能够依据对象的类型确认调用那个函数的能力。o 通过虚函数实现o 一种程序设计语言,如果只有类和继承是基于对象的语言;只有能够实现运行时的多态才是真正面向对象的程序设计语言。3916.5多态性o 为什么需要动态多态呢?class Student public:double calcTuition();/学生学费计算学生学费计算 /;class GraduateStudent:public Studentpublic:double calcTuition();/.;40void fn(Student&x)/交费处理交费处理 x.calcTuition();/学费计算学费计算int main()S
25、tudent ds;GraduateStudent gs;fn(ds);/计算学生学费计算学生学费 fn(gs);/计算研究生学费计算研究生学费o 希望系统能够根据对象类型的不同采用不同的处理,但并未实现,需要运行时多态。16.5多态性说明:说明:子类对象就是父类子类对象就是父类对象的性质是一句空话对象的性质是一句空话因为操作起来原来是将子因为操作起来原来是将子类对象粗暴的同化,类对象粗暴的同化,根本扼杀了子类对象的个根本扼杀了子类对象的个性。性。4116.6多态的思考方式o 类型域方案 o 思路:在基类中增加一个反映类型的数据成员enum StudentType(STUDENT,GRADUA
26、TESTUDENT);class Student/学生类学生类 public:Student(char*pName=no name)strncpy(name,pName,sizeof(name);average=semesterHours=0;type=STUDENT;float calcTuition();/学生学费计算学生学费计算 StudentType type;/说明为公共的,便于应用程序访问说明为公共的,便于应用程序访问 /;4216.6多态的思考方式class GraduateStudent:public Student /研究生类研究生类 public:GraduateStude
27、nt()type=GRADUATESTUDENT;/覆盖刚赋的覆盖刚赋的STUDENT值值 float calcTuition();/研究生学费计算研究生学费计算 /;43void fn(Student&s)/交费处理交费处理switch(s.type)/判断对象的判断对象的type,以确定调用哪个学费计算成员以确定调用哪个学费计算成员 case STUDENT:s.STUDENT:calcTuition();break;case GRADUATESTUDENT:s.GraduateeStudent:calcTuition();break;int main()Student ds;Gradua
28、teStudent gs;fn(ds);/计算学生学费计算学生学费 fn(gs);/计算研究生学费计算研究生学费 但不敢恭维这种方法,因为它但不敢恭维这种方法,因为它导致类编程与应用编程互相依导致类编程与应用编程互相依赖,因而破坏了抽象编程赖,因而破坏了抽象编程44o 破坏抽象编程的后果是:n 可维护性,可扩展性受到伤害。n 若增加一个博士类,则类代码与应用程序代码都得改,而这本来不是应用程序的份内事。o 因而呼吁从语言内部来支持这种动态多态性。16.6多态的思考方式4516.7多态性如何工作o 虚函数 o 运行时的多态性通过虚函数实现的。o 虚函数:是一种动态的函数重载的方式,虚函数允许函数
29、调用与函数体之间的联系在运行时才建立。o 为了指明某个成员函数具有多态性,用关键字virtual来标志其为虚函数。o 在基类中声明为virtual(虚函数),其虚函数的性质自动地向下带给其子类,所以子类中的虚函数的virtual可以省略。46/*ch16_2.cpp *#includeclass Base public:virtual void fn()cout In Base classn;class SubClass:public Base public:virtual void fn()cout In SubClassn;void test(Base&b)b.fn();int main(
30、)Base bc;SubClass sc;cout Calling test(bc)n;test(bc);cout Calling test(sc)n;test(sc);运行结果:运行结果:Calling test(bc)In Base ClassCalling test(sc)In SubClass4716.7多态性如何工作o 说明:虚函数在子类和基类中的函数声明完全相同;只有通过基类的指针(或引用)的调用虚函数,才会发生多态;虚函数具有传递特性,在基类中声明以后,子类中相同的函数就自动成为虚函数了;48/ch16_3.cpp求图形的面积o#includeo#includeo class S
31、hapeo o public:o Shape(double x,double y):xCoord(x),yCoord(y)o virtual double Area()const return 0.0;/虚函数o protected:o double xCoord,yCoord;o;o class Circle:public Shapeo o public:o Circle(double x,double y,double r):Shape(x,y),radius(r)o virtual double Area()const return 3.14*radius*radius;o protec
32、ted:o double radius;o;4916.7多态性如何工作19.class Rectangle:public Shape20.21.public:22.Rectangle(double x1,double y1,double x2,double y2):Shape(x1,y1),x2Coord(x2),y2Coord(y2)23.virtual double Area()const;24.protected:25.double x2Coord,y2Coord;26.;27.double Rectangle:Area()const28.29.return fabs(xCoord-x2
33、Coord)*(yCoord-y2Coord);30.5016.7多态性如何工作31.void fun(const Shape&sp)/计算图形面积32.33.coutsp.Area()endl;34.35.int main()36.37.Circle c(2.0,5.0,4.0);38.fun(c);39.Rectangle t(2.0,4.0,1.0,2.0);40.fun(t);41.运行结果:运行结果:50.24251o 优点:o 多态性使得当要增加一个求新的形状的面积时,只要简单地增加一个类就行了,应用程序不用作修改。o 多态性让类的设计者去考虑工作的细节,而且这个细节简单到在成员函
34、数上加一个virtual关键字。多态性使应用程序代码极大地简化,它是开启继承能力的钥匙。o 在继承结构中,应尽量将成员函数设计成虚函数。16.7 多态性如何工作52o 虚函数的机理:o 包含虚函数的类,其对象空间比类的数据成员所需的空间多了一个指针空间;指针指向该类的虚函数列表;编译时,遇到对虚函数的调用,便对它做滞后处理,等到运行时再根据具体对象的类型确定调用哪一个虚函数16.7 多态性如何工作53o 虚函数的传播o 虚函数会自动从基类传播下去,即也可以进行隔代传播1.class A 2.private:3.int x,y;4.public:5.virtual void show(int a
35、=0)coutA:endl;6./声明为虚函数7.;8.class B:public A 9.public:10.void showB(int a=0)coutB:endl;11.;5412.class C:public B 13.public:14.void show(int b=0)coutC:endl;15./虚函数虚函数16.;17.class D:public C 18.public:19.void show(int b=0)cout D:show();26.pa=&b;27.pa-show();28.pa=&c;29.pa-show();30.pa=&d;31.pa-show();
36、32.o 说明:虚函数可以隔代传播。运行结果运行结果:A:A:C:D:5616.8不恰当的虚函数o 搞清楚重载与覆盖o 在继承关系的类中,各虚函数的函数名,参数类型,个数和顺序,返回类型都要相同;o 如果只有函数名相同,被编译器认为是普通函数重载,即使函数之前有virtual编译器也不会对它进行滞后处理,失去虚特性。5716.8不恰当的虚函数/*ch16_4.cpp *#include class Base public:virtual void fn(int x)cout In Base class,int x=x endl;class SubClass:public Base public
37、:virtual void fn(float x)cout In SubClass,float x=x endl;参数类型与基类参数类型与基类中不同,是普通中不同,是普通重载重载5816.8不恰当的虚函数void test(Base&b)int i=1;b.fn(i);float f=2.0;b.fn(f);int main()Base bc;SubClass sc;cout Calling test(bc)n;test(bc);cout Calling test(sc)n;test(sc);o 说明:参数不同不能实现虚函数;所以,称多态为“一种接口,多种实现”运行结果为:运行结果为:Call
38、ing test(bc)In Base class,int x=1 In Base class,int x=2 Calling test(sc)In Base class,int x=1 In Base class,int x=2调用调用Base中的中的fn函数函数59o 返回类型例外o 函数重载对于编译识别来说,返回类型是不起作用的 void fn(int);int fn(int);n 对于fn(2);无法判断应该调用哪个函数而使调用遭遇编译失败。o 对于虚函数来说,声明:virtual Base*fn();virtual SubClass*fn();被编译器看作实现多态调用的虚函数。16.
39、8不恰当的虚函数/*ch16_5.cpp *#includeclass Base public:virtual Base*afn()/虚函数虚函数 coutThis is Base class.n;return this;class SubClass:public Base public:SubClass*afn()/虚函数虚函数 coutWithdrawal(100.0);void otherFunc()Savings s;Checking c;func(&s);/合法,因为一个Savings是一个Account func(&c);/合法,一个Checking也是一个Account8016.15 纯虚函数的需要性o 关于抽象类和纯虚函数的说明:1.抽象类只能作为其他类的基类,不能创建对象。2.抽象类不能用作函数形参的类型、函数返回类型,不能进行显式类型转换。3.可以定义抽象类的指针(引用),此指针可以指向其非抽象类的派生类的对象,以实现动态多态性。4.在抽象类的派生类中,如果没有给出基类的纯虚函数的具体定义,则该派生类仍是一个抽象类;如果在派生类的派生类中,还没有给出基类的纯虚函数的具体定义,则该派生类的派生类还是一个抽象类。81作业16.5