第8讲基于多态性的实践王力虎lhwang@gxnu.edu.cn本讲主要内容继承派生多态虚函数综合应用§8.1多态性多态性(polymorphism)是面向对象程序设计的一个重要特征。利用多态性可以设计和实现一个易于扩展的系统。多态性一般是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。多态性的分类多态性可分为两类:静态多态性和动态多态性。以前学过的函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时系统就能决定调用的是哪个函数,因此静态多态性又称编译时的多态性。静态多态性是通过函数的重载实现的(运算符重载实质上也是函数重载)。动态多态性是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。动态多态性是通过虚函数(virtualfunction)实现的。要研究的问题是:当一个基类被继承为不同的派生类时,各派生类可以使用与基类成员相同的成员名,如果在运行时用同一个成员名调用类对象的成员,会调用哪个对象的成员?也就是说,通过继承而产生了相关的不同的派生类,与基类成员同名的成员在不同的派生类中有不同的含义。也可以说,多态性是“一个接口,多种方法”。8.2虚函数在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。编译系统按照同名覆盖的原则决定调用的对象。用这种方法来区分两个同名的函数。但是这样做很不方便。人们设想能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。例如,用同一个语句“pt-display();”可以调用不同派生层次中的display函数,只需在调用前给指针变量pt赋以不同的值(使之指向不同的类对象)即可。p可以指向派生类numnamesexdisplay()指向基类的指针std*p;numnamesexdisplay()ageaddrdisplay()基类std派生类std1指向派生类的指针std1*p;C++中的虚函数就是用来解决这个问题的。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。p可以指向派生类numnamesexdisplay()指向基类的指针std*p;numnamesexdisplay()ageaddrdisplay()基类std派生类std1指向派生类的指针std1*p;8.3静态关联与动态关联调用同一类族中的虚函数,应当告诉编译系统,你调用的是哪个类对象中的函数。这样编译系统在对程序进行编译时,即能确定调用的是哪个类对象中的函数。确定调用的具体对象的过程称为关联(binding)。关联是指把一个函数名与一个类对象捆绑在一起,建立关联。一般地说,关联指把一个标识符和一个存储地址联系起来。静态关联前面所提到的函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联(staticbinding),由于是在运行前进行关联的,故又称为早期关联(earlybinding)。函数重载属静态关联。动态关联先定义了一个指向基类的指针变量,并使它指向相应的派生类对象,然后通过这个基类指针去调用虚函数。显然,对这样的调用方式,编译系统在编译该行时是无法确定调用哪一个类对象的虚函数的。因为编译只作静态的语法检查,光从语句形式是无法确定调用对象的。基类指针变量先指向了某一个类对象,然后通过此指针变量动态调用该对象中的函数。因此,此过程称为动态关联(dynamicbinding)。这种多态性是动态的多态性,即运行阶段的多态性。8.4何时应当声明虚函数(1)可能作为基类,且在派生类中有可能更改其功能,一般应该将它声明为虚函数。(2)应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数需要说明的是:使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtualfunctiontable,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的。问题:编程输出正弦曲线思考:1、如何实现?核心内容?使用数学函数#includemath;2、如何输入?使用i++,循环360次生成(i,sin(i))3、如何输出?设法在屏幕上输出(i,sin(i))8.5简单问题输出设想:i(度)04590135180225270315360sin(i)00.510.50-0.5-1-0.50所以,图案竖向输出比较现实设计代码:首先:#includeiostreamusingnamespacestd;#includemath.hintmain(){inti;for(i=0;i=360;i++)coutisin(i)endl;return0;}360个数据太多,设法减少设计代码:#includeiostreamusingnamespacestd;#includemath.hintmain(){inti;for(i=0;i=360;i+=20)coutisin(i)endl;return0;}自变量为弧度单位设计代码:#includeiostreamusingnamespacestd;#includemath.hintmain(){inti;for(i=0;i=360;i+=20)coutisin(i*3.14/180)endl;return0;}输出数据内容正确如何对应??**********isin(i)#includeiostreamusingnamespacestd;#includemath.hintmain(){inti;for(i=0;i=360;i+=20){for(intj=0;j30+30*sin(i*3.1415926/180);j++)cout;cout*endl;}return0;}#includeiostreamusingnamespacestd;#includemath.hintmain(){inti;for(i=0;i=360;i+=20){for(intj=0;j30+30*sin(i*3.1415926/180);j++)cout;cout*endl;}return0;}8.6如何在windows窗口实现建立一个对话框AppWizard的对话框生成的程序为:C???App类C???Dlg类CAboutDlg类点击对话框加入一个picture双击ok键,进入OnOK()按键响应代码区,加入代码voidCEight_1Dlg::OnOK(){CWnd*pWnd=GetDlgItem(IDC_STATIC);//IDC_STATIC是picture的IDCClientDCdc(pWnd);dc.SetPixel(50,100,RGB(0,12,255));dc.MoveTo(0,0);dc.LineTo(100,100);}注意,要删除原有的CDialog::OnOK();语句编译运行后,得结果如下:至此,完成在windows窗口的画图然后,设想建立一个point类有x,y数据成员有setP()输入、showP()输出显示成员函数再通过sin()函数完成画正弦曲线图。类的设计classpoint{public:point();virtual~point();virtualvoidshowP(CWnd*pWnd);voidsetP(float,float);protected:COLORREFclr;floatx;floaty;};类的设计point::point(){x=0;y=0;}point::~point(){}voidpoint::setP(floatx1,floaty1){x=x1;y=y1;}voidpoint::showP(CWnd*pWnd){CClientDCdc(pWnd);dc.SetMapMode(MM_LOENGLISH);dc.SetViewportOrg(0,150);dc.SetPixel(x,y,RGB(0,12,255));}类CClientDC派生于CDC,在构造时调用了Windows函数GetDC,在析构时调用了ReleaseDC。这意味着和CClientDC对象相关的设备上下文是窗口的客户区。showP()成员函数是用RGB(0,12,255)(蓝)色在pWnd指示的设备上在(x,y)位置画一点更改按键响应程序OnOK()voidCEight_1Dlg::OnOK(){pointp;CWnd*pWnd=GetDlgItem(IDC_STATIC);//IDC_STATIC是picture的IDfor(inti=0;i=360;i++){p.setP(i,100*sin(i*3.1415926/180));p.showP(pWnd);}//CDialog::OnOK();}编译运行后,得结果如下:更改为指针形式,运行结果不变voidCEight_1Dlg::OnOK(){point*p=newpoint();CWnd*pWnd=GetDlgItem(IDC_STATIC);//IDC_STATIC是picture的IDfor(inti=0;i=360;i+=5){p-setP(i,100*sin(i*3.1415926/180));p-showP(pWnd);}deletep;//CDialog::OnOK();}8.7如何改善已有代码如何据为己用?学术语言:如何继承?多态性如何表达?为什么有虚函数存在?8.7.1将点显示成圆考察基类:pointx,y少一个radiussetP()少一个radiusshowP()需要重建classpoint{public:virtualvoidshowP(CWnd*);voidsetP(float,float);protected:floatx;floaty;};设计圆类:circle加一个radius加一个setR()重建showP()classcircle:publicpoint{public:virtualvoidshowP(CWnd*);voidsetR(float);protected:floatradius;};设计圆类:circle1)如果改变circle坐标内容时调用继承成员setP()+新成员setR()2)如果输出circle坐标内容(画图)时调用新成员showP()由于showP()是虚函数在circle类中新showP()将代替旧showP()3)要特别注意,继承、覆盖完全取决于代码设计者(但要求基类设计者正确使用虚函数)classcircle:publicpoint{public:virtualvoidshowP(CWnd*);voidsetR(float);protected:floatradius;};实际设计新类:circle[原型]classcircle:publicpoint{public:circle():point(),radius(){}virtual~circle(){};virtualvoidshowP(CWnd*pWnd);voidsetR(float);protected:floatradius;};实际设计新类:circle[定义]voidcircle::setR(floatr){radius=r;}voidcircle::showP(CWnd*pWnd){CClientDCdc(pWnd);dc.SetMapMode(MM_LOENGLISH);dc.SetViewportOrg(