C++课件第10章多态Word文件下载.docx
《C++课件第10章多态Word文件下载.docx》由会员分享,可在线阅读,更多相关《C++课件第10章多态Word文件下载.docx(9页珍藏版)》请在冰豆网上搜索。
。
下一章再讲。
⏹函数覆盖:
这是本章所要讨论的。
一、静态联编和动态联编
联编是指一个计算机程序自身彼此关联的过程。
按照联编所进行的阶段不同,可分为两种不同的联编方法:
静态联编和动态联编。
1静态联编
静态联编是指联编工作出现在编译连接阶段,这种联编又称早期联编,因为这种联编过程是在程序开始运行之前完成的,在编译时就解决了程序中的操作调用与执行该操作代码间的关系。
下面举一个静态联编的例子,该例子是图形面积的计算。
例1:
classPoint{//该类计算点的面积
private:
doublex,y;
//点的坐标
public:
Point(doublei,doublej){x=i;
y=j;
}//构造函数
doublearea()const{return0.0;
}//计算面积
};
classRectangle:
publicpoint{//计算矩型面积
doublew,h;
//矩型长、宽
//构造函数,包含对父类成员变量的初始化
Rectangle(doublei,doublej,doublek,doublel);
//覆盖了父类同名函数area()
//area()const表示函数内不可更改成员变量的值,
//保证在该函数内使用的成员变量只是传值,并不发生值的变化
doublearea()const{returnw*h;
}
Rectangle:
:
Rectangle(doublei,doublej,doublek,doublel):
point(i,j)
{w=k;
h=l;
voidfun(Point&
s)//全局函数,传递对象引用
{cout<
s.area();
}
voidmain(){
Rectanglerec(3.0,5.2,15.0,25.0);
fun(rec);
}
该程序的运行结果为:
0。
算出了点的面积而不是矩形面积,为什么?
虽然实例化了一个矩形对象Rectangle:
但是,在调用fun(rec)时,由于Rectangle类继承了Point类,因此Rctangle对象rec实例化时肯定要实例化一个Point,而函数voidfun(Point&
s)的形参类型正好是Point对象引用,因此编译器在编译时把函数实参绑定到Point对象的引用上。
这是静态联编的结果,导致程序输出了所不期望的结果。
虽然上面的例子将voidfun(Point&
s)改为voidfun(Rectangle&
s)可轻易解决问题,但这不符合多态“一个概念,多种实现”的思想,我们希望的是;
维持Point这“一个概念”,让voidfun(Point&
s)作为对外服务的接口,持有一个指向Point的对象&
s,而在具体的矩型、多边型、圆型对象中,动态的按需调用所需的对象。
这需要动态联编技术。
2动态联编
动态联编实际可以在程序运行时动态识别所需调用的对象。
c++规定动态联编是在虚函数的支持下实现的。
二、虚函数
1虚函数基本特点
如果某类中的一个成员函数被说明为虚函数,这就意味着该成员函数在派生类中可能有不同的实现。
当使用一个指针或引用所标识对象操作该成员函数时,对该成员函数调用采取动态联编方式。
声明一个成员函数为虚函数,只需在函数声明前加virtual关键字即可。
C++对virtual函数进行动态联编。
下面将例1的area()声明为虚函数:
例2(为简化,删掉了构造函数):
classPoint{//该类计算点的面积
virtualdoublearea()const{return0.0;
}//虚函数
//矩型长、宽
//子类也可以不写virtual
virtualdoublearea()const{returnw*h;
}};
输出结果是375。
因为area()声明为虚函数,C++对其动态联编,运行时确定,将fun(rec)中的&
s形参绑定到子类对象rec。
形象的说,动态联编是“自下向上”绑定对象的,先看看有没有本类对象,有则绑定本类对象,没有则从最低一级逐级向上绑定,如例2所示。
而静态联编是“自上向下”先看看本类有没有合适对象,有则绑定本类对象,没有则从最高一级开始向下查找绑定对象的,如例1所示。
2运行时多态的好处
可以只曝露较抽象的基类,而隐藏具体类的实现,用户调用抽象的服务得到的是具体的服务。
运行时多态也有利于实现分布式智能化服务。
这些好处是与具体语言无关的。
对例2作伪代码扩展,可以看出运行时多态的好处。
例3(伪代码):
classPOINT{virtualarea()}//抽象服务类
//以下是具体服务类,继承了POINT
classSHAPE1:
POINT{virtualarea()}
classSHAPE2:
POINT{virtualarea()}
classSHAPE3:
fun(Point&
s)//一个抽象服务接口
{s.area()}
SHAPE1S1;
fun(S1);
//具体服务,系统动态确定
SHAPE1S2;
fun(S2);
SHAPE1S3;
fun(S3);
用户只需知道fun()的位置,知道fun()提供计算面积的服务,需要计算具体图形面积时,创建一个该图形对象即可,不必关心这个具体图形面积是哪个对象计算的。
这是一种非常优雅的体系结构。
3进一步理解虚函数
C++动态联编必需的条件是:
⏹要建立子类型关系。
⏹要有虚函数的说明,只有虚函数才可以动态联编。
⏹要虚函数发挥作用,必须用基类的指针(或引用)指向派生类的对象(比如例2中的fun(Point&
s)),
⏹只有地址才能体现多态性,要用指针(或引用)调用虚函数。
如果在一个函数内调用虚函数,如果默认的this指针有效,是符合本条要求的。
例4符合上述要求的动态联编:
#include
<
iostream.h>
classA
{
public:
virtualvoidact1();
//有虚函数
voidact2(){
act1();
//类成员调用虚函数,有默认的this指针
this->
//类函数调用虚函数,显式的this
A:
}//
使用了类限定符,this无效,静态联编
voidA:
act1(){cout<
"
act1()called"
;
}
classB:
publicA{//建立子类型关系
virtualvoidact1();
//有虚函数
};
void
B:
act1()
{cout<
act1()called“;
}
voidmain()
Bb;
b.act2();
运行结果是:
act1()calledB:
act1()calledA:
act1()called
3构造函数调用虚函数
在“第9章继承”曾讲过,派生类构造函数的调用顺序如下:
●先调基类的构造函数产生基类对象
●再调用派生类构造函数
当构造函数内调用虚函数时,仍沿用上述规则,但对虚函数的调用采用的是静态联编而不是动态联编,
为什么会这样,先看例5:
例5:
构造函数中调用虚函数。
classA{
A(){}
//无参构造函数
virtualvoidf(){cout<
f()called."
classB:
publicA{
B(){f();
}//构造函数中调用虚函数
voidg(){f();
classC:
publicB
C(){}
virtualvoidf(){cout<
C:
voidmain()
Cc;
c.g();
输出结果为:
f()called.C:
f()called.
下面分析该程序的运行过程:
(1)当执行Cc;
时,调用构造函数初始化对象c,按构造函数调用规则:
●先调A的构造函数,无输出。
●再调B的构造函数,B()要调虚函数f(),由于B没有实现f(),按虚函数动态绑定“自下而上”的规律,应该先绑定C:
f(),但此时C的构造函数还没有被调用,无法绑定C:
f()。
C++只好规定,构造函数内调用虚函数时用静态绑定。
于是按静态绑定的规律:
●向上找A类,有,输出A:
f()called。
(2)当执行c.g();
c对象已实例化了(所有构造函数都调用完成),回到正常的虚函数动态绑定“自下而上”的规律,g()函数内绑定了C:
:
f(),输出C:
(3)通过上述讨论可知:
C++之所以规定构造函数内调用虚函数是静态联编,原因是无法找到动态联编所需的对象(比如本例中的对象c)。
4语法约定
对于虚函数的使用,C++做了一些规定,把重要的几条罗列如下:
⏹派生类虚函数的返回类型、参数个数要与基类一样。
⏹派生类虚函数参数类型要与基类一样,否则C++按基类虚函数形参类型进行强制转换。
⏹派生类虚函数声明可以不写virtual,只在基类中写就可以了。
5虚函数和虚表
简要讨论一下虚函数在c++中的实现机制。
虚函数在c++中的实现机制用的是虚表和虚指针。
每个类用了一个虚表,每个类的对象用了一个虚指针。
对于以下代码:
classA{
virtualvoidf();
virtualvoidg();
inta};
classB:
publicA{
voidg();
intb;
A有virtualvoidf()和virtualvoidg(),编译器为A类准备了一个虚表vtableA,:
f()的地址
g()的地址
因为B继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:
注意:
因为B:
g是重写了的,所以B的虚表的g放的是B:
g的入口地址,但是f是从A继承的,所以f的地址是A:
f的入口地址。
然后当Bb实例化对象的时候,编译器开始给对象分配空间,除了A的inta,B的成员intb;
以外,还分配了一个虚指针vptr,指向B的虚表vtableB,对象b的布局如下:
vptr:
指向B的虚表vtableB
inta:
继承A的成员
intb:
B成员
当有如下符合动态联编条件的语句:
A*pa=&
b;
pa能访问到b对象的前两项,但访问不到第三项intb。
这行代码反映了多态思想的关键:
将已创建的子类对象的引用或指针指向父类对象---可以是显式的也可以是隐式,这样可以使父类对象为子对象提供外界访问的接口(JAVA也是这样要求的,需要将一个子类对象的引用隐式或显式的传给父类对象),由父类对象对虚函数的访问进行动态绑定。
当pa->
g()开始绑定对象时,编译器知道的是,g()是一个声明为virtual的成员函数,而且其入口地址放在表格(无论是vtalbeA表还是vtalbeB表)的第2项,那么编译器编译这条语句的时候就如是转换:
call*(pa->
vptr)[2]),(这一行可看作伪代码)。
这一项放的是B:
g()的入口地址,则就实现了多态。
(注意b的vptr指向的是B的虚表vtableB)。
C++标准只要求用虚标机制实现多态,至于虚指针vptr到底放在一个对象布局的哪里,标准没有要求,每个编译器自己决定。
如果继承体系的基类的virtual成员不多,而且在派生类要重写的部分占了其中的大多数时候,用C++的虚函数机制是比较好的;
但是如果继承体系的基类的virtual成员很多,或者是继承体系比较庞大的时候,而且派生类中需要重写的部分比较少,那就用另一种多态实现机制:
“名称查找表”效率会高一些,很多的GUI库都是这样的。
三、纯虚函数和抽象类
JAVA语言中有抽象类和接口的语法。
JAVA引入接口的目的主要是将抽象类与子类间的isakind关系与接口与实现类的关系区分开。
C++有抽象类而无接口的语法。
但C++的抽象类也可当作接口使用。
1纯虚函数
纯虚函数是一种特殊的虚函数,它的一般格式如下:
class
类名>
virtual
返回类型函数名(参数表)=0;
…
在许多情况下,在基类中不能对虚函数给出有意义有实现,而把它说明为纯虚函数,它的实现留给该基类的派生类去做。
这就是纯虚函数的作用。
2抽象类
带有纯虚函数的类称为抽象类。
抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的较上层。
抽象类是不能定义对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限。
抽象类的主要作用是将有关的组织在一个继承层次结构中,由它来为它们提供一个公共的根,相关的子类是从这个根派生出来的。
抽象类刻画了一组子类的操作接口的通用语义,这些语义也传给子类。
一般而言,抽象类只描述这组子类共同的操作接口,而完整的实现留给子类。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。
如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。
如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。
本章小结:
动态联编下的多态性(主要体现在函数覆盖)是不太容易理解的,可以分成几个层次理解它:
1设计概计和思想,本章的例3作了较好的说明。
这是与语言无关的,也是最重要的。
2知道动态联编是“自下向上”而静态联编是“自上向下”绑定对象,对于阅读和编写程序很有帮助。
这虽是实现层面的东西,但几乎也与语言无关,因为面向对象语言都按此顺序绑定对象。
3具体到C++,通过理解虚表,理解动态联编是怎样实现的,为什么需要指针或引用调用虚函数。
本章课后习题:
理解例1-例5的代码