1、第8章 多态性,8.1 多态性概述 8.2 运算符重载 8.3 虚函数 8.4 抽象类,8.1 多态性概述,通俗地说,多态性是指用一个相同的名字定义不同的函数,这些函数的执行过程不同,但是有相似的操作,即用同样的接口访问不同的函数。面向对象的多态性从实现的角度来讲,可以分为静态多态性和动态多态性两种。静态多态性是在编译的过程中确定同名操作的具体操作对象的,而动态多态性则是在程序运行过程中动态地确定操作所针对的具体对象的。这种确定操作具体对象的过程就是联编(binding),也称为绑定。联编是指计算机程序自身彼此关联的过程。也就是把一个标识符名和一个存储地址联系在一起的过程。用面向对象的术语讲,
2、就是把一条消息和一个对象的方法相结合的过程。,所谓消息,是指对类的成员函数的调用。不同的方法是指不同的实现,也就是调用了不同的函数。按照联编进行阶段的不同,联编方法可以分为两种:静态联编和动态联编。这两种联编过程分别对应着多态的两种实现方式。联编工作在编译连接阶段完成的情况称为静态联编。在编译、连接过程中,系统就可以根据类型匹配等特征确定程序中操作调用与执行该操作的代码的关系,即确定某一个同名标识到底是要调用哪一段程序代码。函数重载和运算符重载就属于静态多态性。 和静态联编相对应,如果联编工作在程序运行阶段完成,则称为动态联编。在编译、连接过程中无法解决的联编问题,要等到程序开始运行之后再来确
3、定。例如,本章将要介绍的虚函数就是通过动态联编完成的。函数重载在函数及类的章节中曾做过详细的讨论,所以在本章中,静态多态性主要介绍运算符重载;对于动态多态性,将对虚函数作详细介绍。,8.2 运算符重载,C+中预定义的运算符的操作对象只能是基本数据类型。实际上,对于很多用户自定义的类型(如类),也需要有类似的运算操作。例如点类point。class point private:int x , y;public: /构造函数point ( int xx=0,int yy =0) x = xx ; y = yy ; int get_x(); /显示x值int get_y(); /显示y值/.;,声明
4、点类的对象: point p1(1,1) , p2(3,3)如果我们需要对p1和p2进行加法运算,如果写出表达式“p1+p2”,编译时却会出错,因为编译器不知道该如何完成这个加法。这时我们就需要编写程序来说明“+”怎样作用于point类对象时,该实现什么样的功能,这就是运算符重载。运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时,导致不同类型的行为。在运算符重载的实现过程中,首先把指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参,然后,根据实参的类型来确定需要调用的函数。这个过程是在编译过程中完成的。,8.2.1 运算符重载的规则运算符重载时
5、必须要遵守一定的规则。C+中的运算符除了少数几个(类属关系运算符“.”、作用域分辨符“:”、成员指针运算符“*”、sizeof运算符和三目运算符“?:”)之外,全部可以重载,而且只能重载C+中已有的运算符,不能臆造新的运算符。重载之后运算符的优先级和结合性都不能改变,也不能改变运算符的语法结构,即单目运算符只能重载为单目运算符,双目运算符只能重载为双目运算符。运算符重载后的功能应当与原有功能相类似。重载运算符含义必须清楚,不能有二义性。运算符的重载形式有两种:重载为类的成员函数和重载为类的友元函数。,运算符重载为类的成员函数的一般语法形式如下:operator (形参表)函数体; 运算符重载为
6、类的友元函数的一般语法形式如下:friend operator (形参表)函数体;,其中:函数类型指定了重载运算符的返回值类型,也就是运算结果类型。operator是定义运算符重载函数的关键字。运算符是要重载的运算符名称。形参表给出重载运算符所需要的参数和类型。friend是对于运算符重载为友元函数时,在函数类型说明之前使用的关键字。 特别需要注意的是,当运算符重载为类的成员函数时,函数的参数个数比原来的操作数个数要少一个(后置“+”、“-”除外);当重载为类的友元函数时,参数个数与原操作数的个数相同。原因是重载为类的成员函数时,如果某个对象使用重载了的成员函数,自身的数据可以直接访问,就不需
7、要再放在参数表中进行传递,少了的操作数就是该对象本身。,8.2.2 运算符重载为成员函数运算符重载实质上就是函数重载,当运算符重载为成员函数之后,它就可以自由地访问本类的数据成员了。实际使用时,总是通过该类的某个对象来访问重载的运算符。如果是双目运算符,一个操作数是对象本身的数据,由this指针指出,另一个操作数则需要通过运算符重载函数的参数表来传递;如果是单目运算符,操作数由对象的this指针给出,就不再需要任何参数。下面分别介绍这两种情况。1双目运算:oprdl B oprd2对于双目运算符B,如果要重载B为类的成员函数,使之能够实现表达式oprdl B oprd2(其中oprdl为A类的
8、对象),则应当把B重载为A类的成员函数,该函数只有一个形参,形参的类型是oprd2所属的类型。经过重载之后,表达式oprdl B oprd2就相当于函数调用oprdl.operator B (oprd2)。,2单目运算1)前置单目运算:U oprd对于前置单目运算符U,如“-”(负号)、“+”等,如果要重载U为类的成员函数,用来实现表达式U oprd(其中oprd为A类的对象),则U应当重载为A类的成员函数,函数没有形参。经过重载之后,表达式U oprd相当于函数调用oprd.operator U()。例如,前置单目运算符“+”重载的语法形式如下:operator+();使用前置单目运算符“+
9、”的语法形式如下:+;,2) 后置单目运算:opr dV再来看后置运算符V,如“+”和“-”,如果要将它们重载为类的成员函数,用来实现表达式oprd+或oprd-(其中oprd为A类的对象),那么运算符就应当重载为A类的成员函数,这时函数要带有一个整型(int)形参。重载之后,表达式oprd+和oprd-就相当于函数调用oprd.operator+(0)和oprd.operator-(0)。例如,后置单目运算符“+”重载的语法形式如下:operator+(int);使用后置单目运算符“+”的语法形式如下:+;,在C+中,可以通过在运算符函数参数表中是否插入关键字int来区分前缀和后缀这两种方式
10、。对于前缀方式+ob,可以用运算符函数重载为ob.operator +(); / 成员函数重载或 operator + (X / 友元函数重载 在调用后缀方式的函数时,参数int一般被传递给值0。(例test_operator.cpp),【例8-1】双目运算符重载为成员函数例题。/test_operator.cpp,#include class point private:float x,y;public:point(float xx=0,float yy=0) x = xx ; y = yy;float get_x()return x;float get_y()return y;point
11、operator+(point p1);point operator-(point p1); /重载运算符“+”和“-”为成员函数 ;,point point:operator+(point q) return point(x+q.x,y+q.y); point point:operator-(point q) return point(x-q.x,y-q.y); void main()point p1(3,3),p2(2,2),p3,p4p3=p1+p2; /两点相加p4=p1-p2; /两点相减 coutp3.get_x()“ “p3.get_y()endl;coutp4.get_x()“
12、 “p4.get_y()endl;,本例中重载的“+”、“-”函数中,都是创建一个临时的无名对象作为返回值:return point(x+q.x,y+q.y);这表面上看起来像是对构造函数的调用,但其实并非如此。这是临时对象语法,它的含义是创建一个临时对象并返回它。当然,也可以按如下形式返回函数值:point point:operator+(point q)point p;p.x=x+q.x; p.y=y+q.y;return p;这两种方法的执行效率是完全不同的。后者的执行过程是这样的:创建一个局部对象p(这时会调用构造函数),执行return语句时,会调用拷贝构造函数,将p的值拷贝到主调函
13、数中的一个无名临时对象中。当函数operator+结束时,会调用析构函数析构对象p,然后p消亡。两种方法相比,前一种方法的效率高,因为它是直接将一个无名临时对象创建到主调函数中,【例8-2】单目运算符重载为成员函数例题。,#include class point private:float x,y;public:point(float xx=0,float yy=0)x=xx;y=yy;float get_x()return x;float get_y()return y;point operator+(); /重载前置运算符“+”point operator-(); /重载前置运算符“-”
14、;,point point:operator+() if(x0) -x;if(y0) -y;return *this; ,void main() point p1(10,10),p2(200,200); /声明point类的对象for(int i=0;i5;i+) cout“p1:x=“p1.get_x()“,y=“p1.get_y()endl;+p1;for ( i = 0;i5;i+) cout“p2:x=“p2.get_x()“,y=“p2.get_y()endl;-p2; ,前置单目运算符和后置单目运算符重载的最主要的区别就在于重载函数的形参。语法规定,前置单目运算符重载为成员函数时没
15、有形参,而后置单目运算符重载为成员函数时,需要有一个int型形参。这个int型参数在函数体中并不使用,纯粹是用来区别前置与后置,因此,参数表中可以只给出类型名,没有参数名。(例test_operator.cpp)8.2.3 运算符重载为友元函数运算符也可以重载为类的友元函数,这样它就可以自由地访问该类的任何数据成员。这时,运算所需要的操作数都需要通过函数的形参表来传递,在参数表中形参从左到右的顺序就是运算符操作数的顺序。但是,有些运算符不能重载为友元,如“=”、“()”、“”和“-”。,1双目运算:oprdl B oprd2对于双目运算符B,如果opr dl为A类的对象,则应当把B重载为A类的
16、友元函数,该函数有两个形参,其中一个形参的类型是A类。经过重载之后,表达式oprdl B oprd2就相当于函数调用operator B(opr dl,opr d2)。 2单目运算1)前置单目运算:U oprd对于前置单目运算符U,如“-”(负号)等,如果要实现表达式U oprd(其中oprd为A类的对象),则U可以重载为A类的友元函数,函数的形参为A类的对象。经过重载之后,表达式U oprd相当于函数调用operator U(oprd)。2)后置单目运算:oprd V对于后置运算符V,如“+”和“-”,如果要实现表达式oprd +或oprd -(其中oprd为A类的对象),那么运算符就可以重
17、载为A类的友元函数,这时函数的形参有两个,一个是A类的对象oprd,另一个是整型(int)形参。重载之后,表达式oprd+和oprd-就相当于函数调用operator+(oprd,0)和operator-(oprd,0)。,【例8-3】双目运算符重载为友元重载例题。,friend point operator+(point p1,point p2) /重载运算符“+”friend point operator-(point p1,point p2) /和“-”为友元函数point operator+(point p1,point p2) return point(p1.x+p2.x,p1.y+
18、p2.y); point operator-(point p1,point p2)return point(p1.x-p2.x,p1.y-p2.y);,8.2.4 其它运算符重载比较运算符重载(如,=,=,!=)。赋值运算符重载(如=,+=,-=,*=,/=)。下标运算符“”重载。下标运算符“”通常用于取数组中的某个元素,通过下标运算符重载,可以实现数组下标的越界检测等。运算符new和delete重载。通过重载new和delete,可以克服new和delete的不足,使其按要求完成对内存的管理。逗号运算符“,”重载。逗号运算符构成的表达式为“左操作数,右操作数”,该表达式返回右操作数的值。,8
19、.2.5 赋值运算符“=”的重载 (例test_array_constructor.cpp),对任一类X,如果没有用户自定义的赋值运算符函数,那么系统自动地为其生成一个缺省的赋值运算符函数,定义为类X中的成员到成员的赋值,例如:X 就调用缺省的赋值运算符函数,将对象obj2的数据成员的值逐个赋给对象obj1的对应数据成员中。,1.指针悬挂问题在某些特殊情况下,如类中有指针类型时,使用缺省的赋值运算符函数会产生错误。,例 使用缺省的赋值运算符函数产生错误的例子。#include #includeclass string public:string(char *s)ptr=new charstrl
20、en(s)+1;strcpy(ptr,s);string() delete ptr; void print() coutptrendl; private:char *ptr; ;,void main()string p1(“book“);string p2(“pen“);p1=p2;cout“p2:“;p2.print();cout“p1:“;p1.print();,2. 重载赋值运算符解决指针悬挂问题为了解决上述使用缺省的赋值运算符所遇到的指针悬挂问题,必须重载赋值运算符,使得对目标对象数据成员指针的赋值,是把原对象指针ptr所指向的内容传递给它,而不是简单地传递指针值,例: 重载赋值运算符
21、解决指针悬挂问题。#include #includeclass string public:string(char *s) ptr=new charstrlen(s)+1; strcpy(ptr,s);string() delete ptr; void print() coutptrendl; string,string,8.3 虚函数,8.3.1为什么要引入虚函数/test_virtual.cpp 【例8-4】没有使用虚函数的例题。 #include class base /定义基类base public:void who()cout“this is the class of base!“e
22、ndl;,class derive1:public base /定义派生类derive1 public:void who()cout“this is the class of derive1!“endl; ;,class derive2 : public base /定义派生类derive2 public:void who()coutwho();ptr= ,程序运行结果:This is the class of base! (a)This is the class of base! (b)This is the class of base! (c)This is the class of de
23、rive1! (d)This is the class of derive2! (e),就是说,通过指针引起的普通成员函数调用,仅仅与指针的类型有关,而与指针正指向什么对象无关。在这种情况下,必须采用显式的方式调用派生类的函数成员。,要实现这种功能,就需要引入虚函数的概念。这里,只需将基类的who()函数声明为虚函数即可。 8.3.2 虚函数的定义及使用1.虚函数的定义虚函数的定义是在基类中进行的。它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual。当基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义,在派生类中重新定义时,其函数原型,包括返
24、回类型、函数名、参数个数、参数类型以及参数的顺序都必须与基类中的原型完全相同。,一般虚函数的定义语法如下:virtual(形参表)函数体其中,被关键字virtual说明的函数为虚函数。特别要注意的是,虚函数的声明只能出现在类声明中的函数原型声明中,而不能出现在成员的函数体实现的时候。需要注意,动态联编只能通过成员函数来调用或者通过指针、引用来访问虚函数。如果使用对象名的形式访问虚函数,则将采用静态联编方式调用虚函数,而无需在运行过程中进行调用。下面是通过指针访问虚函数的例题。,例8-5 虚函数的作用。/test_virtual.cpp #include class Base public:Ba
25、se(int x,int y) a=x; b=y; virtual void show() /定义虚函数show() cout“Base-n“; couta“ “bendl;private:int a,b; class Derived : public Base public:Derived(int x,int y,int z):Base(x,y)c=z; void show() /重新定义虚函数show() cout “Derived-n“; coutcendl;private:int c; ;,void main() Base mb(60,60),*pc;Derived mc(10,20,
26、30);pc= /调用派生类Derived的show()版本 程序运行结果如下: Base- 60 60 Derived- 30,2. 虚函数与重载的关系在一个派生类中重新定义基类的虚函数是函数重载的另一种特殊形式,但它不同于一般的函数重载。一般的函数重载,只要函数名相同即可,函数的返回类型及所带的参数可以不同。但当重载一个虚函数时,也就是说在派生类中重新定义此虚函数时,要求函数名、返回类型、参数个数、参数类型以及参数的顺序都与基类中的原型完全相同,不能有任何的不同。3多继承中的虚函数在多继承中由于派生类是由多个基类派生而来的,因此,虚函数的使用就不像单继承那样简单。请看下面的例题。,【例8-
27、6】多继承中使用虚函数例题。,#include class base1 public:virtual void who() /函数who()为虚函数cout“this is the class of base1!“endl; ; class base2 /定义基类base2 public:void who() /此函数who()为一般的函数 cout“this is the class of base2!“endl; ;,class derive : public base1,public base2 public:void who()coutwho();ptr2= ,此时,程序执行的结果为t
28、his is the class of base1!this is the class of base2!this is the class of derive!this is the class of base2!从上面的例子看出,派生类derive中的函数who()在不同的场合呈现不同的性质。如相对base1路径,由于在base1中的who()函数前有关键字virtual,所以它是一个虚函数;若相对于base2派生路径,在base2中的who()函数为一般函数,所以,此时它只是一个重载函数。,若一个派生类,它的多个基类中有公共的基类,在公共基类中定义一个虚函数,则多重派生以后仍可以重新定义
29、虚函数,也就是说,虚特性是可以传递的。请看下面的例题。,【例8-7】多继承中虚特性的传递例题。 #include class base public:virtual void who() /定义虚函数cout“this is the class of base!“endl; ;,class base1:public base /定义派生类base1 public:void who()cout“this is the class of base1!“endl; ; class base2 : public base /定义派生类类base2 public:void who()cout“this
30、is the class of base2!“endl; ;,class derive:public base1,public base2 /定义派生类derive public:void who()coutwho();ptr2 = ,此时,程序执行的结果为 This is the class of derive! This is the class of derive!,假设基类和派生类都只有一个公有的数据成员,其中类A有vfunc1和vfunc2两个虚函数和func1和func2两个实函数。类A公有派生类B,类B改写vfunc1和func1函数,它又作为类C的基类,公有派生类C。类C也改写
31、vfunc1和func1函数。下图给出3个类建立的vptr和vtable之间的关系图解以及实函数与虚函数的区别。,8.3.3 进一步探讨虚函数与实函数的区别,首先给vptr分配地址,它所占字节数决定对象中最长数据成员的长度。因为3个类的数据成员都是整型,所以VC为vptr分配4个字节。如果有double型的数据,则要分配8个字节。【例9.3】是演示这一关系的程序。 从图中可见,对象的起始地址是vptr。它指向vtable,vtable为每个虚函数建立一个指针函数,如果只是继承基类的虚函数,则它们调用基类的虚函数,这就是b和c的vtable表中(*vfunc2)( )项所描述的情况。如果派生类改
32、写了基类的虚函数,则调用自己的虚函数,这就是b和c的vtable表中(*vfunc1)( )项所描述的情况。 实函数不是通过地址调用,用带底纹的方框表示,它们由对象的名字支配规律决定。 【例9-7】是程序实现。,【例8-7】实函数和虚函数调用过程。 #include using namespace std; class A public:int m_A;A(int a)m_A=a;void func1()cout“A:func1( )“endl;void func2()cout“A:func2( )“endl;virtual void vfunc1()cout“A:vfunc1( )“endl
33、;virtual void vfunc2()cout“A:vfunc2( )“endl; ;,class B:public A public:int m_B;B(int a, int b):A(a),m_B(b)void func1()cout“B:func1( )“endl;void vfunc1()cout“B:vfunc1( )“endl; ; class C:public B public:int m_C;C(int a, int b, int c):B(a,b),m_C(c)void func1()cout“C:func1( )“endl;void vfunc1()cout“C:vf
34、unc1( )“endl; ;,void main() /输出类的长度(字节数)coutsizeof(A)“,“sizeof(B)“.“ sizeof(C)endl;A a(11);B b(21,22);C c(31,32,33);/输出类的首地址及数据成员地址,验证首地址是vptr地址cout,/使用基类指针A* pa=,pa= /静态联编,只能调用A:func2(),coutvfunc1(); /调用C:vfunc1()pb-vfunc2(); /调用A:vfunc2()pb-func1(); /静态联编,只能调用B:func1()pb-func2(); /静态联编,只能调用A:func2
35、()coutvfunc1(); /调用C:vfunc1( )pc-vfunc2(); /调用A:vfunc2( )pc-func1(); /静态联编,调用C:func1()pc-func2(); /静态联编,只能调用A:func2( ) ,对象a有一个整型数据,应分配4个字节,vptr也是4个字节,总共8个字节。对象b和c依次增加一个整型数据成员,内存分配也顺增4个字节。输出结果如下: 8,12.16 0012FF78,0012FF7C /vptr, m_A 0012FF6C,0012FF70,0012FF74 /vptr, m_A, m_B 0012FF5C,0012FF60,0012FF6
36、4,0012FF68 /vptr, m_A,/ m_B, m_C / A* pa= A:vfunc1() A:vfunc2() A:func1(),A:func2() / pa= B:vfunc1() A:vfunc2() B:func1(),A:func2() / pb= C:vfunc1() A:vfunc2() C:func1() A:func2(),8.3.4 构造函数和析构函数调用虚函数 在构造函数和析构函数中调用虚函数时,采用静态联编,即它们所调用的虚函数是自己的类或基类中定义的函数,但不是任何在派生类中重定义的虚函数。下面给出一个具体的例子。 【例9.4】在构造函数和析构函数中调
37、用虚函数。#include using namespace std;class A public:A()virtual void func( ) cout “Constructing A “ endl; A( ) virtual void fund( ) cout “Destructor A “ endl; ;,class B : public A public:B( ) func( ); void fun( ) cout“Come here and go.“; func( ); B( ) fund( ); ;class C : public B public:C( ) void func(
38、) cout “Class C“ endl; C( ) fund();void fund( ) cout “Destructor C “ endl; ;,void main( ) C c;c.fun( ); 输出结果如下: Constructing A / 建立对象c调用B( )产生 Come here and go.Class C / c.fun( )输出 Destructor C / 析构对象c时,由C()产生 Destructor A / 析构对象c时调用B()产生 在建立C类的对象c时,它所包含的基类子对象在派生类中定义的成员建立之前被建立。 在对象撤消时,该对象所包含的在派生类中定义
39、的成员要先于基类子对象之前撤消。,函数func是虚函数,构造对象c时,类A构造函数是空函数,没有输出。执行类B的构造函数时调用func,但B没有定义func,所以调用基类A定义的虚函数func。类C的构造函数为空函数,所以构造对象c时,只有一句输出信息。 执行语句“c.fun();”时,类C自己没有函数fun,转去执行它的直接基类B的fun,输出“Come here and go.”。这个函数fun接着调用func,此时是执行基类B的func还是派生类的func?显然,c是派生类C的对象,类C有自己的func。按照虚函数调用规则,它不会去调用基类A的func,而应该执行自己的func,输出“C
40、lass C”。 析构时应先调用C的析构函数,输出“Destructor C”。接着调用类B的析构函数,这个析构函数调用虚函数fund。这个虚函数分别在类B的基类A和派生类C中定义,它只能调用它的基类中的虚函数fund,输出“Destructor A”。基类A中的析构函数没有输出信息,程序结束运行。 目前推荐的C+标准不支持虚构造函数。由于析构函数不允许有参数,因此一个类只能有一个虚析构函数。虚析构函数使用virtual 说明。只要基类的析构函数被说明为虚,函数,则派生类的析构函数,无论是否使用virtual进行说明,都自动地成为虚函数。 delete运算符和析构函数一起工作(new 和构造函
41、数一起工作),当使用delete删除一个对象时,delete隐含着对析构函数的一次调用,如果析构函数为虚函数,则这个调用采用动态联编。一般说来,如果一个类中定义了虚函数,析构函数也应说明为虚函数,尤其是在析构函数要完成一些有意义的任务时,例如释放内存等。 如果基类的析构函数为虚函数,则在派生类未定义析构函数时,编译器所生成的析构函数也为虚函数。,8.3.5 虚函数的限制如果我们将所有的成员函数都设置为虚函数,当然是很有益的。它除了会增加一些额外的资源开销,没有什么坏处。但设置虚函数须注意以下几点。只有成员函数才能声明为虚函数。因为虚函数仅适用于有继承关系的类对象,所以普通函数不能声明为虚函数。
42、虚函数必须是非静态成员函数。这是因为静态成员函数不受限于某个对象。内联函数不能声明为虚函数。因为内联函数不能在运行中动态确定其位置。构造函数不能声明为虚函数。多态是指不同的对象对同一消息有不同的行为特性。虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,因此,虚构造函数是没有意义的。,析构函数可以声明为虚函数。析构函数的功能是在该类对象消亡之前进行一些必要的清理工作。析构函数没有类型,也没有参数,和普通成员函数相比,虚析构函数情况略为简单些。(动态对象的释放)虚析构函数的声明语法如下:virtual类名例如:class Bpublic:/virtual B()
43、;,8.4 抽象类,8.4.1 纯虚函数一个抽象类至少带有一个纯虚函数。纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的实现内容。纯虚函数的声明形式如下:virtual (参数表) = 0纯虚函数与一般虚函数在书写形式上的不同在于其后面加了“=0”,表明在基类中不用定义该函数,它的实现部分函数体留给派生类去做。,8.4.2 抽象类抽象类的主要作用是通过它为一个类族建立一个公共的接口,使它们能够更有效地发挥多态特性。使用抽象类时需注意以下几点。抽象类只能用作其它类的基类,不能建立抽象类对象。一个抽象类自身无法实例化,而只能通过继承机制,生
44、成抽象类的非抽象派生类,然后再实例化。抽象类不能用作参数类型、函数返回值或显式转换的类型。可以声明一个抽象类的指针和引用。通过指针或引用,我们就可以指向并访问派生类对象,以访问派生类的成员。注:如果派生类给出所有纯虚函数的函数实现,这个派生类就可以声明自己的对象,因而不再是抽象类;反之,如果派生类没有给出全部纯虚函数的实现,这时的派生类仍然是一个抽象类。,【例8-8】抽象类例题。/test_abstract.cpp,#includeconst double PI = 3.14159;class Shapes /抽象基类Shapes声明 protected:int x , y;public:vo
45、id setvalue(int xx ,int yy = 0)x = xx ;y = yy;virtual void display() = 0; /纯虚函数成员;,class Rectangle : public Shapes /派生类Rectangle声明public: /虚成员函数void display()cout“The area of rectangle is:“x*yendl; ;class Circle : public Shapes /派生类Circle声明public: /虚成员函数void display()cout“The area of circle is:“PI*x*xendl; ;void main() Shapes* ptr2; /声明抽象基类指针Rectangle rect1;Circle cir1;,ptr0= ,本例的程序运行结果为The area of rectangle is:40The area of circle is : 314.159 另外,程序中派生类的虚成员函数display()并没有用关键字virtual显式说明,因为它们与基类的纯虚函数具有相同的名称及参数和返回值,由系统自动判断确定其为虚成员函数。,