1、C+程序设计,第8章(2) 多态性与虚函数,主要内容,C+的多态性 动态多态性的实现条件 虚函数的声明 虚函数的特性与调用 静态关联、动态关联 虚析构函数 纯虚函数 抽象类 综合实例,C+的多态性,多态性:指对不同类型的对象发送同样的消息(即调用同名的函数),不同类型的对象在接收时会产生不同的行为(即执行各自同名的函数)。 编译时多态性(静态多态性):指在编译阶段,系统就可根据所操作的对象,确定其具体的操作。编译时多态性是通过函数重载、运算符重载来实现的。函数重载是根据函数调用式中所给出的实参的类型或实参的个数,在编译阶段系统就可确定调用的是同名函数中的哪一个。运算符重载是根据运算式中所给出的
2、运算对象的类型,在编译阶段系统就可确定执行的是同种运算中的哪一个。 运行时多态性(动态多态性):指在编译阶段,系统仅根据函数调用式是无法确定调用的是同名函数中的哪一个;必须在程序运行过程中,动态确定所要调用函数的当前对象,并根据当前对象的类型来确定调用的是同名函数中的哪一个。 运行时多态性是通过 “类的继承关系” 加上 “虚函数” 联合起来实现的。,动态多态性的实现条件,要有类的继承层次结构:一个基类可以派生出不同的派生类,各派生类中可以新增与基类中的函数名字相同、参数个数及类型也相同的成员,这些同名的成员函数在不同的派生类中就有不同的含义!这样,在类的继承结构中,不同的层次上出现了名字相同、
3、参数个数及类型也相同、但功能不同的函数! 引入虚函数:作用是在由一个基类派生出的类体系中实现 “一个接口,多种方法”,主要用于建立通用程序。对于同一类体系中的各层次派生类,使用虚函数可实现统一的类接口,以便用相同的方式对各层次派生类的对象进行操作。虚函数是基类的成员函数,是为了实现某一种功能而假设的虚拟函数,在该基类的各层次派生类中对该虚函数可有各自不同的定义 ! 要能体现虚函数的特性:必须通过基类的对象指针、基类的对象引用来调用各层次派生类对象的同名虚函数,才能体现虚函数的特性!因为只有这样才能用相同的调用方式去调用不同层次派生类对象的同名虚函数,从而实现动态多态性。,虚函数的声明,虚函数的
4、声明:class 基类 virtual 返回值类型 成员函数名 ( 形参表 ) 函数体 ;当一个基类的某成员函数声明为虚函数,则在该基类的所有派生类中,与虚函数同名、参数个数及类型相同、且返回值类型也相同的,不论是否有关键字 virtual 修饰,都是虚函数,反之不然。但要注意:若在派生类中只是与虚函数同名,而参数个数或类型有不同时,属于函数重载,不是虚函数! virtual 只是用在类中声明虚函数,若在类外定义虚函数前面不要加 virtual。构造函数、静态成员函数不能声明为虚函数!析构函数可以声明为虚函数。,虚函数的特性与调用,如何体现虚函数的特性?只有通过基类的对象指针、基类的对象引用来
5、调用派生类对象的虚函数时,才能体现虚函数的特性!而通过派生类对象的对象名、对象指针、对象引用来调用虚函数时,无法体现虚函数的特性! 派生类对象中一般成员函数的调用方法:可通过派生类对象的对象名、对象指针、对象引用来调用!调用过程:若派生类新增成员函数中存在该函数,则被调用;若不存在,则调用上一层基类中的该函数;若这一层基类中也不存在,就继续往上一层寻找 ,直至找到该函数并被调用。 派生类对象中一般成员函数的调用方法:可通过基类的对象指针、基类的对象引用来调用派生类对象中的一般成员函数!但只能调用派生类中从该基类继承过来的那部分成员函数!,虚函数的特性与调用,派生类对象中虚函数的调用方法:派生类
6、对象的虚函数也是成员函数,可按一般成员函数的方式调用!即:可通过派生类对象的对象名、对象指针、对象引用来调用!调用过程与一般成员函数的调用过程相同!由此可见,这种调用方式无法体现虚函数的特性! 派生类对象中虚函数的调用方法:可通过基类的对象指针、基类的对象引用来调用派生类对象的虚函数!调用过程:调用的是派生类中的虚函数!若派生类中没有重新定义该虚函数,则调用的是上一层基类中的该虚函数;若在这一层基类中也没有重新定义该虚函数,就继续往上一层寻找 ,直至基类的对象指针、基类的对象引用它们本身所属的那一层基类! 动态多态性的实现:可以让基类的对象指针(或基类的对象引用)先后指向(或先后引用)同一类族
7、中不同派生类的对象,以便用相同的调用方式去调用不同派生类对象中的同名虚函数,从而实现动态多态性。,【例】(派生类对象中一般成员函数的调用方法:可通过派生类对象的对象名、对象指针、对象引用来调用!注意调用过程。) # include class A int x ; public: A ( int a ) x = a ; void g( ) cout f( ) ; pd-g( ) ; D ,【例】(派生类对象中虚函数的调用方法:可通过派生类对象的对象名、对象指针、对象引用来调用!注意调用过程与一般成员函数相同!由此可见,这种调用方式无法体现虚函数的特性!) # include class A in
8、t x ; public: A ( int a ) x = a ; virtual void g( ) cout f( ) ; pd-g( ) ; D ,【例】(派生类对象中一般成员函数的调用方法:可通过基类的对象指针、基类的对象引用来调用!但只能调用派生类中从该基类继承来的那部分成员函数!) # include class A int x ; public: A ( int a ) x = a ; void g( ) cout g( ) ; aa1. g( ) ; /pa-f( ); 为什么不行? B *pb = ,【例】(派生类对象中虚函数的调用方法:可通过基类的对象指针、基类的对象引用来
9、调用!注意:调用的是派生类中的虚函数!若派生类中没有重新定义该虚函数,则调用的是上一层基类中的该虚函数, ) # include class A int x ; public: A ( int a ) x = a ; virtual void g( ) cout g( ) ; aa1. g( ) ; /pa-f( ); 为什么不行? B *pb = ,【例】(请注意调用过程。) # include class A int x ; public: A ( int a ) x = a ; virtual void g( ) cout g( ) ; aa1. g( ) ; /pa-f( ); 为什么
10、不行? B *pb = ,【例】 (请注意调用过程。) # include class A int x ; public: A ( int a ) x = a ; void g( ) cout g( ) ; aa1. g( ) ; /pa-f( ); 为什么不行? B *pb = ,【例】(注意,若派生类中的某函数只是与虚函数同名,但参数个数或类型有不同时,则属于函数的重载,而不是虚函数!) # include class A int x ; public: A ( int a ) x = a ; virtual void g( ) cout g( ) ; aa1. g( ) ; /pa-f(
11、 ); 为什么不行? B *pb = ,【例】(动态多态性的实现:可以让基类的对象指针,先后指向同一类族中不同派生类的对象,这样就可以用相同的调用方式去调用不同派生类对象中的同名虚函数,从而实现动态多态性。) # include class A int x ;public:A ( int a=0 ) x = a ; cout “调用A类构造了!n” ; virtual void show( ) cout “A类 x=” x endl ; ; class B : public A int y ;public: B ( int a=0, int b=0 ) : A( a ) y = b ; cou
12、t “调用B类构造了!n” ; void show( ) cout “B类 y=” y endl ; ; class C : public B int z ;public:C( int a=0, int b=0, int c=0 ) : B( a, b ) z = c ; cout “调用C类构造了!n” ; void show( ) cout “C类 z=” z endl ; ;,void main ( ) A a1( 1 ) ; B b1( 20, 30 ) ; C c1( 400, 500, 600 ) ; A *p ;p = ,静态关联、动态关联,关联(绑定、联编):指程序自身彼此关联
13、的过程,即程序中不同部分互相绑定的过程,以确定程序中的各个函数调用式与所执行的相应函数代码的关系,简单的说,关联就是把某个标识符与某个存储地址联系起来。 静态关联(静态绑定、静态联编):指关联(绑定、联编)工作出现在编译阶段。例如,程序中使用对象名来调用成员函数时,在编译阶段系统就能根据对象的类型确定所要调用的是哪一个类的成员函数并进行关联。再如,使用对象名来调用某个类族中的虚函数时,在编译阶段系统也能确定要调用的虚函数属于哪一个类并进行关联。 此外,函数重载和运算符重载也是在编译阶段进行关联的。 动态关联(动态绑定、动态联编):指绑定(联编、关联)工作出现在运行阶段。例如,程序中使用基类的对
14、象指针(或基类的对象引用)来调用某个类族中的虚函数时,只有在程序运行过程中,才能根据其具体指向(或具体引用)该类族中的哪一个类的对象,确定出调用的是哪一个类的虚函数。,静态关联、动态关联,虚函数的动态关联:只有通过基类的对象指针(或基类的对象引用)来调用同一类族中派生类的虚函数时,才属于动态关联!原因是:只有在程序运行过程中,系统才能根据该指针(或该引用)具体指向(或具体引用)的是同一类族中的哪一个派生类的对象,确定出调用的是哪一个派生类的虚函数。 注意:虚函数与一般成员函数比较,调用时执行速度要慢一些。使用虚函数,系统要有一定的空间和时间开销。当一个类中有虚函数时,编译系统会为该类构造一个虚
15、函数表,它是一个指针数组,存放该类的每个虚函数的入口地址。由于虚函数的调用机制是间接实现的,且动态关联是在程序运行阶段,相对会降低程序的运行效率。 但通用性也是程序追求的主要目标之一。 虚函数必须是非静态的成员函数:因为静态成员函数不受限于某个对象。,【例】(分别通过对象名、基类的对象指针去调用派生类的虚函数。) # include class A int x ;public: A ( ) x = 10 ; virtual void show( ) cout show( ) ;p = /此处调用属于动态关联!,【例】(在成员函数中调用虚函数:关键是分析在调用该成员函数时,其 this 指针是基
16、类的对象指针还是派生类的对象指针! ) # include class A public: virtual void f1( ) cout f2( );void f2( ) cout f3( );virtual void f3( ) cout f4( );virtual void f4( ) cout f5( );void f5( ) cout f4( ); void f4( ) cout f5( ); void f5( ) cout “B:f5n” ; ; void main ( ) B b ; b. f1( ) ; ,运行: A : f1 A : f2 B : f3 B : f4 B :
17、f5,【例】(在成员函数中调用虚函数:关键是分析在调用该成员函数时,其 this 指针是基类的对象指针还是派生类的对象指针! ) # include class A public: void f1( ) cout f2( );void f2( ) cout f3( );virtual void f3( ) cout f4( );virtual void f4( ) cout f5( );void f5( ) cout f2( );void f3( ) cout f4( ); void f4( ) cout f5( ); void f5( ) cout f1( ) ;A *pa = ,【例】(切
18、记!在构造函数中调用虚函数与在成员函数中调用虚函数有区别! ) # include class A public: A( ) f( ) ; virtual void f( ) cout “A:fn” ; ; class B : public A public: B( ) f( ) ; void g1( ) cout “B:fn” ; void g2( ) f( ) ; ; class C : public B public: C( ) f( ) ; void f( ) cout “C:fn” ; ; class D : public C public: D( ) f( ) ; void g3(
19、 ) cout “D:fn” ; ; void main ( ) D d ; d. g2( ) ; ,【例】(使用动态多态性,编写程序求球体、圆柱体的体积和表面积。) 分析:若球体半径为r,则:其体积 V = 4r3 / 3;表面积 S = 4r2 。若圆柱底圆半径为r,高为h,则:其体积 V =r2 h;表面积 S = 2r ( r + h ) 。由于球体、圆柱体均可从圆继承而来,所以先定义基类 Circle,在基类中定义两个虚函数求体积虚函数 volume( )、求表面积虚函数 area( )。球体类 Sphere 和圆柱体类 Cylinder 可由 圆类 Circle 派生而来,在两个派
20、生类中分别对两个虚函数进行重新定义,用于计算球体、圆柱体的体积和表面积。在主函数 main( ) 中,定义一个基类 Circle 的对象指针 p,先后用来指向球体类Sphere 、圆柱体类 Cylinder 这同一类族中不同派生类的对象,当基类指针 p 指向包含虚函数的 Sphere 类、Cylinder 类的对象时,系统会根据该指针所指向对象的类型,来决定调用的是哪一个类的虚函数。,# include const double PI = 3.14159 ; class Circle protected: float radius ;public: Circle ( float a=0 ) :
21、 radius ( a ) virtual double volume( ) return 0 ; /虚函数virtual double area( ) return ( PI * radius * radius ) ; /虚函数 ; class Sphere : public Circle public: Sphere ( float a=0 ) : Circle ( a ) double volume( ) return ( 4 * PI * radius * radius * radius / 3 ) ; /虚函数double area( ) return ( 4 * PI * radi
22、us * radius ) ; /虚函数;,class Cylinder : public Circle float h ;public: Cylinder ( float a=0, float b=0 ) : Circle ( a ) h = b ; double volume( ) return ( PI * radius * radius * h ) ; /虚函数double area( ) return ( 2 * PI * radius * ( h + radius ) ) ; /虚函数 ; void main( ) Circle *p ;Sphere s( 10 ) ;p = ,虚
23、析构函数,虚析构函数的声明:若将基类的析构函数声明为虚函数,则由该基类派生的所有派生类的析构函数都自动成为虚函数,即使这些派生类的析构函数名与基类的析构函数名并不相同! 必须声明虚析构函数的情况:在由一个基类派生出的类体系中,若需要动态创建派生类对象,就必须将析构函数声明为虚函数,以实现撤消对象时的多态性!这样,若程序中用 delete 运算符去撤消动态分配的派生类对象,而 delete 运算符后面跟着的是指向派生类对象的基类指针,则系统调用的不是基类的析构函数,而是派生类的析构函数! 习惯的做法:一般在基类中都将析构函数声明为虚析构函数,即使基类并不需要自定义析构函数时,也显式定义一个函数体
24、为空的虚析构函数,以保证在撤消动态分配的派生类对象时能得到正确的处理。,【例】(必须声明虚析构函数的情况。) #include class A public: A( ) cout g( ) ; delete pa ; ,改成:virtual A( ) cout“A类析构了!n”; ,纯虚函数,纯虚函数:定义一个基类时,若无法给出某虚函数的具体实现,而该虚函数的实现完全依赖于不同的派生类,则在基类中将该虚函数声明为纯虚函数。 纯虚函数的声明: class 抽象类 virtual 返回值类型 成员函数名 ( 形参表 )= 0 ;因为无法给出纯虚函数的具体实现,所以没有函数体,但不是函数体为空!函数
25、体为空的函数称为空函数,调用空函数时,不执行任何操作。将函数名赋值为0,本质上是将指向函数体的指针赋值为0。在派生类中没有重新定义纯虚函数之前,是不能调用这种函数的!,抽象类只能作为基类使用,不能定义对象!但可定义抽象类的指针!,抽象类包含有纯虚函数的类当一个类中包含纯虚函数时,称其为抽象类。由于纯虚函数没有实现部分,所以抽象类不能定义对象。抽象类的唯一作用就是为派生类提供基类,而纯虚函数的作用则是为派生类中的成员函数提供基础!方法是:定义一个抽象类的指针,使其指向不同派生类的对象,而这些派生类中重新定义了纯虚函数,从而实现动态多态性。 抽象类构造函数或析构函数的访问属性为 protected
26、 的类当一个类的构造函数或析构函数的访问属性为保护型时,也称其为抽象类。由于构造函数或析构函数为保护型时,类外不能自动调用,所以不能定义对象。 但这种抽象类可以作为基类来派生子类!原因是:在派生类中允许调用基类的保护成员。当创建派生类对象时,在派生类的构造函数中可以调用基类的保护型构造函数;当撤消派生类对象时,在析构函数中也可以调用基类的保护型析构函数。,综合实例,【例】(有各种平面图形若干,例如:圆形、三角形、矩形,试通过定义纯虚函数的方法,求各平面图形的周长和面积。 ) 分析:定义一个描述抽象平面图形的类 Shape,成员函数有:求周长的函数 distance( ) 、求面积的函数 are
27、a( ) 。对于抽象平面图形而言,求周长、求面积的公式均无法给出, 所以 distance( ) 、area( ) 函数就没有函数体,是纯虚函数, Shape 类是抽象类!抽象类的唯一用途就是为派生类提供基类,纯虚函数的作用是为派生类中的成员函数提供基础,目的是实现动态多态性。圆形类 Circle、三角形类 Triangle、矩形类 Rectangle 均可由抽象类 Shape 派生而来,并在各自的类中对纯虚函数 distance( )、area( ) 进行重新定义。通过基类 Shape 的指针,调用各派生类对象的求周长函数 distance( ) 、求面积函数 area( ),利用动态联编的
28、方法获得派生类对象的具体函数。,一、定义一个描述抽象平面图形的类 Shape ,要求: Shape 类的成员函数: virtual float distance( ) = 0 ; /求抽象平面图形的周长,为纯虚函数 virtual float area( ) = 0 ; /求抽象平面图形的面积,为纯虚函数 二、由抽象类 Shape 派生出一个描述圆形的类 Circle,要求: Circle 类的成员数据:float radius ; /存放圆半径 Circle 类的成员函数、友元函数: Circle ( float r=0 ) ; /构造函数 float distance ( ) ; /求圆周
29、长 float area ( ) ; /求圆面积 friend istream /存放三角形的三条边,Triangle 类的成员函数、友元函数: Triangle ( float x=0 , float y=0 , float z=0 ) ; /构造函数 float distance ( ) ; /求三角形周长 float area ( ) ; /求三角形面积 friend istream /重载,# include # include class Shape public: virtual float distance ( ) = 0 ; virtual float area ( ) = 0
30、 ; ; class Circle : public Shape float radius ;public: Circle ( float r=0 ) radius = r ; float distance( ) return ( 2 * 3.14 * radius ) ; float area ( ) return ( 3.14 * radius * radius ) ; friend istream ,class Triangle : public Shape float a , b , c ;public: Triangle ( float x=0, float y=0, float z
31、=0 ) a = x ; b = y ; c = z ; float distance ( ) return ( a + b + c ) ; float area ( ) float s=(a+b+c)/2 ; return sqrt( s*(s-a)*(s-b)*(s-c) ) ; friend istream ,class Rectangle : public Shape float length , width ;public: Rectangle ( float L=0, float W=0 ) length = L ; width = W ; float distance ( ) return ( 2 * ( length + width ) ) ; float area ( ) return ( length * width ) ; friend istream ,void main ( ) Shape *p ;Circle c1( 3 ) ; cout c1 ;p = ,