零点起飞学C++之灵活的代码——多态_第1页
零点起飞学C++之灵活的代码——多态_第2页
零点起飞学C++之灵活的代码——多态_第3页
零点起飞学C++之灵活的代码——多态_第4页
零点起飞学C++之灵活的代码——多态_第5页
已阅读5页,还剩47页未读 继续免费阅读

下载本文档

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

文档简介

第16章灵活的代码多态,多态指同一个实体同时具有多种形式。如果一个语言只支持类而不支持多态,只能说它是基于对象的,而不是面向对象的。学习本章,读者可以掌握面向对象开发中重要的多态,这样就能够用相似的接口实现不同功能。,16.1什么是多态,多态(polymorphism),从字面来理解就是“多种形态,多种形式”。它是一种将不同的特殊行为泛化为单个特殊记号的机制。在面向对象中将多态描述为:向不同的对象发送同一个消息,在接收后,不同的对象产生不同的行为。这里的消息即调用,行为即实现。生活中的多态现象非常普遍。例如,几个重名的人在一起,这就是一种多态的现象。他们虽然名字相同,但却是完全不同的个人。如果有人喊他们的名字,这就相当于系统发出了一个消息,每个人都会对这个消息做出不同的反应。如果认为是呼叫自己,则予以响应;否则就置之不理。这就是说,“名字呼叫”这个消息所处的环境直接决定了由哪个对象来响应,即环境具体决定了相同名字掩盖下的不同实现。多态的性质可以从3个方面来理解。,1名字,多态对象都是同名的,即多个对象共享了单个的特殊记号,具有相同的调用标记。无论使用哪个对象,都使用一致的名字来调用。,2实现,虽然这些对象具有相同的名称,但是却具有特例化的行为,每个对象的实现都完全不一样。如何从相同的名称调用不同的实现,这要依赖对象的参数和对象所处的位置来决定。,3环境,环境是指被调用对象所处的上下文环境。调用多态对象时,传给它的参数的个数、类型,以及该多态对象所属对象的不同,就是多态对象的生存环境。这些因素具体决定了调用的是哪个实现,区分了相同名称掩盖下的不同实现。C+的多态性具体体现在运行和编译两个方面。在程序运行时的多态性通过继承和虚函数来体现,这属于动态多态性。动态多态性直到运行时才能确定要调用哪个对象,这叫后期绑定,其主要优点是灵活,但速度慢。后期绑定使程序员编写的程序可以对程序运行时发生的时间做出响应,而不必编写处理大量偶发事件的代码。在程序编译时的多态性体现在重载上,这是静态的多态性。静态多态性在编译的过程中就知道调用某个函数所需要的全部信息,因此可以在编译时就将对象和函数绑定在一起。这种机制叫早期绑定,其优点是高效,函数调用速度快。,16.2宏的多态,宏就是替换,即在编程时用一个标记替代一个字符串,并在编译时将该标记替换为对应的字符串。它由#define定义,有带参数和不带参数两种。不带参数的宏是一种纯粹的字符串替换,带参数的宏类似于内联函数。带参数的宏在定义时,并没有规定它的参数必须是什么样的类型。这就意味着该宏仅仅是定义了一个处理参数的模板,具体完成执行什么样的动作由参数的类型来决定。,【示例16-1】,自定义断言和加法两个宏,这两个宏可像函数一样使用。分析:该示例定义了两个宏_MY_ASSERT_和_ADD_,前者是一个断言,后者是一个加运算。断言宏的第一次调用中,参数为整数,故执行的是判断整数是否为0的操作。第二次调用中参数为指针,故执行的是判断指针是否为空的操作。加运算的第一次调用中,两个参数都是整数,执行的是整数加运算。第二次调用中,参数是指针和整数1,执行的是指针地址的增1,即指向数组的下一个整数。最后一次调用中,两个参数都是字符串,故进行的是串的连接。,说明:严格来讲宏多态不能算是多态,因为多态性需要在调用对象和被调用对象之间进行绑定,这种绑定分为静态和动态两种。但是宏只是一段代码的替换,没有对象的存在,所以不能进行绑定操作。但是从表面来看,它确实像一个多态的函数。从该示例可以看出,宏多态的实现依赖于参数的类型,参数类型的不同决定了宏完成什么样的功能。这实质是因为宏是在编译时替换的,这种替换是不加任何改动的替换。因此,替换后,相当于在代码中出现宏的地方直接写了一段代码。显然,有什么样的参数,就会有什么样的动作。,16.3虚函数,虚函数就是虚拟函数,它在基类中被声明为虚拟的,并在派生类中被定义,实现其具体操作。虚函数实现了不同派生层次对同一函数的多态实现,本节将详细讲解虚函数的概念。,16.3.1虚函数的作用,声明一个函数是虚拟的,只要在它的声明前加关键字virtual即可。把一个函数声明为虚拟的,将使得对该函数的调用变成后期绑定。直到运行时,才能由发出调用动作的对象来决定要调用的是哪个派生层次的函数。具体做法是首先在基类中定义一个虚函数,即用virtual修饰。当从此基类派生子类时,子类重新定义基类的函数以满足自己的特殊要求。如果通过对象来访问虚函数时,方式和结果与普通函数一样。若用指针来访问才会体现虚函数的强大功能。它允许将子类对象的地址赋值给父类对象的指针,当调用虚函数时,该父对象将根据当前赋给它的子对象的不同而调用不同的虚函数的实现。当不同子类的对象指针赋值给父类指针时,就可通过父类指针调用同一名称的函数,但却实现不同的行为。这样就实现了多态,它允许用同一种方式调用同一类族中不同类的所有同名函数。,虚函数的格式如下:virtual();虚函数一旦定义后,在同一类族的类中,所有与该虚函数具有相同参数(个数与类型)和函数返回值类型的同名函数都将自动成为虚函数,无论是否加virtual说明。说明:类的构造函数本身不能是虚拟函数,并且虚机制在构造函数中不起作用,在构造函数中的虚拟函数只会调用它的本地版本;若类中使用了虚拟函数,析构函数一定要是虚拟函数。,【示例16-2】,演示虚函数的使用方法。分析:该示例定义了一个基类CBase,从该基类派生了两个子类CChild1和CChild2,又从CChild1派生了CChild11类。直接访问类本身时,调用了类本身的fun()函数。当将子类的地址赋给父类的指针时,调用者虽然是父类,但执行的却是子类的函数。从该示例也可看出,如果基类中某函数是虚拟的,则所有子类、子类的子类中对应的函数都将是虚拟的。如果将基类CBase中函数fun()的虚函数属性去掉,则将是本章后面章节中的另一个多态方式。但是,再想通过父类的指针访问子类的函数就不可能了。,16.3.2静态绑定,绑定就是在函数名和类对象间建立关联,即对同名函数的调用做出判断,指出调用的是哪个类对象的哪个函数。绑定分为静态绑定和动态绑定两种。静态绑定也叫早期绑定,在编译时就可确定调用者与被调用者间的关系。编译系统根据调用者本身的类型来进行绑定,而且在编译时就可知道如何进行这种绑定。,【示例16-3】,下述代码节选自示例16-2,主要是为了说明静态绑定的概念。child1.fun(child1.fun);child2.fun(child2.fun);child11.fun(child11.fun);分析:这些语句都是直接通过“.”操作来完成。在编译时就可明确地将函数fun()与调用它的对象关联起来,或者说可以确切地知道每个对象调用了哪一层次的fun对象。因此,它们都是静态绑定的。调用编译时绑定的函数,优点是高效率,因为编译系统可以在运行前对代码进行优化;缺点是缺少灵活性,不能满足程序的可扩充性要求。,16.3.3动态绑定,动态绑定,也叫后期绑定,必须在运行时才能确定调用者与被调用者之间的关系。在运行时,根据对象类型的不同来选择合适的函数调用,这些类型信息在编译时是不可知的,故只能用后绑定解决这一问题。,【示例16-4】,代码节选自示例16-2,主要是为了学习动态绑定的概念。base-fun(base.fun);base=,分析:base和child1在定义时既被初始化为指向CBase和CChild1类型的指针,然后,又分别指向了CChild1和CChild11型的变量。显然,这种调用方式编译系统在编译时是无法确定到底是调用哪一个类对象的fun()函数。因为编译时,只是做了语法的静态检查,单从语句无法判别出调用了哪个函数。但是在运行阶段,可以清楚地看出指针base先是指向了CBase类对象,然后又指向了CChild2。动态绑定的好处是灵活,适应性强。但是运行速度慢,不容易分析和阅读代码。,16.3.4纯虚函数,将一个函数定义为虚函数,是为了允许用基类的指针来调用子类的这个函数,并不表示该函数为不被实现的函数。但是在许多情况下,在基类中并不能对虚函数给出有意义的实现,这时就需要将它定义为纯虚函数。纯虚函数在基类中只声明不定义,它的定义在基类的派生类实现。其格式如下所示。classvirtual()=0;.;,纯虚函数的声明与虚函数很类似,区别就在于末尾的“=0”,这将一个虚函数变成了纯虚函数。定义一个函数为纯虚函数,是为了实现一个接口,起到一个规范的作用,要求继承这个类的程序员必须实现该函数。警告:在虚函数和纯虚函数的定义中不能有static标识符。因为被static修饰的函数在编译时候要求静态绑定,而虚函数和纯虚函数却是动态绑定,而且函数生命周期也不一样。,【示例16-5】,演示纯虚函数的使用方法。分析:该示例中定义了一个基类CBase和它的一个派生类CChild。基类中的函数fun()被声明为纯虚函数,因此示例中不能出现它的定义。但在子类CChild中则给出了该函数的具体实现。如果加上示例中被注释掉的两行代码,在编译时将会报错。这是因为有纯虚函数的类是抽象基类,不允许实例化。其具体的原因将在16.4节中讲解。,16.4抽象类,抽象类就是带有纯虚函数的类。它提供了子类的模板,规定子类必须实现哪些操作。抽象类在面向对象的程序设计中应用很广泛,本节将详细讲解它的概念。,16.4.1什么是抽象类,如果一个类带有纯虚函数,就称该类为抽象类。它是一种特殊的类,是为了抽象和设计的目的而建立的,处于继承层次结构的上层。抽象类的主要作用是刻画一组子类的操作接口的通用语义,只提供子类共同的操作接口,而完整的实现则留给子类。虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只有含有虚函数的类不能被称为抽象类。例如,示例16-5的基类CBase就是抽象类。如果去掉函数fun末尾的“=0”,则该类必须为函数fun提供定义。示例16-2中的基类CBase则不能称为抽象类,因为它不含纯虚函数。,16.4.2抽象类的派生,如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都将导致错误的产生,因为抽象基类是不能被直接调用的。纯虚函数必须被子类定义之后才能被调用。因此,抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它就是一个可以建立对象的具体类了。,【示例16-6】,演示抽象类的派生方法。分析:该类定义了4个子类,其中基类CBase是纯虚函数。子类CChild2中没有对纯虚函数fun()进行定义,因此fun()仍是纯虚函数,该类也是抽象类。子类CChild1和孙子类CChild21中都对fun()进行了定义,它们不再是抽象类。,16.5运算符的多态运算符重载,运算符重载就是对运算符的重新定义,即一个运算符可以用于多种数据类型的运算中。运算符的重载在实际编程中使用很普遍,例如,“+”运算既可以用于整数加,也可以用于浮点数加,甚至是字符串的连接。本节将详细讲解运算符重载的概念和用法。,16.5.1为什么要重载运算符,运算符为程序员提供了表示对象间操作的简略的表达式符号,C+为内部类型提供了丰富的运算符。例如同样是“+”运算,用于整数类型和字符串类型的意义是完全不一样的。这就是对运算符的重载。运算符重载就是赋予已有的运算符多重含义。C+中通过重新定义运算符,使它能够用于特定类的对象执行特定的功能,这增强了C+语言的扩充能力。如果不重载“+”运算符,就必须为每一种数据类型的加运算分别定义函数,,【示例16-7】,每个不同数据类型的加运算函数。intadd_int(intx,inty);doubleadd_double(doublex,doubley);char*add_string(char*str1,char*str2);这样对于不同的数据类型必须用不同的形式,不具有直观性,也不符合人的习惯。运算符重载则允许为用户提供一个直观、统一的接口,符合使用的习惯。这使得用户程序所用的语言是面向问题的,而不是面向机器的。,运算符重载实际是一个函数,运算符重载的声明方式与方法的声明方式相同,所以运算符的重载实际上是函数的重载。但是运算符重载需要用关键字operator来说明,也具有返回类型和参数列表。其格式如下:typeoperatorsign(参数列表);其中,sign是运算符,type是要执行sign运算的数据类型,operator是运算符重载的关键字。operator关键字告诉编译器,它实际上是一个运算符重载,不是简单的函数重载。函数的名字必须是operator后跟运算符构成。编译程序对运算符重载的选择,遵循着函数重载的选择原则。当遇到不很明显的运算时,编译程序将去寻找参数相匹配的运算符函数。,16.5.2重载的限制,不是所有的运算符都能重载,也不可以对运算符进行任意的重载,它需要遵循一定的限制。(1)虽然几乎所有的运算符都可用做重载,但也有一些不允许重载。可重载的运算符具体包括:算术运算符:+、*、/、%、+、;位操作运算符:&、|、;逻辑运算符:!、&、|;比较运算符:、=、=;其他运算符:、()、,(逗号运算符)、new、delete、new、delete、*。但下列运算符不允许重载::、?。,(2)用户重载运算符时,不会改变原运算符的优先级和结合性。(3)运算符重载,不能改变运算符的语法结构。单目运算符只能重载为单目运算符,双目运算符只能重载为双目运算符。(4)不可臆造新的运算符。必须把重载运算符限制在C+语言中已有的运算符范围内的允许重载的运算符之中。运算符重载只能对类类型进行。一般类的数据会被声明为私有的,为了使运算符能访问该私有成员,必须将运算符的函数声明为类的成员函数或友元。由于成员函数带有隐含的自引用参数,而友元没有,所以两种定义格式是不一样的。例如表达式“a+b”,如果用友元定义,则调用方式为operator+(a,b),而如果是成员函数,则是a.operator+(b)。,具体的注意事项为:对于一元运算符,重载为友元函数时,有且只能有一个参数;而重载为成员函数时,不能有形参,可直接访问数据成员。对于二元运算符,重载为友元函数时,有且只能有两个参数;而重载为成员函数时,有且只能有一个参数,该参数是右操作数,左操作数直接访问得到。注意:VisualC+6.0对友元支持不是很好。编译时,有时会提示不能从友元函数内访问类的私有成员。读者可以选择支持标准C+的编译器,如MinGW。,16.5.3重载一元运算符,一元运算符只能重载为一元运算符,双目运算符只能重载为双目运算符。本小节将以实数类为例讲解一元运算符的重载。,【示例16-8】,通过实数类的定义演示运算符的重载方法。分析:该示例定义了一个实数类,并为它定义了自增和负运算。两个运算符都被重载为类的成员。“”运算被解释为r1.operator-(),“+”将被解释为r1.operator+()。,16.5.4重载二元运算符,本节将对示例16-8进行扩充,增加二元运算符。,【示例16-9】,带有二元运算的实数类的定义。分析:该示例为示例16-8增添了加法和乘法运算。其中,加法运算被定义为类的成员,乘法运算被定义为友元函数。因此“r1+r2”将被解释为r1.oprator+(r2),而“r1*r2”被解释为operator*(r1,r2)。,16.6函数重载,函数重载就是函数名称相同,但定义却不同。函数重载使得程序员可以将一系列函数族定义为一个统一的界面,但是却可以处理不同类型的数据或接受不同个数的参数。这实现了同一接口,不同定义的思想。本节将详细讲解函数重载的思想。,16.6.1参数类型不同,函数在定义后,系统区分不同函数的依据是函数的签名,这个签名包括函数名称、参数类型、参数个数3个要素。为了保证类型安全链接,编译器利用每个函数的参数个数和类型对其标志符实际编码。类型安全可以保证调用到恰当的重载函数,以及形参和实参之间的对应。这就产生了因参数类型和参数个数不同而形成的两种重载方法。参数类型不同的重载函数具有相同的名称,但却具有不同的参数类型。,【示例16-10】,演示参数类型不同的重载函数。分析:本示例定义了add()函数,并对其进行了3次重载,分别接受int、double、string这3种参数。因此,编译时将根据参数类型的不同对add()函数的调用进行解析。所以,add(i1,i2)将被编译为add(int,int),add(d1,d2)被编译为add(double,double),add(s1,s2)被编译为add(string,string)。,16.6.2参数个数不同,参数个数不同的重载函数具有相同的名称,但却具有不同的参数个数。,【示例16-11】,演示参数个数不同的重载函数。分析:该示例定义了add()函数的4个重载形式。其中第1个和第2个相比是参数个数不同的重载,第3个和第4个相比也是参数不同的重载。但是,第1个和第4个相比就是参数个数和类型均不同的重载,第2个和第3个相比也是参数个数和类型均不相同的重载。,16.7流的重载,C+的流提取运算符和流插入运算符和进行了重载,使得能它用来输入/输出标准类型的数据。但如果是自定义类型,就需要重载这两个运算符,以便它们能输入/输出该自定义类型的数据。,16.7.1流插入的重载,流插入运算符是“,表示将左边的输入流中的数据送给右边的变量。流提取运算符的格式如下:istream该形式表示重载时,返回值必须是istream型,第一个参数也必须是istream型,第二个参数是自定义的类型。从16.5.2节可知,这种形式的重载只能定义为友元或普通函数,而不能定义为类的成员函数。,【示例16-13】,给示例16-12中的日期类增加流输入重载。分析:该示例对上一个示例作了扩充,加入了输入流的重载操作。重载输入流时,程序从命令行读入一个日期字符串,以“/”为分隔符。,16.8覆盖,覆盖就是子类对父类的函数和变量重新定义,导致父类的函数或变量被子类屏蔽的机制。覆盖与重载和虚函数有一定区别,本节将对它进行详细讲解。,16.8.1覆盖函数,虚函数是子类与父类的垂直关系,函数的名称和参数列表完全相同,而且父类中的虚函数必须由virtual修饰。重载是同一类中同名方法之间的水平关系

温馨提示

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

评论

0/150

提交评论