多态与动态联编.ppt
第11章 多态与动态联编,本章教学目标:理解虚拟函数、动态多态性和动态联编地概念。了解虚拟函数和动态联编对实现动态多态地作用。理解虚拟函数地声明、定义方法和访问属性。理解纯虚拟函数和抽象类地概念,掌握纯虚拟函数和抽象类的定义方法。了解抽象类和具体类的区别。了解虚拟析构函数的概念和作用,掌握其声明和使用方法。理解动态联编的实现机理,学会使用动态多态特性。,第11章 多态与动态联编,11.1虚拟函数、动态多态性与动态联编 11.2虚拟函数的访问属性 11.3纯虚拟函数与抽象类 11.4虚拟函数应用举例 11.5虚拟析构函数 11.6动态联编的实现机理,11.1 虚拟函数、动态多态性与动态联编,虚拟函数(virtual function)是 C+进行 OOP 最富魅力的特征,灵活运用虚拟函数可以构造高度抽象和重用性好的程序。/例11.1 虚拟函数的定义和使用。#includeclass Base public:virtual void vfun(void)/用关键字virtual定义虚拟函数vfun()coutBase vfunn;class Drv1:public Base public:void vfun(void)coutDrv1 vfunn;,class Drv2:public Base;class Drv3:public Base public:void vfun(void)coutDrv3 vfunn;class DDrv:public Drv1 public:void vfun(void)coutDDrv vfunn;void main()Base*vp;/同一接口,多种方法:,vp=new Base;vp-vfun();vp=new Drv1;vp-vfun();vp=new Drv2;vp-vfun();vp=new Drv3;vp-vfun();vp=new DDrv;vp-vfun();/*程序的执行结果如下:Base vfunDrv1 vfunBase vfunDrv3 vfunDDrv vfun*/,此例中,用关键字virtual在基类Base中定义了一个虚拟函数vfun(),同时在派生类Drv1和Drv3以及Drv1的派生类DDrv中分别定义了一个接口完全相同的函数(可省略关键字 virtual,系统自动地将其定义为virtual,但从可读性方面考虑建议不要省略virtual),但在派生类 Drv2中没有定义 vfun(),这样做是为了对比地说明虚拟函数的用法。为了使用虚拟函数必须使用指向基类的指针,本例中,在主函数中定义了一个指向基类Base的指针vp,在C+中一个指向基类的指针可以用来指向其派生类的对象。访问非虚拟函数是编译时决定的,称为“静态联接”(或“静态约束”、“静态装订”:static binding,或“早期约束”:early binding);访问虚拟函数是在程序运行时动态决定的,称为“动态联接”(或“动态约束”、“动态装订”:dynamic binding,或“晚期约束”:lately binding)。,虚拟函数:基类中被关键字 virtual说明并在一个或多个派生类中被重新定义的函数原型完全一致的成员函数。关于虚拟函数应注意以下几点:除虚拟析构函数外,虚拟函数要求函数原型必须完全相同(函数体可不同),否则视为重载函数;虚拟函数必须是类的成员函数(静态成员函数和构造函数除外),一般说来,外部函数不能声明为虚拟函数;友元不能声明为虚拟函数,但虚拟函数可以是另一个类的友元;析构函数可以是虚拟函数,但构造函数不能为虚拟函数。当基类中的析构函数定义为虚拟函数以后,派生类中的析构函数都自动定义为虚拟函数,很显然,派生类的析构函数与基类的析构函数是不同名的。如果想使用虚拟函数进行动态约束,则必须用指向基类的指针或引用来访问它,否则将为静态联编,使用虚拟函数的最大优点是:通过动态装订构造了一个抽象接口,提高了模块的独立性和易维护性。在C+中,利用虚拟函数和多态性,使得基类和派生类之间形成一种从抽象到具体、从一般到特殊的层次关系。在基类中我们可以声明本类和派生类都共有的函数(即一般化、抽象化、通用化、泛化),同时允许在派生类中对其中的某些或全部函数进行特殊定义(具体化、特殊化、特化、)。这种方法更接近于人类的思维方法,这也是虚拟函数和多态的重要性所在。,11.2 虚拟函数的访问属性,虚拟函数的访问属性不决定于动态联编函数的访问属性,而决定于访问语句中虚拟函数的静态访问属性,即决定于访问语句:指针变量-虚拟函数或者:引用变量虚拟函数中指针变量或引用变量所属类中的虚拟函数的访问属性。/例11.2 虚拟函数的访问属性。#includeclass A public:/virtual void vfun(void)/公有的虚拟函数 coutA vfunn;,class B:public A private:/void vfun(void)coutvfun();/通过指针pA调用虚拟函数vfun(),B b;/定义B类的对象bA/*程序的执行结果如下:B vfunB vfunC vfun*/,在上面的程序中,定义了三个类:A派生出B,B派生出C。在A、B、C中都定义了虚拟函数vfun(),但它们的访问属性不同。在A中,vfun()定义在public段内,是公有虚拟函数,在B和C中,vfun()都定义在private段内,都是私有虚拟函数,在main()函数中第句以及运行结果的第一行表明,虽然B类的vfun()是私有函数,但是仍然能够通过指针pA访问它,这是因为pA是指向A类的指针,而在A类中vfun()是公有函数,即虚拟函数的访问属性是由访问形式:pA-vfun();的属性决定的,而不是由它实际访问的虚拟函数的属性决定的,也就是说,虚拟函数的访问属性是静态决定的,而不是动态决定的。main()函数中第和句也是基于同样理由。其中第句是通过引用来访问虚拟函数,第句还说明指向基类的指针可以访问其下两级甚至更多级派生类中的虚拟函数。main()函数中第句以注释的形式出现是因为它们无法通过编译,其原因是访问形式:pB-vfun();是私有的,因此编译时会产生“B:vfun()is not accessible”(“B:vfun()是不可访问”)的错误信息。,11.3 纯虚拟函数与抽象类,与普通函数一样,基类中的虚拟函数一般必须用一个函数体来定义其操作(可为空)。如果不作定义,可以把它声明为纯虚拟函数(pure virtual function)。纯虚拟函数的定义格式如下:virtual 返回值类型 函数名(参数表)0;例如:class human.virtual int general()=0;/纯虚拟函数.;带有一个或多个纯虚拟函数的类叫做抽象类(abstract class)。抽象类只能作为其他类的基类,因此通常把抽象类称为抽象基类。,C+规定,不能建立抽象类的对象,也不能把抽象类作为函数的参数类型或返回值类型,但可以定义指向抽象类的指针和不需初始化的抽象类引用。任何由抽象类派生出来的派生类都必须重新声明它所继承的全部纯虚拟函数,不管这些纯虚拟函数在该派生类中作为普通的虚拟函数还是作为纯虚拟函数。/例11.3 动态多态的实现。#includeclass plane/抽象类 protected:double h,b,r,s;public:virtual void set_hb()=0;/纯虚拟函数 virtual void set_r()=0;/纯虚拟函数 virtual void area()=0;/纯虚拟函数 void display()coutThe area=sendl;,class circle:public plane/圆,公有派生于平面 public:void set_hb();void set_r()coutr;void area()s=3.14*r*r;class triangle:public plane/三角形,也公有派生于平面 public:void set_hb()couthb;void set_r()cout triangleendl;void area()s=h*b/2;,class Eqtriangle:public triangle/等边三角形,公有派生于三角形 public:void set_hb()coutb;void area()s=0.866*b*b/2;void Do(plane*p)/多态执行函数 p-set_hb();p-set_r();p-area();p-display();void main()plane*p=new circle;/动态创建circle类的对象Do(p);/调用多态执行函数,delete p;p=new triangle;/动态创建triangle类的对象Do(p);/调用多态执行函数delete p;p=new Eqtriangle;/动态创建Eqtriangle类的对象Do(p);/调用多态执行函数delete p;/*程序的执行结果如下:Input circles r:10The area=314Input triangles h and b:20 30triangleThe area=300Input the side:26triangleThe area=292.708*/,该程序定义了三个类,其中plane为基类,其余两个为派生类。由于plane中含有三个纯虚拟函数set_hb()、set_r()和area(),因此该类是一个抽象基类。基类中的protected成员h、b、r和s分别为三角形的高、底边长、圆的半径和图形的面积,函数display()用来显示面积值。在派生类triangle和circle中都重新定义了基类中的三个纯虚拟函数,在triangle中函数set_hb()用来设置h、b的值,函数area()用来计算三角形的面积,而函数set_r()在triangle中用不着,所以将其函数体定义为空,目的是main()函数中要用动态存储分配的方法来建立triangle类的对象,如果仍将set_r()函数声明为纯虚拟函数或干脆将其去掉,则triangle仍为抽象基类而不能建立其动态对象。同理,circle中的set_hb()也定义成函数体为空的函数。在circle中,set_r()用来设置圆的半径r的值,函数area()则用来计算圆的面积,它的计算方法不同于三角形面积的计算。在主程序中定义了一个指向抽象基类的指针p,然后用动态存储分配的方法来建立类triangle的对象,用指针访问triangle的对象中的成员函数,分别设置一个三角形的参数、计算其面积并显示计算结果,最后释放triangle的对象所占用的存储空间。对于circle类亦同样处理。在这里用到了:p-area();但计算的是不同形体的面积,它们的计算方法是不同的,体现出“同一接口,不同方法”的程序设计技术。,11.4 虚拟函数应用举例,在本小节内我们将对比地介绍非多态实现与多态实现的区别,从而说明多态实现在程序开发和维护中的作用。面是一个产品管理中的小例子:对半成品semifinished、零件component和总成assembly三种产品进行管理,程序采用链表结构。例5.4是非多态性实现,例5.5是多态性实现。11.4.1 产品管理程序的非多态解 例11.4由三个文件组成,在头文件exp11_4.h中定义了节点类Node和链表类chain,文件exp11_4a.cpp是类方法的具体实现,exp11_4b.cpp是主程序。/例11.4产品管理非多态解。(略)11.4.2 产品管理程序的多态解 例11.5由三个文件组成,在头文件exp11_5.h中定义了节点类Node和它的三个派生类semifinished、component、assembly以及链表类chain,文件exp11_5a.cpp是类方法的具体实现,exp11_5b.cpp是主程序。,/例11.5 产品管理多态解。(略)11.4.3 非多态解和多态解系统的维护 多态解较非多态解对系统维护提供了明显的巨大优点。为了说明这一点,假定我们要在产品种类中增加一个新的品种purchases(外购件),它的独特属性有供应商firm和单价price。对于例5.4,我们要在头文件的枚举类型中增加purchases,要增加一个新的全局结构类型:struct PurcType char firm20;float price;以表示外购件的独特属性,并且要将PurcType u增加到无名共用体中去,以便能在节点中表示外购件的独特属性。要在exp5_4a.cpp文件中的Node:sets()函数体的switch语句中增加如下的case语句块:,case purchase:coutu.firm;coutu.price;break;以便输入外购件的独特属性。要在chain:print()函数体的switch语句中增加如下的case语句块:case purchase:coutu.firm u.price;break;以便输出外购件的独特属性。最后,在主程序中增加如下语句:temp=new Node(345678912,Tyre,purchase);temp-sets();products.insert(temp);以便将外购件产品轮胎(Tyre)插入到链表中。,从上面的过程我们可以看出,这种维护对于一个大型系统将是相当繁琐的,其每一步都必须经过慎密的考虑,所有修改过的文件都必须重新编译,并且还必须经过严格的正确性验证。对于例5.5,我们不必修改类的定义头文件和类的实现文件,只需增加一个purchase派生类的定义文件和一个实现文件:(略)多态解系统的维护具有高度的局部性和可靠性,11.5 虚拟析构函数,在一个类的层次系统中,如果对象是用多态性动态分配的,那么,使用通常的析构函数来Delete这些对象就会出错。解决方法:将基类析构函数声明为虚拟析构函数。这样就会使所有派生类的析构函数自动成为虚拟析构函数。构造函数是不能定义为虚拟函数的。,11.6 动态联编的实现机理,系统通过查虚拟函数表VFT(Virtual Function Table)的方法实现动态联编功能。具体情况结合例11.3说明:C为每个拥有虚拟函数的类建立一个与之相关的VFT,该表有一些函数指针组织,指向相应的虚拟函数。一个类只有一个虚拟函数表VFT,不管该类建立多少个对象,它们共享本类的VFT。VFT和 vptr都是自动生成的。派生类circle的VFT包含三个函数指针p1、p2、p3,分别指向本类的虚拟函数。非虚拟函数表VFT中没有入口地址,即使没有函数指针指向它们,它们是静态联编的。调用虚拟函数时,C通过对象的vptr指针查找到对应类的VFT。,