1、第七章,继承和派生,北京林业大学 软件教研室,2,本章主要内容,(1)理解继承的概念和意义。 (2)理解单一继承、多重继承中基类与派生类之间的关系。 (3)理解并掌握派生类构造函数的编写要求以及派生类对象的构造过程和机理。 (4)掌握虚函数和多态性的概念。 (5)掌握虚函数的定义方法、调用方法及其在实现多态性方面所起到的作用。 (6)了解纯虚函数与抽象基类的概念。,北京林业大学 软件教研室,3,7.1 继承和派生的概念,继承(Inheritance)就是在一个已存在的类的基础上建立一个新类,实质就是利用已有的数据类型定义出新的数据类型。 在继承关系中: 被继承的类称为基类(Base class
2、)(或父类) 定义出来的新类称为派生类(Derived class)(子类),北京林业大学 软件教研室,4,派生类不仅可以继承原来类的成员,还可以通过以下方式扩充新的成员: (1)增加新的数据成员 (2)增加新的成员函数 (3)重新定义已有成员函数 (4)改变现有成员的属性,北京林业大学 软件教研室,5,多层次继承: 在派生过程中,派生出来的新类同样可以作为基类再继续派生出更新的类,依此类推形成一个层次结构。 直接参与派生出某类称为直接基类; 而基类的基类,以及更深层的基类称为间接基类。 在类的层次结构中,处于高层的类表示最一般的特征,而处于底层的类则表示更具体的特征。类族: 同时一个基类可以
3、直接派生出多个派生类。 这样形成了一个相互关联的类族。 如,MFC就是这样的类族,它由一个CObject类派生出200个MFC类中的绝大多数。,北京林业大学 软件教研室,6,多重继承,单继承:派生类只有一个直接基类。 多重继承:派生类同时有多个直接基类。,单继承,北京林业大学 软件教研室,7,7.2 单继承,7.2.1 单继承的定义方式 class 派生类名: 访问方式 基类名 派生类中的新成员 ,北京林业大学 软件教研室,8,【例7-1】类Build_1是一个关于楼房数据的类。它的数据成员有posi_x、posi_y和area,分别表示楼房位置的经、纬度和建筑面积。它的成员函数只有set1,
4、用于设置数据成员posi_x、posi_y和area的值。让Build_1作为基类,再增加数据成员height、成员函数set2和show来定义派生类Build_2。,北京林业大学 软件教研室,9,7.2.2 派生类的成员构成,派生新类经历了三个步骤: (1)吸收基类成员 派生类继承和吸收了基类的全部数据成员和除了构造函数、析构函数之外的全部成员函数。 (2)改造基类成员 一是基类成员的访问方式问题; 二是对基类数据成员或成员函数的覆盖。 (3)添加新成员 保证了派生类在功能上比基类有所发展。,北京林业大学 软件教研室,10,7.2.3 继承方式对基类成员的访问属性控制,1. 公有继承(pub
5、lic继承方式) 基类中public和protected成员的访问属性在派生类中不变; 而基类中的不可访问成员和private成员在派生类中不可访问。 注意:不可访问成员与私有成员的区别。,北京林业大学 软件教研室,11,2. 私有继承(private继承方式) 基类中public和protected成员都以private成员出现在派生类中; 而基类中的不可访问成员和private成员在派生类中不可访问。 相当于中止了基类功能的继续派生!,北京林业大学 软件教研室,12,3. 保护继承(protected继承方式) 基类中public和protected成员都以protected成员出现在派生
6、类中; 而基类中的不可访问成员和private成员在派生类中不可访问。,北京林业大学 软件教研室,13,类的继承方式对基类成员的访问属性控制,北京林业大学 软件教研室,14,【例7-2】验证公有继承方式下,类成员的访问特性。,北京林业大学 软件教研室,15,【例7-3】验证私有继承方式下,类成员的访问特性。,北京林业大学 软件教研室,16,【例7-4】验证保护继承方式下,类成员的访问特性。,北京林业大学 软件教研室,17,7.2.4 派生类的构造函数,派生类的构造函数: 一方面负责调用基类的构造函数对基类成员进行初始化; 另一方面还要负责对基类的构造函数所需要的参数进行必要的设置。派生类名:
7、:派生类构造函数名(总参数列表):基类构造函数名(参数列表) 派生类中新增数据成员初始化语句 ,北京林业大学 软件教研室,18,在定义派生类的对象时: 系统首先执行基类的构造函数; 然后执行派生类的构造函数。 【例7-5】定义一个点类Point,并由此派生出一个圆类Circle,并测试派生类的构造函数和基类的构造函数的执行顺序。,北京林业大学 软件教研室,19,派生类对基类成员重新定义: 通过派生类的对象调用一个被重新定义过的基类的成员函数,被调用的是派生类的成员函数; 此时,若想调用基类的同名成员函数,必须在成员函数名前加基类名和作用域运算符“:”。【例7-6】本例中分别定义一个描述圆的类C
8、circle和一个描述圆柱体的类Ccylinder。,北京林业大学 软件教研室,20,在C+中,处理同名函数时有以下3种基本方法: 根据函数参数的特征进行区分,即编译器根据参数的类型或个数进行区分。如: max(int,int) max(float,float) 根据类对象进行区分。如:在上例中的main函数中, cylinder.area() circle.area() 使用作用域运算符“:”进行区分,如: Ccircle:area() 以上3种方法都是在程序编译过程中完成的,因此称为静态联编。,北京林业大学 软件教研室,21,7.2.5 有子对象的派生类的构造函数,派生类名:派生类构造函数
9、名(总参数列表):基类构造函数名(参数列表),子对象名(参数列表), 派生类中新增数据成员初始化语句 此时,构造函数执行的一般次序为: 调用基类的构造函数。 调用子对象的构造函数。当派生类中含有多个子对象时,各子对象的构造函数的调用顺序按照它们在类中说明的先后顺序进行。 执行派生类构造函数的函数体。,北京林业大学 软件教研室,22,7.2.6 派生类的析构函数,析构函数的作用是在对象撤销之前,进行必要的清理工作。 当对象被删除时,系统会自动调用析构函数。 析构函数的调用顺序与构造函数的调用顺序正好相反: 先执行派生类自己的析构函数; 然后调用子对象的析构函数; 最后调用基类的析构函数。 【例7
10、-7】分析以下程序的执行结果。,北京林业大学 软件教研室,23,练习,类有两种用法:一种是类的实例化,即生成类的对象;另一种是通过_,派生出新的类。 如果类A继承了类B,则类A称为_,类B称为_。,北京林业大学 软件教研室,24,判断题,在派生类的构造函数的初始化表中不能包含对基类的子对象的初始化。 一个派生类还可以作为另一个类的派生类。 派生类至少有一个基类。 派生类中继承的基类的成员的访问权限到派生类保持不变。 派生类的对象可以对它的基类成员中公有继承的保护成员进行访问。 派生类的构造函数的执行顺序是:对象成员的构造函数、基类的构造函数、派生类本身的构造函数。 基类的私有成员不能为派生类的
11、成员所访问。 基类的公有成员在私有继承时在派生类中成为私有成员,而在保护继承时在派生类中成为保护成员。 在公有和保护继承方式下,派生类的成员可以对基类的保护成员进行访问。 在公有和保护继承方式下,派生类的对象可以对基类的保护成员进行访问。,北京林业大学 软件教研室,25,读程序,#include class Base public:Base() cout“执行基类构造函数“endl; Base() cout“执行基类析构函数“endl; ; class Derive:public Base public:Derive() cout“执行派生类构造函数“endl; Derive() cout“执
12、行派生类析构函数“endl; ; void main() Derive d;,执行基类构造函数 执行派生类构造函数 执行派生类析构函数 执行基类析构函数,北京林业大学 软件教研室,26,7.3 多重继承,7.3.1 多重继承的定义方式 class 派生类名:访问方式 基类名1,访问方式 基类名2, ;,北京林业大学 软件教研室,27,多重继承下,派生类的构造函数的定义格式: 派生类构造函数名(参数表): 基类名1(参数表1),基类名2(参数表2), 在多重继承下,系统首先执行各基类的构造函数,然后再执行派生类的构造函数; 处于同一层次的各基类构造函数的执行顺序与声明派生类时所指定的各基类顺序一
13、致,而与派生类的构造函数定义中所调用的基类构造函数的顺序无关。 【例7-8】测试在多重继承关系下基类和派生类的构造函数的执行顺序。,北京林业大学 软件教研室,28,练习,C+提供的_机制允许一个派生类继承多个基类,即使这些基类是相互无关的。,在多重继承的构造函数定义中,几个基类的构造函数之间用_分隔。,多重继承,逗号,北京林业大学 软件教研室,29,读程序,#include class A public: A(char *s) coutsendl; ; class B:public A public: B(char *s1,char *s2):A(s1) couts2endl; ; class
14、 C:public A public: C(char *s1,char *s2):A(s1) couts2endl; ; class D:public B, C public: D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s3,s4) couts4endl; ; void main() D d(“class A“,“class B“,“class C“,“class D“); ,class A class B class C class D class D,北京林业大学 软件教研室,30,7.3.2 多重继承的二义性,多重继承下,可能会产
15、生一个类是通过多条路径从一个给定的类中派生出来的情况。,通过派生类D3的对象访问类B的成员?,B,D1,D2,D3,北京林业大学 软件教研室,31,1多重继承的二义性 情况一:被继承的多个基类中具有同名成员,在派生类中对该同名成员的访问会产生二义性。 情况二:被继承的多个基类有一个共同的基类,在派生类中访问这个共同基类的成员会产生二义性。,北京林业大学 软件教研室,32,2多重继承的二义性问题的解决方法 一是使用作用域。 二是将直接基类的共同基类设置为虚基类。 直接基类名:数据成员名 直接基类名:成员函数名(参数表),北京林业大学 软件教研室,33,【例7-9】阅读以下程序,分析程序中的错误及
16、产生的原因。,北京林业大学 软件教研室,34,7.3.3 虚基类及其派生类的构造函数,虚基类的声明是在定义派生类时完成:class 派生类名:virtual 访问方式 基类名 /声明派生类成员 ; 虚基类:虽然被一个派生类间接地多次继承,但派生类却只继承一份该基类的成员。 对于虚基类的任何派生类,其构造函数不仅负责调用直接基类的构造函数,还需调用虚基类的构造函数。,北京林业大学 软件教研室,35,若基类B被声明为虚基类,则: 派生类D3负责调用3个基类(直接基类D1、D2和虚基类B)的构造函数; 而派生类D1和D2不会调用虚基类B的构造函数,只由最终端的派生类D3负责调用虚基类的构造函数。,【
17、例7-10】按照如上图所示的类和它们之间的继承与派生关系,设计对应的类和相关的构造函数,将类B声明为虚基类,并测试虚基类的作用,注意虚基类构造函数的执行次数。,北京林业大学 软件教研室,36,北京林业大学 软件教研室,37,解决二义性的两种方法比较,作用域运算符: 在派生类中拥有同名成员的多个拷贝,分别通过直接基类名来惟一标识同名成员,可以存放不同的数据,进行不同的操作。 可以容纳更多的数据。 虚基类: 只维护一个成员拷贝。 使用更为简洁,内存空间更为节省。,北京林业大学 软件教研室,38,读程序,#include class A public: A(char *s) coutsendl; ;
18、 class B:virtual public A public: B(char *s1,char *s2):A(s1) couts2endl; ; class C:virtual public A public: C(char *s1,char *s2):A(s1) couts2endl; ; class D:public B, C public: D(char *s1,char *s2,char *s3,char *s4) :B(s1,s2),C(s3,s4),A(s1) couts4endl; ; void main() D d(“class A“,“class B“,“class C“
19、,“class D“); ,class A class B class D class D,去掉该virtual: class A class A class B class D class D,去掉该virtual: class A class B class C class D class D,北京林业大学 软件教研室,39,7.4 虚函数与多态性,多态就是考虑在不同层次的类中,以及在一个类的内部,同名成员函数之间的关系问题,是解决功能和行为的再抽象问题。 也就是说,多态是指类族中具有相似功能的不同函数使用同一个名称来实现,从而可以使用相同的调用方式来调用这些具有不同功能的同名函数。,北京
20、林业大学 软件教研室,40,7.4.1 多态性,多态性就是指同样的消息被类的不同对象接收时导致的完全不同的行为的一种现象。 这里所说的消息即对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。 多态性实质是指同一个函数的多种形态。,北京林业大学 软件教研室,41,联编: 是指把一个消息和一个方法联系在一起 也就是把一个函数名与其实现代码联系在一起 实质是把一个标识符名和一个存储地址联系在一起的过程。 根据实现联编的阶段的不同,可将其分为静态联编和动态联编两种。 这两种联编过程分别对应着多态的两种实现方式。 在编译时的多态是通过静态联编实现的 而在运行时的多态则是通过动态联编
21、实现的。 普通函数及类的成员函数的重载就实现了一种多态性。,北京林业大学 软件教研室,42,在继承与派生的环境中: 当通过对象名调用某个成员函数时,只可能是调用对象自身的成员,所以,这种情况可采用静态联编实现。 当通过基类指针调用成员函数时,只有在运行时才能确定实际操作对象的类,并由此确定应该调用哪个类中的成员函数,这种运行时的多态性是由对象赋值的兼容规则所引起的。,北京林业大学 软件教研室,43,7.4.2 赋值兼容规则,赋值兼容规则是指在公有继承情况下,对于某些场合,一个派生类的对象可以作为基类对象来使用,也就是在需要基类对象的任何地方都可以使用公有派生类的对象来替代。,北京林业大学 软件
22、教研室,44,赋值兼容规则包括以下三种情况(假设类B为基类,类D为类B的公有派生类): (1)派生类的对象可以赋值给基类对象 D d; B b; b=d; (2)派生类的对象可以初始化为基类的引用 D d; B ,北京林业大学 软件教研室,45,基类指针指向公有派生类对象,Base,Derive,基类成员,北京林业大学 软件教研室,46,由上述对象赋值兼容规则可知: 一个基类的对象可兼容派生类的对象; 一个基类的指针可指向派生类的对象; 一个基类的引用可引用派生类的对象; 于是,对于通过基类的对象指针(或引用)对成员函数的调用,编译时无法确定对象的类,而只有在运行时才能确定并由此确定该调用哪个
23、类的成员函数。 一个公有派生类的对象可以提供其基类对象的全部行为(基类的全部接口); 也就是说,在程序中可以把一个公有派生类对象当作其基类对象来处理。 【例7-11】赋值兼容规则的概念及实现示例。,北京林业大学 软件教研室,47,7.4.3 用基类指针指向公有派生类对象,基类指针、派生类指针、基类对象和派生类对象四者间有以下4种组合的情况: (1)直接用基类指针指向基类对象。 (2)直接用派生类指针指向派生类对象。 (3)用基类指针引用其派生类对象。 (4)用派生类指针引用基类对象。,【例7-12】基类指针、派生类指针、基类对象和派生类对象4者间组合的使用情况示例。,基类指针仅能访问派生类中的
24、基类部分,强制类型转换,北京林业大学 软件教研室,48,引入虚函数,阅读下列程序,并写出相应的执行结果。 #include #include class Person public:Person(char* n) name=new charstrlen(n)+1;strcpy(name,n);void print() cout“My name is “name“.n“; protected:char* name; ;,北京林业大学 软件教研室,49,class Student : public Person public:Student(char* n,int s):Person(n) sco
25、re=s; void print() cout“My name is “name“ and my score is “score“.n“; private:int score; ; class Professor : public Person public:Professor(char* n,char *c):Person(n) course=new charstrlen(c)+1;strcpy(course,c); void print() cout“My name is “name“ and my course is “course“.n“; private:char* course;
26、;,北京林业大学 软件教研室,50,void main() Person* p;Person x(“wang“);p= 程序运行结果为: My name is wang. My name is zhang. My name is liu.,程序运行结果为: My name is wang. My name is zhang and my score is 97. My name is liu and my course is computer.,将Person类中的 void print(),virtual void print(),北京林业大学 软件教研室,51,7.4.4 虚函数,clas
27、s 类名 virtual 类型 函数名(参数表); ; 说明: (1)虚函数声明只能出现在类声明中的函数原型声明中,而不能在成员函数的函数体实现的时候。 (2)只有类的普通成员函数才能声明为虚函数,全局函数及静态成员函数不能声明为虚函数。,北京林业大学 软件教研室,52,(3)虚函数可以在一个或多个派生类中被重新定义,因此,它属于函数重载的情况。 它要求在派生类中重新定义时必须与基类中的函数原型完全相同。 这时,无论在派生类的相应成员函数前是否加上关键字virtual,都将其视为虚函数; 系统会遵循以下规则来判断一个派生类的成员函数是不是虚函数: 该函数是否与基类的虚函数有相同的名称; 该函数
28、是否与基类的虚函数有相同的参数个数及相同的对应参数类型; 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值。,北京林业大学 软件教研室,53,(4)当一个类的成员函数声明为虚函数后,就可以在该类的(直接或间接)派生类中定义与其基类虚函数原型相同的函数。 这时,当用基类指针指向这些派生类对象时,系统会自动用派生类中的同名函数来代替基类中的虚函数。 也就是说,当用基类指针指向不同派生类对象时,系统会在程序运行中根据所指向对象的不同,自动选择适当的成员函数,从而实现了运行时的多态性。 【例7-13】虚函数的定义与应用举例。,北京林业大学 软件教研室,54,运行过程中的
29、多态需要满足三个条件: 首先类之间应满足赋值兼容规则; 其次是要声明虚函数; 第三是要由成员函数来调用或者通过指针、引用来访问虚函数。 如果使用对象名来访问虚函数,由于联编在编译过程中就可以进行(静态联编),而无需在运行过程中进行。,北京林业大学 软件教研室,55,读程序,#include class base private: int b;public: base(int x=0) b=x; virtual void show() cout“base:b=“bendl;cout“base:show()被调用!“endl; ; class derived:public base private
30、: int d;public: derived(int y=-1) d=y; virtual void show() cout“derived:d=“dendl;cout“derived:show()被调用!“endl; ;,void main() derived d;void (base:*fp)();fp=base:show;(d.*fp)(); ,derived:d=-1 derived:show()被调用!,北京林业大学 软件教研室,56,读程序,#include class Base public:virtual void fun(int x) cout“base class,x=“
31、xendl; ; class Derive:public Base public:void fun(float x) cout“derive class,x=“xendl; ; void globefun(Base ,base class,x=1 base class,x=2 base class,x=1base class,x=2,基类Base声明了虚函数fun(),但在派生类中重新定义该函数时,其函数原型与基类中的函数原型不同,所以不能实现多态性。,北京林业大学 软件教研室,57,7.4.5 纯虚函数与抽象类,1纯虚函数 在定义一个表达抽象概念的基类时,有时可能会无法给出某些成员函数的具体
32、实现。这时,就可以将这些函数声明为纯虚函数。 virtual 函数原型=0; 纯虚函数是只在基类中声明虚函数但未给出具体的函数定义体; 它的具体定义放在各派生类中。,北京林业大学 软件教研室,58,2抽象类 声明了纯虚函数的类,称为抽象类。 抽象类的主要作用:通过它为一个类族建立一个公共的接口,使它们能够更有效地发挥多态特性。 抽象类声明了一族派生类的共同接口,而接口的完整实现,即纯虚函数的函数体,要由派生类自己定义。,北京林业大学 软件教研室,59,3使用纯虚函数与抽象类的注意事项 (1)抽象类只能用作基类来派生新类,不能声明抽象类的对象,但可以声明指向抽象类的指针变量和引用变量。 (2)抽
33、象类中可以有多个纯虚函数。 (3)抽象类中也可以定义其他非纯虚函数。 (4)抽象类派生出新类之后,如果在派生类中没有重新定义基类中的纯虚函数,则必须再将该虚函数声明为纯虚函数,这时,这个派生类仍然是一个抽象类; (5)在一个复杂的类继承结构中,越上层的类抽象程度就越高,有时甚至无法给出某些成员函数的具体实现。 (6)引入抽象类的目的主要是为了能将相关类组织在一个类继承结构中,并通过抽象类来为这些相关类提供统一的操作接口。,北京林业大学 软件教研室,60,【例7-14】设计一个抽象类Shape,它表示具有形状的东西,并体现了抽象的概念,在它下面可以派生出多种具体形状,比如三角形、矩形等。,北京林
34、业大学 软件教研室,61,选择题,下列对派生类的描述中,错误的是( )。 A) 一个派生类可以作为另一个派生类的基类 B) 派生类至少有一个基类 C) 派生类的缺省继承方式是private D) 派生类只继承了基类的公有成员和保护成员 下列说法中错误的是( )。 A) 公有继承时基类中的public成员在派生类中仍是public的 B) 公有继承时基类中的private成员在派生类中仍是private的 C) 私有继承时基类中的public成员在派生类中是private的 D) 保护继承时基类中的public成员在派生类中是protected的 下列虚基类的声明中正确的是( )。 A) cla
35、ss virtual B : public A B) virtual class B: public A C) class B : public A virtual D) class B : virtual public A,北京林业大学 软件教研室,62,填空题,(1) 面向对象程序设计的3大机制为: 、 、 。 (2)可以把派生类的对象当作基类对象来处理,这是 的概念。 (3) 声明虚函数的方法是在基类中的成员函数原型前加上关键字 。 (4) 在编译时就解决的函数调用称为 联编;在运行时才解决的函数调用称为 联编。 (5) 是一个在基类中说明的虚函数,但未给出具体的实现,要求在其派生类实现
36、。 (6) 在类定义中,将 置于虚函数的函数原型的末尾可以声明该函数为纯虚函数。 (7) 如果一个类中有一个或多个纯虚函数,则这个类称为 。,北京林业大学 软件教研室,63,7.5 综合实例,【例7-15】将如下继承关系中的3个类,分别用相应的定义文件(.h)和实现文件(.cpp)来管理。,北京林业大学 软件教研室,64,北京林业大学 软件教研室,65,/main.cpp #include “Fish.h“ #include “Elephant.h“ void main() CFish f;CElephant e; ,北京林业大学 软件教研室,66,/Animal.h #ifndef _ANI
37、MAL_H_ #define _ANIMAL_H_ #include class CAnimal public :CAnimal(); ; #endif,北京林业大学 软件教研室,67,【例7-16】设计一个基类Person,包含name和age两个数据成员;由它派生出学生类Student和教师类Teacher,其中学生类添加学号no数据,教师类添加职称title数据;每个类均有构造函数和析构函数;编程实现,并用一些数据进行测试。,(1)派生类的有参构造函数中需要为基类的有参构造函数提供参数。 (2)由于在派生类中重新定义了显示函数,若要调用基类的显示函数,则需要使用Person:show()
38、的形式。,北京林业大学 软件教研室,68,【例7-17】设计一个抽象类Vehicle,其中包含2个纯虚函数setdata()和show();由它派生出类Car和类Truck,类Car包含名称、颜色、载客数3个数据成员,类Truck包含名称、颜色、载重量3个数据成员。,北京林业大学 软件教研室,69,小结,(1)在C+中,允许以某个类为基类并用继承的方式定义新的类,继承性是C+的一个重要机制; C+支持单继承和多重继承,这样便形成了类的层次结构,处在最高层的类具有一般特征,而越处在底层的类就越详细、越具体。,北京林业大学 软件教研室,70,小结,(2)派生新类需要经历三个步骤:吸收基类成员、改造
39、基类成员和添加新成员。 派生类继承和吸收了基类的全部数据成员和除了构造函数、析构函数之外的全部成员函数; 对基类成员的改造包括两个方面:一是基类成员的访问方式问题,这由派生类定义时的访问方式来控制;二是对基类数据成员或成员函数的覆盖,对基类的功能进行改造; 派生类中新成员的加入是保证派生类在功能上有所发展的关键。,北京林业大学 软件教研室,71,小结,(3)在多继承关系中,被继承的多个基类中如果有具有同名的成员时,则在派生类中对该同名成员的使用会产生二义性,即编译系统将无法知道是使用的哪个基类中的成员。 为了解决该问题,我们可以采用作用域运算符和虚基类两种方法来实现。,北京林业大学 软件教研室,72,小结,(4)赋值兼容规则是指在公有继承的情况下,对于某些场合,一个派生类的对象可以作为基类对象来使用,也就是在需要基类对象的任何地方都可以使用公有派生类的对象来替代。 它包括三种情况: 派生类的对象可以赋值给基类对象; 派生类的对象可以初始化为基类的引用; 派生类对象的地址可以赋给指向基类的指针。,北京林业大学 软件教研室,73,小结,(5)C+的多态性是通过虚函数实现的,在继承环境下通过虚函数达到动态联编的效果,从而实现了程序运行时的多态性。,