第6讲 虚基类多态性和虚函数new_第1页
第6讲 虚基类多态性和虚函数new_第2页
第6讲 虚基类多态性和虚函数new_第3页
第6讲 虚基类多态性和虚函数new_第4页
第6讲 虚基类多态性和虚函数new_第5页
已阅读5页,还剩58页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第六讲虚基类、多态性和虚函数武汉大学王泉德1一、虚基类1.单继承classAclassBclassC2多重继承和虚基类2.多重继承classAclassBclassC3多重继承派生类的定义设类B是类A1、A2、…、An的派生类,多重继承的派生类的定义形式为:class<B>:[<派生方式1>]<A1>, [<派生方式2>]<A2>,…, [<派生方式3>]<An>{... //派生类新增加的成员列表

};4例定义一个派生类C,它是类A和B的派生类。classA {protected:

inta;public:voidSetA(int

na);};classB {protected:

intb;public:voidSetB(int

nb);};classC:publicA,publicB

{private:

intc;public:

int

SetAB(int

na,int

nb);};

5多重继承中的二义性问题classC:publicA

{public:

intc;};classD:publicB,publicC

{public:

intd;};main(){Dd1;d1.a=100;}classA{public:

inta;};classB:publicA{public:

intb;};6多重继承中的二义性问题classBclassCclassDclassADBACA派生类D的对象中存在间接基类A的两份副本

7解决方法一利用作用域限定符(::)把基类的成员与下一层基类关联起来:

d1.B::a=100;//d1.C::a=100

缺点:浪费了存储空间;在访问基类的成员时,要求指明访问路径。大部分情况下不需要保存基类多个相同的副本。8解决方法二(虚基类)虚基类并不是一种新的类型的类,而是一种派生方式。采用虚基类方式定义派生类,在创建派生类的对象时,类层次结构中虚基类的成员只出现一次,即基类的一个副本被所有派生类对象所共享。9虚基类classBclassCclassDclassADBAC10虚基类派生方式的定义采用虚基类方式定义派生类的方法是在基类的前面加上关键字virtual,而定义基类时与一般基类完全一样。语法如下:class<派生类名>:virtual<派生方式><共同基类名>例采用virtual虚基类方式定义派生类。classB:virtualpublicA{public:

intb;};classC:virtualpublicA{public:

intc;};主函数中:

d1.a=100;

√11虚基类的初始化虚基类的初始化与一般多继承的初始化在语法上相同,但构造函数的调用顺序有所不同,规则如下:先调用虚基类的构造函数,再调用非虚基类的构造函数。若同一层次包含多个虚基类,其调用顺序为定义时的顺序。若虚基类由非虚基类派生而来,则仍按先调用基类构造函数,再调用派生类构造函数的顺序。12引入虚基类后构造函数的调用顺序举例#include<iostream.h>classBase1{public: Base1(){

cout<<"classBase1";

cout<<endl; }};classBase2{public: Base2(){

cout<<"classBase2";

cout<<endl;} };classLevel1:publicBase2,virtualpublicBase1{public: Level1(){

cout<<"classLevel1";

cout<<endl;}};classLevel2:publicBase2,virtualpublicBase1{public: Level2(){

cout<<"classLevel2";

cout<<endl; }};classTopLevel:publicLevel1,virtualpublicLevel2{public:

TopLevel(){

cout<<"classTopLevel";

cout<<endl; }};voidmain(){

TopLevel

obj;}运行结果:classBase1classBase2classLevel2classBase2classLevel1classTopLevel13使用虚基类派生方式的好处节约内存空间;避免在多重派生类中类成员的不明确性。14虚基类构造函数的调用说明建立对象时所指定的类称为最(远)派生类。虚基类的成员是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中给出对虚基类的构造函数的调用。如果未列出,则表示调用该虚基类的缺省构造函数。在建立对象时,只有最派生类的构造函数调用虚基类的构造函数,该派生类的其它基类对虚基类构造函数的调用被忽略。15二、多态性和虚函数何谓多态性?多态性也是面向对象程序设计方法的一个重要特征,它主要表现在函数调用时实现“一种接口、多种方法”。16多态性是指类中同一函数名对应多个具有相似功能的不同函数,可以使用相同的调用方式来调用这些具有不同功能的同名函数的特性。在C++程序中,多态性表现为同一种调用方式完成不同的处理。从实现角度来划分,多态可分为编译时多态和运行时多态。编译时多态是指在编译阶段由编译系统根据操作数据确定调用哪个同名函数。(函数重载)运行时多态是指在运行阶段才根据产生的信息确定需要调用哪个同名的函数。(虚函数)17两种多态性:编译时多态性和运行时多态性编译时多态性:在函数名或运算符相同的情况下,编译器在编译阶段就能够根据函数参数类型的不同来确定要调用的函数——通过重载实现。运行时多态性:在函数名、函数参数和返回类型都相同的情况下,只能在程序运行时才能确定要调用的函数——通过虚函数实现。18联编多态性实现过程中,确定调用哪个同名函数的过程就是联编,又称绑定。联编是指计算机程序自身彼此关联的过程,也就是将函数调用语句与函数代码相关联。两种联编方式:静态联编和动态联编静态联编是指编译器在编译阶段就确定了要调用的函数,即早期绑定。重载采用静态联编方式。动态联编是指在程序执行过程中根据具体情况再确定要调用的函数,即后期绑定。当通过基类指针调用虚函数时,C++采用动态联编方式。虚函数体现出一种动态多态性或运行时多态性。19静态联编在编译阶段完成的联编称为静态联编。在编译过程中,编译系统可根据参数不同来确定哪一个同名函数。函数重载和运算符重载就是通过静态联编方式实现编译时多态性的体现。优点:函数调用速度快,效率高。缺点:编程不够灵活。20静态联编举例#include<iostream.h>classStudent{public: voidprint(){

cout<<"AStudent"<<endl; }};classGStudent:publicStudent{public: voidprint(){

cout<<"AgraduateStudent<<endl; }};21静态联编举例voidmain(){ Students1,*ps;

GStudents2; s1.print(); s2.print(); s2.Student::print();

ps=&s1;

ps->print();

ps=&s2;

ps->print();}运行结果:AStudentAgraduateStudentAStudent

AStudentAStudent希望调用对象s2的输出函数,但是实际调用的却是对象s1的输出函数。22静态联编举例说明:基类指针ps指向派生类对象s2时并没有调用派生类的print(),而仍然调用基类的print(),这是因为静态联编的结果。在程序编译阶段,基类指针ps对print()的操作只能绑定到基类的print()上,导致程序输出了不期望的结果。而期望的是执行派生类的print函数。23多态性用法:用基类指针指向派生类对象声明一个派生类的对象的同时也自动声明了一个基类的对象。 ——3.3小节内容派生类的对象可以认为是其基类的对象。C++允许一个基类对象的指针指向其派生类的对象。

——这是实现虚函数的关键24注意不允许派生类对象的指针指向其基类的对象。即使将一个基类对象的指针指向其派生类的对象,通过该指针也只能访问派生类中从基类继承的公有成员,不能访问派生类自定义的成员,除非通过强制类型转换将基类指针转换为派生类指针。25基类指针与派生类指针之间的相互转换classA{private:

inta;public:voidSetA(inti) {a=i;};voidShowA(){cout<<a<<endl;};};classB:publicA{private:

intb;public:voidSetB(inti) {b=i;};voidShowB(){cout<<b<<endl;};};26基类指针与派生类指针之间的相互转换voidmain(){Aa,*pa; //pa为基类对象的指针

Bb,*pb; //pb为派生类对象的指针

pa=&b; //基类指针pa指向派生类对象b//通过基类指针pa访问B中从基类A继承的公有成员

pa->SetA(100); pa->ShowA();

pb=(B*)pa; //将基类指针转化为派生类指针

pb->SetB(200);

pb->ShowB();}pb=&apa->SetB() pa->ShowB()

27虚函数classA{public:voidShow(){cout<<"A::Show";};};voidmain(){A*pa;Bb;pa=&b; pa->Show();}classB:publicA{public:voidShow(){cout<<"B::Show";};};

调用哪一个Show()?如果想通过基类指针调用派生类中覆盖的成员函数,只有使用虚函数。

28虚函数的声明虚函数是在某一个基类中声明为virtual,并在一个或多个派生类中被重新定义的成员函数。要将一个成员函数声明为虚函数,只需在定义基类时在成员函数声明的开始位置加上关键字virtual声明虚函数的格式:virtual<返回值类型><函数名>(<参数表>);29虚函数的声明虚函数描述类层次体系中相似的行为,是非静态的成员函数,经过派生之后,虚函数在类族中可以实现运行时多态。一个函数一旦被声明为虚函数,则无论声明它的类被继承了多少层,在每一层派生类中该函数都保持虚函数特性。因此在派生类中重新定义该函数时,可省略关键字virtual。但是为了提高程序的可读性,往往不省略。30使用虚函数的结果classA{public:

virtualvoidShow(){cout<<"A::show\n";};};classB:publicA{public:voidShow(){cout<<"B::show\n";};};voidmain()

{

Aa,*pa;

Bb;

pa=&a;pa->Show();

//调用函数A::Show()

pa=&b;pa->Show();

//调用函数B::Show()

}程序运行结果:

A::ShowB::Show31虚函数实现同一个接口来处理不同的对象CShapeCEllipseCCircleCTriangleCRectangleCSquare32voidmain(){

CShape

aShape;

CEllipse

aEllipse;

CCircle

aCircle;

CTriangle

aTriangle;

CRectangle

aRect;

CSquare

aSquare;

CShape*pShape[6]={&aShape, &aEllipse, &aCircle, &aTriangle, &aRect, &aSquare,};

for(inti=0;i<6;i++){

pShape[i]->Draw(); }}33对象在内存里的存放方式classA{private:

int

m_na;public: voidSetA(inta) {

m_na=a; }};Aa1;a1.SetA(5);Aa2;a2.SetA(10);A1.SetA和a2.SetA调用的是用一个地址的函数。它是通过this来取得各自的成员变量。m_na对象a1m_na对象a2A::SetA(inta,A*this){

this->m_na=a;}thisthis34虚函数的实现-通过虚函数地址表classA{public:

inta;virtualvoidvFunc1();virtualvoidvFunc2();voidshow();};vptra(*vFunc1)()(*vFunc2)()classA::vFunc1()classA::vFunc2()classA::show()A的对象35虚函数的实现-通过虚函数地址表classB:publicA{public:

intb;

virtualvoidvFunc2();voidshow();};vptra(*vFunc1)()(*vFunc2)()classA::vFunc1()classB::vFunc2()classB::show()A的对象b36虚函数使用实例:图形绘制RectTriShape在图形类层次体系中有如下行为RotateDrawArea希望通过指向基类Shape对象的指针统一完成绘制不同图形的工作Circle37虚函数使用实例:图形绘制classShape{public: doublet;

int

ntype; doubles; Circle*pCir; Tri*pTri;

CRect*pRect;public: voidarea(); voidarea(Shape*,int);};classCircle{private: doubler;doubles;public:

Circle(int

a){r=a;} staticint

ntype; voidarea(){s=PI*r*r;cout<<"theareaofciris:"<<s<<endl;}};int

Circle::ntype=0;38虚函数使用实例:图形绘制classCRect{private: doublewi,hi; doubles; doublexl,yl;public: staticint

ntype;

CRect(double

w,double

h){wi=w;hi=h;} voidarea(){ s=hi*wi;

cout<<"theareaofciris:"<<s<<endl; } };int

CRect::ntype=1;39虚函数使用实例:图形绘制classTri{private: doublea,b,c;doubles; public: staticint

ntype;

Tri(double

ai,doublebi,doubleci){a=ai;b=bi;c=ci;} voidarea(){ doublep=(a+b+c)/2; s=sqrt(p*(p-a)*(p-b)*(p-c));

cout<<"theareaofciris:"<<s<<endl; }};int

Tri::ntype=2;40虚函数使用实例:图形绘制voidShape::area(Shape*pShape,int

ntypeobj){

ntype=ntypeobj;

switch(ntype){ case0://circle

pCir=(Circle*)pShape; pCir->area();break; case1:

pRect=(CRect*)pShape;pRect->area();break; case2:

pTri=(Tri*)pShape;pTri->area(); break; default: break; }}这种处理方式中,area函数必须理解现存的所有形状,当有新的类加入系统,则处理形状的所有操作都必须修改。如果无法接触新类的代码,则无法实现新类的绘制。41虚函数使用实例:图形绘制intmain(){ Tritriobj(3,4,5);

CRectrectobj(4,5); Circlecirobj(5); Shape*pshobj; Shapeshobj;

pshobj=(Shape*)&rectobj;

shobj.area(pshobj,rectobj.ntype); return0;}//运行结果:Theareaofcircleis:20基于多态和虚函数,我们可以使得程序大为简化。42虚函数使用实例:图形绘制classShape{pointcenter; colorcol;virtualvoiddraw(); virtualvoidrotate(); virtualvoidarea(); };intmain(){

CRectrectobj(4,5); Circlecirobj(5); Shape*pshobj;

pshobj=&rectobj;

shobj->area(); return0;}//首先定义一个类shape描述所有形状的普遍性质,,在这里只给出调用接口,不给出具体实现。具体实现在特定的具体类中给出.43虚函数使用实例:图形绘制voidAreaAll(shape*v,intsize){

for(intI=0;i<size;i++)

v[i]->Draw();};有了这个定义,可以给出一个对各种形状进行求面积的通用函数。44总结利用虚函数可以在基类和派生类中使用相同的函数名和参数类型,但定义不同的操作。这样,就为同一个类体系中所有派生类的同一类行为(其实现方法可以不同)提供了一个统一的接口。

例如,在一个图形类继承结构中,设类CShape是所有具体图形类(如矩形、三角形或圆等)的基类,则函数调用语句“pShape->Draw()”可能是绘制矩形,也可能是绘制三角形或圆。具体绘制什么图形,取决于pShape所指的对象。45构造函数与虚函数基于构造函数的特点,不能将构造函数定义为虚函数。按照c++的标准语法,构造函数不能是虚函数,因为构造函数是不会通过指针调用的,虚函数没有意义。46构造函数与虚函数的结论构造函数不用设置为虚函数。47析构函数与虚函数当撤消派生类的对象时,先调用派生类析构函数,然后自动调用基类析构函数,如此看来析构函数没必要定义为虚函数。但是,假如使用基类指针指向其派生类的对象,而这个派生类对象是用new运算创建的。当程序使用delete运算撤消派生类对象时,这时只调用了基类的析构函数,而没有调用派生类的析构函数。48析构函数与虚函数的结论析构函数视情况而定,可设置为虚函数。用类向导添加的类,析构函数默认是虚函数。49虚析构函数的使用classA{public://构造函数不能是虚函数 A(){};

//析构函数是虚函数

virtual~A() {cout<<“A::析构\n";};};classB:publicA{public: B(){};

//虚析构函数

~B() {cout<<“B::析构\n";}; };voidmain(){ A*pA=newB;

//...

deletepA;

//先调用派生类B的析构函数,再调用基类A的析构函数}程序运行结果:

B::析构

A::析构50虚析构函数的使用classA{public: A(){};//构造函数不能是虚函数

//析构函数是虚函数

virtual~A(){cout<<“A::析构\n";};};classB:publicA{public: B(){};

//虚析构函数

~B(){cout<<“B::析构\n";}; };如果析构函数不是虚函数,则得不到刚才的运行结果。请思考会是什么结果

总结:由于使用了虚析构函数,当撤消pA所指派生类B的对象时,首先调用派生类B的析构函数,然后再调用基类A的析构函数。51虚析构函数的使用一般来说,可将类族中的具有共性的成员函数声明为虚函数,而具有个性的函数则没有必要声明为虚函数。但是以下情况例外:静态成员函数不能声明为虚函数。因为静态函数不属于某一个对象,没有多态性的特征。内联函数不能声明为虚函数。因为内联函数的执行代码明确,没有虚函数的特征。构造函数不能声明为虚函数。析构函数可以是虚函数。52有些类是抽象的CShape是抽象的,它根本不该有Draw()的动作。但为了统一接口,又必须定义它。虽然可以让它为空,但却不是一个高明的做法。因为它根本就不应该被调用。所以有下面的…53抽象类和纯虚函数何谓抽象类?抽象类是类的一些行为(成员函数)没有给出具体定义的类,即纯粹的一种抽象。抽象类只能用于类的继承,其本身不能用来创建对象,抽象类又称为抽象基类。54抽象类和纯虚函数抽象基类只提供了一个框架,仅仅起着一个统一接口的作 用,而很多具体的功能由派生出来的类去实现。虽然不能声明抽象类的对象,但可以声明指向抽象类的指针。

在一般的类库中都使用了抽象基类,如类CObject就是微软基础类库MFC的抽象基类。55什么样的类为抽象基类?一个类如果满足以下两个条件之

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论