1、1,第3章多态性,本章学习重点掌握内容: 多态的概念和作用,多态的实现方法 常见运算符的重载 静态联编和动态联编 虚函数、纯虚函数和抽象基类的概念和用法 虚析构函数的概念和作用,虚析构函数的用法,2,3.1多态性的概念,多态性(Polymorphism)是面向对象程序设计的重要特性之一。多态性是指当不同的对象收到相同的消息时,产生不同的动作。利用多态性可以设计和实现一个易于扩展的系统。 多态性主要体现在:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为。也就是说,每个对象可以用自己的方式去响应共同的消息。C+支持两种形式的多态性,一种是编译时的多态性,称为静态联编。一种是运行时多
2、态,称为动态联编。,3,3.2.1 运算符重载,3.2.1 运算符重载概述 在以前的学习中,C+中预定义的运算符的操作对象只能是基本数据类型如int或float等。实际上,对于很多用户自定义的类型(如类),也需要有类似的运算操作。 例如复数类Complex。 class Complex public:Complex () real=image=0; Complex (double r, double i),4,3.2.1 运算符重载概述, real = r, image = i; void Print(); private:double real, image; ; void Complex:
3、Print() if(image0) coutrealimagei;else coutreal+imagei; ,5,3.2.1 运算符重载概述,声明复数类的对象:complex c1(2.0, 3.0), c2(4.0, -2.0), c3。如果我们需要对c1和c2进行加法运算, “c3=c1+c2”,编译时却会出错,这是因为编译器不知道该如何完成这个加法。这时我们就需要编写程序来实现“+”运算符来作用于complex类的对象,这就是运算符的重载。运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时,导致不同类型的行为。,6,3.2.1 运算符重载概述,C+中运算符的
4、重载虽然给我们设计程序带来很多的方便,但对运算符的重载时,以下的几种情况需要注意: (1)一般来说,不改变运算符原有含义,只让它能针对新类型数据的实际需要,对原有运算符进行适当的改造。例如,重载“+”运算符后,它的功能还是进行加法运算。 (2)重载运算符时,不能改变运算符原有的优先级别,也不能改变运算符需要的操作数的数目。重载之后运算符的优先级和结合性都不会改变。,7,3.2.1 运算符重载概述,(3)不能创建新的运算符,只能重载c+中已有的运算符。 (4)有些运算符不能进行重载。如:“.”类成员运算符、“*”类指向运算符、“:”类作用域运算符、“?:”条件运算符及“sizeof”求字节数运算
5、符。,8,3.2.2 运算符重载的实现,运算符重载的本质就是函数重载。在实现过程中,首先把指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参,然后根据实参的类型来确定需要调用的函数,这个过程是在编译过程中完成的。运算符重载形式有两种:重载为类的成员函数和重载为类的友元函数。1. 运算符重载为类的成员函数语法形式如下:函数类型 operator 运算符(形参表) 函数体;,9,3.2.2 运算符重载的实现,2. 运算符重载为类的友元函数运算符重载还可以为友元函数。当重载友元函数时,将没有隐含的参数this指针。语法形式如下:friend 函数类型 operator 运算符(
6、形参表); 运算符重载为友元函数时,要在函数类型说明之前使用friend关键词来说明。,10,3.2.3 双目运算符重载,双目运算符就是运算符作用于两个操作数。下面通过一个例子对“+”运算符重载来学习一下双目运算符重载的应用。 【例3.1】定义一个复数类,重载“+”运算符为复数类的成员函数,使这个运算符能直接完成两个复数的加法运算,以及一个复数与一个实数的加法运算。,11,#include class Complex public:Complex () real=image=0; Complex (double r, double i) real = r, image = i; void Pr
7、int();Complex operator + (Complex ,C/C+程序设计教程-面向对象分册,void Complex:Print() if(image0) coutrealimageiendl;else coutreal+imageiendl; Complex Complex:operator + (Complex ,13,Complex Complex:operator + (float s) Complex t;t.real=real+s;t.image=image;return t;,14,void main(void) Complex c1(25,50),c2(100,2
8、00),c3;coutc1=; c1.Print();coutc2=; c2.Print();c3=c1+c2; coutc3=c1+c2=;c3.Print();c1 = c1 + 200; coutc1=;c1.Print(); ,15,【例3.2】重载“+”运算符为复数类的友元函数,使这个运算符能直接完成两个复数的加法运算,以及一个复数与一个实数的加法运算。 class Complex public:Complex (double r, double i);friend Complex operator +(const Complex ,16,Complex operator +(con
9、st Complex ,17,void main() Complex c1(2.0, 3.0), c2(4.0, -2.0), c3(0,0); c3 = c1 + c2;cout“c1+c2=“;Print(c3);c3 = c1 + 5;cout“c1+5=“;Print(c3); ,18,【例3.3】日期类date中采用友元形式重载“+”运算符,实现日期加上一个天数,得到新日期。 static int mon_day = 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ; class CDate public: CDate (int m=0,
10、int d=0,int y=0);void Display() ;friend CDate operator + (int d, CDate dt); /友元形式重载“+“运算符 private:int month, day, year; ;,19,CDate operator + (int d, CDate dt) dt.day = dt.day + d; while(dt.day mon_daydt.month-1)/ 少一个闰年判断dt.day = dt.day- mon_daydt.month-1; dt.month+;if(dt.month= =13) dt.month=1; dt.
11、year+; return dt;,20,3.2.4 赋值运算符重载,在C+中有两种类型的赋值运算符:一类是“+=”和“-=”等先计算后赋值的运算符,另一类是“=”即直接赋值的运算符。下面分别进行讨论。 1运算符“+=”和“- =”的重载 【例3.4】 实现复数类“+=”和“-=”的重载。 #include class Complex public:,21,Complex(double r,double i) real=r;image=i; Complex operator -=(Complex ,22,Complex:Print() if(image0) coutrealimageiendl
12、;else coutreal+imageiendl; void main() Complex c1(5.0,3.0),c2(2.1,1.8),c3(5.3,4.2);c1-=c2; coutc1=; c1.Print();c3+=c2; coutc3=;c3.Print(); ,23,3.2.4 赋值运算符重载,2运算符“=”的重载 【例3.5】 实现“=”运算符重载的示例。 #include #include class CMessage public:CMessage( ) buffer = NULL; CMessage() delete buffer; ,24,void Display(
13、 ) coutbufferendl; void Set(char *string) if(buffer != NULL) delete buffer;buffer= new charstrlen(string)+1;strcpy(buffer, string);void operator=(const CMessage ,25,private:char *buffer; ;void main( )CMessage c1;c1.Set(“initial c1 message“);c1.Display( );CMessage c2;c2.Set(“initial c2 message“);c2.D
14、isplay( );c1=c2;c1.Display( ); ,26,3.2.5 单目运算符重载,类的单目运算符可重载为一个没有参数的非静态成员函数或者带有一个参数的非成员函数,参数必须是用户自定义类型的对象或者是对该对象的引用。在C+中,单目运算符有+和-,它们是变量自动增1和自动减1的运算符。在类中可以对这两个单目运算符进行重载。,27,3.2.5 单目运算符重载,如同“+”运算符有前缀、后缀两种使用形式,“+”和“-”重载运算符也有前缀和后缀两种运算符重载形式,以“+”重载运算符为例,其语法格式如下:函数类型 operator +(); /前缀运算函数类型 operator +(int)
15、;/后缀运算使用前缀运算符的语法格式如下:+对象;使用后缀运算符的语法格式如下:对象+;,28,【例3.6】重载单目运算符“+”。#include class Counter public:Counter()v=0;Counter operator +(); /前置单目运算符Counter operator +(int); /后置单目运算符void Display() coutvendl; private:int v; ;,29,Counter Counter:operator +() /前置单目运算 +v;return *this; Counter Counter:operator +(in
16、t) /后置单目运算 Counter t;t.v=v+1;return t; ,30,void main() Counter c1, c2;int i;for(i=0;i4;i+) /后置单目运算符c1+;coutc1=;c1.Display();for(i=0;i4;i+) /前置单目运算符+c2;coutc2=;c2.Display(); ,31,3.2.6 下标运算符重载,下标运算符“ ”通常用于在数组中标识数组元素的位置,下标运算符重载可以实现数组数据的赋值和取值。下标运算符重载函数只能作为类的成员函数,不能作为类的友元函数。下标运算符“ ”函数重载的一般形式为:函数类型 operat
17、or (形参表);其中形参表为该重载函数的参数列表。重载下标运算符只能且必须带一个参数,该参数给出下标的值。,32,【例3.7】定义一个字符数组类,其中对下标运算符“ ”进行重载。 #include #include class MyCharArray public:MyCharArray(int m) len=m; str=new charlen; MyCharArray(char *s)str=new charstrlen(s)+1;strcpy(str,s); len=strlen(s);,33,MyCharArray() delete str; char ,34,void Disp()
18、 coutstrendl; private:int len;char *str; ; void main() MyCharArray word(“This is a C+ program.“);word.Disp();cout“位置0:“; coutword0endl;,35,cout“位置15:“; coutword15endl;cout“位置25:“; coutword25endl;word0=t; word.Disp();int f=10; MyCharArray word2(f); for(int i=0;i10;i+) word2i=wordi; word2.Disp(); ,36,
19、3.2.7 关系运算符重载,关系运算符也可以被重载,例如定义一个日期类date,重载运算符“= =”和“ class Date public: Date(int m,int d,int y) month=m; day=d; year=y; ,37,void Display() cout t2返回0,否则返回1 if(t1.yeart2.year) return 1;else if(t1.montht2.month ,38,friend int operator =(Date,39,void main() Date date1(11,25,90), date2(11,22,90);if(date
20、1date2) date1.Display(); cout “is less than “; date2.Display(); coutendl;else if(date1=date2) date1.Display();,40,coutdate2) date1.Display(); cout“ is more than “; date2.Display();coutendl; ,41,3.2.8 类型转换运算符重载,类型转换运算符重载函数的格式如下:operator 类型名() 函数体 与前面重载运算符函数不同,类型转换运算符重载函数没有返回类型,因为类型名就代表了返回类型,也没有任何参数。在
21、调用过程中要带一个对象实参。实际上,类型转换运算符将对象转换成类型名规定的类型。转换时的形式就像强制转换。如果没有转换运算符定义,直接用强制转换是不行的,因为强制转换只能对标准数据类型进行操作,对类类型的操作是没有定义的。,42,【例3.9】实现人民币Money与double的转换。 #include class Money /人民币类 public:Money(double value=0.0)yuan = (int)value;fen = (value-yuan)*100+0.5;void Show() coutyuan “元“fen “分“ endl; ,43,operator doub
22、le () /类型转换运算符重载函数 return yuan+fen/100.0; private:int yuan, fen; ; void main() Money r1(1.01),r2(2.20);Money r3; r3 = Money(double(r1)+double(r2) ); r3.Show(); r3=r1+2.40; /隐式转换类型r3.Show(); 3 =2.0-r1; /隐式转换类型r3.Show(); ,44,3.3联编和虚函数,3.3.1 静态联编和动态联编面向对象的多态性从实现的角度来讲,可以分为静态多态性和动态多态性两种。在源程序编译的时候就能确定具有多态
23、性的语句调用哪个函数,称为静态联编。对于重载函数的调用就是在编译的时候确定具体调用哪个函数,所以是属于静态联编。,45,3.3.1 静态联编和动态联编,从对静态联编的上述分析中可以知道,编译程序在编译阶段并不能确切知道将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切知道该调用的函数,要求联编工作要在程序运行时进行,这种在程序运行时进行联编工作被称为动态联编,或称动态束定,又叫晚期联编。,46,3.3.2 虚函数,在C+中,动态联编是通过虚函数来实现的。虚函数的本质是将派生类类型的指针赋给基类类型的指针,虚函数被调用时会自动判断调用对象的类型,从而做出相应的响应。【例3.10】
24、虚函数引例#include class CPerson public:void PrintInfo() cout“Personn“; ;,47,class CWorker: public CPerson private:int kindofwork; public:void PrintInfo () /在派生类worker中重新定义 cout“Workern“; ; class CTeacher: public CPerson private:int subject;,48,public:void PrintInfo () coutPrintInfo(); p = ,49,从前面的知识,我们可
25、以很容易分析出它的运行结果。因为指针的类型决定调用那一个成员函数,所以,一个person *调用person成员函数,即使它指向派生类的对象。同样,一个teacher*也调用teacher的成员函数。我们把这称为早期联编或静态联编,因为指针要调用那一个函数是在编译时就确定的。那么,当person *指向派生类对象时,我们能不能通过该指针来调用派生类的成员函数呢?在C+中,我们是可以做到的,这要用到C+的多态特性。,3.3.3 虚函数定义,50,也就是说,要等到程序运行时,确定了指针所指向的对象的类型时,才能够确定。在C+语言中,是通过将一个函数定义成虚函数来实现运行时的多态的。虚函数的定义很简
26、单,只要在成员函数原型前加一个关键字virtual即可。virtual将一个成员函数说明为虚函数,对于编译器来讲,它的作用是告诉编译器,这个类含有虚函数,对于这个函数不使用静态联编,而是使用动态联编机制。编译器就会按照动态联编的机制进行一系列的工作。,3.3.3 虚函数定义,51,【例3.11】对于上面的例子,把基类的成员函数定义为虚函数,分析运行结果。 #include class CPerson public:virtual void PrintInfo() /基类中的虚函数 cout“Personn“; ; class CWorker: public CPerson private:in
27、t kindofwork;,52,public:void PrintInfo () /在派生类worker中重新定义 cout“Workern“; void PrintotherInfo () /在派生类中重新定义 cout“other information of Workern“; ; class CTeacher: public CPerson private:int subject; public:void PrintInfo () /在派生类中重新定义 cout“Teachern“; ,53,void PrintotherInfo () /重新定义 coutPrintInfo();p
28、 = ,54,虚函数与重载函数的比较,(1)重载函数要求函数有相同的函数名,并有不同的参数序列;而虚函数则要求这三项(函数名、返回值类型和参数序列)完全相同; (2)重载函数可以是成员函数或友员函数,而虚函数只能是成员函数; (3)重载函数的调用是以所传递参数序列的差别作为调用不同函数的依据;虚函数是根据对象的不同去调用不同类的虚函数; (4)虚函数在运行时表现出多态功能,这是C+的精髓;而重载函数则在编译时表现出多态性。,55,3.3.4动态联编的工作机制,编译器在执行过程中遇到virtual关键字的时候,将自动安装动态联编需要的机制,首先为这些包含virtual函数的类(注意不是对象)建立
29、一张虚拟函数表VTABLE。在这些虚拟函数表中,编译器将依次按照函数声明次序放置类的特定虚函数的地址。同时在每个带有虚函数的类中放置一个称之为vpointer的指针,简称vptr,这个指针指向这个类的VTABLE。,56,3.3.4动态联编的工作机制,编译器在每个类中放置一个vptr,一般置于对象的起始位置,继而在对象的构造函数中将vptr初始化为本类的VTABLE的地址。如图下页图。C+ 编译程序时候按下面的步骤进行工作:1.为各类建立虚拟函数表,如果没有虚函数则不建立。2.暂时不连接虚函数,而是将各个虚函数的地址放入虚拟函数表中。3.直接连接各静态函数。,57,58,3.3.4动态联编的工
30、作机制,所有的基类的派生类的虚拟函数表的顺序与基类的顺序是一样的,对于基类中不存在方法再按照声明次序进行排放。这样不管是CPerson还是CWorker或者CTeache类它们的虚拟函数表的第一项总是printInf函数的地址。对于CWorker类,printInf函数下面的才是printotherInfo函数的地址。因此不管对于CPerson还是CWorker,this-printInf总是编译成 call this-VTABLE0。,59,3.3.4动态联编的工作机制,程序到真正运行时候将会发现this的真正指向的对象,如果是CPerson,则调用CPerson-VTABLE0,如果是CW
31、orker,则调用CWorker-VTABLE0,就这样,编译器借助虚拟函数表实现了动态联编的过程,从而使多态的实现有了可能。,60,关于虚函数有以下几点说明,(1)当基类中把成员函数定义为虚函数后,要达到动态联编的效果,派生类和基类的对应成员函数不仅名字相同,而且返回类型、参数个数和类型也必须相同。否则不能实现运行时多态。 (2)基类中虚函数前的virtual不能省略,派生类中的虚函数的virtual关键字可以生省略,缺省后仍为虚函数。 (3)运行时多态必须通过基类对象的引用或基类对象的指针调用虚函数才能实现。 (4)虚函数必须是类的成员函数,不能是友员函数,也不能是静态成员函数。 (5)不
32、能将构造函数定义为虚函数,但可将析构函数定义为虚函数。,61,3.3.5 虚析构函数,在C+中,不能声明虚构造函数,因为在构造函数执行时,对象还没有完全构造好,不能按虚函数方式进行调用。但是可以声明虚析构函数,如果用基类指针指向一个new生成的派生类对象,通过delete作用于基类指针删除派生类对象时,有以下两种情况:,62,3.3.5 虚析构函数,(1)如果基类析构函数不为虚析构函数,则只会调用基类的析构函数,而派生类的析构函数不会被调用,因此派生类对象中派生的那部分内存空间无法析构释放。 (2)如果基类析构函数为虚析构函数,则释放基类指针的时候会调用基类和派生类中的所有析构函数,派生类对象
33、中所有的内存空间都将被释放,包括继承基类的部分。所以C+中的析构函数通常是虚析构函数。,63,【例3.12】虚析构函数的用法和作用示例。 #include class Base1 public:Base1() cout “Base1()n“; ; class Derived1 : public Base1 public:Derived1() cout “Derived1()n“; ; class Base2 public:virtual Base2() cout “Base2()n“; ;,64,class Derived2 : public Base2 public:Derived2() c
34、out “Derived2()n“; ; void main() Base1* bp = new Derived1; delete bp;Base2* b2p = new Derived2; delete b2p; 运行结果:Base1() Derived2()Base2(),65,3.4 纯虚函数和抽象类,3.4.1 纯虚函数在许多情况下,在基类中不能给出有意义的虚函数定义。 在C+中,对于那些在基类中不需要定义具体的行为的函数,可以定义为纯虚函数。声明纯虚函数的一般形式是class 类名virtual 类型 函数名(参数表)=0; /纯虚函数.;,66,注意: (1)纯虚函数没有函数体。
35、(2)最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。 (3)这是一个声明语句,最后应有分号。 (4)如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。,3.4.1 纯虚函数,67,3.4.2 抽象类,如果一个类中至少有一个纯虚函数,那么这个类被称为抽象类(abstract class)。因此抽象类的定义是基于纯虚函数的。抽象类中不仅包括纯虚函数,也可包括虚函数。抽象类中的纯虚函数可能是在抽象类中定义的,也可能是从它的抽象基类中继承下来且重定义的。抽象类有一个重要特点,即抽象类必须用作派生其他类的基类,而
36、不能用于直接创建对象实例。抽象类不能直接创建对象的原因是其中有一个或多个函数没有定义,但仍可使用指向抽象类的指针支持运行时多态性。,68,3.4.2 抽象类,抽象类定义的一般形式是: class 类名 public:virtual 函数名(参数表) = 0;其他函数的声明;. ;,69,【例3.13】抽象基类示例。 #include class Person /抽象基类 public:virtual void PrintInfo() /基类中的函数 cout“Personn“; virtual void DisplaySalary(int m,double s)=0; ; class Work
37、er: public Person private:int kindofwork;,70,public:void PrintInfo () /在派生类中重新定义 cout“Workern“; void DisplaySalary(int m, double s) cout“工人全年的工资是:“m*sendl; ; class Teacher: public Person private:int subject; public:void PrintInfo () cout“Teachern“; ,71,void DisplaySalary(int m, double s) cout“教师全年的工资是:“m*sendl; ; class Driver: public Person private:int subject; public:void DisplaySalary(int m,double s) cout“司机全年的工资:“m*sendl; ;,72,void main() Worker w; Teacher t;Driver d; Person * p;p = ,