零点起飞学C++之灵活的代码——多态.pptx
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 零点 起飞 C++ 灵活 代码 多态
- 资源描述:
-
第,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-fun(base-fun);,base=,base-fun(base-fun);,base=,base-fun(base-fun);,pchild1=,pchild1-fun(pchild1-fun);,分析:,base,和,child1,在定义时既被初始化为指向,CBase,和,CChild1,类型的指针,然后,又分别指向了,CChild1,和,CChild11,型的变量。显然,这种调用方式编译系统在编译时是无法确定到底是调用哪一个类对象的,fun(),函数。因为编译时,只是做了语法的静态检查,单从语句无法判别出调用了哪个函数。但是在运行阶段,可以清楚地看出指针,base,先是指向了,CBase,类对象,然后又指向了,CChild2,。,动态绑定的好处是灵活,适应性强。但是运行速度慢,不容易分析和阅读代码。,16.3.4,纯虚函数,将一个函数定义为虚函数,是为了允许用基类的指针来调用子类的这个函数,并不表示该函数为不被实现的函数。但是在许多情况下,在基类中并不能对虚函数给出有意义的实现,这时就需要将它定义为纯虚函数。纯虚函数在基类中只声明不定义,它的定义在基类的派生类实现。其格式如下所示。,class,virtual()=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,】,每个不同数据类型的加运算函数。,int add_int(int x,int y);,double add_double(double,x,double y);,char,*,add_string(char,*,str1,char,*,str2);,这样对于不同的数据类型必须用不同的形式,不具有直观性,也不符合人的习惯。运算符重载则允许为用户提供一个直观、统一的接口,符合使用的习惯。这使得用户程序所用的语言是面向问题的,而不是面向机器的。,运算符重载实际是一个函数,运算符重载的声明方式与方法的声明方式相同,所以运算符的重载实际上是函数的重载。但是运算符重载需要用关键字,operator,来说明,也具有返回类型和参数列表。其格式如下:,type operator sign(,参数列表,);,其中,,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),。,具体的注意事项为:,对于一元运算符,重载为友元函数时,有且只能有一个参数;而重载为成员函数时,不能有形参,可直接访问数据成员。,对于二元运算符,重载为友元函数时,有且只能有两个参数;而重载为成员函数时,有且只能有一个参数,该参数是右操作数,左操作数直接访问得到。,注意:,Visual C+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,流插入的重载,流插入运算符是“,”,表示将右边的数据送到输出流,cout,中,输出相应的信息。当遇到自定义类型时,必须对该运算符重载,以便可以支持将自定义类型的数据插入输出流的能力。流插入重载的格式为:,ostream&operator,,表示将左边的输入流中的数据送给右边的变量。流提取运算符的格式如下:,istream&operator(istream&,自定义类型,该形式表示重载时,返回值必须是,istream,型,第一个参数也必须是,istream,型,第二个参数是自定义的类型。从,16.5.2,节可知,这种形式的重载只能定义为友元或普通函数,而不能定义为类的成员函数。,【,示例,16-13】,给示例,16-12,中的日期类增加流输入重载。,分析:,该示例对上一个示例作了扩充,加入了输入流的重载操作。重载输入流时,程序从命令行读入一个日期字符串,以“,/,”为分隔符。,16.8,覆 盖,覆盖就是子类对父类的函数和变量重新定义,导致父类的函数或变量被子类屏蔽的机制。覆盖与重载和虚函数有一定区别,本节将对它进行详细讲解。,16.8.1,覆盖函数,虚函数是子类与父类的垂直关系,函数的名称和参数列表完全相同,而且父类中的虚函数必须由,virtual,修饰。重载是同一类中同名方法之间的水平关系,在同一个命名空间内,这些函数只是名字相同,参数类型和个数不尽相同。覆盖则是子类与父类的函数具有相同的名字。无论参数是否相同,也无论是否有,virtual,,只要子类与父类的成员具有相同的名字,父类的成员就会被覆盖。,因此,重载是根据实参类型和个数决定调用哪个函数,虚函数是根据对象类型的不同而调用不同的函数,覆盖则一定是调用本地的成员。,【,示例,16-14】,演示函数的覆盖用法。,分析:,该示例中,定义了两个类,A,和,B,,且,B,继承于,A,。其中,类,B,的函数,fun1(),覆盖了类,A,的同名函数。类,A,的两个同名函数,fun1(),相互重载。类,B,的函数,fun2(),继承了类,A,的虚函数,fun2(),。类,B,的函数,fun3(),给出了调用被覆盖的父函数的一种方法。输出语句中,语,句,b.A:fun1(call from a.B),;则给出了另一种调用覆盖函数的方法。,函数的覆盖遵循下述原则:,如果子类与基类的函数同名,但参数不同,且基类中同名函数不是虚函数,则基类的函数将被子类的同名函数覆盖。,如果子类与基类的函数同名,但参数不同,且基类中同名函数是虚函数,则基类的同名函数将被隐式继承到子类中。这时,子类的同名函数与该虚函数是重载关系。,如果子类与基类的函数同名,且参数相同,但基类中同名函数不是虚函数,则基类的函数将被子类的同名函数覆盖。,如果子类与基类的函数同名,且参数相同,但基类中同名函数是虚函数。此时,子类的同名函数继承父类的虚函数。,16.8.2,覆盖变量,除了,16.8.1,节讲到的函数覆盖外,也可以对变量进行覆盖。覆盖规则是只要子类中的变量与父类中的变量同名,就发生覆盖。,【,示例,16-15】,演示变量覆盖的方法。,分析:,该示例定义了两个类,类,B,继承于类,A,。类,B,的变量,x,和,xx,分别覆盖了同名父类中的变量。可以通过类名限定来直接访问覆盖掉的父类变量。,16.9,小 结,本章主要讲解了,C+,中的多态性。重点是运算符的重载,以便用流运算符输出自定义类型,难点是理解虚函数的内部实现,然后对虚函数加以应用,第,17,章将讲解,C+,中另一个重要的概念,模板。,展开阅读全文

咨信网声明:本文档由用户上传并分享,仅供学习交流、研究之用,未经授权,严禁复制、发行、汇编、翻译或网络传播等,侵权必究。
咨信网小贴示:
1、浏览或下载可查看了解,请点击【咨信网告知】。
2、下载文件中如有侵权或不适当内容,将立即纠正。
3、如果因为网速或其他原因下载失败请重新下载,重复下载[60天内]不扣币。
4、开具发票登录电脑端申请,请点击【索取发票】。
咨信网客服组:【微信客服】
1、浏览或下载可查看了解,请点击【咨信网告知】。
2、下载文件中如有侵权或不适当内容,将立即纠正。
3、如果因为网速或其他原因下载失败请重新下载,重复下载[60天内]不扣币。
4、开具发票登录电脑端申请,请点击【索取发票】。
咨信网客服组:【微信客服】


零点起飞学C++之灵活的代码——多态.pptx
















自信AI助手














微信客服
客服QQ
发送邮件
意见反馈



链接地址:https://www.zixin.com.cn/doc/13996293.html