400 likes | 549 Views
面向对象的 C++ 程序设计 基础. 第 5 章 多态性与 虚函数. 本章主要内容. 1 多态性 2 虚函数 3 纯虚函数与抽象类. 多态性的概念. 多态性. 多态性是面向对象的重要特性之一,是指不同对象收到相同的消息时产生不同的行为。消息是指调用类的成员函数,不同的行为指不同的实现,即调用不同的函数; C++ 支持两种多态性:编译时的多态性和运行时的多态性; 编译时的多态性是在程序编译过程中确定函数操作的具体对象,通过函数重载来实现;运行时的多态性是在程序运行过程中才能确定函数操作的具体对象,通过虚函数来实现。. 多态的具体实现.
E N D
面向对象的C++程序设计基础 第5 章 多态性与虚函数
本章主要内容 1 多态性 2 虚函数 3纯虚函数与抽象类
多态性的概念 多态性 • 多态性是面向对象的重要特性之一,是指不同对象收到相同的消息时产生不同的行为。消息是指调用类的成员函数,不同的行为指不同的实现,即调用不同的函数; • C++支持两种多态性:编译时的多态性和运行时的多态性; • 编译时的多态性是在程序编译过程中确定函数操作的具体对象,通过函数重载来实现;运行时的多态性是在程序运行过程中才能确定函数操作的具体对象,通过虚函数来实现。
多态的具体实现 多态性 • 在面向对象系统中,将确定操作对象(确定调用哪个函数实现)的过程称为联编,即把一条消息(调用类的成员函数)与一个对象的方法(具体的函数版本)相结合的过程。 • 联编有两种: • 静态联编,在编译阶段确定操作的具体对象,也称早期联编、早绑定; • 动态联编,在系统运行过程中确定某一操作对象,也称滞后联编、晚绑定。
编译时的多态性 • 编译时的多态是指在编译阶段就决定调用哪个同名函数; • 通过函数重载来实现。函数重载有两种方式: • 在一个类中说明的函数重载; • 基类成员函数在派生类中重载。 多态性
在一个类中说明的函数重载 例子: class Cube { public: Cube( ); Cube(int h,int w,int l) { height=h; width=w; length=l; } private: int height; int width; int length; }; Cube::Cube( ) { height=1; width=2; length=3;} void main( ) { Cube cube1; Cube cube2(4,5,6); } 多态性 • 普通的函数重载:在一个类中说明的重载函数靠所带参数的个数、参数类型的不同来区分。
基类成员函数在派生类中重载 多态性 • 函数名相同,只是它们属于不同的类。如果在派生类中声明了与基类成员函数同名的新函数,则从基类继承的同名函数的所有重载形式都会被覆盖,即使函数的参数表不同。 • 区分这种函数的方式有两种: (1)使用“类名::”加以区分(限定作用域); 例: Circles::show 和 Point::show (2)根据对象加以区分; 例:Circles Acircle; Point Apoint; Acircle.show( ); Apoint.show( );
基类成员函数在派生类中重载 多态性 • 例: 从基类的点类中派生出一个圆类。基类Point中的成员函数area和派生类Circles中的成员函数area都不带参数,重载时依靠对象来进行区分。
基类成员函数在派生类中重载 class Circles: public Point { public: Circles(int x,int y,int ardius):Point(x,y) {Circles::radius=radius;} float area( ) {return 3.1416*radius*radius;} private: int radius; }; void main( ) { Point point1(10,10); Circles circle1(5,5,20); cout<<point1.area( )<<endl; cout<<circle1.area( )<<endl; cout<<circle1.Point::area( )<<endl; } 多态性 class Point { public: Point(int x,int y) { Point::x=x; Point::y=y;}; float area( ) {return 0.0;} private: int x; int y; };
运行时的多态性 多态性 • 运行时的多态性是指许多对象及对象的操作不能在编译时就确定下来,需要在运行过程中确定; • 这种多态性是用虚函数来实现的。
多态性小结 • 多态性是面向对象的基本特性之一; • 多态性的实现有两种:早期联编通过重载实现;滞后联编通过虚函数实现。
本章主要内容 1 多态性 2 虚函数 3纯虚函数与抽象类
虚函数 虚 函 数 在类的声明中,在函数原型之前写virtual。 • virtual只用来说明类声明中的原型,不能用在函数实现时。 • 具有继承性,基类中声明了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数。 • 本质:不是重载声明而是覆盖。 • 调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类,决定调用哪个函数。
对象指针 虚 函 数 • 对象指针存放对象存储区的首地址。指向对象的指针有: • 一般对象的指针; • 引入派生类后的对象指针。
一般对象的指针 虚 函 数 • 定义对象时要初始化,初始化后的对象会占有一定的内存空间。如果将对象存储空间的首地址赋给了一个对象的指针,这个指针就是指向对象的指针。 例如: Base *p_Base; //声明对象指针 Base obj; //定义对象obj p_Base=&obj; //指针p_Base指向对象obj … … p_Base->show(); //通过对象指针访问成员函数
引入派生类后的对象指针 虚 函 数 • 引入派生类概念后,任何一个被说明为指向基类对象的指针都可以指向它的公有派生类; • 指向基类对象的指针,可以指向它的公有派生类的对象,而不能指向它的私有派生类的对象。
引入派生类后的对象指针 虚 函 数 • 不能将一个声明为指向派生类对象的指针指向其基类的一个对象; • 声明为指向基类对象的指针,当其指向公有派生类对象时,只能用它来访问派生类中从基类继承来的成员,不能访问公有派生类中新添的成员。 • 例5.1.1:计算和显示研究生和本科生的学费
虚函数的作用 • 在例5.1.1中,基类中calfee( )和disp( )无virtual声明,则当指向基类对象的指针ptr指向其派生类Graduate对象时,通过ptr只能访问Graduate中从基类继承下来的成员如calfee( )和disp( ),而不能访问派生类的calfee( )和disp( ) ;
虚函数的作用 • 给基类calfee( )和disp( )加上virtual声明(即虚函数)后,当ptr指向其派生类Graduate对象时,调用ptr->calfee( )和ptr->disp( )时就覆盖了基类的同名函数,调用派生类的成员calfee( )和disp( ); • 虚函数的定义,使用户可通过将指向基类对象的指针指向不同的派生类对象,来调用派生类中与基类同名、实现算法不同的函数,实现运行时的多态。
虚函数的作用 • 多态性的实现与联编(或绑定binding)有关; • 联编是确定函数调用的具体对象的过程,分为静态联编(编译时确定)和动态联编(运行时确定);
虚函数的作用 • 在上例中有“ptr->disp( );”;单凭此语句,编译系统无法确定调用那个类对象的函数,因为编译阶段只作语法检查; • 到了运行阶段,ptr指向哪个对象,就调用哪个对象的disp函数。例如ptr先指向基类、再指向派生类…,那么先后将调用同一类族中不同类的虚函数; • 虚函数是实现动态联编的基础,在运行阶段将虚函数与类对象关联在一起的过程即动态联编。
虚函数的声明 • 虚函数的定义在基类中进行,即在基类中给想定义为虚函数的成员函数声明前加关键词 virtual,即: virtual 类型说明符 函数名(参数表); • 基类中的某成员函数被声明为虚函数后,此虚函数可以在一个或多个派生类中被重新定义; • 虚函数重新定义时,不需要virtual声明;
虚函数的声明 • 在派生类中重新定义虚函数,要求函数原型包括返回类型、函数名、参数个数、参数类型的顺序,必须与基类中的原型完全一致,仅函数实现不同; • 可通过成员函数调用、指针或引用来访问虚函数; • 定义一个指向基类对象的指针ptr,并让它指向同一类族中要调用此虚函数的对象; • 通过ptr调用的虚函数就是ptr指向的对象的函数。
例 : 虚 函 数 #include <iostream.h> class B0 //基类B0声明 {public: //外部接口 virtual void display( ) //虚成员函数 {cout<<"B0::display( )"<<endl;} }; class B1: public B0 //公有派生 { public: void display( ) { cout<<"B1::display( )"<<endl; }}; class D1: public B1 //公有派生 { public: void display( ) { cout<<"D1::display( )"<<endl; }};
void fun(B0 *ptr) //普通函数 { ptr->display( ); } void main( ) //主函数 { B0 b0, *p; //声明基类对象和指针 B1 b1; //声明派生类对象 D1 d1; //声明派生类对象 p=&b0; fun(p); //调用基类B0函数成员 p=&b1; fun(p); //调用派生类B1函数成员 p=&d1; fun(p); //调用派生类D1函数成员 } 运行结果: B0::display( ) B1::display( ) D1::display( ) 25
虚函数与重载函数的区别 虚 函 数 • 函数重载中要求重载函数的参数类型、参数个数不同,仅是函数名相同; • 虚函数的重定义,要求函数名、返回类型、参数个数、参数类型和顺序都与基类中的函数原型完全一致。若不一致,按如下情况处理: 1、函数参数相同,仅返回类型不同,作出错处理; 2、函数参数有差异,仅函数名相同,系统会将它认为是一般的函数重载,将丢失虚特性。 例:5.2.2
什么时候使用虚函数? 虚 函 数 • 如果将某成员函数所在的类作为基类,且该成员函数需要在派生类中重新定义,就将其声明为虚函数; • 不要将那些不需要在派生类中重新定义的基类成员函数声明为虚函数; • 如果不是通过指向派生类对象的基类对象指针来访问被声明为虚函数的成员函数,就不要声明虚函数,因为通过对象访问的成员函数在编译时就能完全确定。
虚函数使用注意: • 在类的体系中访问虚函数,可使用指向基类对象的指针或对基类对象的引用,也可以使用对象名直接访问; • 派生类中没重新定义虚函数时,该类的对象将使用基类中的虚函数代码(继承下来的); • 虚函数必须是一个类的成员函数,不能是友元函数,但它可以是另一个类的友元成员; • 构造函数不能被声明为virtual,但析构函数可以被声明为virtual; • 静态成员函数(不受限于某个对象)和内联函数(不在运行中动态确定其位置)不能声明为virtual。
虚析构函数 • 析构函数可以被声明为虚函数,例如: class B { public: … virtual~B( ); … }; • 若基类的析构函数声明为虚函数,则由该基类派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数不同名; • 虚函数析构函数的作用:当用delete删除对象时,能确保析构函数被正确执行。
将析构函数声明为虚函数,可以正确地执行析构函数,即先调用派生类的析构函数,再调用基类的析构函数;将析构函数声明为虚函数,可以正确地执行析构函数,即先调用派生类的析构函数,再调用基类的析构函数; 当基类的析构函数为虚函数时,无论指针指向同一类族中的哪个对象,系统都会动态联编, 调用相应的析构函数,否则系统只会执行基类的析构函数而不执行派生类的析构函数; 如果delete的操作对象使用了指向派生类对象的基类指针,系统就会调用相应类的析构函数; 一般都将析构函数声明为虚函数,而不管基类是否需要析构函数。 虚析构函数的要点:
虚函数小结 • 对象指针:指向基类类型的指针可以指向其公有派生类对象,但只能访问从基类继承的成员; • 虚函数:用于实现滞后联编,在基类中将成员函数声明为virtual特性,就可以在派生类中对该成员函数重新定义(定义原型与基类中的完全相同)。当基类指针指向派生类时,即可访问派生类重新定义的函数,从而实现多态性。
本章主要内容 1 多态性 2 虚函数 3纯虚函数与抽象类
纯虚函数 纯虚 函 数与抽象类 • 如果基类只表达一些抽象的概念,而不和具体的事物相联系,就不在此基类中定义函数。但希望基类必须为它的派生类提供一个公共的界面,可以通过在基类中加入纯虚函数(prue virtual function)来实现。 • 纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义,但要求任何派生类都必须定义自己的虚函数版本。纯虚函数定义形式: virtual type函数名(参数表)= 0; • “=0”只表示该成员函数是纯虚的。
例如: #include<iostream.h> class shape //定义基类 { public: virtual float area ( ) = 0;//定义纯虚函数 virtual void Disp( ) = 0; //定义纯虚函数 }; class rectangles:public shape //定义图形的派生类矩形类 { public: float area( ); //可以定义 void Disp( );//可以定义 };
纯虚函数使用注意: • 纯虚函数只有函数的名字,不具备函数功能,不能被调用。只有在派生类中定义以后,它才有函数的功能,才可以调用; • 纯虚函数的作用只是在基类中保留一个函数的名字,以便在派生类中定义它的版本; • 如果基类中没有保留名字,无法实现多态性; • 如果在派生类中没有定义纯虚函数的版本,则该虚函数在派生类中仍然是纯虚函数。
抽象类 纯虚函数与抽象类 • 抽象类:指包含纯虚函数的类。 • 作用 • 抽象类为抽象和设计的目的而声明,将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。 • 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。 • 注意 • 抽象类只能作为基类来使用。 • 不能声明抽象类的对象。
例 : 纯虚函数与抽象类 #include <iostream.h> class B0 //抽象基类B0声明 { public: //外部接口 virtual void display( )=0; //纯虚函数成员}; class B1: public B0 //公有派生 { public: void display( ){cout<<"B1::display()"<<endl;} //虚成员函数}; class D1: public B1 //公有派生 { public: void display( ){cout<<"D1::display()"<<endl;} //虚成员函数};
void fun(B0 *ptr) //普通函数 { ptr->display( ); } void main( ) //主函数 { B0 *p; //声明抽象基类指针 B1 b1; //声明派生类对象 D1 d1; //声明派生类对象 p=&b1; fun(p); //调用派生类B1函数成员 p=&d1; fun(p); //调用派生类D1函数成员 } 运行结果: B1::display( ) D1::display( ) 38
纯虚函数与抽象类小结 • 纯虚函数:提供了抽象类对派生类的接口。纯虚函数是仅在基类中声明,但必须在派生类中定义不同的实现版本。 • 抽象类:是指包含纯虚函数的类。抽象类只能作为基类来使用。不能声明抽象类的对象。
本章小结 • 多态性的实现有两种方式:早期联编通过重载实现;滞后联编通过虚函数实现; • 对象指针:指向基类类型的指针可以指向其公有派生类对象,但只能访问从基类继承的成员; • 虚函数:用于实现滞后联编,在基类中将成员函数声明为virtual特性,就可以在派生类中对该成员函数重新定义(定义原型与基类中的完全相同),是特殊的函数重载。当基类指针指向派生类时,即可访问派生类重新定义的函数,从而实现多态性; • 抽象类:是指包含纯虚函数的类,纯虚函数提供了抽象类对派生类的接口。纯虚函数仅在基类中声明,可以在派生类中定义不同的实现版本。