1、第9章 多态性、虚函数及其他类型C+支持两种多态性,即编译时的多态性和运行时的多态性。编译时的多态性通过使用重载函数或模板获得,运行时的多态性通过使用继承和虚函数来获得。 联编是描述编译器决定在程序运行时,一个函数调用应执行哪段代码的一个术语,是实现多态性的基础。,主要内容,9.1 多态性 9.2 虚函数 9.3 对象的基类指针与多态性 9.4 多重继承与虚函数 9.5 多重继承与虚基类 9.6 结构和联合 9.7 枚举 9.8 再谈转换函数,9.1 多态性,所谓多态性就是不同对象收到相同的请求消息时,产生不同的动作。 直观地说,多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操
2、作,从而可以使用相同的调用方式来调用这些具有不同功能的同名函数。 1 多态的分类 C+中的多态性可以分为四类: 参数多态:函数模板、类模板实例化产生的多态行为 包含多态:通过虚函数实现的、定义于不同类中的同名成员函数的多态行为 重载多态:通过函数重载和运算符重载实现的多态行为 强制多态:通过强制类型转换实现的多态行为。前面两种统称为通用多态,而后面两种统称为专用多态。,2 多态的实现 多态从实现的角度来讲可以划分为两类:编译时的多态和运行时的多态。 编译时的多态是通过静态联编来实现的。静态联编就是在编译阶段完成的联编。 运行时的多态是用动态联编实现的。动态联编是运行阶段完成的联编。 虚函数提供
3、了一种更为灵活的多态性机制。虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是在运行时才决定如何动作,即所谓的动态联编。,9.1.1 静态联编中的赋值兼容性及名字支配规律派生一个类的原因并非总是为了添加新的数据成员或成员函数,有时是为了重新定义基类的函数。类的对象和调用的函数一一对应,编译时即可确定调用关系,从而产生编译时的多态性。 【例9.1】分析下面程序的输出结果。 #include using namespace std;const double PI=3.14159;class Point private:double x,y;,public: Point(double i,
4、double j) x=i; y=j; double area( ) return 0; ;class Circle : public Point private:double radius;public:Circle(double a, double b,double r):Point(a,b) radius=r; double area( ) return PI*radius*radius; ;,void main() Point a(1.5,6.7);Circle c(1.5, 6.7, 2.5);coutarea()endl; /(4)Point /(6) ,编译器对(1)的解释是:a
5、.area()表达式明确告诉编译器,它调用的是对象a的成员函数area,输出0。 对于(2)而言,c.area()表达式明确表示调用的是对象c的成员函数area,输出19.6349。名字支配规律决定它们调用各自的同名函数area。 (3)和(4)的问题实质是:如果基类和派生类都定义了“相同名称的成员函数”,通过对象指针调用成员函数时,是取决于该指针的基类类型,还是取决于指针实际所指的类型?也就是说,表达式 “p-area()”应该调用Point:area(),还是调用Circle:area()?根据第7章的赋值兼容性规律,应该调用基类的area函数,输出为0。 (5)和(6)的道理与此一样,输
6、出为0。,由此可见,对象的内存地址空间中只包含数据成员,并不存储有关成员函数的信息。这些成员函数的地址翻译过程与其对象的内存地址无关。编译器只根据数据类型翻译成员函数的地址并判断调用的合法性。,9.1.2 动态联编的多态性 如果让编译器动态联编,也就是在编译“Point *p= ”语句时,只根据兼容性规则检查它的合理性。而 “p-area()”调用哪个函数,等程序运行到这里时再决定。让程序给出如下输出: area of Point is 0 area of Circle is 19.6349 area of Circle is 19.6349 area of Circle is 19.6349
7、 为了实现这一目的,就要使类Point的指针p指向派生类函数area的地址。必须给这两个函数一个新的标识符,以便使它们与目前介绍的成员函数区别开来。 假设使用关键字virtual声明Point类的area函数,将这种函数称为虚函数。,virtual double area( ) return 0.0; 当编译系统编译含有虚函数的类时,为它建立一个虚函数表,表中的每一个元素都指向一个虚函数的地址。此外,编译器也为类增加一个数据成员,这个数据成员是一个指向该虚函数表的指针,通常称为vptr。如果派生类Circle没有重写这个area虚函数,则派生类的虚函数表里的元素所指向的地址就是基类Point的
8、虚函数area的地址,即派生类仅继承基类的虚函数,它调用的也是基类的area函数。现在将它改写如下:virtual double area( ) return PI*radius*radius; 这时,编译器也将派生类虚函数表里的元素指向Circle:area(),即指向派生类area函数的地址。,由此可见,虚函数的地址翻译取决于对象的内存地址。编译器为含有虚函数类的对象首先建立一个入口地址,这个地址用来存放指向虚函数表的指针vptr,然后按照类中虚函数的声明次序,一一填入函数指针。当调用虚函数时,先通过vptr找到虚函数表,然后再找出虚函数的真正地址。,派生类能继承基类的虚函数表,而且只要是
9、和基类同名的(参数也相同)成员函数,无论是否使用virtual声明,它们都自动成为虚函数。 如果派生类没有改写继承基类的虚函数,则函数指针调用基类的虚函数。如果派生类改写了基类的虚函数,编译器将重新为派生类的虚函数建立地址,则函数指针调用这个改写过的虚函数。,9.2 虚 函 数虚函数是实现多态性的基础。一旦基类定义了虚函数, 该基类的派生类中的同名函数也自动成为虚函数。9.2.1 虚函数的定义为实现某种功能而假设的函数称作虚函数。虚函数只能是类中的一个成员函数,但不能是静态成员,关键字virtual用于类中该函数的声明中。例如:class A public:virtual void fun(
10、); /声明虚函数;void A:fun( ) / /定义虚函数当在派生类中定义了一个同名的成员函数时,只要该成员函数的参数个数和相应类型以及它的返回类型与基类中同名的虚函数完全一样(例如void area(void) 函数),则无论是否为该成员函数使用 virtual,它都将成为一个虚函数。,9.2.2 虚函数实现多态性的条件 关键字 virtual 指示 C+编译器对调用虚函数进行动态联编。这种多态性是程序运行到此处才动态确定的,所以称为运行时的多态性。产生这种多态性的前提有如下3条: (1) 类之间的继承关系满足赋值兼容性规则; (2) 改写了同名虚函数; (3) 根据赋值兼容性规则使用
11、指针(或引用)。 满足前两条并不一定产生动态联编,必须有第3条才能保证实现动态联编。第3条又有两种情况: 第1种是按赋值兼容性定义使用基类指针(或引用)访问虚函数。 第2种是把指针(或引用)作为函数参数。即这个函数不一定是类的成员函数,可以是普通函数,而且可以重载。,【例9.2】分别使用指针和引用的display函数。 分析下面程序的输出结果: #include using namespace std; const double PI=3.14159; class Point private:double x,y;public:Point(double i, double j) x=i; y=
12、j; virtual double area( ) return 0; ;,class Circle : public Point private:double radius;public:Circle(double a, double b,double r):Point(a,b) radius=r; double area( ) return PI*radius*radius; ; void display(Point *p) coutarea()endl; void display(Point,void main() Point a(1.5,6.7);Circle c(1.5, 6.7,
13、2.5);Point *p= /display(c) 程序输出如下: 0 19.6349 19.6349,9.2.3 进一步探讨虚函数与实函数的区别假设基类和派生类都只有一个公有的数据成员,其中类A有vfunc1和vfunc2两个虚函数和func1和func2两个实函数。类A公有派生类B,类B改写vfunc1和func1函数,它又作为类C的基类,公有派生类C。类C也改写vfunc1和func1函数。,首先给vptr分配地址,它所占字节数决定对象中最长数据成员的长度。因为3个类的数据成员都是整型,所以VC为vptr分配4个字节。如果有double型的数据,则要分配8个字节。从图中可见,对象的起始
14、地址是vptr。它指向vtable,vtable为每个虚函数建立一个函数指针,如果只是继承基类的虚函数,则它们调用基类的虚函数。如果派生类改写了基类的虚函数,则调用自己的虚函数。实函数不是通过地址调用,用带底纹的方框表示,它们由对象的名字支配规律决定。,【例9.3】实函数和虚函数调用过程。 #include using namespace std; class Apublic:int m_A;A(int a)m_A=a;void func1()cout“A:func1( )“endl;void func2()cout“A:func2( )“endl;virtual void vfunc1()c
15、out“A:vfunc1( )“endl;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;vo
16、id vfunc1()cout“C:vfunc1( )“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(
17、); /静态联编,只能调用A:func2()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 0012F
18、F5C,0012FF60,0012FF64,0012FF68 /vptr, m_A, m_B, m_C / A* pa= B:vfunc1() A:vfunc2() A:func1() A:func2(),/pa= C:vfunc1() A:vfunc2() C:func1() A:func2(),9.2.4 构造函数和析构函数调用虚函数在构造函数和析构函数中调用虚函数时,采用静态联编,即它们所调用的虚函数是自己的类或基类中定义的函数,但不是任何在派生类中重定义的虚函数。 【例9.4】在构造函数和析构函数中调用虚函数。#include using namespace std;class A p
19、ublic: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( ) cout “Class C“ endl; C( ) fund();void fun
20、d( ) 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的析构函数,输出“Destructor C”。接着调用类B的析构函数,这个析构函数调用虚函数fund。这个虚函数分别在类B的基类A和派生类C中定义,它只能调用它的基类中的虚函数fund,输出“
21、Destructor A”。基类A中的析构函数没有输出信息,程序结束运行。,目前推荐的C+标准不支持虚构造函数。由于析构函数不允许有参数,因此一个类只能有一个虚析构函数。虚析构函数使用virtual 说明。只要基类的析构函数被说明为虚函数,则派生类的析构函数,无论是否使用virtual进行说明,都自动地成为虚函数。 delete运算符和析构函数一起工作(new 和构造函数一起工作),当使用delete删除一个对象时,delete隐含着对析构函数的一次调用,如果析构函数为虚函数,则这个调用采用动态联编。 一般说来,如果一个类中定义了虚函数,析构函数也应说明为虚函数,尤其是在析构函数要完成一些有意
22、义的任务时,例如释放内存等。 如果基类的析构函数为虚函数,则在派生类未定义析构函数时,编译器所生成的析构函数也为虚函数。,9.2.5 纯虚函数与抽象类 在许多情况下,在基类中不能为虚函数给出一个有意义的定义,这时可以将它说明为纯虚函数。它的定义留给派生类来做。说明纯虚函数的一般形式为:class 类名 virtual 函数类型 函数名(参数列表)=0;点没有面积,可以说明为:virtual double area( )=0;,一个类可以说明多个纯虚函数,包含有纯虚函数的类称为抽象类。一个抽象类只能作为基类来派生新类,不能说明抽象类的对象。例如,将Point类的area( )函数声明为纯虚函数,
23、则Point a(1.5,6.7); /错 但可以说明指向抽象类对象的指针(或引用),例如: Point *pa;,从一个抽象类派生的类必须提供纯虚函数的实现代码,或在该派生类中仍将它说明为纯虚函数,否则编译器将给出错误信息。 说明了纯虚函数的派生类仍是抽象类。如果派生类中给出了基类所有纯虚函数的实现,则该派生类不再是抽象类。 抽象类的这一特点保证了进入类等级的每个类都提供纯虚函数所要求的行为,这保证了围绕这个类等级所建立起来的软件能正常运行,避免了这个类等级的用户由于偶然的失误而影响系统正常运行。,抽象类至少含有一个虚函数,而且至少有一个虚函数是纯虚函数,以便将它与空的虚函数区分开来。下面是
24、两种不同的表示方法:virtual void area( )=0; / 纯虚函数virtual void area( ) / 空的虚函数 在成员函数内可以调用纯虚函数,但在构造函数或析构函数内调用一个纯虚函数将导致程序运行错误,因为没有为纯虚函数定义代码。,【例9.5】 编写一个程序,用于计算正方形、矩形、 直角三角形和圆的总面积。 class shapepublic:virtual double area( )=0; /纯虚函数 ; class square : public shapeprotected:double H;public:square(double i) H=i;double
25、 area( ) return H * H; ;,class circle : public squarepublic:circle(double r) : square(r) double area( ) return H * H * 3.14159; ; class triangle : public squareprotected:double W;public:triangle(double h, double w):square(h) W=w; double area( ) return H * W * 0.5; ; class rectangle : public triangle
26、public:,rectangle(double h, double w) : triangle( h, w ) double area( ) return H * W; ; double total(shape *s,int n) double sum=0.0;for(int i=0; iarea();return sum; #include using namespace std; void main( ) ,shape *s5;s0=new square(4);s1=new triangle(3,6);s2=new rectangle(3,6);s3=new square(6);s4=n
27、ew circle(10);for(int i=0; iarea()endl;double sum=total(s,5);cout“The total area is:“sumendl; ,程序输出结果如下: s0=16 s1=9 s2=18 s3=36 s4=314.159 The total area is:393.159shape类中的虚函数area仅起到为派生类提供一个一致的接口的作用,派生类中重定义的area用于决定以什么样的方式计算面积。由于在shape类中不能对此做出决定,因此被说明为纯虚函数。由此可见,赋值兼容规则使人们可将正方形、三角形和圆等都视为形状,多态性又保证了函数to
28、tal在对各种形状求面积之和时,无须关心当前正在计算哪种具体形状的面积。在需要时,函数total可从这些形状的对象那里获得该对象的面积,成员函数area保证了这点。,9.3 对象的基类指针与多态性 在派生类中,当一个指向基类成员函数的指针指向一个虚函数,并且通过指向对象的基类指针(或引用)访问这个虚函数时,仍发生多态性。 【例9.6】使用基类成员函数的指针产生多态型。#include using namespace std;class base public:virtual void print( ) cout “In Base“ endl; /虚函数;,class derived : pub
29、lic base public:void print( ) cout *pf)( );void main( ) base b;derived d;display( /输出In Derived,9.4 多重继承与虚函数 多重继承可以被视为多个单一继承的组合,因此,分析多重继承情况下的虚函数调用与分析单一继承有相似之处。例如设计下面的类: 【例9.7】多重继承使用虚函数。 #include using namespace std;class A public:virtual void f( )coutCall Aendl;class B public:void f( ) coutCall Bend
30、l;,class C : public A, public B public:void f( ) cout f( ); /输出Call Cpb - f( ); /输出Call Cpc - f( ); /输出Call C,9.5 多重继承与虚基类在C+中,如果在多条继承路径上有一个汇合处,则称这个汇合处的基类为公共基类。显然,可以通过不同的访问路径访问这个基类,从而使这个公共的基类会产生多个实例,引起二义性。如果想使这个公共的基类只产生一个实例,则可以将这个基类说明为虚基类。这要求在从这个公共基类派生新类时,使用关键字virtual将公共基类说明为虚基类。一般的声明形式如下:class 派生类名
31、:virtual 访问控制 基类名,一个派生类可以公有或私有地继承一个或多个虚基类,关键字virtual和关键字public或private的相对位置无关紧要,但要放在基类名之前,并且关键字virtual只对紧随其后的基类名起作用。例如: class D : virtual public A, private B, virtual public C/ 类体 ; 派生类D从虚基类A和C以及非虚基类B派生。关键字virtual 的位置可以在“访问控制”之后,但必须在基类名之前。,base1类和base2类在从base类派生时,使用关键字virtual指明将base类作为它们的虚基类。这样,在der
32、ived类中,base类就只有一个实例。当derived类的对象或成员函数在使用虚基类base类中的成员时,就不会再产生二义性问题。 例如: derived d; int i = d.b; /正确,【例9.8】使用虚基类的例子。 #include using namespace std; class base public:int b;base(int i):b(i)cout“base=“bendl; ; class base1 : virtual public base public:int b1; base1(int i, int j):base(i),b1(j)cout“base1=“b
33、1“,base=“bendl; ;,class base2 : virtual public basepublic:int b2;base2(int i, int j):base(i),b2(j)cout“base2=“b2“,base=“bendl; ; class derived : public base1, public base2 float d1; public:derived(int a, int b, int c,float e) :base1(a,b), base2(b,a),base(c),d1(e)void display()cout“derived=“d1endl;co
34、ut“base=“bendl;cout“base1=“b1endl;cout“base2=“b2endl; ,void main( ) derived d(1,2,3,5.5); d.display();coutd.bendl;coutd.base1:bendl;coutd.base2:bendl; 程序执行结果如下: base=3 base1=2,base=3 base2=1,base=3 derived=5.5 base=3 base1=2 base2=1 3 3 3 由于base类在derived类中只有一个实例,所以可从任何一条继承路径访问该虚基类base类的成员,并且都使用相同的ba
35、se类的实例,例如,d.base1 : b和d.base2 : b 使用的是同一个虚基类base类中的数据成员b,因而具有相同的值。,派生类derived的构造函数初始化列表中,必须单独调用虚基类的构造函数。例如:derived(int a, int b, int c, float e):base1(a,b),base2(b,a),base(c),d1(e) 当初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非基类的构造函数执行,并且只被调用一次,从而保证只建立一个虚基类对象,消除了二义性。对于上面的构造函数derived,优先执行base(c),当再执行base1
36、(a,b)时,base1不再调用base的构造函数对虚基类初始化。同理,base2(b,a)也不调用base的构造函数。从执行结果中可知看出,无论从那条路经访问base的数据成员b,其效果都是一样的。,9.8 再谈转换函数 转换函数可以被派生类继承,也可被说明为虚函数。下面是一个例子。 【例9.11】转换函数说明为虚函数并进行派生的例子。 class number private:int val;public:number( int i ) val = i; virtual operator int( ); /声明为虚函数 ; number : operator int ( ) return
37、val; class num : public number /派生类private:int val2;public:num( int i, int j) : number( i ) val2=j; operator int( ); ; num : operator int ( ) return val2; ,#include /测试程序 using namespace std; void main( ) num n(15,20); /派生类int i =n; / n.operator int ( )=20 cout operator int ()operator int ()“,“; /派生类cout*pendl; 程序运行结果如下: 40,50 25,25 20,20,