1、面向对象程序设计语言C+,电子科技大学示范性软件学院,1,第七章 虚函数和多态性,现实生活中,经常出现这种情况:面对同样的消息,不同的人,面对同样的消息,产生不同的反应。 面向对象语言是解决现实世界问题的,也需要对这种实际情况进行处理。C+程序设计语言使用多态性实现同一个消息,不同接收者采取不同的响应方式的这种现象。 顾名思义,多态性是一个事务有多种形态。在面向对象语言中,一般这样描述多态:向不同对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。,2,第七章 虚函数和多态性,C+语言的多态性有两种类型:静态多态性和动态多态性。
2、前面学习的函数重载和运算符重载就是静态多态性的具体示例。在程序编译时系统就能够决定调用哪个函数,因此静态多态性又称为编译时的多态性。动态多态性时程序运行过程中才动态的确定操作所针对的对象。它又称为运行时的多态性。动态多态性是通过虚函数实现的。,3,第七章 虚函数和多态性,7.1 虚函数 7.1.1静态多态性 对于普通成员函数的重载,可表达为下面的方式: (1)在同一个类中重载; (2)在不同类中重载; (3)基类的成员函数在派生类中重载; 因此,重载函数的访问是在编译时区分的,这种程序运行之前就能够在多个函数中确定当前访问的函数的方法称为静态多态性。,4,第七章 虚函数和多态性,7.1 虚函数
3、 7.1.1静态多态性 有以下三种区分方法: 据参数的特征加以区分,例如: Show(int, char) 与 Show(char * , float) 使用”:”加以区分,例如: Circle : Show 有别于 Point : Show 根据类对象加以区分 ACircle.Show() 调用 Circle : Show(),5,第七章 虚函数和多态性,7.1 虚函数 7.1.1静态多态性 子类可以重载父类的成员: class A public:void fun() cout“In A“endl; ; class B : public A public:void fun() cout“In
4、 B“endl; ;,6,第七章 虚函数和多态性,7.1 虚函数 7.1.1静态多态性C Cobj;Cobj.fun(); /调用C:fun()Cobj.B:fun(); /调用B:fun()Cobj.A:fun(); /调用A:fun()A /调用A:fun(),7,第七章 虚函数和多态性,7.1 虚函数 7.1.2基类和派生类的指针与对象的关系 (1)可以用一个指向基类的指针指向其公有派生类的对象。这时,基类指针访问的是派生对象的拥有的基类部分,派生类自身的部分不能被基类指针访问。但是用指向派生类的指针指向一个基类的对象是不正确的,因为派生类指针可以访问派生类公有成员,但基类对象没有派生类
5、自身成员,因此用派生类指针指向基类对象时,如果访问派生类公有成员,而该成员实际不存在,所以不能用指向派生类的指针指向一个基类的对象。 (2)希望用基类指针访问其公有派生类的特定成员,必须将基类指针用显示类型转换为派生类指针。,8,第七章 虚函数和多态性,7.1 虚函数 7.1.3 虚函数与多态性 1虚函数的概念 一个指向基类的指针可用来指向从基类公有派生的任何对象,这一事实是非常重要的,是 C+ 实现运行时多态性的关键途径。如果有多个或者多层派生类,通过一个基类指针可以访问所有派生类对象的成员函数,这样就可以实现一个接口,多个实现的访问了。但是观察例73,基类指针能否访问不同派生类对象的成员函
6、数呢?,9,第七章 虚函数和多态性,7.1 虚函数 7.1.3 虚函数与多态性 class Base public:Base(int a)x=a;void who() cout “base “x“n“; protected:int x; ;,10,class First_d: public Base public: First_d (int a ):Base(a) void who() cout “First derivation “ x“n“; ; class Second_d :public Base public:Second_d (int a):Base(a) void who() c
7、out “Second derivation “ x“n“; ;,第七章 虚函数和多态性,7.1 虚函数 7.1.3 虚函数与多态性Base * p;Base base_obj(1);First_d first_obj(2);Second_d second_obj(3);p= 请问程序的输出是什么?,11,第七章 虚函数和多态性,7.1 虚函数 7.1.3 虚函数与多态性 程序的输出是: base 1 base 2 base 3 很奇怪,是吗?明明是指向子类的指针,为什么调用的却是父类的函数版本呢?,12,第七章 虚函数和多态性,7.1 虚函数 7.1.3 虚函数与多态性 答案很简单,因为通过
8、父类指针来看,该指针所指向的是一个父类对象。,13,第七章 虚函数和多态性,7.1 虚函数 7.1.3 虚函数与多态性 如果随着p所指向的对象的不同p-who()能调用不同类中who()的版本,这样就可以用一个界面p-who()访问多个实现版本,这在编程时非常有用。实际上,这表达了一种动态的性质,函数调用p-who()依赖于运行时p所指向的对象。虚函数提供的就是这种解释机制。虚函数是在基类中被冠以virtual的成员函数,它提供了一种接口界面。,14,第七章 虚函数和多态性,7.1 虚函数 7.1.3 虚函数与多态性 虚函数可以在一个或多个派生类中被重新定义,但要求在派生类中重新定义时,虚函数
9、的函数原型,包括返回类型,函数名,参数个数,参数类型的顺序,必须完全相同。 前面的程序修改为下页所示就能达到目的。,15,第七章 虚函数和多态性,7.1 虚函数 7.1.3 虚函数与多态性 class Base public:Base(int a)x=a;virtual void who() cout “base “x“n“; protected:int x; ;,16,class First_d: public Base public: First_d (int a ):Base(a) void who() cout “First derivation “ x“n“; ; class Sec
10、ond_d :public Base public:Second_d (int a):Base(a) void who() cout “Second derivation “ x“n“; ; (例7-4),第七章 虚函数和多态性,7.1 虚函数 2运行时的多态性与虚特性 (1)运行时的多态性 晚期匹配如何发生?所有的工作都由编译器在幕后完成。当我们告诉它去晚期匹配(用创建虚函数告诉它),编译器安装必要的晚期匹配机制。关键字virtual告诉编译器它不应当完成早期匹配,相反,它应当自动安装实现晚期匹配所必须的所有机制。 那么,编译器会怎么做呢?请先看一个例子。,17,第七章 虚函数和多态性,7.
11、1 虚函数 class NoVirtual int i;public:void f(); ;,18,class OneVirtual int i;public:void f();virtual void g(); ;,class TwoVirtual int i;public:void f();virtual void g();virtual void h(); ;,第七章 虚函数和多态性,7.1 虚函数 int main() cout “size of int: “ sizeof(int) endl;cout “size of pointer: “ sizeof(void *) endl;
12、cout “size of NoVirtual: “ sizeof(NoVirtual) endl;cout “size of OneVirtual: “ sizeof(OneVirtual) endl;cout “size of TwoVirtual: “ sizeof(TwoVirtual) endl;return 0; ,19,第七章 虚函数和多态性,7.1 虚函数 假设这个程序的编译环境和运行环境都是32位的,那么请问程序的输出是什么?,20,程序的输出如下: size of int:4 size of pointer:4 size of NoVirtual:4 size of One
13、Virtual:8 size of TwoVirtual:8 很奇怪,是吗?拥有虚函数的类尺寸多了4个字节。,第七章 虚函数和多态性,7.1 虚函数 很明显,这是4个字节是编译器插入到类中的。那么它是什么呢? 在每个带有虚函数的类中,编译器秘密地置一指针,称为虚指针vpointer(缩写为VPTR)。此外,编译器对每个包含虚函数的类创建一个虚表(称为VTABLE)。在VTABLE中,编译器放置特定类的虚函数地址。VPTR指向这个对象的VTABLE。通过基类指针做虚函数调用时(也就是做多态调用时),编译器取得这个VPTR,找到该类的VTABLE,并在其中查找相应虚函数地址,这样就能调用正确的函数
14、,使晚捆绑发生。,21,第七章 虚函数和多态性,7.1 虚函数,22,第七章 虚函数和多态性,7.1 虚函数 一个成员函数什么时候需要声明为虚函数呢?主要考虑以下几点: 首先考虑成员函数所在的类是否会做为基类。然后看成员函数在类的继承后有无功能被修改?如果希望修改其功能,一般将它声明为虚函数。 还应当考虑对成员函数的调用是通过对象名还是基类指针或引用去访问。如果通过基类指针或引用去访问,则声明为虚函数。 如果希望通过基类指针或者引用访问派生类成员函数,但基类功能比较抽象或者不能确定功能,可以将基类定义为抽象类,即只定义函数名字,没有函数体,具体功能由派生类添加。,23,第七章 虚函数和多态性,
15、7.1 虚函数 (2)虚特性 用虚函数实现运行时多态性的关键之处是,必须用指向基类的指针(或者引用)访问虚函数。尽管可以像调用其他成员函数那样显式地用对象名来调用一个虚函数,但只有在一个指向基类的指针(或者引用)访问虚函数时,运行时多态性才能实现。这时,称为函数具有虚特性。,24,第七章 虚函数和多态性,7.1 虚函数 基类函数具有虚特性的条件是: 在基类中,将该函数说明为虚(virtual)函数。这样可以在派生类中重新定义此函数,为它赋予新的功能,并能够方便调用。在类外定义虚函数时,不必再加virtual关键字。 定义基类的公有派生类; 在基类的公有派生类中原型一致地重载该虚函数; 定义指向
16、基类的指针变量,它指向基类的公有派生类的对象(或定义基类的引用,它引用基类的公有派生类的对象)。,25,第七章 虚函数和多态性,7.1 虚函数 重载一个虚函数时,要求函数名、返回类型、参量个数、参数类型和顺序是完全相同的。如果不同,会产生什么情况呢? (1)仅仅返回类型不同,其余相同。C+认为这是错误的,因为仅仅返回类型不同的函数本质上是含糊的。 (2)函数原型不同,仅函数名相同,C+认为这是一般的函数重载,此时虚特性丢失。(例7-5),26,第七章 虚函数和多态性,7.1 虚函数 定义虚函数时需要注意: (1) 虚函数必须是类的成员函数。不能将虚函数说明为全局(非成员的)函数,也不能说明为静
17、态成员函数。不能将友元说明为虚函数,但虚函数可以是另一个类的友元。 (2) 析构函数可以是虚函数,但构造函数不能为虚函数。 (3)一旦一个函数被说明为虚函数,不管经历了多少派生类层,都将保持其虚特性。,27,第七章 虚函数和多态性,7.1 虚函数 3虚析构函数 请看这个例子: class base public:base()cout“base()“endl;base()cout“base()“endl; ; class derived:public base public:derived()cout“derived()“endl;derived()cout“derived()“endl; ;,
18、28,int main() base *pb=new derived;delete pb;return 0; ,第七章 虚函数和多态性,7.1 虚函数 请问程序的输出是什么?,29,程序的输出如下: base() derived(); base(); 构造函数base()和derived()都被调用了,但是析构函数只有base()调用了。,第七章 虚函数和多态性,7.1 虚函数 原因很简单:基类指针只调用基类成员函数,不能够调用派生类成员函数,即使是析构函数也是如此。如果希望能够执行派生类的析构函数,则需要将基类的析构函数声明为虚析构函数: virtual base() cout“base()
19、“endl; 当基类的析构函数声明为虚函数时,无论指针指向的是同一类族中的哪一个对象,当对象撤销时,系统会采用动态关联,调用相应的析构函数,对该对象进行清理工作。,30,第七章 虚函数和多态性,7.2 纯虚函数和抽象类 基类往往表示一些抽象的概念。例如,shape是一个基类,它表示具有形状的东西,从shape可以派生出封闭图形和非封闭图形两个派生类。封闭图形又可以派生出椭圆形、多边形, 这个类等级的基类shape体现了一个抽象的概念,在shape中定义一个求面积的函数显然是无意义的,但可以将其说明为虚函数,提供各派生类一个公共的界面,并由各派生类提供求面积函数的各自版本。,31,第七章 虚函数
20、和多态性,7.2 纯虚函数和抽象类 在这类情况下,基类的有些虚函数没有定义是很正常的,但是要求派生类必须重定义这些虚函数,以使派生类有意义。为此,C+ 引入了纯虚函数的概念。 纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义,要求任何派生类都必须定义自己的版本。为说明一纯虚函数,使用下列一般形式: virtual type func_name(参数表) = 0; 这里,type 是函数的返回类型,func_name 是函数名。,32,第七章 虚函数和多态性,7.2 纯虚函数和抽象类 将一虚函数说明成纯虚函数,就要求任何派生类都应该定义自己的实现。 在构造函数和析构函数中调用虚函数时,采用静态联编,因此,在构造函数和析构函数中不能够调用纯虚函数。但其他的成员函数可以调用纯虚函数。 如果一个类至少有一个纯虚函数,那么就称该类为抽象类。抽象类机制支持一般概念的表示。 抽象类只能用作其他类的基类,抽象类不能建立对象。抽象类不能用作参数类型、函数返回类型或显式转换的类型。但可以声明抽象类的指针和引用。,33,第七章 虚函数和多态性,7.2 纯虚函数和抽象类 纯虚函数和抽象类的例子: class shape public: virtual void rotate( int )=0; virtual void draw( )=0; ;,34,