1面向对象程序设计第13章多态2主要内容1.多态性2.虚函数3.纯虚函数和抽象类4.运算符重载31多态性•一个名称(函数名)可以有多种语义。用户只需发送同样的消息,而不同类型的对象接收导致不同的行为。加法intadd(inta,intb)complexadd(complexa,complexb)Intadd(inta,intb,intc)add计算面积areaarea()shapearea()squrearea()circle能否用相同调用方式来调用类族中属于不同类功能类似的同名函数?4多态性的概念•多态性是面向对象程序设计的重要特征之一。•多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。•多态的实现:–函数重载(重载多态)–运算符重载(重载多态)–虚函数(包含多态)–类型强制转换(强制多态)–类模板(参数多态)52虚函数静态绑定带来的问题:(Ex_PolyFunc.cpp)2)解决方法:虚函数--动态绑定(Ex_VirtualFunc.cpp)6静态绑定和动态绑定•绑定(联编)(binding)–一个标志符与一个内存地址联系在一起的过程–一条消息和一个对象的方法向结合的过程–把函数调用与适当的函数代码相对应的过程•绑定分为–静态绑定•在编译阶段决定执行哪个同名的被调用函数(重载多态)–动态绑定。•在编译阶段不能决定执行哪个同名的被调用函数,只在执行阶段才能依据要处理的对象类型来决定执行哪个类的成员函数(包含多态)7多态性也分为静态和动态两种:•静态多态性——函数重载——静态绑定–一个类中的同名函数–同名不同型,可根据参数类型及个数区别语义,通过实际的参数由编译系统决定调用哪个函数,如在一个类中参数不同的构造函数,运算符重载函数等•动态多态性——虚函数——动态绑定–类层次中的不同类中的同名函数,–同名同型(参数一样),因而要根据指针指向的对象所在类来区别语义,来决定调用哪个类的成员函数。(采用动态绑定)8继承讨论的是类与类的层次关系多态则是考虑在不同层次的类中,以及在一个类的内部,同名成员函数之间的关系问题。多态是指类族中具有相似功能的不同函数使用同一个名称来实现,从而可以使用相同的调用方式来调用这些具有不同功能的同名函数。(Ex_VirtualFunc.cpp)9普通虚函数•虚函数声明virtual函数类型函数名(参数表)–virtual只用来说明类声明中的原型,不能用在函数实现时。–具有继承性,基类中声明了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数。–本质:不是重载声明而是覆盖。–调用方式:只能通过基类指针或引用,执行时会采用动态绑定方式,根据指针指向的对象的类,决定调用哪个函数。10使用虚函数的限制某类中的一个成员函数被说明为虚函数,意味着该成员函数在派生类中可能有不同的实现。设置虚函数必须注意下列事项:(1)只有类的成员函数才能说明为虚函数这是因为,虚函数仅适用于有继承关系的类对象,所以普通函数不能说明为虚函数。(2)静态成员函数不能是虚函数因为静态成员函数不受限于某个对象。(3)内联函数不能是虚函数因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看作是非内联的。(4)默认形参值是静态绑定的,因此不能被派生类重写11(5)派生类说明的虚函数应同基类同型(参个数类型同)一般要求基类中声明了虚函数后,派生类声明的虚函数应该与基类中虚函数有相同名称,参数个数相等,对应参数的类型相同,有相同的返回值或者满足类型兼容规则的指针、引用型的返回值。例(6)构造函数不能是虚函数因为构造时对象还是一片未定型的空间。只有在构造完成后,对象才能成为一个类的名副其实的实例。(7)析构函数可以是虚函数,且通常为虚函数。说明虚析构函数的目的在于:使用delete运算符删除一个对象时,能确保析构函数被正确地执行,这是因为,设置虚析构函数后,可以利用动态绑定方式选择析构函数。12[例]虚函数不同型,不能动态绑定classA{public:virtualvoiddisp(intn){coutA::dispn=nendl;}};classB:publicA{public:virtualvoiddisp(doublem){coutB::dispm=mendl;}};voidfn(A&a){a.disp(5.5);}voidmain(){Bb;fn(b);}结果:A::dispn=5子类B中的虚函数disp的参数与基类A中的不同,所以强制转换成A类的,执行基类的disp()函数,未使用动态联编。将类B中的disp()虚函数改为:virtualvoiddisp(intm)则结果:B::dispm=513classBase{public:~Base(){cout调用Base::~Base()endl;}};classDrived:publicBase{char*buf;public:Drived(inta){buf=newchar[a];}~Drived(){delete[]buf;cout调用Drived::~Drived()endl;}};voidfun(Base*a){deletea;}voidmain(){Base*a=newDrived(10);fun(a);}结果:调用Base::~Base()虚析构函数14虚析构函数何时需要虚析构函数?•当你可能通过基类指针删除派生类对象时•如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),并且被析构的对象是有重要的析构函数的派生类的对象,就需要让基类的析构函数成为虚拟的。15classBase{public:virtual~Base(){cout调用Base::~Base()endl;}};classDrived:publicBase{char*buf;public:Drived(inta){buf=newchar[a];}~Drived(){delete[]buf;cout调用Drived::~Drived()endl;}};voidfun(Base*a){deletea;}voidmain(){Base*a=newDrived(10);fun(a);}结果:调用Drived::~Drived()调用Base::~Base()虚析构函数Ex_Vdestructor.cpp163纯虚函数和抽象类1)纯虚函数•纯虚函数的一般格式如下:virtual类型函数名(参数表)=0许多情况下,基类中不对虚函数给出有意义的实现,而把它说明为纯虚函数,它的实现留给该基类的派生类去做。2)抽象类有时,基类往往表示一些抽象的概念,它的成员函数没有什么实际意义——可定义成——抽象类•抽象类:至少有一个纯虚函数的类。–是特殊的类,为了抽象和设计的目的而建立的,处于继承结构的上层。是将有关的类组织在类层次中,并作为根,相关的子类是从这个根中派生出来的(Ex_PureVirtualFunc.cpp)17抽象类小结•作用–抽象类为抽象和设计的目的而声明,将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。–对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。•注意:–抽象类只能用作其它类的基类,不能建立抽象类的对象。–抽象类不能用作参数、函数返回值、显式转换的类型。–可以说明指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。18例:用继承构造系统人们习惯把相同的性质抽取处理,成了一个基类,再从中衍化出派生类,所以我们有了这个类层次结构。ShapeEllipseTriangleRectangle19classShape{protected:intcolor;public:voidsetcolor(intcolor);};classRect:publicShape{public:voiddisplay();...protected:Pointlefttop;intw,h;}classEllipse:publicShape{public:voiddisplay();...protected:Pointcenter;intr1,r2;}classTriangle:publicShape{public:voiddisplay();...protected:Pointp1,p2,p3;}20问题:实现把所有的形状都display出来的一般化操作Shape*s[5];思考:既然都有display操作,把它提升到基类Shape中然后再继承,好吗?•不好,display函数应该因不同形状而操作不同。•但display不提到基类,就不能以一个循环完成下列操作:for(inti=0;i5;i++)s[i]-display();解决:虚函数–Shape是抽象的形状,不该有display操作,但为在具体的派生类中绘图,不得不在基类中加上这个虚函数。什么都不做——纯虚函数–Shape因包含纯虚函数,而成为抽象类不能被实例化(不能定义其对象)21classPoint{private:intx,y;public:Point(intx1,inty1){x=x1;y=y1;}Point(constPoint&p){x=p.x;y=p.y;}voidsetp(intx1,inty1){x=x1;y=y1;}voidmove(intx1,inty1){x=x+x1;y=y+y1;}};classShape//抽象类{protected:intcolor;public:Shape(intc){color=c;}voidsetcolor(intc){color=c;}virtualvoiddisplay()=0;//纯虚函数};22classRect:publicShape{protected:Pointtopleft;intw,h;public:Rect(intc,intx1,inty1,intw1,inth1):Shape(c),topleft(x1,y1){w=w1;h=h1;}voiddisplay(){coutRectendl;}};classEllipse:publicShape{protected:Pointp;intr1,r2;public:Ellipse(intc,intx,inty,intrr1,intrr2):Shape(c),p(x,y){r1=rr1;r2=rr2;}voiddisplay(){coutEllipseendl;}};23classTriangle:publicShape{protected:Pointp1,p2,p3;public:Triangle(intc,intx1,inty1,intx2,inty2,intx3,inty3):Shape(c),p1(x1,y1),p2(x2,y2),p3(x3,y3){}voiddisplay(){coutTriangleendl;}};voidmain(){Rectr(1,0,0,3,4);Ellipsee(1,4,5,2,3);Trianglet(1,0,0,2,3,6,7);Shape*p[3];p[0]=&r;p[1]=&e;p[2]=&t;for(inti=0;i3;i++)p[i]-display();}24classcomplex{private:doublereal;doubleimag;public:complex(doubler=0.0,doublei=0.0){real=r;imag=i;}voiddisplay(){coutreal“+”image“i”endl;}}voidmain(){intia=1,ib=2,ic;ic=ia+ib;complexa(10,20),b(5,8),c;c=a+b?}C++预定义的运算符的操作对象只能是基本数据类型,那么用户自定义类型呢?——运算符重载!