《《多态和虚函数》PPT课件.ppt》由会员分享,可在线阅读,更多相关《《多态和虚函数》PPT课件.ppt(69页珍藏版)》请在三一办公上搜索。
1、北京亚嵌教育-中国嵌入式技术的黄埔军校,多态和虚函数,北京亚嵌教育研究中心2010 AKAE,多态性是面向对象程序设计的重要特征之一,多态性机制:增加了面向对象软件系统的灵活性 减少了冗余信息 提高了软件的可重用性和可扩充性,多态性是面向对象的一个重要特征。什么是多态性?以下为结构化编程中的一个例子:void fuite_eat(int objFruit)switch(objFruit)case 0:/apple apple_eat();/printf(apple eat);break;case 1:/orange orange_eat();/printf(orange eat);break;
2、case 2:/peal peal_eat();/printf(peal eat);break;,上面的例子中以水果为例,不同的水果的吃法可能各不相同,这样要根据不同的水果类型来判断调用特定的水果的吃的方法。在程序编译时系统预先绑定了各种水果吃的方法,这就是“前期绑定”。但当增加一种水果时,不得不更改上述程序,新增加一个入口,并调用新水果的吃法。,面向对象技术中的多态性运用“后期绑定”技术解决此问题。,可以在基类中定义一个虚函数,然后在派生类中覆盖它,当调用此方法时,系统会根据对象的类型而决定调用哪一个对象的方法(即:不同对象收到相同消息时,产生不同的动作)。,如:class fruit vi
3、rtual void eat()=0;class apple:public fruit void eat()printf(apple eat);class orange:public fruit void eat()printf(orange eat);void fruite_eat(fruit*f)f-eat();,编译器发现一个类中有被声明为virtual的函数,就会为它建一个虚函数表,即 VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是,派生类的VTABLE与基类的
4、VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。,上例中:系统在得到对象的指针后会查找VTABLE,找到方法的函数指针,然后调用该方法。如果此方法未被派生类实现,则系统会调用其基类的方法。函数地址的后期绑定在面向对象的编程语言中是非常重要的,在java中对类中所有函数都进行了后期绑定,但C+,则是让程序开发者决定对哪一个函数进行后期绑定。为多态性行为提供后期绑定是要付出代价的,因为在类的实现时VTABLE要进行初始化且在调用虚函数时必须在运行时查找虚函数表。,1 多
5、态性概述,多态性含义:指不同对象收到相同的消息时,产生不同的动作。体现在程序中为:多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,从而可以使用相同的调用方式来调用这些具有不同功能的同名函数。,1.1 多态的分类,C+中的多态性可以分为四类:参数多态:由类模板实例化各个类都有的具有相同的操作,而操作对象的类型却各不相同包含多态:主要通过虚函数来实现。强调不同类中的同名成员函数的多态行为强制多态:即,将一个变元的类型实例化,比如加法运算时候浮点数与整数的强制转换,即运算符重新定义(重载)。重载多态前面两种统称为通用多态,而后面两种统称为专用多态。,1.2 多态的实现,多态从实现
6、的角度分为两类:编译时的多态:是通过静态联编来实现的。静态联编就是在编译阶段完成的联编。编译时多态性主要是通过函数重载和运算符重载实现的。运行时的多态 是用动态联编实现的。动态联编是运行阶段完成的联编。运行时多态性主要是通过虚函数来实现的。,2 运算符重载,什么是运算符重载?重载,即重新赋予新的含义。函数重载就是对一个已有的函数赋予新的含义,使之实现新功能。运算符也可以重载,对C+已提供的运算符进行重载,赋予它们新的含义,使之一名多用。譬如,用“+”号如何进行两个复数的相加?C+中不能直接用运算符“+”对复数进行相加运算。可以通过定义一个专门的函数来实现复数相加。,例1 通过函数来实现复数相加
7、。#include using namespace std;class Complex/定义Complex类public:Complex()real=0;imag=0;/定义构造函数Complex(double r,double i)/构造函数重载real=r;imag=i;Complex complex_add(Complex,Complex Complexcomplex_add(Complex,int main()Complex c1(3,4),c2(5,-10),c3;/定义3个复数对象 c3=plex_add(c2);/调用复数相加函数 coutc1=;c1.display();/输出
8、c1的值 coutc2=;c2.display();/输出c2的值 coutc1+c2=;c3.display();/输出c3的值 return 0;运行结果如下:c1=(3+4i)c2=(5-10i)c1+c2=(8,-6i),结果是正确的,但调用方式不直观、太烦琐,不方便。能否也和整数的加法运算一样,直接用加号“+”来实现复数运算呢?如c3=c1+c2;如果能做到,就为对象的运算提供了很大的方便。这就需要对运算符“+”进行重载。,2.1 运算符重载的方法,运算符重载是通过定义函数实现的:定义一个重载运算符的函数,在需要执行被重载的运算符时,系统就自动调用该函数,以实现相应的运算。重载运算符
9、的函数一般格式如下:函数类型 operator 运算符名称(形参表列)对运算符的重载处理 例如,想将“+”用于Complex类(复数)的加法运算,函数的原型:Complex operator+(Complex 函数operator+重载了运算符+。,用函数的方法理解运算符:在运算符重载后,执行表达式就是调用函数的过程。可以把两个整数相加也想像为调用函数:int operator+(int a,int b)return(a+b);如果有表达式5+8,就调用此函数,将5和8作为调用函数时的实参,函数的返回值为13。,例2 改写例1,重载运算符“+”,使之能用于两个复数相加。class Comple
10、xpublic:Complex()real=0;imag=0;Complex(double r,double i)real=r;imag=i;Complex operator+(Complex,void Complexdisplay()cout(real,imagi)endl;int main()Complex c1(3,4),c2(5,-10),c3;c3=c1+c2;/运算符+用于复数运算 coutc1=;c1.display();coutc2=;c2.display();coutc1+c2=;c3.display();return 0;运行结果与例7.1相同:c1=(3+4i)c2=(5
11、-10i)c1+c2=(8,-6i),比较例1和例2,只有两处不同:(1)在例2中以operator+函数取代了例1中的complex_add函数,而且只是函数名不同,函数体和函数返回值的类型都是相同的。(2)在main函数中,以“c3=c1+c2;”取代了例1中的“c3=plex_add(c2);”。在将运算符+重载为类的成员函数后,C+编译系统将程序中的表达式c1+c2解释为:c1.operator+(c2)/其中c1和c2是Complex类的对象即以c2为实参调用c1的运算符重载函数operator+(Complex&c2),进行求值,得到两个复数之和。,对上面的运算符重载函数opera
12、tor+还可以改写得更简练一些:Complex Complexoperator+(Complex 需要说明的是:运算符被重载后,其原有的功能仍然保留,没有丧失或改变。通过运算符重载,扩大了C+已有运算符的作用范围,使之能用于类对象。运算符重载使C+具有更强大的功能、更好的可扩充性和适应性,这是C+最吸引人的特点之一。,2.2 重载运算符的规则,(1)C+不允许用户自己定义新的运算符,只能对已有的C+运算符进行重载。(2)C+中绝大部分的运算符允许重载。不能重载的运算符只有5个:.(成员访问运算符).*(成员指针访问运算符)(域运算符)sizeof(长度运算符)?:(条件运算符),前两个运算符不
13、能重载是为了保证访问成员的功能不能被改变,域运算符和sizeof运算符的运算对象是类型而不是变量或一般表达式,不具重载的特征。,(3)重载不能改变运算符运算对象(即操作数)的个数。(4)重载不能改变运算符的优先级别。(5)重载不能改变运算符的结合性。(6)重载运算符的函数不能有默认的参数,否则就改变了运算符参数的个数,与前面第(3)点矛盾。(7)重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应有一个是类对象(或类对象的引用)。也就是说,参数不能全部是C+的标准类型,以防止用户修改用于标准类型数据的运算符的性质。,(8)用于类对象的运算符一般必须重载,但有两个例外,运算符“=”
14、和“&”不必用户重载。赋值运算符(=)可以用于每一个类对象,可以利用它在同类对象之间相互赋值。地址运算符&也不必重载,它能返回类对象在内存中的起始地址。(9)应当使重载运算符的功能类似于该运算符作用于标准类型数据时所实现的功能。(10)运算符重载函数可以是类的成员函数(如例7.2),也可以是类的友元函数,还可以是既非类的成员函数也不是友元函数的普通函数。,2.3 运算符重载函数作为类成员函数和友元函数,例7.2对“+”进行了重载,使之能用于两个复数的相加。其中运算符重载函数operator+作为Complex类中的成员函数。“+”是双目运算符,为什么在例7.2程序中的重载函数中只有一个参数呢?
15、实际上,运算符重载函数有两个参数,由于重载函数是Complex类中的成员函数,有一个参数是隐含的,运算符函数是用this指针隐式地访问类对象的成员。,重载函数operator+访问了两个对象中的成员,一个是this指针指向的对象中的成员,一个是形参对象中的成员。如this-real+c2.real,this-real就是c1.real。前面已说明,在将运算符函数重载为成员函数后,如果出现含该运算符的表达式,如c1+c2,编译系统把它解释为:c1.operator+(c2)运算符重载函数的返回值是Complex类型,返回值是复数c1和c2之和(Complex(c1.real+c2.real,c1
16、.imag+c2.imag)。,运算符重载函数除了可以作为类的成员函数外,还可以是非成员函数。例3 将运算符“+”重载为适用于复数加法,重载函数不作为成员函数,而放在类外,作为Complex类的友元函数。class Complexpublic:Complex()real=0;imag=0;Complex(double r,double i)real=r;imag=i;friend Complex operator+(Complex,Complex operator+(Complex,将运算符“+”重载为非成员函数后,C+编译系统将程序中的表达式c1+c2解释为:operator+(c1,c2)
17、即执行c1+c2相当于调用以下函数:Complex operator+(Complex 为什么把运算符函数作为友元函数呢?因为运算符函数要访问Complex类对象中的成员。如果运算符函数不是Complex类的友元函数,而是一个普通的函数,它是没有权利访问Complex类的私有成员的。,2.4 重载双目运算符,双目运算符有两个操作数,通常在运算符的左右两侧,如3+5,a=b,i10等。重载双目运算符时,函数中应该有两个参数。,例4 定义一个字符串类String,用来存放不定长的字符串,重载运算符“=”,“”,用于两个字符串的等于、小于和大于的比较运算。分几步来介绍编程过程:(1)先建立一个Str
18、ing类:#include using namespace std;class Stringpublic:String()p=NULL;/默认构造函数String(char*str);/构造函数void display();private:char*p;/字符型指针,用于指向字符串;,StringString(char*str)/定义构造函数 p=str;/使p指向实参字符串void Stringdisplay()/输出p所指向的字符串 coutp;int main()String string1(Hello),string2(Book);string1.display();coutendl;
19、string2.display();return 0;运行结果为HelloBook,(2)有了这个基础后,再增加其他必要的内容。现在增加对运算符重载的部分。为便于编写和调试,先重载一个运算符“”。程序如下:#include#include using namespace std;class String public:String()p=NULL;String(char*str);friend bool operator(String,void Stringdisplay()/输出p所指向的字符串 cout(String 程序运行结果为1。这只是一个并不很完善的程序,但是,已经完成了实质性的工
20、作了,运算符重载成功了。其他两个运算符的重载如法炮制即可。,(3)扩展到对3个运算符重载。在String类体中声明3个成员函数:friend bool operator(String,bool operator(String,再修改主函数:int main()String string1(Hello),string2(Book),string3(Computer);coutstring2)endl;/比较结果应该为truecout(string1string3)endl;/比较结果应该为false cout(string1=string2)endl;/比较结果应该为false return 0
21、;运行结果为:100,(4)再进一步修饰完善,使输出结果更直观。#include using namespace std;class String public:String()p=NULL;String(char*str);friend bool operator(String,void display();private:char*p;StringString(char*str)p=str;void Stringdisplay()/输出p所指向的字符串cout(String,bool operator(string1,string2)=1)string1.display();cout;st
22、ring2.display();else if(operator(string1,string2)=1),string1.display();coutBookBookComputerHello=Hello,在C+中,运算符重载是很重要的。有了运算符重载,在声明了类之后,人们就可以像使用标准类型一样来使用自己声明的类。类的声明往往是一劳永逸的,有了好的类,用户在程序中就不必定义许多成员函数去完成某些运算和输入输出的功能,使主函数更加简单易读。好的运算符重载能体现面向对象程序设计思想。,应当注意到,在运算符重载中使用引用(reference)的重要性。利用引用作为函数的形参可以在调用函数的过程中不
23、是用传递值的方式进行虚实结合,而是通过传址方式使形参成为实参的别名,因此不生成临时变量(实参的副本),减少了时间和空间的开销。此外,如果重载函数的返回值是对象的引用时,返回的不是常量,而是引用所代表的对象,它可以出现在赋值号的左侧而成为左值(left value),可以被赋值或参与其他操作(如保留cout流的当前值以便能连续使用“”输出)。但使用引用时要特别小心,因为修改了引用就等于修改了它所代表的对象。,3 虚函数,虚函数提供了一种更为灵活的多态性机制。虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是在运行时才决定如何动作,即所谓的动态联编。3.1 虚函数的引入,例9 虚函数的引例
24、1。#includeclass Apublic:void show()coutshow();pc=,?运行结果:AA预想结果:AB问题:指向基类对象的指针可以指向它的公有派生类对象。但这个对象指针调用同名但不同级的成员函数时会有问题。,#includeclass base int a,b;public:base(int x,int y)a=x;b=y;void show()coutbase-n;couta bendl;,class derived:public baseprivate:int c;public:derived(int x,int y,int z):base(x,y)c=z;vo
25、id show()coutshow();pc=,运行结果:base-60 60base-10 20,例10 虚函数的引例2,错误的原因:C+的静态联编机制。静态联编首先将指向基类对象的Pc与基类的成员函数show()连接在一起,以后不管pc再指向哪个对象,pc-show调用的总是基类的成员函数show().,void main()base mb(60,60),*pc;derived mc(10,20,30);pc=,void main()/pc=,但这两方法都没有实现一种动态的绑定,即当指针指向不同的对象时执行不同的操作。解决方法:虚函数,3.2 虚函数的作用和定义,1.虚函数的作用虚函数同派
26、生类的结合可使C+支持运行时的多态性,实现了在基类定义派生类所拥有的通用接口,而在派生类定义具体的实现方法,即常说的“同一接口,多种方法”,它帮助程序员处理越来越复杂的程序。,例11 虚函数的作用。#includeclass Base public:Base(int x,int y)a=x;b=y;virtual void show()/定义虚函数show()coutBase-n;couta bendl;private:int a,b;class Derived:public Basepublic:Derived(int x,int y,int z):Base(x,y)c=z;void sho
27、w()/重新定义虚函数show()cout Derived-n;coutcendl;private:int c;,void main()Base mb(60,60),*pc;Derived mc(10,20,30);pc=/调用派生类Derived的show()版本 程序运行结果如下:Base-60 60Derived-30,结果正确关键字virtual指示c+编译器,函数调用pc-show()在运行时确定调用的函数,即对调用进行动态联编程序在运行时根据指针pc所指向的实际对象调用该对象的成员函数,2.虚函数的定义 定义虚函数的方法如下:virtual 函数类型 函数名(形参表)/函数体 基类
28、中声明为虚函数的成员函数在派生类中被派生类重定义,并要求函数原型(包括返回类型、函数名、参数个数、参数类型的顺序)都必须与基类中的原型完全相同。,对虚函数的定义的几点说明:通过定义虚函数使用C+提供的多态性机制时,派生类应从其基类公有派生必须首先在基类中定义为虚函数C+规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此,派生类中的virtual可写可不写,为避免混乱,最好在派生类的虚函数进行重新定义时加上关键字virtual。一般是先定义基类指针,然后通过基类指针指向派生类,访问虚函数获取运行时多态性。,一个虚函数无论被公有继承多少次,仍保持其虚函数特性虚函数必须
29、是类的成员函数,不能是友员函数,也不能是静态成员函数。内联函数不能是虚函数,因为内联函数是在编译地确定位置。虚函数虽然定义在类内部,但编译时仍将其视为非内联。构造函数不能是虚函数,因为虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,因此虚构造函数无意义。析构函数可以是虚函数。,3.3虚析构函数,析构函数前面加上关键字virtual进行说明,称该析构函数为虚析构函数。例如:class B virtual B();/虚析构函数;基类的析构函数被说明为虚析构函数,则其派生类中的析构函数也是虚析构函数。说明虚析构函数的目的在于在使用delete运算符删除一个对象时,
30、能保析构函数被正确地执行。因为设置虚析构函数后,可以采用动态联编方式选择析构函数。,#include class A public:virtual A()coutA:A()Called.n;class B:public A public:B(int i)buf=new chari;virtual B()delete buf;coutB:B()Called.n;private:char*buf;void main()A*a=new B(15);delete a;,运行结果:B:B()Called.A:A()Called.,3.4 虚函数与重载函数的关系,在派生类中重新定义基类的虚函数是函数重载的
31、另一种形式,但它不同于一般的函数重载。,Class basepublic:virtual void f1();virtual void f2();virtual void f3();void f4();,Class derived:public basepublic:virtual void f1();/注1void f2(int x);/注2Char f3();/注3 void f4();/注4,注1:虚函数,virutal可省去,注2:派生类虚函数的参数与基类虚函数参数不同,不属于同一组虚函数,是隐藏关系。,注3:一组虚函数中,两个虚函数仅返回值不同,参数和名字相同,编译错。,注4:普通函
32、数重载,不是虚函数,4 纯虚函数和抽象类,4.1 纯虚函数纯虚函数(pure virtual function)是一个在基类中说明的虚函数,它在该基类中没有定义,但要求在它的派生类中必须定义自己的版本,或重新说明为纯虚函数。纯虚函数的定义形式如下:virtual 函数类型 函数名(参数表)=0;,注意:纯虚函数没有函数体;最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”;这是一个声明语句,最后应有分号。,纯虚函数只有函数的名字而不具备函数的功能,不能被调用。只是通知编译系统:“在这里声明一个虚函数,留待派生类中定义”。在派生类中对此函数提供定义后,它才能
33、具备函数的功能。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。,例12 纯虚函数的使用。#includeclass Circle public:void setr(int x)r=x;virtual void show()=0;/纯虚函数protected:int r;class Area:public Circlepublic:void show()coutArea is 3.14*r*rendl;/重定义虚函数show()class Perimeter:
34、public Circlepublic:void show()coutPerimeter is 2*3.14*rendl;/重定义虚函数show(),void main()Circle*ptr;Area ob1;Perimeter ob2;ob1.setr(10);ob2.setr(10);ptr=,4.2 抽象类,一般地,类用于定义对象。而有一些类不用来生成对象。定义这些类的惟一目的是用它作为基类去建立派生类。它们作为一种基本类型提供给用户,用户在这个基础上根据自己的需要定义出功能各异的派生类。用这些派生类去建立对象。这种不用来定义对象而只作为一种基本类型用作继承的类,称为抽象类(abstr
35、act class),由于它常用作基类,通常称为抽象基类(abstract base class)。,凡是包含纯虚函数的类都是抽象类(至少有一个纯虚函数)。抽象类只能作为其他类的基类来使用,不能建立抽象类对象。因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的,其纯虚函数的实现由派生类给出。,已知String类定义如下:class Stringpublic:String(const char*str=NULL);/通用构造函数String(const String 尝试写出类的成员函数实现。,String:String(const char*str)if(str=NULL)/strlen在参数为NULL时会抛异常才会有这步判断m_data=new char1;m_data0=0;elsem_data=new charstrlen(str)+1;strcpy(m_data,str);,String:String(const String,String,String:String()delete m_data;,