C++程序设计课件(第3章).ppt
第第3章多态性章多态性n本章学习重点掌握内容:本章学习重点掌握内容:n多态的概念和作用,多态的实现方法多态的概念和作用,多态的实现方法n常见运算符的重载常见运算符的重载n静态联编和动态联编静态联编和动态联编n虚函数、纯虚函数和抽象基类的概念和虚函数、纯虚函数和抽象基类的概念和用法用法n虚析构函数的概念和作用,虚析构函数虚析构函数的概念和作用,虚析构函数的用法的用法13.1多态性的概念多态性的概念n多态性(多态性(Polymorphism)是面向对象程序设计)是面向对象程序设计的重要特性之一。多态性是指当不同的对象收的重要特性之一。多态性是指当不同的对象收到相同的消息时,产生不同的动作。利用多态到相同的消息时,产生不同的动作。利用多态性可以设计和实现一个易于扩展的系统。性可以设计和实现一个易于扩展的系统。n多态性主要体现在:向不同的对象发送同一个多态性主要体现在:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为。消息,不同的对象在接收时会产生不同的行为。也就是说,每个对象可以用自己的方式去响应也就是说,每个对象可以用自己的方式去响应共同的消息。共同的消息。C+支持两种形式的多态性,支持两种形式的多态性,一种是编译时的多态性,称为静态联编。一种一种是编译时的多态性,称为静态联编。一种是运行时多态,称为动态联编。是运行时多态,称为动态联编。23.2.1 运算符重载运算符重载3.2.1 运算符重载概述运算符重载概述在以前的学习中,在以前的学习中,C+中预定义的运算符的操作中预定义的运算符的操作对象只能是基本数据类型如对象只能是基本数据类型如int或或float等。实际等。实际上,对于很多用户自定义的类型上,对于很多用户自定义的类型(如类如类),也需要,也需要有类似的运算操作。有类似的运算操作。例如复数类例如复数类Complex。class Complexpublic:Complex()real=image=0;Complex(double r,double i)33.2.1 运算符重载概述运算符重载概述 real=r,image=i;void Print();private:double real,image;void Complex:Print()if(image0)coutrealimagei;else coutreal+imagei;43.2.1 运算符重载概述运算符重载概述 声明复数类的对象:声明复数类的对象:complex c1(2.0,3.0),c2(4.0,-2.0),c3。如果我们需要对。如果我们需要对c1和和c2进行加法运算,进行加法运算,“c3=c1+c2”,编译时,编译时却会出错,这是因为编译器不知道该如何完成却会出错,这是因为编译器不知道该如何完成这个加法。这时我们就需要编写程序来实现这个加法。这时我们就需要编写程序来实现“+”运算符来作用于运算符来作用于complex类的对象,这类的对象,这就是运算符的重载。运算符重载是对已有的运就是运算符的重载。运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不算符赋予多重含义,使同一个运算符作用于不同类型的数据时,导致不同类型的行为。同类型的数据时,导致不同类型的行为。53.2.1 运算符重载概述运算符重载概述 C+中运算符的重载虽然给我们设计程中运算符的重载虽然给我们设计程序带来很多的方便,但对运算符的重载时,序带来很多的方便,但对运算符的重载时,以下的几种情况需要注意:以下的几种情况需要注意:(1)一般来说,不改变运算符原有含义,只)一般来说,不改变运算符原有含义,只让它能针对新类型数据的实际需要,对原有让它能针对新类型数据的实际需要,对原有运算符进行适当的改造。例如,重载运算符进行适当的改造。例如,重载“+”运运算符后,它的功能还是进行加法运算。算符后,它的功能还是进行加法运算。(2)重载运算符时,不能改变运算符原有的)重载运算符时,不能改变运算符原有的优先级别,也不能改变运算符需要的操作数优先级别,也不能改变运算符需要的操作数的数目。重载之后运算符的优先级和结合性的数目。重载之后运算符的优先级和结合性都不会改变。都不会改变。63.2.1 运算符重载概述运算符重载概述(3)不能创建新的运算符,只能重载)不能创建新的运算符,只能重载c+中已中已有的运算符。有的运算符。(4)有些运算符不能进行重载。如:)有些运算符不能进行重载。如:“.”类成类成员运算符、员运算符、“*”类指向运算符、类指向运算符、“:”类类作用域运算符、作用域运算符、“?:?:”条件运算符及条件运算符及“sizeof”求字节数运算符。求字节数运算符。73.2.2 运算符重载的实现运算符重载的实现运算符重载的本质就是函数重载。在实现过程运算符重载的本质就是函数重载。在实现过程中,首先把指定的运算表达式转化为对运算符函数中,首先把指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参,然后的调用,运算对象转化为运算符函数的实参,然后根据实参的类型来确定需要调用的函数,这个过程根据实参的类型来确定需要调用的函数,这个过程是在编译过程中完成的。运算符重载形式有两种:是在编译过程中完成的。运算符重载形式有两种:重载为类的成员函数和重载为类的友元函数。重载为类的成员函数和重载为类的友元函数。1.运算符重载为类的成员函数运算符重载为类的成员函数 语法形式如下:语法形式如下:函数类型函数类型 operator 运算符(形参表)运算符(形参表)函数体;函数体;83.2.2 运算符重载的实现运算符重载的实现 2.运算符重载为类的友元函数运算符重载为类的友元函数 运算符重载还可以为友元函数。当重载友元运算符重载还可以为友元函数。当重载友元函数时,将没有隐含的参数函数时,将没有隐含的参数this指针。语法形式指针。语法形式如下:如下:friend 函数类型函数类型 operator 运算符(形参表)运算符(形参表);运算符重载为友元函数时,要在函数类型说运算符重载为友元函数时,要在函数类型说明之前使用明之前使用friend关键词来说明。关键词来说明。93.2.3 双目运算符重载双目运算符重载 双目运算符就是运算符作用于两个操作数。双目运算符就是运算符作用于两个操作数。下面通过一个例子对下面通过一个例子对“+”运算符重载来学习一运算符重载来学习一下双目运算符重载的应用。下双目运算符重载的应用。【例例3.1】定义一个复数类,重载定义一个复数类,重载“+”运算符运算符为复数类的成员函数,使这个运算符能直接完为复数类的成员函数,使这个运算符能直接完成两个复数的加法运算,以及一个复数与一个成两个复数的加法运算,以及一个复数与一个实数的加法运算。实数的加法运算。10#include class Complexpublic:Complex()real=image=0;Complex(double r,double i)real=r,image=i;void Print();Complex operator+(Complex&);Complex operator+(float);private:double real,image;11void Complex:Print()if(image0)coutrealimageiendl;else coutreal+imageiendl;Complex Complex:operator+(Complex&c)Complex t;t.real=real+c.real;t.image=image+c.image;return t;12Complex Complex:operator+(float s)Complex t;t.real=real+s;t.image=image;return t;13void main(void)Complex c1(25,50),c2(100,200),c3;coutc1=;c1.Print();coutc2=;c2.Print();c3=c1+c2;coutc3=c1+c2=;c3.Print();c1=c1+200;coutc1=;c1.Print();14【例例3.23.2】重载重载“+”运算符为复数类的友元函数,使运算符为复数类的友元函数,使这个运算符能直接完成两个复数的加法运算,以及一这个运算符能直接完成两个复数的加法运算,以及一个复数与一个实数的加法运算。个复数与一个实数的加法运算。class Complexpublic:Complex(double r,double i);friend Complex operator+(const Complex&c1,const Complex&c2);friend Complex operator+(const Complex&c1,double t);private:double real,image;15Complex operator+(const Complex&c1,const Complex&c2)Complex c(0,0);c.real=c1.real+c2.real;c.image=c1.image+c2.image;return c;Complex operator+(const complex&c1,double t)Complex c(0,0);c.real=c1.real+t;c.image=c1.image;return c;16void main()Complex c1(2.0,3.0),c2(4.0,-2.0),c3(0,0);c3=c1+c2;coutc1+c2=;Print(c3);c3=c1+5;cout mon_daydt.month-1)/少一个闰年判断少一个闰年判断 dt.day=dt.day-mon_daydt.month-1;dt.month+;if(dt.month=13)dt.month=1;dt.year+;return dt;193.2.4 赋值运算符重载赋值运算符重载在在C+中有两种类型的赋值运算符:一类是中有两种类型的赋值运算符:一类是“+=”和和“-=”等先计算后赋值的运算符,另一等先计算后赋值的运算符,另一类是类是“=”即直接赋值的运算符。下面分别进行即直接赋值的运算符。下面分别进行讨论。讨论。1运算符运算符“+=”和和“-=”的重载的重载【例例3.4】实现复数类实现复数类“+=”和和“-=”的重载。的重载。#includeclass Complexpublic:20 Complex(double r,double i)real=r;image=i;Complex operator-=(Complex&t);Complex operator+=(Complex&t);Print();private:double real,image;Complex Complex:operator-=(Complex&t)real-=t.real;image-=t.image;return*this;21Complex:Print()if(image0)coutrealimageiendl;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();223.2.4 赋值运算符重载赋值运算符重载2运算符运算符“=”的重载的重载【例例3.5】实现实现“=”运算符重载的示例。运算符重载的示例。#include#includeclass CMessage public:CMessage()buffer=NULL;CMessage()delete buffer;23 void Display()coutbufferendl;void Set(char*string)if(buffer!=NULL)delete buffer;buffer=new charstrlen(string)+1;strcpy(buffer,string);void operator=(const CMessage&Message)if(buffer!=NULL)delete buffer;buffer=new charstrlen(Message.buffer)+1;strcpy(buffer,Message.buffer);24private:char*buffer;void main()CMessage c1;c1.Set(initial c1 message);c1.Display();CMessage c2;c2.Set(initial c2 message);c2.Display();c1=c2;c1.Display();253.2.5 单目运算符重载单目运算符重载 类的单目运算符可重载为一个没有参数的类的单目运算符可重载为一个没有参数的非静态成员函数或者带有一个参数的非成员函非静态成员函数或者带有一个参数的非成员函数,参数必须是用户自定义类型的对象或者是数,参数必须是用户自定义类型的对象或者是对该对象的引用。对该对象的引用。在在C+C+中,单目运算符有中,单目运算符有+和和-,它们是,它们是变量自动增变量自动增1 1和自动减和自动减1 1的运算符。在类中可以的运算符。在类中可以对这两个单目运算符进行重载。对这两个单目运算符进行重载。263.2.5 单目运算符重载单目运算符重载 如同如同“+”运算符有前缀、后缀两种使用运算符有前缀、后缀两种使用形式,形式,“+”和和“-”重载运算符也有前缀和重载运算符也有前缀和后缀两种运算符重载形式,以后缀两种运算符重载形式,以“+”重载运算重载运算符为例,其语法格式如下:符为例,其语法格式如下:函数类型函数类型 operator+();();/前缀运算前缀运算 函数类型函数类型 operator+(int);/后缀运算后缀运算 使用前缀运算符的语法格式如下:使用前缀运算符的语法格式如下:+对象;对象;使用后缀运算符的语法格式如下:使用后缀运算符的语法格式如下:对象对象+;27【例例3.6】重载单目运算符重载单目运算符“+”。#includeclass Counterpublic:Counter()v=0;Counter operator+();/前置单目运算符前置单目运算符Counter operator+(int);/后置单目运算符后置单目运算符void Display()coutvendl;private:int v;28Counter Counter:operator+()/前置单目运算前置单目运算 +v;return*this;Counter Counter:operator+(int)/后置单目运后置单目运算算Counter t;t.v=v+1;return t;29void main()Counter c1,c2;int i;for(i=0;i4;i+)/后置单目运算符后置单目运算符 c1+;coutc1=;c1.Display();for(i=0;i4;i+)/前置单目运算符前置单目运算符 +c2;coutc2=;c2.Display();303.2.6 下标运算符重载下标运算符重载 下标运算符下标运算符“”通常用于在数组中标识数组通常用于在数组中标识数组元素的位置,下标运算符重载可以实现数组数据的赋元素的位置,下标运算符重载可以实现数组数据的赋值和取值。下标运算符重载函数只能作为类的成员函值和取值。下标运算符重载函数只能作为类的成员函数,不能作为类的友元函数。数,不能作为类的友元函数。下标运算符下标运算符“”函数重载的一般形式为:函数重载的一般形式为:函数类型函数类型 operator(形参表);(形参表);其中形参表为该重载函数的参数列表。重载下标其中形参表为该重载函数的参数列表。重载下标运算符只能且必须带一个参数,该参数给出下标的值。运算符只能且必须带一个参数,该参数给出下标的值。31【例例3.7】定义一个字符数组类,其中对下标运算定义一个字符数组类,其中对下标运算符符“”进行重载。进行重载。#include#include class MyCharArraypublic:MyCharArray(int m)len=m;str=new charlen;MyCharArray(char*s)str=new charstrlen(s)+1;strcpy(str,s);len=strlen(s);32 MyCharArray()delete str;char&operator(int n)static char ch=0;if(nlen-1)cout整数下标越界整数下标越界;return ch;else return*(str+n);33 void Disp()coutstrendl;private:int len;char*str;void main()MyCharArray word(This is a C+program.);word.Disp();cout位置位置0:;coutword0endl;34 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();353.2.7 关系运算符重载关系运算符重载 关系运算符也可以被重载,例如定义一个关系运算符也可以被重载,例如定义一个日期类日期类date,重载运算符,重载运算符“=”和和“”,用于,用于两个日期的等于和小于的比较运算。两个日期的等于和小于的比较运算。【例例3.8】日期类重载关系运算符日期类重载关系运算符“=”、“”等。等。#includeclass Datepublic:Date(int m,int d,int y)month=m;day=d;year=y;36void Display()cout month/day/year;friend int operator t2返回返回0,否则返回,否则返回1 if(t1.yeart2.year)return 1;else if(t1.montht2.month&t1.year=t2.year)return 1;else if(t1.day(Date&t1,Date&t2)return(t2t1);private:int month,day,year;38void main()Date date1(11,25,90),date2(11,22,90);if(date1date2)date1.Display();cout is less than;date2.Display();coutendl;else if(date1=date2)date1.Display();39 cout is equal to;date2.Display();coutdate2)date1.Display();cout is more than;date2.Display();coutendl;403.2.8 类型转换运算符重载类型转换运算符重载类型转换运算符重载函数的格式如下类型转换运算符重载函数的格式如下:operator operator 类型名类型名()()函数体函数体 与前面重与前面重载运算符载运算符函数不同,类型函数不同,类型转换运转换运算符重载函数没有算符重载函数没有返回类型,因为返回类型,因为类型名就代类型名就代表表了返回类型,也了返回类型,也没有没有任何参数。在任何参数。在调用过程调用过程中要带一个中要带一个对象实参。对象实参。实际上,类型转换运算符将对象转换成类实际上,类型转换运算符将对象转换成类型名规定的类型。转换时的形式就像强制转换。型名规定的类型。转换时的形式就像强制转换。如果没有转换运算符定义,直接用强制转换是如果没有转换运算符定义,直接用强制转换是不行的,因为强制转换只能对标准数据类型进不行的,因为强制转换只能对标准数据类型进行操作,对类类型的操作是没有定义的。行操作,对类类型的操作是没有定义的。41【例【例3.9】实现人民币】实现人民币Money与与double的转换的转换。#includeclass Money /人民币类人民币类public:Money(double value=0.0)yuan=(int)value;fen=(value-yuan)*100+0.5;void Show()coutyuan 元元fen 分分 endl;42 operator double()/类型转换运算符重载函数类型转换运算符重载函数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();433.3联编和虚函数联编和虚函数 3.3.1 静态联编和动态联编静态联编和动态联编 面向对象的多态性从实现的角度来讲,面向对象的多态性从实现的角度来讲,可以分为静态多态性和动态多态性两种。可以分为静态多态性和动态多态性两种。在源程序编译的时候就能确定具有多态性在源程序编译的时候就能确定具有多态性的语句调用哪个函数,称为静态联编。对的语句调用哪个函数,称为静态联编。对于重载函数的调用就是在编译的时候确定于重载函数的调用就是在编译的时候确定具体调用哪个函数,所以是属于静态联编。具体调用哪个函数,所以是属于静态联编。443.3.1 静态联编和动态联编静态联编和动态联编 从对静态联编的上述分析中可以知从对静态联编的上述分析中可以知道,编译程序在编译阶段并不能确切知道,编译程序在编译阶段并不能确切知道将要调用的函数,只有在程序执行时道将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切才能确定将要调用的函数,为此要确切知道该调用的函数,要求联编工作要在知道该调用的函数,要求联编工作要在程序运行时进行,这种在程序运行时进程序运行时进行,这种在程序运行时进行联编工作被称为动态联编,或称动态行联编工作被称为动态联编,或称动态束定,又叫晚期联编。束定,又叫晚期联编。453.3.2 虚函数虚函数 在在C+中,动态联编是通过虚函数来实现的。中,动态联编是通过虚函数来实现的。虚函数的本质是将派生类类型的指针赋给基类类虚函数的本质是将派生类类型的指针赋给基类类型的指针,虚函数被调用时会自动判断调用对象型的指针,虚函数被调用时会自动判断调用对象的类型,从而做出相应的的类型,从而做出相应的响应。响应。【例例3.10】虚函数引例虚函数引例#include class CPerson public:void PrintInfo()coutPersonn;46class CWorker:public CPersonprivate:int kindofwork;public:void PrintInfo()/在派生类在派生类worker中重新定中重新定义义 coutWorkern;class CTeacher:public CPersonprivate:int subject;47public:void PrintInfo()coutPrintInfo();p=&t;p-PrintInfo();t1=&t;t1-PrintInfo();48从前面的知识,我们可以很容易分析出它的运行从前面的知识,我们可以很容易分析出它的运行结果。因为指针的类型决定调用那一个成员函数,所结果。因为指针的类型决定调用那一个成员函数,所以,一个以,一个person*调用调用person成员函数,即使它指向成员函数,即使它指向派生类的对象。同样,一个派生类的对象。同样,一个teacher*也调用也调用teacher的的成员函数。我们把这称为早期联编或静态联编,因为成员函数。我们把这称为早期联编或静态联编,因为指针要调用那一个函数是在编译时就确定的。指针要调用那一个函数是在编译时就确定的。那么,当那么,当person*指向派生类对象时,我们能指向派生类对象时,我们能不能通过该指针来调用派生类的成员函数呢?在不能通过该指针来调用派生类的成员函数呢?在C+中,我们是可以做到的,这要用到中,我们是可以做到的,这要用到C+的多态的多态特性。特性。3.3.3 虚函数定义虚函数定义49 也就是说,要等到程序运行时,确定了指也就是说,要等到程序运行时,确定了指针所指向的对象的类型时,才能够确定。在针所指向的对象的类型时,才能够确定。在C+语言中,是通过将一个函数定义成虚函数语言中,是通过将一个函数定义成虚函数来实现运行时的多态的。来实现运行时的多态的。虚函数的定义很简单,只要在成员函数原虚函数的定义很简单,只要在成员函数原型前加一个关键字型前加一个关键字virtual即可。即可。virtual将一个将一个成员函数说明为虚函数,对于编译器来讲,它成员函数说明为虚函数,对于编译器来讲,它的作用是告诉编译器,这个类含有虚函数,对的作用是告诉编译器,这个类含有虚函数,对于这个函数不使用静态联编,而是使用动态联于这个函数不使用静态联编,而是使用动态联编机制。编译器就会按照动态联编的机制进行编机制。编译器就会按照动态联编的机制进行一系列的工作。一系列的工作。3.3.3 虚虚函数定义函数定义50【例【例3.11】对于上面的例子,把基类的成员函数定】对于上面的例子,把基类的成员函数定义为虚函数,分析运行结果。义为虚函数,分析运行结果。#include class CPersonpublic:virtual void PrintInfo()/基类中的虚函数基类中的虚函数 coutPersonn;class CWorker:public CPersonprivate:int kindofwork;51public:void PrintInfo()/在派生类在派生类worker中重新定义中重新定义 coutWorkern;void PrintotherInfo()/在派生类中重新定义在派生类中重新定义 coutother information of Workern;class CTeacher:public CPersonprivate:int subject;public:void PrintInfo()/在派生类中重新定义在派生类中重新定义 coutTeachern;52 void PrintotherInfo()/重新定义重新定义 coutPrintInfo();p=&t;p-PrintInfo();p=&d;p-PrintInfo();t.PrintInfo();53虚函数与重载函数的比较虚函数与重载函数的比较(1 1)重载函数要求函数有相同的函数名,并有不)重载函数要求函数有相同的函数名,并有不同的参数序列;而虚函数则要求这三项(函数名、同的参数序列;而虚函数则要求这三项(函数名、返回值类型和参数序列)完全相同;返回值类型和参数序列)完全相同;(2 2)重载函数可以是成员函数或友员函数,而虚)重载函数可以是成员函数或友员函数,而虚函数只能是成员函数;函数只能是成员函数;(3 3)重载函数的调用是以所传递参数序列的差别)重载函数的调用是以所传递参数序列的差别作为调用不同函数的依据;虚函数是根据对象的作为调用不同函数的依据;虚函数是根据对象的不同去调用不同类的虚函数;不同去调用不同类的虚函数;(4 4)虚函数在运行时表现出多态功能,这是)虚函数在运行时表现出多态功能,这是C+C+的的精髓;而重载函数则在编译时表现出多态性。精髓;而重载函数则在编译时表现出多态性。543.3.4动态联编的工作机制动态联编的工作机制 编译器在执行过程中遇到编译器在执行过程中遇到virtual关键字的关键字的时候,将自动安装动态联编需要的机制,首时候,将自动安装动态联编需要的机制,首先为这些包含先为这些包含virtual函数的类(注意不是对象)函数的类(注意不是对象)建立一张虚拟函数表建立一张虚拟函数表VTABLE。在这些虚拟。在这些虚拟函数表中,编译器将依次按照函数声明次序函数表中,编译器将依次按照函数声明次序放置类的特定虚函数的地址。放置类的特定虚函数的地址。同时在每个带有虚函数的类中放置一个同时在每个带有虚函数的类中放置一个称之为称之为vpointer的指针,简称的指针,简称vptr,这个指针,这个指针指向这个类的指向这个类的VTABLE。553.3.4动态联编的工作机制动态联编的工作机制编译器在每个类中放置一个编译器在每个类中放置一个vptr,一般置于对象,一般置于对象的起始位置,继而在对象的构造函数中将的起始位置,继而在对象的构造函数中将vptr初初始化为本类的始化为本类的VTABLE的地址。如图下页图。的地址。如图下页图。C+编译程序时候按下面的步骤进行工作:编译程序时候按下面的步骤进行工作:1.为各类建立虚拟函数表,如果没有虚函数则不为各类建立虚拟函数表,如果没有虚函数则不建立。建立。2.暂时不连接虚函数,而是将各个虚函数的地址暂时不连接虚函数,而是将各个虚函数的地址放入虚拟函数表中。放入虚拟函数表中。3.直接连接各静态函数。直接连接各静态函数。56573.3.4动态联编的工作机制动态联编的工作机制 所有的基类的派生类的虚拟函数表的顺序与基所有的基类的派生类的虚拟函数表的顺序与基类的顺序是一样的,对于基类中不存在方法再按照类的顺序是一样的,对于基类中不存在方法再按照声明次序进行排放。这样不管是声明次序进行排放。这样不管是CPerson还是还是CWorker或者或者CTeache类它们的虚拟函数表的第一类它们的虚拟函数表的第一项总是项总是printInf函数的地址。对于函数的地址。对于CWorker类,类,printInf函数下面的才是函数下面的才是printotherInfo函数的地址。函数的地址。因此不管对于因此不管对于CPerson还是还是CWorker,this-printInf总是编译成总是编译成 call this-VTABLE0。583.3.4动态联编的工作机制动态联编的工作机制 程序到真正运行时候将会发现程序到真正运行时候将会发现this的真正的真正指向的对象,如果是指向的对象,如果是CPerson,则调用,则调用CPerson-VTABLE0,如果是,如果是CWorker,则调用则调用CWorker-VTABLE0,就这样,就这样,编译器借助虚拟函数表实现了动态联编的过编译器借助虚拟函数表实现了动态联编的过程,从而使多态的实现有了可能。程,从而使多态的实现有了可能。59关于虚函数有以下几点说明关于虚函数有以下几点说明(1)当基类中把成员函数定义为虚函数后,要达到)当基类中把成员函数定义为虚函数后,要达到动态联编的效果,派生类和基类的对应成员函数动态联编的效果,派生类和基类的对应成员函数不仅名字相同,而且返回类型、参数个数和类型不仅名字相同,而且返回类型、参数个数和类型也必须相同。否则不能实现运行时多态。也必须相同。否则不能实现运行时多态。(2)基类中虚函数前的)基类中虚函数前的virtual不能省略,派生类不能省略,派生类中的虚函数的中的虚函数的virtual关键字可以生省略,缺省后关键字可以生省略,缺省后仍为虚函数。仍为虚函数。(3)运行时多态必须通过基类对象的引用或基类对)运行时多态必须通过基类对象的引用或基类对象的指针调用虚函数才能实现。象的指针调用虚函数才能实现。(4)虚函数必须是类的成员函数,不能是友员函数,)虚函数必须是类的成员函数,不能是友员函数,也不能是静态成员函数。也不能是静态成员函数。(5)不能将构造函数定义为虚函数,但可将析构函)不能将构造函数定义为虚函数,但可将析构函数定义为虚函数。数定义为虚函数。603.3.5 虚析构函数虚析构函数 在在C+中,不能中,不能声明虚构声明虚构造函数,因为造函数,因为在构造函数在构造函数执行时,对象执行时,对象还没有完全还没有完全构造构造好,不能好,不能按虚函数方式按虚函数方式进行调用。但是进行调用。但是可可以声明虚析以声明虚析构函数,如果构函数,如果用基类指针指向用基类指针指向一个一个new生成的派生生成的派生类对象,通过类对象,通过delete作用于基类指针删除派生类作用于基类指针删除派生类对象时,有对象时,有以以下两种情况下两种情况:613.3.5 虚析构函数虚析构函数(1)如果基类析构函数不为虚析构函数,则只)如果基类析构函数不为虚析构函数,则只会调用基类的析构函数,而派生类的析构函数会调用基类的析构函数,而派生类的析构函数不会被调用,因此派生类对象中派生的那部分不会被调用,因此派生类对象中派生的那部分内存空间无法析构释放。内存空间无法析构释放。(2)如果基类析构函数为虚析构函数,则释放)如果基类析构函数为虚析构函数,则释放基类指针的时候会调用基类和派生类中的所有基类指针的时候会调用基类和派生类中的所有析构函数,派生类对象中所有的内存空间都将析构函数,派生类对象中所有的内存空间都将被释放,包括继承基类的部分。所以被释放,包括继承基类的部分。所以C+中的中的析构函数通常是虚析构函数。析构函数通常是虚析构函数。62【例例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;63class Derived2:public Base2 public:Derived2()cout Derived2()n;void main()Base1*bp=new Derived1;delete bp;Base2*b2p=new Derived2;delete b2p;运行结果:运行结果:Base1()Derived2()Base2()643.4 纯虚函数和抽象类纯虚函数和抽象类 3.4.1 纯虚函数纯虚函数 在在许多情况下,在基类中不能给出有意义的许多情况下,在基类中不能给出有意义的虚虚函数定义。函数定义。在在C+中,对于那些在基类中不中,对于那些在基类中不需要定义具体的行为的函数,可以定义为纯虚函需要定义具体的行为的函数,可以定义为纯虚函数。声明纯虚函数的一般形式是数。声明纯虚函数的一般形式是 class 类名类名 virtual 类型类型 函数名函数名(参数表参数表)=0;/纯虚函数纯虚函数 .;65 注意:注意:(1)纯虚函数没有函数体。)纯虚函数没有函数体。(2)最后面的)最后面的“=0”并不表示函数返回值为并不表示函数返回值为0,它,它只起形式上的作用,告诉编译系统只起形式上的作用,告诉编译系统“这是纯虚这是纯虚函数函数”。(3)这是一个声明语句,最后应有分号。)这是一个声明语句,最后应有分号。(4)如果在一个类中声明了纯虚函数,而在其)如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。生类中仍然为纯虚函数。3.4.1 纯虚函数纯虚函数663.4.2 抽象类抽象类 如果一个类中至少有一个纯虚函数,那么这如果一个类中至少有一个纯虚函数,那么这个类被称为抽象类(个类被称为抽象类(abstract classabstract class)。因此抽)。因此抽象类的定义是基于纯虚函数的。象类的定义是基于纯虚函数的。抽象类中不仅包括纯虚函数,也可包括虚函抽象类中不仅