【大学课件】C面向对象程序设计 多态性与虚函数.ppt
1,C+面向对象程序设计,第九章 多态性与虚函数,http:/,2,学习目标,理解多态性和虚函数的概念了解静态多态性和动态多态性掌握虚函数的定义和调用方法掌握多态性的实现方法以及虚函数在其实现中起到的作用掌握纯虚函数和抽象类的概念及应用,http:/,3,9.1 为什么需要多态性,前面章节讲述了面向对象程序设计的重要机制:数据抽象、封装和继承,多态性也是面向对象的重要特征之一。若语言不支持多态,则不能称为面向对象的。只支持类而不支持多态,只能称为基于对象的,如Ada,VB等。多态性的应用,使面向对象编程技术比较容易处理各种对象之间的相互作用;可以使编程显得更为简捷、便利,易于对程序进行开发和扩展,它为程序的模块化设计提供了又一手段。在进行面向对象编程时,C+力求模仿客观世界的规律。多态性概念也是体现了现实社会中各个事物之间的联系和作用。对同一个消息,不同的对象会有不同的反应。比如一个经理要到外地出差,他会把这个消息告诉他身边的人:他的妻子、秘书、下属;这些人听到这个消息会有不同的反应:他的妻子会为他准备行李,下属会为他准备出差的材料,秘书会为他安排车票和住宿。这就体现了多态性。多态性的概念是同样的消息被类的不同对象接收时导致完全不同行为的一种现象。消息指对类的成员函数的调用。换句话说,每个不同的对象可以以适合自己的方式去响应对同一个成员函数的调用。简单地讲,多态性就是一种实现“一种接口,多种方法”的技术,是面向对象程序设计的重要特性。,http:/,4,9.1.1 多态性的实现方法,同一段代码,当用不同的对象去调用时,该代码具有不同的功能,这称为多态性。C+提供的多态性分为静态多态性(编译时多态)和动态多态性(运行时多态)。静态多态性是一种编译时的多态,是通过重载和模板实现的。动态多态性是一种运行时的多态,其基础是数据封装和继承机制,通过继承建立类层次,并通过在基类中定义虚函数来实现多态性,即在基类和派生类中建立同名的函数,但是函数的功能是不同的。函数重载实现多态:对同一个函数名,当用不同的实参调用时,会调用到不同的重载函数版本,因而完成不同的功能,这是一种多态性的体现。模板实现多态:模板是一类函数或类的样板,通过用不同的模板实参调用模板,同一个名字可生成不同的具体函数或具体类,从而实现不同的功能,这也是一种多态性的体现。虚函数实现多态:通过动态束定机制,使相同的函数调用代码可能调用不同的类(基类或派生类)的虚函数,从而完成不同的功能,这又是一种多态性的体现。,http:/,5,9.1.2 静态多态性和动态多态性,编译时多态通过静态联编实现,运行时多态通过动态联编实现。1 联编在面向对象程序设计中,联编(binding)的含义是把一个函数名与其实现的代码联系在一起,即主调函数代码必须与被调函数代码连接起来。按照联编所在的阶段,联编分为静态联编(static binding)和动态联编(dynamic binding)。静态联编又称先期联编(early binding),是在编译时进行的,即是在编译阶段就必须确定函数名与代码间的对应关系。换句话说,主调函数和被调代码的关系早在编译时就确定了。动态联编又称迟后联编(late binding),是在程序运行过程中根据程序运行时的需要进行的。根据对象的不同来决定调用哪个(成员)函数进行联编。在编译阶段,系统还不能确定两者的对应关系。动态多态性是与动态联编相联系的。,http:/,6,静态联编的最大优点是速度快,运行时的开销仅仅是传递参数、执行函数调用、清除栈等。不过,程序员必须预测在每一种情况下,在所有的函数调用中,将要使用哪些对象。这不仅具有局限性,有时也是不可能的。动态联编的问题显然是执行效率。这必须由代码本身在运行时刻推测调用哪个函数,然后再调用它。有些语言,如Smalltalk,仅使用动态联编。仅用动态联编大大加强了语言的功能,但速度浪费也很严重。ANSI C只使用静态联编,结果是速度快但灵活性不够。,http:/,7,2 静态多态性,在没有类层次的场合,使用函数重载的方式实现静态多态性。各个重载函数名称相同,但参数表应在参数个数、类型和次序上有所不同。编译器根据参数表来识别各个重载函数。根据参数表,系统在编译时就完成静态联编的过程。关于没有类层次的函数重载实现多态的例子前面已经介绍,这里不再赘述。在建立了类层次的场合,各个类可以有名字和参数表相同的成员函数。图9.1显示出单继承建立的类层次。Student类描述学生的特征。派生类Smallstudent描述小学生特征。派生类中重载了函数print()。每个类的成员函数print()功能是显示相应对象的数据成员。我们想实现不同的对象调用print()时,输出不同的内容实现多态性。但是,如果基类中的没有将函数print()定义为虚函数,那么,即使在类层次中该函数被重载(函数名字和参数表完全相同),仍然不可能实现任何多态性。在这种场合,联编还是静态的。,http:/,8,【例9-2】派生类对象替换基类对象。,见教材P114从运行结果可以看出,以上几种方式调用的都是基类的Speak()函数。因而可以得到下列重要结论:不论哪一种情形,派生类对象替换基类对象后,只能当作基类对象来使用。不论派生类是否存在同名覆盖成员,这样的基类对象所访问的成员都只能来自基类。那么如何才能得到我们需要的结果呢(形成多态)?可以使用虚函数来实现动态联编。,http:/,9,3 虚函数和动态多态性,要获得多态性的对象,必须建立一个类等级。在建立了类层次后,实现动态多态性的方法是在基类中定义虚函数(virtual functions)。虚函数的声明方法是在基类中的成员函数前用关键字virtual说明,在派生类中,重载的虚函数名之前可略去关键字virtual。其原型格式如下:virtual 函数返回类型 函数名(参数表);虚函数存在于继承关系中,当一个类的成员函数声明为虚函数后,在该类的直接或间接的派生类中就可以定义与该基类中虚函数相同的函数(函数返回类型、函数名和参数表完全相同,但函数体不同。注意与函数的重载不同)。当编译器看见虚函数时,就会实行动态联编。因此当用基类指针指向带有虚函数的基类的派生类对象时,系统会自动用派生类中的同名函数去代替基类中的虚函数,这个过程称为覆盖(overriding)。而对于非虚函数,编译器则会采用静态联编。,http:/,10,【例9-3】利用虚函数输出学生的不同的信息。,见教材P116区别在于:将基类Student中的成员函数print()定义为虚函数。在主函数main()中,分别建立了Student和Smallstudent类的对象x,y。pt是Student类型指针。先使pt指向基类的对象x,并调用函数print(),然后用指针pt指向派生类对象y,然后调用成员函数print(),由于基类中的print()为虚函数,所以系统自动用派生类中的print()去代替(覆盖)基类中虚函数,从而实现多态性。,http:/,11,Student,SmallStudent,http:/,12,总的来说,取得动态多态性的过程是:建立类层次在基类中定义虚函数在派生类中重载那个虚函数定义基类指针,使该指针指向所需的派生类对象动态多态性是通过建立类层次和在基类中定义虚函数来实现的。,http:/,13,9.2 对虚函数的限制,9.2.1 声明虚函数的限制一般情况下,可将类中具有共性的成员函数声明为虚函数,而个性的函数往往为某一个类独有,声明为一般成员函数。将类的成员函数声明为虚函数有利于编程,但下面的函数不能声明为虚函数:构造函数不能声明为虚函数。构造函数在对象创建时调用,完成对象的初始化,此时对象正在创建中,基类指针无从指向。只有在构造过程完成后,对象才存在,才能被基类指针指向。静态成员函数不能是虚函数。因为静态成员函数属于整个类的所有对象,不受限于某个对象,没有多态性的特征。内联函数不能是虚函数。内联函数是在原地展开,其功能形式相当于将函数中的语句直接写到函数调用之处,不具有多态性的特征。在基类类体中,将某成员函数定义为虚函数,则编译器不会视该虚函数为内联函数。即使用关键字inline说明它是内联函数,编译器也会把它视作非内联函数。,http:/,14,【例9-4】声明虚函数的限制。,见教材P118程序运行结果:在基类中.Data=10在基类中.Data=20,http:/,15,9.2.2 虚函数的使用限制,【例9-5】虚函数实现多态性。见教材P119程序运行结果:How does a pet speak?miao!miao!wang!wang!miao!miao!wang!wang!miao!miao!,http:/,16,虚函数的使用限制:应通过指针或引用调用虚函数,而不要用对象名调用虚函数,这样才能保证多态性的成立。在派生类中重定义的基类虚函数仍为虚函数,可以省略virtual。虚函数重定义时,函数的名称、返回值类型、参数类型、个数及顺序与原函数完全一致。虚函数的重定义与一般函数重载不同,一般函数重载只要求函数名称相同,而虚函数定义要求严格一致。构造函数不可以是虚函数,但是析构函数可以是虚函数,而且通常将其声明为虚函数。将析构函数声明为虚函数的目的是使用delete操作符删除一个对象时,能保证对象所属类的析构函数被正确执行。,http:/,17,【例9-6】析构函数的使用。,见教材P121程序运行结果:基类中的析构函数程序说明:在本例中,基类的析构函数不是虚函数。在函数foo中,用运算符new创建派生类B的一个对象(其数据成员为20个字符)。然后将该对象的地址赋值给基类的指针k,即用基类指针指向派生类对象,在函数foo中执行语句 delete k 时,实际上是调用基类的析构函数。,http:/,18,【例9-7】虚析构函数的使用。,在【例9-6】的基础上对基类A做如下的修改。class Apublic:A()a=new char10;virtual A()/虚析构函数delete a;cout基类中的析构函数endl;private:char*a;程序运行结果:派生类中的析构函数基类中的析构函数,http:/,19,9.3 在成员函数中调用虚函数,在基类或派生类的成员函数中,可以直接调用该类层次中的虚函数。【例9-8】成员函数中调用虚函数的方法。见教材P123程序运行结果:x=5y=8,http:/,20,9.4 在构造函数中调用虚函数,在建立了类层次,基类定义了虚函数并且在派生类中重新定义了该函数之后,函数的调用采用迟后联编,实现了多态性。在创建类的对象的过程中,总是先调用基类构造函数,建立基类子对象,然后建立自己的成员。所以,在构造函数中调用虚函数时,采用先期联编,即构造函数所调用的虚函数是自己类中或基类定义的函数,而不是在派生类中重新定义的同名函数。应该注意,迟后联编出现在创建了类的对象之后;在对象存在之前,都是采用静态联编。,http:/,21,【例9-9】构造函数中虚函数的应用。,见教材P124程序运行结果:基类中的成员函数基类中的成员函数基类中的成员函数,http:/,22,9.5 纯虚函数和抽象类,在许多情况下,在基类中不能为虚函数给出一个有意义的定义,这时可以将它说明为纯虚函数(null virtual function)。具体的函数定义由派生类给出。纯虚函数的一般形式为:virtual 返回类型 函数名(参数表)=0;纯虚函数的函数体是空的。具有纯虚函数的类称为抽象类(abstract class),反之,称为具体类(concrete class)。一个抽象类至少具有一个纯虚函数。在使用抽象类和纯虚函数时必须注意以下几点:抽象类不能实例化,即不能创建对象。必须从带有纯虚函数的基类(抽象类)中派生出派生类,在派生类中定义出自己的,与纯虚函数同名的重载函数,从而使派生类成为一个具体类,这样就可以用派生类来定义对象。若没有重新定义纯虚函数,则必须将该虚函数声明为纯虚函数,该派生类仍然是一个抽象类。抽象类只作为基类被继承,派生新类。无派生类的抽象类是没有意义的。因此抽象类只是用于被继承,仅作为一个接口,具体的功能在其派生类中实现。不能创建一个抽象类的对象,但可以声明一个抽象类的指针或引用,这个指针或引用必然指向派生类对象,从而实现多态性。,http:/,23,纯虚函数的一个类层次见图5.2所示。在图9-2中,图形类Shape是一个抽象类。图形是一个抽象概念,若不具体地说明是哪种图形,图形面积就没办法计算。因此,在Shape类中无法定义有实际意义的函数具体计算面积,计算面积的函数area()只能定义为纯虚函数。因此在派生类Circle类和Rectangle类中必须给出area()函数的重载,成为具体类,就可以创建具体对象,从而计算具体图形的面积。,图9.2 类层次,http:/,24,【例9-10】纯虚函数的用法。,程序见教材P126程序运行结果:圆的半径:5圆的面积:78.5398矩形的边长:2 4矩形的面积:8,http:/,25,本章小结,多态性是面向对象程序设计的一个重要特征,其本意是“拥有多种形态”。C+中,多态性是指具有不同功能的函数共用同一个函数名。C+中的多态性有两种类型:静态多态性和动态多态性。函数重载属于静态多态性,程序在编译时就能确定调用哪个函数;动态多态性是指在动态联编下实现的多态性,只有在程序运行时才能解决函数的调用问题。虚函数是实现动态多态性的关键。没有虚函数不可能实现动态联编。虚函数的声明比较简单,只要在“感兴趣”的成员函数前加上virtual关键字即可,难的是如何理解虚函数的功能。根据赋值兼容规则,可以把派生类对象地址赋给基类指针,或是用派生类对象来初始化基类引用,如果没有声明虚函数,通过基类指针或基类引用只能访问基类的成员函数;若声明了虚函数,那么通过基类指针或基类引用,就能根据调用对象的不同去访问不同派生类的成员函数,这就是虚函数的神奇功能。虚函数的使用,体现了“一个接口,多种方法”的面向对象编程思想。,http:/,