资源描述
第8章多态性,8.1多态性概述8.2运算符重载8.3虚函数8.4抽象类,8.1多态性概述,所谓多态性是指同一个接口可以通过多种方法调用,如图8-1所示。通俗地说,多态性是指用一个相同的名字定义不同的函数,这些函数的执行过程不同,但是有相似的操作,即用同样的接口访问不同的函数。比如,一个对象中有很多求两个数中最大值的行为,虽然可以针对不同的数据类型,写很多不同名称的函数来实现,但事实上,它们的功能几乎完全相同。这时,就可以利用多态的特征,用统一的标识来完成这些功能。,图8-1多态性为用户提供单一接口示意图,面向对象的多态性从实现的角度来讲,可以分为静态多态性和动态多态性两种。静态多态性是在编译的过程中确定同名操作的具体操作对象的,而动态多态性则是在程序运行过程中动态地确定操作所针对的具体对象的。这种确定操作具体对象的过程就是联编(binding),也称为绑定。联编是指计算机程序自身彼此关联的过程。也就是把一个标识符名和一个存储地址联系在一起的过程。用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。,所谓消息,是指对类的成员函数的调用。不同的方法是指不同的实现,也就是调用了不同的函数。按照联编进行阶段的不同,联编方法可以分为两种:静态联编和动态联编。这两种联编过程分别对应着多态的两种实现方式。联编工作在编译连接阶段完成的情况称为静态联编。在编译、连接过程中,系统就可以根据类型匹配等特征确定程序中操作调用与执行该操作的代码的关系,即确定某一个同名标识到底是要调用哪一段程序代码。函数重载和运算符重载就属于静态多态性。,和静态联编相对应,如果联编工作在程序运行阶段完成,则称为动态联编。在编译、连接过程中无法解决的联编问题,要等到程序开始运行之后再来确定。例如,本章将要介绍的虚函数就是通过动态联编完成的。函数重载在函数及类的章节中曾做过详细的讨论,所以在本章中,静态多态性主要介绍运算符重载;对于动态多态性,将对虚函数作详细介绍。,8.2运算符重载,C+中预定义的运算符的操作对象只能是基本数据类型。实际上,对于很多用户自定义的类型(如类),也需要有类似的运算操作。例如,下面的程序声明了一个点类point。classpoint/point类声明private:intx,y;,public:/构造函数point(intxx=0,intyy=0)x=xx;y=yy;intget_x();/显示x值intget_y();/显示y值/.;,于是我们可以这样声明点类的对象:pointp1(1,1),p2(3,3)如果我们需要对p1和p2进行加法运算,该如何实现呢?我们当然希望能使用“+”运算符,写出表达式“p1+p2”,但是编译的时候却会出错,因为编译器不知道该如何完成这个加法。这时候,我们就需要自己编写程序来说明“+”在作用于point类对象时,该实现什么样的功能,这就是运算符重载。运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时,导致不同类型的行为。,在运算符重载的实现过程中,首先把指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参,然后,根据实参的类型来确定需要调用的函数。这个过程是在编译过程中完成的。,8.2.1运算符重载的规则运算符是在C+系统内部定义的,它们具有特定的语法规则,如参数说明、运算顺序、优先级别等。因此,运算符重载时必须要遵守一定的规则。C+中的运算符除了少数几个(类属关系运算符“.”、作用域分辨符“:”、成员指针运算符“*”、sizeof运算符和三目运算符“?:”)之外,全部可以重载,而且只能重载C+中已有的运算符,不能臆造新的运算符。,重载之后运算符的优先级和结合性都不能改变,也不能改变运算符的语法结构,即单目运算符只能重载为单目运算符,双目运算符只能重载为双目运算符。运算符重载后的功能应当与原有功能相类似。重载运算符含义必须清楚,不能有二义性。运算符的重载形式有两种:重载为类的成员函数和重载为类的友元函数。,运算符重载为类的成员函数的一般语法形式如下:operator(形参表)函数体;运算符重载为类的友元函数的一般语法形式如下:friendoperator(形参表)函数体;,其中:函数类型指定了重载运算符的返回值类型,也就是运算结果类型。operator是定义运算符重载函数的关键字。运算符是要重载的运算符名称。形参表给出重载运算符所需要的参数和类型。friend是对于运算符重载为友元函数时,在函数类型说明之前使用的关键字。,特别需要注意的是,当运算符重载为类的成员函数时,函数的参数个数比原来的操作数个数要少一个(后置“+”、“-”除外);当重载为类的友元函数时,参数个数与原操作数的个数相同。原因是重载为类的成员函数时,如果某个对象使用重载了的成员函数,自身的数据可以直接访问,就不需要再放在参数表中进行传递,少了的操作数就是该对象本身。,8.2.2运算符重载为成员函数运算符重载实质上就是函数重载,当运算符重载为成员函数之后,它就可以自由地访问本类的数据成员了。实际使用时,总是通过该类的某个对象来访问重载的运算符。如果是双目运算符,一个操作数是对象本身的数据,由this指针指出,另一个操作数则需要通过运算符重载函数的参数表来传递;如果是单目运算符,操作数由对象的this指针给出,就不再需要任何参数。下面分别介绍这两种情况。,1双目运算:oprdlBoprd2对于双目运算符B,如果要重载B为类的成员函数,使之能够实现表达式oprdlBoprd2(其中oprdl为A类的对象),则应当把B重载为A类的成员函数,该函数只有一个形参,形参的类型是oprd2所属的类型。经过重载之后,表达式oprdlBoprd2就相当于函数调用oprdl.operatorB(oprd2)。,2单目运算1)前置单目运算:Uoprd对于前置单目运算符U,如“-”(负号)、“+”等,如果要重载U为类的成员函数,用来实现表达式Uoprd(其中oprd为A类的对象),则U应当重载为A类的成员函数,函数没有形参。经过重载之后,表达式Uoprd相当于函数调用oprd.operatorU()。例如,前置单目运算符“+”重载的语法形式如下:operator+();使用前置单目运算符“+”的语法形式如下:+;,2)后置单目运算:oprdV再来看后置运算符V,如“+”和“-”,如果要将它们重载为类的成员函数,用来实现表达式oprd+或oprd-(其中oprd为A类的对象),那么运算符就应当重载为A类的成员函数,这时函数要带有一个整型(int)形参。重载之后,表达式oprd+和oprd-就相当于函数调用oprd.operator+(0)和oprd.operator-(0)。例如,后置单目运算符“+”重载的语法形式如下:operator+(int);使用后置单目运算符“+”的语法形式如下:+;,【例8-1】双目运算符重载为成员函数例题。本例题重载二维点point加减法运算(关于二维点point类的定义在前面章节中已介绍过),将一个双目运算符重载为成员函数。point的加减法是x和y分别相加减,运算符的两个操作数都是point类的对象,因此,可以把“+”、“-”运算符重载为point类的成员函数,重载函数只有一个形参,类型同样也是point类对象。,#includeclasspointprivate:floatx,y;public:point(floatxx=0,floatyy=0)x=xx;y=yy;floatget_x()returnx;floatget_y()returny;,pointoperator+(pointp1);/重载运算符“+”pointoperator-(pointp1);/和“-”为成员函数;pointpoint:operator+(pointq)returnpoint(x+q.x,y+q.y);pointpoint:operator-(pointq)returnpoint(x-q.x,y-q.y);voidmain(),pointp1(3,3),p2(2,2),p3,p4;/声明point类的对象p3=p1+p2;/两点相加p4=p1-p2;/两点相减coutp1+p2:x=p3.get_x(),y=p3.get_y()endl;coutp1-p2:x=p4.get_x(),y=p4.get_y()endl;,在本例中,将point的加减法运算重载为point类的成员函数。可以看出,除了在函数声明及实现的时候使用了关键字operator之外,运算符重载成员函数与类的普通成员函数没有什么区别。在使用的时候,可以直接通过运算符、操作数的方式来完成函数调用。这时,运算符“+”、“-”原有的功能都不改变,对整型数、浮点数等基本类型数据的运算仍然遵循C+预定义的规则,同时添加了新的针对point运算的功能。“+”这个运算符,作用于不同的对象就会导致完全不同的操作行为,具有了更广泛的多态特征。,本例中重载的“+”、“-”函数中,都是创建一个临时的无名对象作为返回值:returnpoint(x+q.x,y+q.y);这表面上看起来像是对构造函数的调用,但其实并非如此。这是临时对象语法,它的含义是创建一个临时对象并返回它。当然,也可以按如下形式返回函数值:pointpoint:operator+(pointq)pointp;p.x=x+q.x;,p.y=y+q.y;returnp;pointpoint:operator-(pointq)pointp;p.x=x-q.x;p.y=y-q.y;returnp;,这两种方法的执行效率是完全不同的。后者的执行过程是这样的:创建一个局部对象p(这时会调用构造函数),执行return语句时,会调用拷贝构造函数,将p的值拷贝到主调函数中的一个无名临时对象中。当函数operator+结束时,会调用析构函数析构对象p,然后p消亡。两种方法相比,前一种方法的效率高,因为它是直接将一个无名临时对象创建到主调函数中。例8-1的程序运行结果为p1+p2:x=5,y=5p1-p2:x=1,y=1,【例8-2】单目运算符重载为成员函数例题。本程序为时钟计时程序。在程序中将单目运算符重载为类的成员函数,单目运算符前置“+”和后置“+”的操作数是时钟类的对象,可以把这些运算符重载为时钟类的成员函数。对于前置单目运算符,重载函数没有形参;对于后置单目运算符,重载函数有一个整数形参。本例中,我们把自增前置“+”和自减前置“-”运算重载为point类的成员函数。,#includeclasspointprivate:floatx,y;public:point(floatxx=0,floatyy=0)x=xx;y=yy;floatget_x()returnx;floatget_y()returny;,pointoperator+();/重载前置运算符“+”pointoperator-();/重载前置运算符“-”;pointpoint:operator+()if(x0)-y;return*this;voidmain()pointp1(10,10),p2(200,200);/声明point类的对象for(inti=0;i5;i+),coutp1:x=p1.get_x(),y=p1.get_y()endl;+p1;for(i=0;i5;i+)coutp2:x=p2.get_x(),y=p2.get_y()”。,1双目运算:oprdlBoprd2对于双目运算符B,如果oprdl为A类的对象,则应当把B重载为A类的友元函数,该函数有两个形参,其中一个形参的类型是A类。经过重载之后,表达式oprdlBoprd2就相当于函数调用operatorB(oprdl,oprd2)。,2单目运算1)前置单目运算:Uoprd对于前置单目运算符U,如“-”(负号)等,如果要实现表达式Uoprd(其中oprd为A类的对象),则U可以重载为A类的友元函数,函数的形参为A类的对象。经过重载之后,表达式Uoprd相当于函数调用operatorU(oprd)。,2)后置单目运算:oprdV对于后置运算符V,如“+”和“-”,如果要实现表达式oprd+或oprd-(其中oprd为A类的对象),那么运算符就可以重载为A类的友元函数,这时函数的形参有两个,一个是A类的对象oprd,另一个是整型(int)形参。重载之后,表达式oprd+和oprd-就相当于函数调用operator+(oprd,0)和operator-(oprd,0)。,【例8-3】双目运算符重载为友元重载例题。本例题用运算符重载为友元函数的方法重做两点加减法运算。#includeclasspointprivate:floatx,y;public:,point(floatxx=0,floatyy=0)x=xx;y=yy;floatget_x()returnx;floatget_y()returny;friendpointoperator+(pointp1,pointp2);/重载运算符“+”friendpointoperator-(pointp1,pointp2);/和“-”为友元函数;pointoperator+(pointp1,pointp2),returnpoint(p1.x+p2.x,p1.y+p2.y);pointoperator-(pointp1,pointp2)returnpoint(p1.x-p2.x,p1.y-p2.y);voidmain(),pointp1(3,3),p2(2,2),p3,p4;/声明point类的对象p3=p1+p2;/两点相加p4=p1-p2;/两点相减coutp1+p2:x=p3.get_x(),y=p3.get_y()endl;coutp1-p2:x=p4.get_x(),y=p4.get_y()endl;从上述程序可以看到,将运算符重载为类的友元函数时,必须把操作数全部通过形参的方式传递给运算符重载函数。和例8-1相比,本例题的主函数根本没有做任何改动,主要的变化在point类的成员,程序运行的结果完全相同。,8.2.4其它运算符重载前面介绍了一些简单运算符的重载,除此之外,还有以下运算符也常被重载。比较运算符重载(如,=,=,!=)。赋值运算符重载(如=,+=,-=,*=,/=)。下标运算符“”重载。下标运算符“”通常用于取数组中的某个元素,通过下标运算符重载,可以实现数组下标的越界检测等。,运算符new和delete重载。通过重载new和delete,可以克服new和delete的不足,使其按要求完成对内存的管理。逗号运算符“,”重载。逗号运算符是一个双目运算符,和其它运算符一样,我们也可以通过重载逗号运算符来达到期望的结果。逗号运算符构成的表达式为“左操作数,右操作数”,该表达式返回右操作数的值。,8.3虚函数,8.3.1为什么要引入虚函数为什么要引入虚函数,我们来看一个例子。【例8-4】没有使用虚函数的例题。#includeclassbase/定义基类basepublic:,voidwho()coutthisistheclassofbase!endl;classderive1:publicbase/定义派生类derive1public:voidwho()coutthisistheclassofderive1!who()调用derive1类的成员函数who();而当ptr指向obj2对象时,则希望ptr-who()调用derive2类的成员函数who()。此程序执行后实际得到的结果为,thisistheclassofbase!(a)thisistheclassofbase!(b)thisistheclassofbase!(c)thisistheclassofderive1!(d)thisistheclassofderive2!(e),在运行结果中,(a)、(d)和(e)与所预想的相符,而(b)和(c)却不是希望得到的。这说明,不管指针ptr当前指向哪个对象(是基类对象还是派生类对象),ptr-who()调用的都是基类中定义的who()函数。也就是说,通过指针引起的普通成员函数调用,仅仅与指针的类型有关,而与指针正指向什么对象无关。在这种情况下,必须采用显式的方式调用派生类的函数成员。,例如:obj1.who()或obj2.who()或者是采用对指针的强制类型转换的方法,例如:(derive1*)ptr)-who()或(derive2*)ptr)-who()本来使用对象指针是为了表达一种动态的性质,即当指针指向不同对象时执行不同的操作,现在看来并没有起到这种作用。要实现这种功能,就需要引入虚函数的概念。这里,只需将基类的who()函数声明为虚函数即可。,8.3.2虚函数的定义及使用1.虚函数的定义虚函数的定义是在基类中进行的。它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual。当基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义,在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型以及参数的顺序都必须与基类中的原型完全相同。,一般虚函数的定义语法如下:virtual(形参表)函数体其中,被关键字virtual说明的函数为虚函数。特别要注意的是,虚函数的声明只能出现在类声明中的函数原型声明中,而不能出现在成员的函数体实现的时候。,需要注意,动态联编只能通过成员函数来调用或者通过指针、引用来访问虚函数。如果使用对象名的形式访问虚函数,则将采用静态联编方式调用虚函数,而无需在运行过程中进行调用。下面是通过指针访问虚函数的例题。,【例8-5】使用虚函数例题。#includeclassbase/定义基类basepublic:virtualvoidwho()/虚函数声明coutthisistheclassofbase!endl;,classderive1:publicbase/定义基类派生类derive1public:voidwho()/重新定义虚函数coutthisistheclassofderive1!who()语句,但是,当ptr指向不同的对象时,所对应的执行动作就不同。由此可见,用虚函数充分体现了多态性。并且,因为ptr指针指向哪个对象是在执行过程中确定的,所以体现的又是一种动态的多态性。,2.虚函数与重载的关系在一个派生类中重新定义基类的虚函数是函数重载的另一种特殊形式,但它不同于一般的函数重载。一般的函数重载,只要函数名相同即可,函数的返回类型及所带的参数可以不同。但当重载一个虚函数时,也就是说在派生类中重新定义此虚函数时,要求函数名、返回类型、参数个数、参数类型以及参数的顺序都与基类中的原型完全相同,不能有任何的不同。,3多继承中的虚函数在多继承中由于派生类是由多个基类派生而来的,因此,虚函数的使用就不像单继承那样简单。请看下面的例题。【例8-6】多继承中使用虚函数例题。#includeclassbase1/定义基类base1public:virtualvoidwho()/函数who()为虚函数,coutthisistheclassofbase1!endl;classbase2/定义基类base2public:voidwho()/此函数who()为一般的函数coutthisistheclassofbase2!endl;classderive:publicbase1,publicbase2,public:voidwho()coutthisistheclassofderive!endl;main()base1obj1,*ptr1;base2obj2,*ptr2;deriveobj3;,ptr1=,此时,程序执行的结果为thisistheclassofbase1!thisistheclassofbase2!thisistheclassofderive!thisistheclassofbase2!从上面的例子看出,派生类derive中的函数who()在不同的场合呈现不同的性质。如相对base1路径,由于在base1中的who()函数前有关键字virtual,所以它是一个虚函数;若相对于base2派生路径,在base2中的who()函数为一般函数,所以,此时它只是一个重载函数。,当base1类指针指向derive类对象obj3时,函数who()就呈现出虚特性;当base2类指针指向derive类对象obj3时,函数只呈现一般的重载特性。若一个派生类,它的多个基类中有公共的基类,在公共基类中定义一个虚函数,则多重派生以后仍可以重新定义虚函数,也就是说,虚特性是可以传递的。请看下面的例题。,【例8-7】多继承中虚特性的传递例题。#includeclassbase/定义基类basepublic:virtualvoidwho()/定义虚函数coutthisistheclassofbase!endl;classbase1:publicbase/定义派生类base1,public:voidwho()coutthisistheclassofbase1!endl;classbase2:publicbase/定义派生类类base2public:voidwho()coutthisistheclassofbase2!endl;,classderive:publicbase1,publicbase2/定义派生类derivepublic:voidwho()coutthisistheclassofderive!endl;main()base1*ptr1;base2*ptr2;,deriveobj;ptr1=此时,程序执行的结果为thisistheclassofderive!thisistheclassofderive!,从本例题可以看出,虚特性是可以传递的。base类作为base1和base2类的直接基类,它的成员函数who()被声明为虚函数,则base1和base2类中的who()都具有虚特性,即均为虚函数;而derive类为base1和base2类的派生类,因此,它的成员函数who()也为虚函数。,8.3.3虚函数的限制如果我们将所有的成员函数都设置为虚函数,当然是很有益的。它除了会增加一些额外的资源开销,没有什么坏处。但设置虚函数须注意以下几点。只有成员函数才能声明为虚函数。因为虚函数仅适用于有继承关系的类对象,所以普通函数不能声明为虚函数。,虚函数必须是非静态成员函数。这是因为静态成员函数不受限于某个对象。内联函数不能声明为虚函数。因为内联函数不能在运行中动态确定其位置。构造函数不能声明为虚函数。多态是指不同的对象对同一消息有不同的行为特性。虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,因此,虚构造函数是没有意义的。,析构函数可以声明为虚函数。析构函数的功能是在该类对象消亡之前进行一些必要的清理工作。析构函数没有类型,也没有参数,和普通成员函数相比,虚析构函数情况略为简单些。,虚析构函数的声明语法如下:virtual类名例如:classBpublic:/virtualB();,8.4抽象类,8.4.1纯虚函数一个抽象类至少带有一个纯虚函数。纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的实现内容。纯虚函数的声明形式如下:virtual(参数表)=0纯虚函数与一般虚函数在书写形式上的不同在于其后面加了“=0”,表明在基类中不用定义该函数,它的实现部分函数体留给派生类去做。,8.4.2抽象类抽象类的主要作用是通过它为一个类族建立一个公共的接口,使它们能够更有效地发挥多态特性。使用抽象类时需注意以下几点。抽象类只能用作其它类的基类,不能建立抽象类对象。抽象类处于继承层次结构的较上层,一个抽象类自身无法实例化,而只能通过继承机制,生成抽象类的非抽象派生类,然后再实例化。,抽象类不能用作参数类型、函数返回值或显式转换的类型。可以声明一个抽象类的指针和引用。通过指针或引用,我们就可以指向并访问派生类对象,以访问派生类的成员。抽象类派生出新的类之后,如果派生类给出所有纯虚函数的函数实现,这个派生类就可以声明自己的对象,因而不再是抽象类;反之,如果派生类没有给出全部纯虚函数的实现,这时的派生类仍然是一个抽象类。,【例8-8】抽象类例题。我们来看这个例题。在基类Shapes中将成员display()声明为纯虚函数,这样,基类Shapes就是一个抽象类,我们无法声明Shapes类的对象,但是可以声明Shapes类的指针和引用。Shapes类经过公有派生产生了Rectangle类和Circle类。使用抽象类Shapes类型的指针,当它指向某个派生类的对象时,就可以通过它访问该对象的虚成员函数。,#includeconstdoublePI=3.14159;classShapes/抽象基类Shapes声明protected:intx,y;public:voidsetvalue(intxx,intyy=0)x=xx;y=yy;,virtualvoiddisplay()=0;/纯虚函数成员;classRectangle:publicShapes/派生类Rectangle声明public:/虚成员函数voiddisplay()coutTheareaofrectangleis:x*yendl;classCircle:publicShapes/派生类Circle声明,public:/虚成员函数voiddisplay()coutTheareaofcircleis:PI*x*xendl;voidmain()Shapes*ptr2;/声明抽象基类指针Rectanglerect1;Circlecir1;,ptr0=,程序中类Shapes、Rectangle和Circle属于同一个类族,抽象类Shapes通过纯虚函数为整个类族提供了通用的外部接口语义。通过公有派生而来的子类给出了纯虚函数的具体函数体实现,因此是非抽象类。我们可以定义非抽象类的对象,同时根据赋值兼容规则,抽象类Shapes类型的指针也可以指向任何一个派生类的对象,通过基类Shapes的指针可以访问到正在指向的派生类Rectangle和Circle类对象的成员,这样就实现了对同一类族中的对象进行统一的多态处理。,本例的程序运行结果为Theareaofrectangleis:40Theareaofcircleis:314.159另外,程序中派生类的虚成员函数display()并没有用关键字virtual显式说明,因为它们与基类的纯虚函数具有相同的名称及参数和返回值,由系统自动判断确定其为虚成员函数。,
展开阅读全文