用的最重要的手段它允许程序员在保持原有类特性的基

上传人:hs****ma 文档编号:585041235 上传时间:2024-09-01 格式:PPT 页数:105 大小:498.52KB
返回 下载 相关 举报
用的最重要的手段它允许程序员在保持原有类特性的基_第1页
第1页 / 共105页
用的最重要的手段它允许程序员在保持原有类特性的基_第2页
第2页 / 共105页
用的最重要的手段它允许程序员在保持原有类特性的基_第3页
第3页 / 共105页
用的最重要的手段它允许程序员在保持原有类特性的基_第4页
第4页 / 共105页
用的最重要的手段它允许程序员在保持原有类特性的基_第5页
第5页 / 共105页
点击查看更多>>
资源描述

《用的最重要的手段它允许程序员在保持原有类特性的基》由会员分享,可在线阅读,更多相关《用的最重要的手段它允许程序员在保持原有类特性的基(105页珍藏版)》请在金锄头文库上搜索。

1、继承(继承(inheritance)机制是面向对象程序设计使代码可以机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。基础上进行扩展,增加功能。这样产生新的类,称派生类。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构。继承呈现了面向对象程序设计的层次结构。体现了由简单体现了由简单到复杂的认识过程到复杂的认识过程。第八章 继承与多态多态性(多态性(polymorphism)多态性是考虑在不同层次的类中,多态性是考虑在不同层次的类中,以及在同一类中,同名的成员函数之间的关系问

2、题。函数以及在同一类中,同名的成员函数之间的关系问题。函数的重载,运算符的重载,属于编译时的多态性。以虚函数的重载,运算符的重载,属于编译时的多态性。以虚函数为基础的运行时的多态性是面向对象程序设计的标志性特为基础的运行时的多态性是面向对象程序设计的标志性特征。征。 体现了类推和比喻的思想方法。体现了类推和比喻的思想方法。 第八章第八章 继承与多态继承与多态8.1继承与派生的概念继承与派生的概念 8.4虚基类虚基类(选读)(选读)8.3多重继承与派生类成员标识多重继承与派生类成员标识8.6多态性与虚函数多态性与虚函数8.5派生类应用讨论派生类应用讨论8.2派生类的构造函数与析构函数派生类的构造

3、函数与析构函数 8.1 继承与派生的概念继承与派生的概念层次概念层次概念是计算机的重要概念。通过是计算机的重要概念。通过继承继承(inheritance)的机制可对类(的机制可对类(class)分层,提供类型分层,提供类型/子类型的关系。子类型的关系。C+通过通过类派生类派生(class derivation)的机制来支持继承。的机制来支持继承。被继承的类称为被继承的类称为基类基类(base class)或或超类超类(superclass),),新产生的类为新产生的类为派生类派生类(derived class)或或子类子类(subclass)。)。基类和派生类的集合称作基类和派生类的集合称作类

4、继承层次结构类继承层次结构(hierarchy)。)。 如果基类和派生类共享相同的公有接口,则派生类被称如果基类和派生类共享相同的公有接口,则派生类被称作基类的子类型(作基类的子类型(subtype)。)。 派生派生反映了事物之间的联系,事物的共性与个性之间的反映了事物之间的联系,事物的共性与个性之间的关系。关系。 派生与独立设计若干相关的类,派生与独立设计若干相关的类,前者工作量少,重前者工作量少,重复的部分可以从基类继承来,不需要单独复的部分可以从基类继承来,不需要单独编程编程。 8.1 继承与派生的概念继承与派生的概念8.1.1类的派生与继承类的派生与继承 8.1.2公有派生与私有派生公

5、有派生与私有派生 由基类派生出派生类的定义的一般形式为由基类派生出派生类的定义的一般形式为class 派生类名:访问限定符派生类名:访问限定符 基类名基类名1,访问限定符,访问限定符 基类名基类名2,访问限定符,访问限定符 基类名基类名n private: 成员表成员表1; /派生类增加或替代的私有成员派生类增加或替代的私有成员public:成员表成员表2; /派生类增加或替代的公有成员派生类增加或替代的公有成员protected:成员表成员表3; /派生类增加或替代的保护成员派生类增加或替代的保护成员;/分号不可少分号不可少其中基类其中基类1 1,基类,基类2 2,是已声明的类。是已声明的类

6、。 在派生类定义的类体中在派生类定义的类体中给出的成员称为给出的成员称为派生类成员派生类成员,它们是新增加成员,它们给派生类,它们是新增加成员,它们给派生类添加了不同于基类的新的属性和功能。派生类成员也包括取代基添加了不同于基类的新的属性和功能。派生类成员也包括取代基类成员的更新成员。类成员的更新成员。8.1.1 类的派生与继承基类基类1基类基类2基类基类n派生类派生类1派生类派生类2基类基类派生类派生类1派生类派生类2(a)多重继承多重继承 (b)单继承单继承 图图8.1多重继承与单继承多重继承与单继承 一个一个基类基类可以可以直接直接派生派生出多出多个派个派生类生类派生派生类可类可以由以由

7、多个多个基类基类共同共同派生派生出来,出来,称多称多重继重继承。承。8.1.1 类的派生与继承如果一个派生类可以同时有多个基类,称为多重继承如果一个派生类可以同时有多个基类,称为多重继承(multiple-inheritance),),这时的派生类同时得到了多个这时的派生类同时得到了多个已有类的特征。一个派生类只有一个直接基类的情况称为单已有类的特征。一个派生类只有一个直接基类的情况称为单一继承(一继承(single-inheritance)。)。编制编制派生派生类时类时可分可分四步四步 吸收基类的成员吸收基类的成员 改造基类成员改造基类成员 发展新成员发展新成员重写构造函数与析构函数重写构造

8、函数与析构函数 8.1.1 类的派生与继承不论是数据成员,还是函数成员,不论是数据成员,还是函数成员,除构造函数与析构函数外全盘接收除构造函数与析构函数外全盘接收 声明一个和某基类成员同名的新成员声明一个和某基类成员同名的新成员,派派生类中的新成员就屏蔽了基类同名成员生类中的新成员就屏蔽了基类同名成员称为同名覆盖(称为同名覆盖(override) 派生类新成员必须与基类成员不同名,它派生类新成员必须与基类成员不同名,它的加入保证派生类在功能上有所发展。的加入保证派生类在功能上有所发展。 8.1.1 类的派生与继承上面的步骤就是继承与派生编程的规范化步骤。上面的步骤就是继承与派生编程的规范化步骤

9、。第二步中,新成员如是成员函数,参数表也必须一样,否则第二步中,新成员如是成员函数,参数表也必须一样,否则是重载。是重载。第三步中,独有的新成员才是继承与派生的核心特征。第三步中,独有的新成员才是继承与派生的核心特征。第四步是重写构造函数与析构函数,派生类不继承这两种函第四步是重写构造函数与析构函数,派生类不继承这两种函数。不管原来的函数是否可用数。不管原来的函数是否可用一律重写可免出错一律重写可免出错。 访问控制访问控制,亦称为,亦称为继承方式继承方式,是对基类成员进一步的限,是对基类成员进一步的限制。访问控制也是三种:制。访问控制也是三种:公有(公有(public)方式,亦称公有继承方式,

10、亦称公有继承保护(保护(protected)方式,亦称保护继承方式,亦称保护继承私有(私有(private)方式,方式, 亦称私有继承。亦称私有继承。8.1.2 公有派生与私有派生不可直接访问 不可直接访问 private 不可直接访问 private portectedprotected 不可直接访问 private portectedpublic 私有派生 保护派生保护派生不可直接访问 不可直接访问 private 不可直接访问 protected protected 可直接访问 public public 公有派生 在派生类对象外访问派生类对象的基类成员 在派生类中对基类成员的访问限定

11、基类中的访问限定 派生方式 访问限定符有两方面含义:访问限定符有两方面含义:派生类成员(新增成员)函数对基派生类成员(新增成员)函数对基类(继承来的)成员的访问(调用和操作)类(继承来的)成员的访问(调用和操作),和,和从派生类对从派生类对象之外对派生类对象中的基类成员的访问象之外对派生类对象中的基类成员的访问。公有派生是绝对主流公有派生是绝对主流。 派生类派生类的构造函数的定义形式为:的构造函数的定义形式为:派生类名派生类名:派生类名(参数总表)派生类名(参数总表):基类名基类名1(参数表(参数表1),基类名基类名2(参数表(参数表2),),基类名,基类名n(参数表参数表n),),成成员对象

12、名员对象名1(成员对象参数表(成员对象参数表1),),成员对象名,成员对象名m(成成员对象参数表员对象参数表m)/派生类新增成员的初始化;派生类新增成员的初始化; /所列出的成员对象名全部为新增成员对象的名字所列出的成员对象名全部为新增成员对象的名字 在构造函数的声明中,冒号及冒号以后部分必须略去。在构造函数的声明中,冒号及冒号以后部分必须略去。 所谓不能继承并不是不能利用,而是把基类的构造函数作所谓不能继承并不是不能利用,而是把基类的构造函数作为新的构造函数的一部分,或者讲调用基类的构造函数。为新的构造函数的一部分,或者讲调用基类的构造函数。基基类名仅指直接基类,写了底层基类,编译器认为出错

13、类名仅指直接基类,写了底层基类,编译器认为出错。 冒号后的基类名,成员对象名的次序可以随意,这里的次冒号后的基类名,成员对象名的次序可以随意,这里的次序与调用次序无关。序与调用次序无关。 8.2 派生类的构造函数与析构函数派生类构造函数各部分的执行次序为:派生类构造函数各部分的执行次序为:1.1.调用基类构造函数,按它们在派生类定义的先后顺序,调用基类构造函数,按它们在派生类定义的先后顺序,对基类成员进行初始化对基类成员进行初始化。2.2.调用成员对象的构造函数,按它们在类定义中声明的先调用成员对象的构造函数,按它们在类定义中声明的先后顺序,顺序调用。后顺序,顺序调用。3.3.执行派生类的构造

14、函数。执行派生类的构造函数。*在派生类构造函数中,只要基类不是使用缺省构造函数都在派生类构造函数中,只要基类不是使用缺省构造函数都要显式给出基类名和参数表要显式给出基类名和参数表。如果基类没有定义构造函数,则派生类也可以不定义,全如果基类没有定义构造函数,则派生类也可以不定义,全部采用系统给定的缺省构造函数。部采用系统给定的缺省构造函数。如果基类定义了带有形参表的构造函数时,派生类就应当如果基类定义了带有形参表的构造函数时,派生类就应当定义构造函数。定义构造函数。析构函数各部分执行次序与构造函数相反,析构函数各部分执行次序与构造函数相反,首先对派生类首先对派生类新增一般成员析构,然后对新增对象

15、成员析构,最后对基新增一般成员析构,然后对新增对象成员析构,最后对基类成员析构类成员析构。【例8.1】由在册人员类公有派生学生类【例【例8.1】由在册人员类公有派生学生类。我们希望基类和派生由在册人员类公有派生学生类。我们希望基类和派生类共享相同的公有接口类共享相同的公有接口,只能采用公有派生来实现。只能采用公有派生来实现。首先来看基类:首先来看基类:class Personstring IdPerson;/身份证号身份证号,18位数字位数字string Name;/姓名姓名Tsex Sex; /性别性别enum Tsexmid,man,woman;int Birthday; /生日生日,格式

16、格式1986年年8月月18日写作日写作19860818string HomeAddress;/家庭地址家庭地址public:Person(string, string,Tsex,int, string); /构造函数构造函数 Person(); /缺省的构造函数缺省的构造函数 Person(); /析构函数析构函数【例8.1】由在册人员类公有派生学生类void SetName(string); /修改名字修改名字string GetName()return Name; /提取名字提取名字void SetSex(Tsex sex)Sex=sex; /修改性别修改性别Tsex GetSex()re

17、turn Sex;/提取性别提取性别void SetId(string id)IdPerson=id;/修改身份证号修改身份证号string GetId()return IdPerson; /提取身份证号提取身份证号void SetBirth(int birthday)Birthday=birthday; /修改生日修改生日int GetBirth()return Birthday; /提取生日提取生日void SetHomeAdd(string ); /修改住址修改住址string GetHomeAdd()return HomeAddress; /提取住址提取住址void PrintPers

18、onInfo(); /输出个人信息输出个人信息;接口函数:接口函数:【例8.1】由在册人员类公有派生学生类派生的学生类派生的学生类:class Student:public Person /定义派生的学生类定义派生的学生类string NoStudent; /学号学号course cs30; /30门课程与成绩门课程与成绩public: Student(string id, string name,Tsex sex,int birthday, string homeadd, string nostud);/注意注意派生类构造函数派生类构造函数声明方式声明方式 Student(); /缺省派生类

19、构造函数缺省派生类构造函数 Student(); /派生类析构函数派生类析构函数 SetCourse(string ,int); /课程设置课程设置 int GetCourse(string ); /查找成绩查找成绩 void PrintStudentInfo(); /打印学生情况打印学生情况;struct course char* coursename; int grade;在在VC+平台上运行例平台上运行例8.1验证验证主函数主函数8.3 多重继承与派生类成员标识由多个基类共同派生出新的派生类,这样的继承结构由多个基类共同派生出新的派生类,这样的继承结构被称为多重继承或多继承(被称为多重继

20、承或多继承(multiple-inheritance) 椅子椅子床床沙发沙发(单继承单继承)躺椅躺椅(多重继承多重继承)两两用用沙沙发发(多多重重继继承承)图图8.2椅子,床到两用沙发椅子,床到两用沙发在册人员在册人员学生学生(单继承单继承)教职工教职工(单继承单继承)兼职教师兼职教师(单继承单继承)教师教师(单继承单继承)行政人员行政人员(单继承单继承)工人工人(单继承单继承)研研究究生生(单单继继承承)行政人员兼教师行政人员兼教师(多重继承多重继承)在职研究生在职研究生(多重继承多重继承)研究生助教研究生助教(多重继承多重继承)图图8.3大学在册人员继承关系大学在册人员继承关系8.3 多重

21、继承与派生类成员标识派生出来派生出来的新类同的新类同样可以作样可以作为基类再为基类再继续派生继续派生出更新的出更新的类,依此类,依此类推形成类推形成一个一个层次层次结构结构。例:输出派生类构造函数与析构函数的调用关系#includeclass Base1int x;public:Base1(int a) x=a;cout调用基类调用基类1的构造函数!的构造函数!n;Base1( )cout调用基类调用基类1的析构函数!的析构函数!n;class Base2int y;public:Base2(int a)y=a;cout调用基类调用基类2的构造函数!的构造函数!n;Base2()cout调用基

22、类调用基类2的析构函数!的析构函数!n;class Derived:public Base1, public Base2int z;public:Derived(int a, int b):Base1(a), Base2(20)z=b;cout调用派生类的构造函数!调用派生类的构造函数!n;Derived()cout调用派生类的析构函数!调用派生类的析构函数!n;void main(void)Derived c(100,200);例:派生类中包含对象成员#inlucde “liti11-4.h”class Der:public Base1, public Base2int z; Base1 b

23、1, b2;public:Der(int a, int b):Base1(a), Base2(20), b1(200), b2(a+b)z=b; cout调用派生类的构造函数!调用派生类的构造函数!n;Der()cout调用派生类的析构函数!调用派生类的析构函数!n;void main(void)Der d(100,200); 本例中类本例中类Circle为圆;类为圆;类Line为高;类为高;类Cone为圆锥,由为圆锥,由Circle和和Line公有派生而来。在公有派生而来。在Cone类中,类中,Circle和和Line类的接口完全不变,可以直接类的接口完全不变,可以直接调用,这就是公有派生的

24、优点。在调用,这就是公有派生的优点。在Cone的成员函的成员函数中可直接访问数中可直接访问Circle和和Line中的公有成员,但不中的公有成员,但不能直接访问私有成员。能直接访问私有成员。 【例8.2】由圆和高多重继承派生出圆锥检证检证主程序:主程序:圆类圆类Circle定义定义高类高类Line定义定义圆锥类圆锥类Cone定义定义8.3.1 冲突冲突在多重派生时,若一个公有的派生类是由两个或多个基类派生,在多重派生时,若一个公有的派生类是由两个或多个基类派生,当基类中成员的访问权限为当基类中成员的访问权限为public,且且不同基类中的成员具有相同的不同基类中的成员具有相同的名字名字时,出现

25、了重名的情况。这时在派生类使用到基类中的同名成员时,出现了重名的情况。这时在派生类使用到基类中的同名成员时,出现了不唯一性,这种情况称为时,出现了不唯一性,这种情况称为冲突冲突。classApublic:intx;voidShow()coutx=xn;A(inta)x=a;A();classBpublic:intx;voidShow()coutx=xn;B(inta)x=a;B();classC:publicA,publicBinty;public:voidSetx(inta)x=a;voidSety(intb)y=b;intGety()returny;voidmain(void)Cc1;c1

26、.Show();8.3 多重继承与派生类成员标识(冲突、支配规则、赋值兼容规则)下面是编译时的出错提示:下面是编译时的出错提示:D:VCsourceL11_7.cpp(19):errorC2385:C:xisambiguous(不明确的不明确的)D:VCsourceL11_7.cpp(19):warningC4385:couldbethexinbaseAofclassCD:VCsourceL11_7.cpp(19):warningC4385:orthexinbaseBofclassCD:VCsourceL11_7.cpp(26):errorC2385:C:ShowisambiguousD:VC

27、sourceL11_7.cpp(26):warningC4385:couldbetheShowinbaseAofclassCD:VCsourceL11_7.cpp(26):warningC4385:ortheShowinbaseBofclassC说明:说明:在派生类在派生类C中访问由基类继承来的变量中访问由基类继承来的变量x时,编译系统无法确定是要时,编译系统无法确定是要访问属于基类访问属于基类A中的中的x还是属于基类还是属于基类B中的中的x,因此编译出错。同样,在基因此编译出错。同样,在基类类C的对象的对象c1中调用函数中调用函数Show()时,也是无法确定是要调用类时,也是无法确定是要调用

28、类A中继承来中继承来的公有成员函数的公有成员函数Show(),还是调用类还是调用类B中继承来的公有成员函数中继承来的公有成员函数Show()。解决冲突问题的方法:解决冲突问题的方法:1.各基类中定义的成员函数名不同;各基类中定义的成员函数名不同;2.对于成员数据,在基类中说明其访问权限为对于成员数据,在基类中说明其访问权限为private,并在并在相应的基相应的基类中提供成员函数对这些成员数据进行操作。类中提供成员函数对这些成员数据进行操作。3.使用使用作用域分辨符作用域分辨符来限定所访问成员的属性,其格式为:来限定所访问成员的属性,其格式为:4.类名类名:成员名;成员名;classC:pub

29、licA,publicBinty;public:voidSetAx(inta)A:x=a; /对类对类A中的中的x设置设置voidSetBx(inta)B:x=a;/对类对类B中的中的x设置设置voidSety(intb)y=b;intGety()returny;voidmain(void)Cc1;c1.SetAx(35);c1.SetBx(100);c1.Sety(300);c1.A:Show();/调用类调用类A中的成员函数中的成员函数c1.B:Show();/调用类调用类B中的成员函数中的成员函数couty=c1.Gety()n;注意:当把派生类作为基类,又派生出新的派生类时,这种限定作

30、用域注意:当把派生类作为基类,又派生出新的派生类时,这种限定作用域的运算符不能嵌套使用,即:类名的运算符不能嵌套使用,即:类名1:类名类名2:.:成员名;成员名;也就是说,也就是说,限定作用域的运算符只能直接限定其成员限定作用域的运算符只能直接限定其成员。8.3.2 8.3.2 支配规则支配规则在在C+中,允许派生类中新增加的成员名与其基类的成员名相同,中,允许派生类中新增加的成员名与其基类的成员名相同,这种相同不产生冲突。当没有使用作用域运算符时,则派生类中定义的这种相同不产生冲突。当没有使用作用域运算符时,则派生类中定义的成员名优先于基类中的成员名,这种优先关系称为成员名优先于基类中的成员

31、名,这种优先关系称为支配规则支配规则。classApublic:intx;voidShow()coutx=xn;classBpublic:inty;voidShow()couty=yn;classC:publicA,publicBpublic:inty;voidmain(void)Cc1;c1.x=100;c1.y=200;c1.B:y=300;c1.A:Show();c1.B:Show();couty=c1.yn;couty=c1.B:yn;运行结果为:运行结果为:X=100Y=300Y=200Y=3008.3.3 8.3.3 继承和对象成员继承和对象成员C+规定,任一基类在派生类中只能继承

32、一次,否则会造成成员名的冲规定,任一基类在派生类中只能继承一次,否则会造成成员名的冲突。突。Class Aclass B:public A, public A public: float x;. ;此时在派生类此时在派生类B中包含了两个继承来的成员中包含了两个继承来的成员X,在使用时产生冲突。在使用时产生冲突。解决的方法是,在类解决的方法是,在类B的定义中加入类的定义中加入类A的两个对象作为类的两个对象作为类B的成员(对的成员(对象成员)。象成员)。Class B A a1, a2; ;注意:在派生类中,如果权限允许的话可以直接使用基类的成员,但这注意:在派生类中,如果权限允许的话可以直接使用

33、基类的成员,但这里使用的是对象成员的成员,必须在对象名后加上成员运算符里使用的是对象成员的成员,必须在对象名后加上成员运算符“.”和和成员名。成员名。/例:基类成员与对象成员在使用上的差别例:基类成员与对象成员在使用上的差别#includeclass Apublic:int x;A(int a=0)x=a;class Bpublic:int y;B(int a=0)y=a;class C:public Aint z;B b1;public:C(int a, int b, int m):A(a),b1(b)z=m;void show()coutx=xt;/直接使用成员名直接使用成员名couty=

34、b1.yt;/不能直接使用对象的成员名不能直接使用对象的成员名ycoutz=zn;void main(void)C c1(100,200,300);c1.show(); 8.3. 4 8.3. 4 赋值兼容规则赋值兼容规则简单的说就是:对于公有派生类来说,可以将派生类的对象赋给其基简单的说就是:对于公有派生类来说,可以将派生类的对象赋给其基类的对象,反之是不允许的。类的对象,反之是不允许的。classApublic:intx;.;classC:publicA,publicBpublic:inty;.;Cc1,c2,c3;Aa1,*pa1;Bb1,*pb1;classBpublic:inty;.

35、;2.不能将基类的对象赋给派生类对象。例如:不能将基类的对象赋给派生类对象。例如:3.c2=a1;c3=b1;3.可以将一个派生类对象的地址赋给基类的指针变量。例可以将一个派生类对象的地址赋给基类的指针变量。例如:如:4.pa1=&c2;pb1=&c3;4.派生类对象可以用来初始化对基类的引用。例如:派生类对象可以用来初始化对基类的引用。例如:5.B&rb=c1;说明:说明:1.派生类的对象可以赋给基类的对象,系统是将派生类对派生类的对象可以赋给基类的对象,系统是将派生类对象中从对应基类中继承来的成员赋给基类对象。例如:象中从对应基类中继承来的成员赋给基类对象。例如:2.a1=c1;/将将c1

36、中从类中从类A中继承来的对应成员中继承来的对应成员x分别分别赋给赋给a1的对应成员。的对应成员。3.b1=c1;/将将c1中从类中从类B中继承来的对应成员中继承来的对应成员y分别分别赋给赋给b1的对应成员。的对应成员。8.4 虚基类图11-2 派生类中包含同一基类的两个拷贝对于某一共公共基类对于某一共公共基类A,设类设类B由类由类A公有派生,类公有派生,类C也也由类由类A公有派生,而类公有派生,而类D是由类是由类B和类和类C共同公有派生。这时共同公有派生。这时在类在类D中将包含类中将包含类A的两个拷贝。的两个拷贝。基类A基类B基类C类Dpublicpublic基类AclassApublic:

37、intx;A(inta=0)x=a;classB:publicA/classB:A(200)x=200public:inty;B(inta=0,intb=0):A(b) /调用基类的构造函数调用基类的构造函数y=a;/y=100,voidPB()coutx=xty=yn;classC:publicA/classC:A(400)x=400public:intz;C(inta=0,intb=0):A(b)/调用基类的构造函数调用基类的构造函数z=a;/z=300voidPC()coutx=xtz=zn;classD:publicB,publicCpublic:intm;D(inta,intb,in

38、td,inte,intf):B(a,b),C(d,e)m=f;/m=500voidPrint(void)PB();/DPC();/Ecoutm=mn;voidmain(void)Dd1(100,200,300,400,500);d1.Print();注:由于在类注:由于在类D中包含了公共基类中包含了公共基类A的两个不同的拷贝,当的两个不同的拷贝,当D、E改为下改为下列情况时:列情况时:coutx=xty=yn;coutx=xtz=zn;编译器认为有错,因无法确定成员编译器认为有错,因无法确定成员x是从类是从类B中继承来的还是从类中继承来的还是从类C中继承来的,从面产生了冲突。为此,必须用作用域

39、运算符来限定成员中继承来的,从面产生了冲突。为此,必须用作用域运算符来限定成员的属性:的属性:B:x;C:x。VC+中提供了将基类说明为中提供了将基类说明为虚基类虚基类的方法,使得在多重派生的过程的方法,使得在多重派生的过程中公共基类只产生一个拷贝。在派生类的定义中,只要在基类的类名前中公共基类只产生一个拷贝。在派生类的定义中,只要在基类的类名前加上关键字加上关键字virtual,就可以将基类说明为就可以将基类说明为虚基类虚基类,格式为:,格式为:class类名类名:vitual基类名基类名;#includeclass Apublic:int x;A(int a=0)x=a;cout虚基类虚基

40、类A的构造函数的构造函数endl;A() cout虚基类虚基类A的析构函数的析构函数endl;class B:virtual public Apublic:int y;B(int a=0, int b=0):A(b) / 不调用虚基类的构造函数不调用虚基类的构造函数y=a;cout派生类派生类B的构造函数的构造函数endl;B()cout派生类派生类B的析构函数的析构函数endl;void PB( )coutx=xty=yn;class C:virtual public Apublic:int z;C(int a=0,int b=0):A(b) / 不调用虚基类的构造函数不调用虚基类的构造函数

41、z=a;cout派生类派生类C的构造函数的构造函数endl;C( )cout派生类派生类C的析构函数的析构函数endl;void PC()coutx=xtz=zn;class D : public B,public Cpublic:int m;D(int a, int b, int d, int e, int f):B(a,b),C(d,e)m=f;cout派生类派生类D的构造函数的构造函数endl;D()cout派生类派生类D的析构函数的析构函数endl;void Print(void)PB( );PC( );coutm=mn;voidmain(void)Dd1(100,200,300,40

42、0,500);d1.Print();d1.x=400;/对象对象d1中只有基类中只有基类A的一个拷贝的一个拷贝d1.Print();基类B基类C类D基类A派生类中包含同一基类的一个拷贝virtual1.X的初值为的初值为0,这是因为调用虚基类的构造函数的方法与调用一般基类,这是因为调用虚基类的构造函数的方法与调用一般基类的构造函数的方法不同,的构造函数的方法不同,编译器约定,在执行类编译器约定,在执行类B和类和类C的构造函数的构造函数时都不调用虚基类时都不调用虚基类A的构造函数,而是在类的构造函数,而是在类D的构造函数中直接调用的构造函数中直接调用虚基类虚基类A的缺省的构造函数,的缺省的构造函

43、数,使得使得x的初值为的初值为0。因此,虚基类定义中。因此,虚基类定义中一般要包含有缺省的构造函数。一般要包含有缺省的构造函数。2.若虚基类中没有定义缺省的构造函数,则在派生的每一个派生类的构若虚基类中没有定义缺省的构造函数,则在派生的每一个派生类的构造函数的初始化成员例表中都必须有对虚基类构造函数的调用造函数的初始化成员例表中都必须有对虚基类构造函数的调用。例如。例如将将F行改为:行改为:A(inta)x=a;则在类则在类D的构造函数初始化成员列表中必须增加直接调用虚基类的构造函数初始化成员列表中必须增加直接调用虚基类A的构造的构造函数:函数:D(inta,intb,intd,inte,in

44、tf):B(a,b),C(d,e),A(1000)此时则将类此时则将类D中的中的x成员初值置为成员初值置为1000。3.在派生类在派生类D的对象的对象d1中只有基类中只有基类A的一个拷贝,当改变成员的一个拷贝,当改变成员X的值时,的值时,由基类由基类B和和C中的成员函数输出的中的成员函数输出的X的值是相同的;的值是相同的;4.在派生类对象的创建中,首先是虚基类的构造函数并按它们声明的在派生类对象的创建中,首先是虚基类的构造函数并按它们声明的顺序构造。第二批是非虚基类的构造函数按它们声明的顺序调用。第顺序构造。第二批是非虚基类的构造函数按它们声明的顺序调用。第三批是成员对象的构造函数。最后是派生

45、类自己的构造函数被调用。三批是成员对象的构造函数。最后是派生类自己的构造函数被调用。书:书:P207【例例8.3】在采用虚基类的多重继承中,构造与析构的次序。在采用虚基类的多重继承中,构造与析构的次序。class Dclass:public Bclass1,virtual Bclass3,virtual Bclass2 Object object;public: Dclass():object(),Bclass2(),Bclass3(),Bclass1() cout派生类建立派生类建立!n; Dclass()cout派生类析构派生类析构!n;void main() Dclass dd; cou

46、t主程序运行主程序运行!n;运行结果运行结果Constructor Bclass3/第一个虚拟基类第一个虚拟基类,与派生类析构函数排列无关与派生类析构函数排列无关Constructor Bclass2/第二个虚拟基类第二个虚拟基类Constructor Bclass1/非虚拟基类非虚拟基类Constructor Object/对象成员对象成员派生类建立派生类建立!主程序运行主程序运行!派生类析构派生类析构!deconstructor Object/析构次序相反析构次序相反deconstructor Bclass1deconstructor Bclass2deconstructor Bclass

47、3 /析构的次序与构造的次序相反。析构的次序与构造的次序相反。8.6 多态性与虚函数 多态性是面向对象程序设计的关键技术之一。若程序设多态性是面向对象程序设计的关键技术之一。若程序设计语言计语言不支持多态性,不能称为面向对象的语言不支持多态性,不能称为面向对象的语言。利用多态。利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功性技术,可以调用同一个函数名的函数,实现完全不同的功能。能。在在C+中有两中有两种多态种多态性性编译时的多态性编译时的多态性运行时的运行时的多态性多态性运行时的多态性是指在程序执行运行时的多态性是指在程序执行前,无法根据函数名和参数来确前,无法根据函数名和参数来

48、确定该调用哪一个函数,必须在程定该调用哪一个函数,必须在程序执行过程中,根据执行的具体序执行过程中,根据执行的具体情况来动态地确定。它是情况来动态地确定。它是通过类通过类继承关系和虚函数来实现的。继承关系和虚函数来实现的。目的目的也是建立一种通用的程序也是建立一种通用的程序。通用性。通用性是程序追求的主要目标之一。是程序追求的主要目标之一。 通过函数的重载和运算通过函数的重载和运算符的重载来实现的。符的重载来实现的。8.6多态性与虚函数多态性与虚函数8.6.1虚函数的定义虚函数的定义8.6.4动态联编动态联编(选读)(选读)8.6.2纯虚函数纯虚函数8.6.3 继承与多态的应用继承与多态的应用

49、单链表派生类(选读)单链表派生类(选读)虚函数既虚拟函数,它必须是成员函数并且不能是静虚函数既虚拟函数,它必须是成员函数并且不能是静态函数。虚函数与成员函数一样包含一定的功能,所不态函数。虚函数与成员函数一样包含一定的功能,所不同的是虚函数可以在其派生类的定义中被重新定义。同的是虚函数可以在其派生类的定义中被重新定义。虚函数的定义格式如下:虚函数的定义格式如下:Virtualfuncname();当在派生类中定义了一个与该虚函数名同名的成员当在派生类中定义了一个与该虚函数名同名的成员函数,并且该成员函数的参数个数、参数的类型以及函函数,并且该成员函数的参数个数、参数的类型以及函数的返回值类型都

50、与基类中的同名虚函数一样,则无论数的返回值类型都与基类中的同名虚函数一样,则无论是否使用是否使用virtual来修饰该成员函数,它都成为一个虚函来修饰该成员函数,它都成为一个虚函数数。8.6.1 虚函数的定义例:使用虚函数例:使用虚函数#includeconst double PI=3.14159;class point double x, y;public:point(float i=0, float j=0)x=i; y=j;virtual double area( )return 0.0;class circle:public pointdouble radius;public:circ

51、le(double r=0)radius=r;double area( )return PI*radius*radius; voidmain(void)pointp; couttheareaofthepointpis:p.area()endl;circlec(3.65);couttheareaofthecirclecis:c.area()endl;point*pp;pp=&c;coutnow the area is: area()area()调用的是派生类调用的是派生类circle的成员函数的成员函数area()。可见,当基类。可见,当基类指针指向不同的派生类对象时,尽管调用的形式完全相同,指

52、针指向不同的派生类对象时,尽管调用的形式完全相同,但却是调用了不同对象中的虚函数,这就是但却是调用了不同对象中的虚函数,这就是运行时的多态运行时的多态性性。因此,输出结果仍为。因此,输出结果仍为:41.8538。关于虚函数的几点说明:关于虚函数的几点说明:1.当在基类中把成员函数定义为虚函数后,在其派生类中定义的虚当在基类中把成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中的虚函数同名,且参数的类型、顺序、个数及函数必须与基类中的虚函数同名,且参数的类型、顺序、个数及函数的返回值的类型也相同。函数的返回值的类型也相同。2.要实现动态(运行时)的多态性,必须使用基类类型的指针变量,要

53、实现动态(运行时)的多态性,必须使用基类类型的指针变量,使该指针指向不同派生类的对象,并通过调用指针所指向的虚函使该指针指向不同派生类的对象,并通过调用指针所指向的虚函数才行。数才行。3.虚函数必须是类的一个成员函数,它不能是友元函数,或静态成虚函数必须是类的一个成员函数,它不能是友元函数,或静态成员函数。员函数。4.在派生类中没有重新定义虚函数时,与一般的成员函数一样,当在派生类中没有重新定义虚函数时,与一般的成员函数一样,当调用这种派生类对象的虚函数时,则调用其基类中的虚函数。调用这种派生类对象的虚函数时,则调用其基类中的虚函数。5.可以把析构函数定义为虚函数,但是不能将构造函数定义为虚函

54、可以把析构函数定义为虚函数,但是不能将构造函数定义为虚函数。数。6.虚函数与一般的成员函数相比较,调用时的执行速度稍慢一些,虚函数与一般的成员函数相比较,调用时的执行速度稍慢一些,这是因为,为了实现多态性,在每一个派生类中均要保存相应的这是因为,为了实现多态性,在每一个派生类中均要保存相应的虚函数的入口地址表,函数的调用机制也是间接实现的。虚函数的入口地址表,函数的调用机制也是间接实现的。思考:在上例中若将基类思考:在上例中若将基类point中的虚函中的虚函数数virtualarea()改为一般成员函数改为一般成员函数,程序程序运行的结果将会如何?运行的结果将会如何?classpointdou

55、blex,y;public:point(floati=0,floatj=0)x=i;y=j;doublearea()return0.0;程序运行的结果变为:程序运行的结果变为:theareaofthepointpis:0theareaofthecirclecis:41.8538nowtheareais:0结果分析:结果分析:point*pp;pp=&c;coutnowtheareais:area()x*this-y;在在执执行行pp-area()时时,由由于于此此时时指指针针pp是是基基类类point的的指指针针,this指指针针是是指指向向基基类类的的,所所以以pp-area()实实际际调调

56、用用的的是是基基类类的的成成员员函函数数area(),而而不不是是派派生生类中的成员函数类中的成员函数area();虚函数怎样实现多态性:虚函数怎样实现多态性:【例【例8.6】计算学分。可由本科生类派生出研计算学分。可由本科生类派生出研究生类,但它们各自的从课程学时数折算为学究生类,但它们各自的从课程学时数折算为学分数的算法是不同的,本科生是分数的算法是不同的,本科生是16个学时一学个学时一学分,而研究生是分,而研究生是20个学时一学分。个学时一学分。【例【例8.7】计算学分。派生类定义不再重复。计算学分。派生类定义不再重复。纯虚函数纯虚函数(pure virtual function)是指被

57、标明是指被标明为不具体实现的虚拟成员函数。它用于这样的情为不具体实现的虚拟成员函数。它用于这样的情况:定义一个基类时,会遇到无法定义基类中虚况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。函数的具体实现,其实现依赖于不同的派生类。8.6.2 纯虚函数定义纯虚函数的一般格式为:定义纯虚函数的一般格式为:virtual 返回类型返回类型 函数名(参数表)函数名(参数表)=0;含有纯虚函数的基类是不能用来定义对象的。纯虚含有纯虚函数的基类是不能用来定义对象的。纯虚函数没有实现部分,不能产生对象,所以含有纯虚函数没有实现部分,不能产生对象,所以含有纯虚函数的类是抽象

58、类。函数的类是抽象类。1定义纯虚函数时,不能定义虚函数的实现部分。定义纯虚函数时,不能定义虚函数的实现部分。即使是函数体为空也不可以,函数体为空就可以执即使是函数体为空也不可以,函数体为空就可以执行,只是什么也不做就返回。而纯虚函数不能调用。行,只是什么也不做就返回。而纯虚函数不能调用。2 “=0”表明程序员将不定义该函数,函数声明是表明程序员将不定义该函数,函数声明是为派生类保留一个位置。为派生类保留一个位置。“=0”本质上是将指向函本质上是将指向函数体的指针定为数体的指针定为NULL。3 在派生类中必须有重新定义的纯虚函数的函数在派生类中必须有重新定义的纯虚函数的函数体,这样的派生类才能用

59、来定义对象。体,这样的派生类才能用来定义对象。8.6.2 纯虚函数定义纯虚函数必须注意:定义纯虚函数必须注意:#includeclassAprotected:intx;public:A()x=1000;virtualvoidprint()=0;/纯虚函数纯虚函数;classB:publicAinty;public:B()y=2000;voidprint()couty=yendl;classC:publicAintz;public:C()z=3000;voidprint()coutz=zprint();pa=&c;pa-print();运行结果为:运行结果为:Y=2000;Z=3000;如果在主

60、函数中增加说明:如果在主函数中增加说明:Aa;则因为抽象类则因为抽象类A不能产生对象,编不能产生对象,编译时将给出错误信息。同样若在主译时将给出错误信息。同样若在主函数中增加:函数中增加:A*pp;pp-print();也要产生运行错误,因为也要产生运行错误,因为pp的值也的值也是不确定的。是不确定的。【例【例8.8】学校对在册人员进行奖励,依据是业学校对在册人员进行奖励,依据是业绩分,但是绩分,但是业绩分的计算方法只能对具体人员进业绩分的计算方法只能对具体人员进行行,如学生,教师,行政人员,工人,算法都不,如学生,教师,行政人员,工人,算法都不同,所以可以将在册人员类作为一个抽象类,同,所以

61、可以将在册人员类作为一个抽象类,业业绩计算方法作为一个纯虚函数绩计算方法作为一个纯虚函数。在主函数中全部用指向基类的指针来调用在主函数中全部用指向基类的指针来调用业绩分业绩分基类定义基类定义业绩分业绩分学生派生类定义学生派生类定义业绩分业绩分教师派生类定义教师派生类定义验证验证主函数主函数8.6.3 8.6.3 继承与多态的应用继承与多态的应用单链表派生类(选读)单链表派生类(选读)【例【例8.10】通用单链表派生类。通用单链表派生类。首先首先改造改造【例【例7.4】的的头头文件,不文件,不采用模板采用模板类类,而采用虚函数,而采用虚函数实现实现多多态态性,达到通用的目的性,达到通用的目的。结

62、结点点类类数据域被改造数据域被改造为为指指针针,而把数据放在一个抽象,而把数据放在一个抽象类类中,由指中,由指针针与与之建立之建立联联系。系。数据域数据域(指向抽象(指向抽象数据类的指数据类的指针)针)由抽象类派由抽象类派生的数据类生的数据类对象(如串对象(如串对象)对象)指针域(指指针域(指向下一结向下一结点)点)结点类对象结点类对象动态建立的动态建立的数据类对象数据类对象图图8.9 结点构造结点构造class Object /数据类为抽象类数据类为抽象类public: Object() virtual bool operator (Object &)=0; /纯虚函数纯虚函数,参数必须为引

63、用或指针参数必须为引用或指针 virtual bool operator != (Object &)=0; /纯虚函数纯虚函数,参数必须为引用或指针参数必须为引用或指针 virtual void Print()=0; /纯虚函数纯虚函数 virtual Object() ; /析构函数可为虚函数,构析构函数可为虚函数,构造函数不行造函数不行首先看结点组织,采用结点类加数据类首先看结点组织,采用结点类加数据类数据类定义数据类定义:本题两个要点:本题两个要点:采用虚函数实现多态性,达到通用的目的。采用虚函数实现多态性,达到通用的目的。堆内存的分配与释放,关键不是创建,而是释放!堆内存的分配与释放,

64、关键不是创建,而是释放!数据抽象数据抽象类类中含有三个中含有三个纯纯虚函数:比虚函数:比较较函数和函数和输输出出函数。当抽象函数。当抽象类类在派生在派生时时重新定重新定义纯义纯虚函数,可以虚函数,可以进进行各种行各种类类型,包括型,包括类类和和结结构构对对象的比象的比较较和和输输出出。本例介绍程序总体组成为主,链表的操作本例介绍程序总体组成为主,链表的操作由学生自己由学生自己仔细仔细阅读。阅读。抽象类中的抽象类中的析构函数也是虚函数,这一点非常重要,析构函数也是虚函数,这一点非常重要,当抽象类派生的数据类的数据部分是动态产生,而当抽象类派生的数据类的数据部分是动态产生,而由结点类删除释放数据类

65、对象时,必须由数据类的由结点类删除释放数据类对象时,必须由数据类的析构函数来释放该类对象数据部分占用的动态分配析构函数来释放该类对象数据部分占用的动态分配的内存的内存。这时必须重新定义析构函数。这时必须重新定义析构函数。Class Node Object* info; /数据域用指针指向数据类对象数据域用指针指向数据类对象 Node* link; /指针域指针域public: Node(); /生成头结点的生成头结点的构造函数构造函数 Node(); /析构函数析构函数 void InsertAfter(Node* P); /在当前结点后插入一个结点在当前结点后插入一个结点 Node* Rem

66、oveAfter(); /删除当前结点的后继结点删除当前结点的后继结点,返回该结点备用,返回该结点备用 void Linkinfo(Object* obj); /把把数据对象连接到结点数据对象连接到结点 friend class List; /以以List为友元类,为友元类,List可直接访问可直接访问Node的私有函数,的私有函数,;结点类定义结点类定义:class List Node *head,*tail; /链表头指针和尾指针链表头指针和尾指针public: List(); /构造函数构造函数,生成头结点,生成头结点(空链表空链表) List(); /析构函数析构函数 void Mak

67、eEmpty(); /清空链表清空链表,只余表头结点,只余表头结点 Node* Find(Object & obj); /搜索数据域与定值相同的结点搜索数据域与定值相同的结点,返回该结点的地址,返回该结点的地址 int Length(); /计算单链表长度计算单链表长度 void PrintList(); /打印链表的数据域打印链表的数据域 void InsertFront(Node* p); /可用来向前生成链可用来向前生成链表表 void InsertRear(Node* p); /可用来向后生成链表可用来向后生成链表 void InsertOrder(Node* p); /按按升序生成链

68、表升序生成链表 Node* CreatNode(); /创建一个结点创建一个结点(孤立结点孤立结点) Node* DeleteNode(Node* p); ; /删除指定结点删除指定结点定义定义链表类链表类第二步第二步,由抽象类派生数据类(取代模板定义泛型类型,由抽象类派生数据类(取代模板定义泛型类型为具体类型)步骤是。这里为具体类型)步骤是。这里数据采用的是字符串数据采用的是字符串,字符,字符串是放在动态分配的堆内存中的,所以串是放在动态分配的堆内存中的,所以析构函数必须重析构函数必须重新定义新定义。为了完成字符串的比较和输出,重新定义了。为了完成字符串的比较和输出,重新定义了比比较和输出函

69、数(虚函数)较和输出函数(虚函数)。class StringObject:public Object string sptr;public: StringObject() sptr=; StringObject(string s)sptr=s; StringObject(); /析构函数析构函数 bool operator(Object &); /大于函数大于函数 bool operator!=(Object &); /不等于函数不等于函数 void Print(); /打印函数打印函数;验证验证主函数主函数运行结果运行结果 在该程序中,特别要仔细揣摩堆内存的分配与释放。删在该程序中,特别要仔

70、细揣摩堆内存的分配与释放。删除一个结点时系统自动调用结点类析构函数释放结点占用的除一个结点时系统自动调用结点类析构函数释放结点占用的动态内存,而结点类析构函数自动调用数据域类虚析构函数,动态内存,而结点类析构函数自动调用数据域类虚析构函数,数据域类析构函数自动调用数据域类析构函数自动调用string类的析构函数释放所占用的类的析构函数释放所占用的动态内存。一环套一环,一步都不能错。这是使用动态内存动态内存。一环套一环,一步都不能错。这是使用动态内存分配的关键。即关键不是创建,而是释放!分配的关键。即关键不是创建,而是释放!在在VC+平台上运行例平台上运行例8.10。动态联编(动态联编(dyna

71、micbinding)亦称滞后联编亦称滞后联编(latebinding),),对应于对应于静态联编(静态联编(staticbinding)。如果使用对象名和点成员选择运算符如果使用对象名和点成员选择运算符“.”引用引用特定的一个对象来调用虚函数,则被调用的虚特定的一个对象来调用虚函数,则被调用的虚函数是在编译时确定的(称为函数是在编译时确定的(称为静态联编静态联编)如果使用基类指针或引用指明派生类对象并使用该指针如果使用基类指针或引用指明派生类对象并使用该指针调用虚函数(成员选择符用箭头号调用虚函数(成员选择符用箭头号“-”),则程序动态),则程序动态地(运行时)选择该派生类的虚函数,称为地(

72、运行时)选择该派生类的虚函数,称为动态联编动态联编。8.6.4 动态联编(选读)联编是指计算机程序自身彼此关联的过程,是把一个标识符联编是指计算机程序自身彼此关联的过程,是把一个标识符名和一个存储地址联系在一起的过程,也就是把一条消息和名和一个存储地址联系在一起的过程,也就是把一条消息和一个对象的操作相结合的过程一个对象的操作相结合的过程 。图图8.9 8.9 虚函数调用的控制流程虚函数调用的控制流程“dogdog”StringObjectStringObject动态无名对象动态无名对象StringObjectStringObject动态无名对象动态无名对象“catcat”指向指向Object

73、Object类指针类指针指向结点类指针指向结点类指针指向指向ObjectObject类指针类指针指向结点类指针指向结点类指针指向指向ObjectObject类指针类指针指向结点类指针指向结点类指针StringObjectStringObject动态无名对象动态无名对象“cockcock” 析构函数指针析构函数指针0 0比较函数指针比较函数指针0 0输出函数指针输出函数指针StringObjectStringObject虚函数表虚函数表抽象类抽象类ObjectObject虚函数表虚函数表析构函数指针析构函数指针比较函数指针比较函数指针输出函数指针输出函数指针ComplexObjectComple

74、xObject虚函数虚函数 析构函数指针析构函数指针 比较函数指针比较函数指针 输出函数指针输出函数指针 缺缺 省省 析析构函数构函数释释放放动动态态串串析构函数析构函数串比较函数串比较函数打印串函数打印串函数缺省析构缺省析构函数函数复复数数模模大大小小比较函数比较函数打印复数函数打印复数函数8.6.4 动态联编(选读) C+编译器编译含有一个或几个虚函数的类及编译器编译含有一个或几个虚函数的类及其派生类其派生类 时,对该类建立时,对该类建立虚函数表(虚函数表(Virtual function table,vtable)。 虚函数表使执行程虚函数表使执行程序正确选择每次执行时应使用的虚函数。序

75、正确选择每次执行时应使用的虚函数。 多态是由复杂的数据结构实现的,参见图多态是由复杂的数据结构实现的,参见图8.10。图。图8.10是以是以【例【例8.10】为基础的,不过增为基础的,不过增加了一个由抽象类加了一个由抽象类Object派生的复数数据类派生的复数数据类ComplexObject。图中列出了基类和各派生类的图中列出了基类和各派生类的虚函数表,这些表是由指向函数的指针组成的虚函数表,这些表是由指向函数的指针组成的。 8.6.4 动态联编(选读) 还有第二层指针,在实例化带虚函数的类(创还有第二层指针,在实例化带虚函数的类(创建对象)时,编译器建对象)时,编译器在对象前加上一个指向该类

76、的在对象前加上一个指向该类的虚函数表的指针虚函数表的指针。 第三层指针是链表结点类对象中第三层指针是链表结点类对象中指向抽象基类指向抽象基类Object的指针的指针(这也可以是引用,但本例是指针)。(这也可以是引用,但本例是指针)。 虚函数的调用是这样进行的,考虑虚函数虚函数的调用是这样进行的,考虑虚函数Compare(),则看含则看含“cat”的结点。由该结点的的结点。由该结点的info指针找指针找到含到含“cat”的无名对象,再由对象前的指针找到的无名对象,再由对象前的指针找到StringObject虚函数表,移动虚函数表,移动4个字节(一个指针占个字节(一个指针占4个字节)找到比较函数指

77、针,进入串比较函数。个字节)找到比较函数指针,进入串比较函数。完完第八章 继承与派生谢谢!谢谢!Person:Person(string id, string name,Tsex sex,int birthday, string homeadd)IdPerson=id;Name=name;Sex=sex;Birthday=birthday;HomeAddress=homeadd; /作为一个管理程序作为一个管理程序,这个构造函数并无必要这个构造函数并无必要,因因为数据总是另外输入的。仅为说明语法存在。为数据总是另外输入的。仅为说明语法存在。分析构造函数:分析构造函数:Person:Person

78、()IdPerson=#;Name=#;Sex=mid;Birthday=0;HomeAddress=#;分析缺省的构造函数:分析缺省的构造函数:分析析构函数:分析析构函数:Person:Person() /string内部动态数组的释放,由内部动态数组的释放,由string自带的自带的析构函数完成析构函数完成void Person:SetName(string name)Name=name; /拷入新姓名拷入新姓名修改名字修改名字:void Person:SetHomeAdd(string homeadd)HomeAddress=homeadd;修改住址修改住址:void Person:Pr

79、intPersonInfo()int i;cout身份证号身份证号:IdPersonn姓名姓名: Namen性别性别:;if(Sex=man)cout男男n;else if(Sex=woman)cout女女n; else cout n;cout出生年月日出生年月日:;i=Birthday;couti/10000年年;i=i%10000;couti/100月月i%100日日n 家庭住址家庭住址:HomeAddressn;输出个人信息输出个人信息:Student:Student(string id, string name,Tsex sex,int birthday, string homeadd

80、, string nostud) :Person(id,name,sex,birthday,homeadd) /注意Person参数表不用类型NoStudent=nostud;for(int i=0;i30;i+) /课程与成绩清空csi.coursename=#;csi.grade=0;派生类构造函数派生类构造函数:Student:Student()/基类缺省的无参数构造函数不必显式给出基类缺省的无参数构造函数不必显式给出int i; NoStudent=;for(i=0;i30;i+) /课程与成绩清零课程与成绩清零,将来由键盘输入将来由键盘输入 csi.coursename=;csi.g

81、rade=0;Student:Student() /基类析构函数以及成员对象析构函数自动调用基类析构函数以及成员对象析构函数自动调用缺省派生类构造函数缺省派生类构造函数:派生类析构函数派生类析构函数:int Student:SetCourse(string coursename,int grade) bool b=false; /标识新输入的课程标识新输入的课程,还是更新成绩还是更新成绩 int i; for(i=0;i30;i+) if(csi.coursename=#) /判表是否进入未使用部分(如有对应删除判表是否进入未使用部分(如有对应删除,应按顺序表方式)应按顺序表方式)csi.co

82、ursename=coursename;csi.grade=grade; b=false;break; else if(csi.coursename=coursename)/是否已有该课程记录是否已有该课程记录 csi.grade=grade;b=true;break; if(i=30) return 0; /成绩表满返回成绩表满返回0 if(b) return 1; /修改成绩返回修改成绩返回1 else return 2; /登记成绩返回登记成绩返回2学生类课程设置函数:学生类课程设置函数:int Student:GetCourse(string coursename) int i; fo

83、r(i=0;i30;i+) if(csi.coursename=coursename) return csi.grade; return -1; /找到返回成绩找到返回成绩,未找到返回未找到返回-1查找学生课程成绩函数:查找学生课程成绩函数:void Student:PrintStudentInfo()int i;cout学号学号:NoStudentn;PrintPersonInfo();for(i=0;i30;i+) /打印各科成绩打印各科成绩if(csi.coursename!=#) coutcsi.coursename tcsi.graden;else break;cout-完完- en

84、dl;打印学生情况函数:打印学生情况函数:例例8.1验证用主函数验证用主函数:int main(void) char temp30; int i,k; Person per1(320102820818161,沈俊沈俊, man,19820818,南京四牌楼南京四牌楼2号号); Person per2; per2.SetName(朱明朱明); per2.SetSex(woman); per2.SetBirth(19780528); per2.SetId(320102780528162); per2.SetHomeAdd(南京市成贤街南京市成贤街9号号); per1.PrintPersonInfo

85、(); per2.PrintPersonInfo(); Student stu1(320102811226161,朱海鹏朱海鹏, man,19811226,南京市黄浦路南京市黄浦路1号号,06000123); cout请输入各科成绩请输入各科成绩:temp; /输入格式输入格式:物理物理 80 if(!strcmp(temp,end) break; cink; i=stu1.SetCourse(temp,k); if(i=0)cout成绩列表已满成绩列表已满!n; else if(i=1)cout修改成绩修改成绩n; else cout登记成绩登记成绩n; stu1.PrintStudentI

86、nfo();while(1) cout查询成绩查询成绩n请输入科目请输入科目:temp; if(!strcmp(temp,end) break; k=stu1.GetCourse(temp); if(k=-1)cout未查到未查到n; else coutkn; return 0;【例8.2】由圆和高多重继承派生出圆锥class Circle float x,y,r; /(x,y)为圆心,r为半径public: Circle(float a=0,float b=0,float R=0)x=a;y=b;r=R; void Setcoordinate(float a,float b)x=a;y=b;

87、 /设置圆心坐标 void Getcoordinate(float &a,float &b)a=x;b=y; void SetR(float R)r=R; /设置半径 float GetR()return r;/取圆半径 float GetAreaCircle()return float(r*r*3.14159); /取圆面积 float GetCircumference()return float(2*r*3.14159); /取圆周长;圆类圆类Circle:高类高类Line:【例8.2】由圆和高多重继承派生出圆锥class Line float High;public: Line(floa

88、t a=0)High=a; void SetHigh(float a)High=a; float GetHigh()return High;class Cone:public Circle,public Linepublic: Cone(float a,float b,float R,float d):Circle(a,b,R),Line(d) float GetCV()return float(GetAreaCircle()*GetHigh()/3); /取得圆锥体积 float GetCA() /取得圆锥表面积 float tr,th; tr=GetR(); /共有派生类中不能直接访问直接

89、基类的私有成员 th=GetHigh(); return float(GetAreaCircle()+tr*3.14159*sqrt(tr*tr+th*th); ;【例8.2】由圆和高多重继承派生出圆锥派生类圆锥:派生类圆锥:在在VC+平台上运行例平台上运行例8.2void main()Cone c1(5,8,3,4);float a,b;cout圆锥体积:c1.GetCV()n;cout圆锥表面积:c1.GetCA()n;cout圆锥底面积:c1.GetAreaCircle()n;cout圆锥底周长:c1.GetCircumference()n;cout圆锥底半径:c1.GetR()n;c1

90、.Getcoordinate(a,b);cout圆锥底圆心坐标:(a,b)n;cout圆锥高:c1.GetHigh()n;【例8.2】由圆和高多重继承派生出圆锥检证主程序:检证主程序:class Student:public virtual Personstring NoStudent; /学号/30门课程与成绩略public:Student(string id, string name,Tsex sex,int birthday, string homeadd, string nostud);Student(); Student()cout析构Studentendl;void PrintSt

91、udentInfo(); ;Student:Student(string id, string name,Tsex sex, int birthday, string homeadd, string nostud) :Person(id,name,sex,birthday,homeadd) /注意Person参数名表不用类型cout构造Studentendl;NoStudent=nostud;例8.4 虚基类与在职研究生以虚基类定义公有派生的学生类以虚基类定义公有派生的学生类class GStudent:public Student /以虚基类定义公有派生的研究生以虚基类定义公有派生的研究生类

92、类 string NoGStudent; /研究生号,其他略研究生号,其他略public: GStudent(string id, string name,Tsex sex,int birthday, string homeadd, string nostud,string nogstudent); /注意派生类构造函数声明方注意派生类构造函数声明方式式 GStudent(); GStudent()cout析构析构GStudentendl; void PrintGStudentInfo();GStudent:GStudent(string id, string name,Tsex sex, i

93、nt birthday, string homeadd, string nostud, string nogstud): Student(id,name,sex,birthday,homeadd,nostud), Person(id,name,sex,birthday,homeadd) /因因Person是虚基类是虚基类,尽管不是直接基类尽管不是直接基类, Person必须出现。必须出现。 /不定义对象可不出现,为通用应出现。如不是虚基类,出现是错误不定义对象可不出现,为通用应出现。如不是虚基类,出现是错误的的 cout构造构造GStudentendl; NoGStudent=nogstud;

94、例8.4 虚基类与在职研究生以虚基类定义公有派生的研究生类以虚基类定义公有派生的研究生类例8.4 虚基类与在职研究生class Employee:public virtual Person string NoEmployee; /教职工号,其他略教职工号,其他略public: Employee(string id, string name,Tsex sex,int birthday, string homeadd, string noempl); Employee(); Employee()cout析构析构Employeeendl; void PrintEmployeeInfo(); void

95、 PrintEmployeeInfo1();/多重继承时避免重复打印虚基类多重继承时避免重复打印虚基类Person的信息的信息;Employee:Employee(string id, string name,Tsex sex,int birthday, string homeadd, string noempl) :Person(id,name,sex,birthday,homeadd) cout构造构造Employeeendl; NoEmployee=noempl;以虚基类定义公有派生的教职工类以虚基类定义公有派生的教职工类例8.4 虚基类与在职研究生class EGStudent:pub

96、lic Employee,public GStudent string NoEGStudent; /在职学习号,其他略在职学习号,其他略public: EGStudent(string id, string name,Tsex sex,int birthday,string homeadd, string nostud,string nogstud, string noempl, string noegstud); EGStudent(); EGStudent()cout析构析构EGStudentendl; void PrintEGStudentInfo();EGStudent:EGStude

97、nt(string id, string name,Tsex sex,int birthday, string homeadd,string nostud, string nogstud, string noempl, string noegstud) :GStudent(id,name,sex,birthday,homeadd,nostud,nogstud), Employee(id,name,sex,birthday,homeadd,noempl), Person(id,name,sex,birthday,homeadd) cout构造构造EGStudentendl; NoEGStuden

98、t=noegstud;多重继承的以虚基类定义多重继承的以虚基类定义公有派生的在职研究生类公有派生的在职研究生类【例例8.5】为例为例8.1自定义拷贝函数,实现深拷贝。自定义拷贝函数,实现深拷贝。Person和和Student拷贝构造函数如下:拷贝构造函数如下:例8.5 深拷贝函数Person:Person(Person &ps)IdPerson=ps.IdPerson;Name=ps.Name;Sex=ps.Sex;Birthday=ps.Birthday;HomeAddress=ps.HomeAddress;Student:Student(Student &Std):Person(Std)

99、/按赋值兼容规则按赋值兼容规则Std可为可为Person实参实参NoStudent=Std.NoStudent;for(int i=0;iPerson:operator=(Std); /注意标准格式注意标准格式NoStudent=Std.NoStudent;for(int i=0;i30;i+)csi.coursename=Std.csi.coursename;csi.grade=Std.csi.grade;return *this;以上定义的实际上就是缺省的按语义的拷贝构造函数和以上定义的实际上就是缺省的按语义的拷贝构造函数和拷贝赋值操作符。可在主程序中增加内容以进行检验:拷贝赋值操作符。可

100、在主程序中增加内容以进行检验:int main(void) string temp; int i,k; Person per1(320102820818161,沈俊沈俊,man,19820818,“ 南京四牌楼南京四牌楼2号号); per1.PrintPersonInfo(); Person per2=per1,per3; /基类对象拷贝初始化基类对象拷贝初始化 per2.PrintPersonInfo(); per3=per1; /基类对象深拷贝赋值基类对象深拷贝赋值 per3.PrintPersonInfo(); Student stu1(320102811226161,朱海鹏朱海鹏,ma

101、n, 19811226,南京市黄浦路南京市黄浦路1号号,06000123); cout请输入各科成绩请输入各科成绩:temp; /输入格式输入格式:物理物理 80if(temp=end) break;cink;i=stu1.SetCourse(temp,k);if(i=0)cout成绩列表已满成绩列表已满!n;else if(i=1)cout修改成绩修改成绩n; else cout登记成绩登记成绩n; stu1.PrintStudentInfo(); Student stu2=stu1,stu3; /派生类对象深拷贝初始化派生类对象深拷贝初始化 stu2.PrintStudentInfo();

102、 stu3=stu2; /派生类对象深拷贝赋值派生类对象深拷贝赋值 stu3.PrintStudentInfo(); return 0;例8.5 深拷贝函数在在VC+平台上运行例平台上运行例8.5,看运行结果。,看运行结果。class Student string coursename;/课程名课程名 int classhour;/学时学时 int credit;/学分学分,未考虑未考虑0.5学分学分public: Student()coursename=#;classhour=0;credit=0; virtual void Calculate()credit=classhour/16; v

103、oid SetCourse(string str,int hour)coursename=str;classhour=hour; int GetHour()return classhour; void SetCredit(int cred)credit=cred; void Print() coutcoursenametclasshour 学时学时tcredit学分学分endl; ;基类定义:基类定义:例8.6 虚函数计算学分classGradeStudent:publicStudentpublic:GradeStudent();/对基类缺省的构造函数不必显式调用对基类缺省的构造函数不必显式调

104、用 voidCalculate()SetCredit(GetHour()/20);派生类定义:派生类定义:例8.6 虚函数计算学分int main() Student s,*ps; GradeStudent g; s.SetCourse(物理物理,80); s.Calculate(); g.SetCourse(物理物理,80); g.Calculate(); cout本科生:本科生:t; s.Print(); cout研究生:研究生:Calculate();cout本科生:本科生:Print();ps=&g;ps-Calculate();cout研究生:研究生:Print(); return

105、0例8.6 虚函数计算学分结果为结果为:本科生:物理本科生:物理80学时学时5学分学分研究生:物理研究生:物理80学时学时4学分学分本科生:数学本科生:数学160学时学时10学分学分研究生:数学研究生:数学160学时学时8学分学分第一行学分是由第一行学分是由Student类的类的成员函数成员函数Calculate()计算。计算。第二行学分是由第二行学分是由GradeStudent重新定义的重新定义的Calculate()计算,它屏蔽了基类的计算,它屏蔽了基类的同名函数。同名函数。第三行用的是指向第三行用的是指向Student类类的对象的对象s的指针,当然用的是的指针,当然用的是Student类

106、的类的Calculate()。指针类型是指向基类的指针,但这里指指针类型是指向基类的指针,但这里指针指向了派生类针指向了派生类GradeStudent的对象的对象g,按赋值兼容规则是准许的,但只能用按赋值兼容规则是准许的,但只能用基类的成员,可实际上用了派生中新定基类的成员,可实际上用了派生中新定义的义的Calculate()。这就是虚函数体现的这就是虚函数体现的多态性多态性,如果不是虚函数,第四行输出,如果不是虚函数,第四行输出是是10学分。学分。【例【例8.7】计算学分。派生类定义不再重复。计算学分。派生类定义不再重复。voidCalfun(Student&ps,stringstr,int

107、hour)ps.SetCourse(str,hour);ps.Calculate();ps.Print();intmain()Students;GradeStudentg;cout本科生本科生:;Calfun(s,物理物理,80);cout研究生研究生:;Calfun(g,物理物理,80);return0;例8.7引用,实现运行时的多态性这里没有用指针,这里没有用指针,而用了而用了Student的的引用,同样实现运引用,同样实现运行时的多态性行时的多态性,加,加了一个了一个Calfun()函函数,使用更为方便。数,使用更为方便。在在VC+平台上运行例平台上运行例8.7,看运行结果。,看运行结果

108、。class Person int MarkAchieve; string Name;public: Person(string name) Name=name; MarkAchieve=0; void SetMark(int mark)MarkAchieve=mark; virtual void CalMark()=0; /CalMark()为纯虚函数为纯虚函数,Person为抽象类为抽象类 void Print() coutName的业绩分为的业绩分为:MarkAchieveCalMark();pp-Print();pp=&t1;pp-CalMark();pp-Print();pp=&t2

109、;pp-CalMark();pp-Print();return 0;例8.8 业绩分的计算class Simpson;/Intevalue积分值,积分值,a积分下限,积分下限,b积分上限积分上限 double Intevalue,a,bpublic: virtual double fun(double x)=0; /被积函数声明为纯虚函数被积函数声明为纯虚函数 Simpson(double ra=0,double rb=0)a=ra;b=rb;Intevalue=0; void Integrate() double dx; int i; dx=(b-a)/2000; Intevalue=fun

110、(a)+fun(b); for(i=1;i2000;i+=2) Intevalue+=4*fun(a+dx*i); for(i=2;i2000;i+=2) Intevalue+=2*fun(a+dx*i); Intevalue*=dx/3; void Print()cout积分值积分值=IntevalueIntegrate(); /动态动态 B b1(0.0,1.0); b1.Integrate(); /静态静态 s-Print(); b1.Print(); return 0;在在VC+平台上运行例平台上运行例8.9。Node:Node()info=NULL;link=NULL;Node:No

111、de()cout删除结点类删除结点类t;delete info; /释放数据域释放数据域,自动调用数据域类析构函数自动调用数据域类析构函数, /而数据域类对象是在而数据域类对象是在main()中建立中建立void Node:Linkinfo(Object * obj)info=obj; /把把数据对象连接到结点数据对象连接到结点【例8.10】通用单链表派生类List:List()head=tail=new Node();List:List() MakeEmpty();cout删除头结点删除头结点link!=NULL) tempP=head-link; head-link=tempP-link;

112、 /把头结点后的第一个节点从链中脱离把头结点后的第一个节点从链中脱离 delete tempP; /释放该结点释放该结点,先自动调用结点类的析构函数先自动调用结点类的析构函数, /再自动调用数据域类的再自动调用数据域类的/析构函数,析构函数, /不可在前面加不可在前面加 delete tempP-info; 以释放数据域类以释放数据域类 tail=head; /表头指针与表尾指针均指向表头结点,表示空链表头指针与表尾指针均指向表头结点,表示空链【例8.10】通用单链表派生类Node* List:Find(Object & obj) /对抽象类只能用对抽象类只能用“引用引用”Node* temp

113、P=head-link;while(tempP!=NULL&*tempP-info!=obj) tempP=tempP-link;return tempP; /搜索成功返回该结点地址,不成功返回搜索成功返回该结点地址,不成功返回NULLvoid List:PrintList()Node* tempP=head-link;while(tempP!=NULL)tempP-info-Print(); /利用数据类的打印虚函利用数据类的打印虚函数数tempP=tempP-link;coutlink,*tempQ=head; /tempQ指向指向tempP前面的一个节点前面的一个节点while(temp

114、P!=NULL)if(*tempP-info*p-info) break; /找第一个比插入结点大的结点,由找第一个比插入结点大的结点,由tempP指向指向tempQ=tempP;tempP=tempP-link;tempQ-InsertAfter(p); /插在插在tempP指向结点之前,指向结点之前,tempQ之后之后if(tail=tempQ) tail=tempQ-link;【例8.10】通用单链表派生类StringObject:StringObject() cout“数据类析构数据类析构”(Object & obj) /虚函数虚函数 StringObject & temp=(Stri

115、ngObject &)obj; /必须转换必须转换 return sptrtemp.sptr;bool StringObject:operator!=(Object & obj) /虚函数虚函数 StringObject & temp=(StringObject &)obj; /必须转换必须转换 return sptr!=temp.sptr;void StringObject:Print() /虚函数虚函数 coutsptrt; 【例8.10】通用单链表派生类int main() Node * P1; StringObject* p; List list1,list2,list3; char

116、*a5=dog,cat,bear,sheep,ox,*sp=cat; int i; for(i=0;iLinkinfo(p); /数据对象连接到结点数据对象连接到结点list1.InsertFront(P1); /向前生成向前生成list1p=new StringObject(ai); /将在将在Node的析构函数中释放的析构函数中释放P1=list2.CreatNode();P1-Linkinfo(p);list2.InsertRear(P1); /向后生成向后生成list2 list1.PrintList(); coutlist1长度:长度:list1.Length()endl; list

117、2.PrintList(); cout要求删除的字符串要求删除的字符串catendl;【例8.10】通用单链表派生类 p=new StringObject(sp); /为了程序的通用性只能多一次转换为了程序的通用性只能多一次转换 P1=list1.Find(*p); delete p; if(P1!=NULL)cout删除删除catendl;P1=list1.DeleteNode(P1);delete P1;list1.PrintList();coutlist1长度:长度:list1.Length()endl; else cout未找到未找到endl; cout清空清空list1endl; l

118、ist1.MakeEmpty(); /清空清空list1 list1.PrintList(); for(i=0;iLinkinfo(p);list3.InsertOrder(P1); /升序创建升序创建list3 list3.PrintList(); cout程序结束程序结束endl; return 0;【例8.10】通用单链表派生类ox sheep bear cat dog /向前生成的链表向前生成的链表list1list1长度:长度:5dogcatbear sheepox /向后生成的链表向后生成的链表list2要求删除的字符串要求删除的字符串cat删除字符串类删除字符串类 /对应对应de

119、lete p;删除删除cat删除结点类删除结点类数据类析构数据类析构ox sheep bear dog /删除删除cat后的后的list1list1长度:长度:4清空清空list1删除结点类删除结点类数据类析构数据类析构 /ox /删除删除(释放释放)该结点该结点,自动调用结点类析自动调用结点类析构构 函数函数, 进一步自动调用数据域类析构函数进一步自动调用数据域类析构函数删除结点类删除结点类数据类析构数据类析构 /sheep 删除结点类删除结点类数据类析构数据类析构 / bear 删除结点类删除结点类数据类析构数据类析构 / dogbear cat dog ox sheep /升序创建的链表

120、升序创建的链表list3程序结束程序结束删除结点类删除结点类数据类析构数据类析构 /自动删除链表自动删除链表list3的的5个结点个结点:bear删除结点类删除结点类 数据类析构数据类析构 / cat 删除结点类删除结点类 数据类析构数据类析构 / dog删除结点类删除结点类 数据类析构数据类析构 / ox 删除结点类删除结点类 数据类析构数据类析构 / sheep 删除头结点删除头结点 删除结点类删除结点类 /删除链表删除链表list3的头结点的头结点删除结点类删除结点类数据类析构数据类析构 /自动删除链表自动删除链表list2的的5个结点个结点: dog删除结点类删除结点类 数据类析构数据类析构 / cat删除结点类删除结点类 数据类析构数据类析构 / bear删除结点类删除结点类 数据类析构数据类析构 / sheep删除结点类删除结点类 数据类析构数据类析构 / ox删除头结点删除头结点 删除结点类删除结点类 /删除链表删除链表list2的头结点的头结点删除头结点删除头结点 删除结点类删除结点类 /删除链表删除链表list1的头结点的头结点

展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 办公文档 > 工作计划

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号