C++多态之动态多态:虚函数,虚表,动态联编

来源:互联网 发布:完整消防队源码 编辑:程序博客网 时间:2024/06/05 04:17

多态指同一个实体同时具有多种形式。C++中的多态性具体体现在运行和编译两个方面。
1、运行时多态是动态多态,其具体引用的对象在运行时才能确定。
2、编译时多态是静态多态,在编译时就可以确定对象使用的形式。
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。
C++中,实现多态有以下方法:虚函数,抽象类,覆盖,模板(重载和多态无关)。

OC中的多态:不同对象对同一消息的不同响应.子类可以重写父类的方法

多态就是允许方法重名 参数或返回值可以是父类型传入或返回。

动态多态(动态联编):主要实现方式 继承,虚函数,重写,虚表
在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。(精髓)
虚函数:带virtual的成员函数:
覆盖(override,重写):这也是虚函数的重点,声明为虚函数不被重写就没啥意义了。
子类重写定义基类中有相同名字相同参数的虚函数。
不同范围内(特指基类派生类)
函数名相同
参数相同
基类函数有virtual关键字修饰
virtual函数是private的,派生类中可重写为public 或者protected。
除了协变(返回值类型不同,都是虚函数的两个函数构成了协变; )

虚函数的特点:
(1)非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。 实际上,优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。

(2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。

(3)当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数(函数名相同、参数列表完全一致、返回值类型相关)自动成为虚函数。

(4)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种同名函数。

(5)不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。

(6)虚函数不能是内联的;

虚表:
带有虚函数的类,就有一个虚表;虚表也就是虚函数的地址;在类内的存在形式是一个指针,放在对象模型的最前面。指针指向一块连续的空间。
如果一个类具有虚函数,那么编译器就会为这个类的对象定义一个指针成员,并让这个指针成员指向一个表格,这个表格里面存放的是类的虚函数的入口地址;比如:一个基类里面有一些虚函数,那么这个基类就拥有这样一个表,它里面存放了自己的虚函数的入口地址,其派生类继承了这个虚函数表,如果在派生类中重写、覆盖、修改了基类中的虚函数,那么编译器就会把虚函数表中的函数入口地址修改成派生类中的对应虚函数的入口地址;这就为类的多态性的实现提供了基础。

虚函数按照其声明顺序存放于虚函数表中;

父类的虚函数存放在子类虚函数的前面;

多继承中,每个父类都有自己的虚函数表;

子类的成员函数被存放于第一个父类的虚函数表中;

对于虚表的几个分析:
分析1:虚拟函数表包含此类及其父类的所有虚拟函数的地址。如果它没有重写父类的虚拟函数,vtable中对应表项指向其父类的此函数。反之,指向重写后的此函数。
分析2:虚拟函数被继承后仍旧是虚拟函数,虚拟函数非常严格地按出现的顺序在 vtable 中排序,所以确定的虚拟函数对应 vtable 中一个固定的位置n,n是一个在编译时就确定的常量。所以,使用vptr加上对应的n,就可得到对应函数的入口地址。
分析3:派生类的虚表指针和基类的虚表指针指向同一个位置;

这是一个简单的多继承,没有重写()

虚析构函数:
虚析构函数是为了解决基类的指针指向派生类对象(为了实现多态这么做了),并用基类的指针删除派生类对象。
将基类的析构函数设为虚函数,虽然基类的虚函数名字和派生类的虚函数名字不一样,但是还是会构成重写(也叫覆盖);
这样delete基类指针时,会动态选择,调用派生类析构函数,派生类的析构函数执行方式是:先析构自己,再析构基类;
无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。没有虚函数,那么就没有动态联编,那么基类指针指向派生类对象就没有意义,把基类析构函数写成虚析构函数也就没有意义了;
可以将虚析构函数声明为内联的,因为运行时才能确认。

C++对象布局:
1、不讨论成员变量
单继承,无覆盖
1)虚函数按照其声明顺序放于表中。
2)子类拷贝父类的虚表,并把父类的虚函数在子类的虚函数前面。
单继承,有覆盖
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
多继承,无覆盖
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
多继承有覆盖
所有父类虚函数表中的f()的位置被替换成了子类的函数指针。
2、讨论成员变量
单继承
1)虚函数表在最前面的位置。
2)成员变量根据其继承和声明顺序依次放在后面。
3)在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。
多继承:一个派生类继承自多个基类
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。
3) 内存布局中,其父类布局依次按声明顺序排列。
4) 每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多态的实现:例如:A类,B和C都继承了它,B和C 大部分的实现方法都和A一样,但是有一些方法B和C有不同的实现方法,这时候只需要让A的对应方法为虚,在B和C中,重写这个方法;调用的时候直接把B或C的指针或引用,传给A的对象或指针,就能实现B和C没重写的函数调用A的方法(注意,如果不是虚函数,B和C里面只要用同名函数就会形成同名隐藏),重写了的就调用B和C自己的方法,这样就实现了多态;(我是这么理解的)

纯虚函数是在基类中只声明虚函数而不给出具体的函数定义体,将它的具体定义放在各派生类中,称此虚函数为纯虚函数.通过该基类的指针或引用就可以调用所有派生类的虚函数,基类只是用于继承,仅作为一个接口,具体功能在派生类中实现.

纯虚函数的声明如下:(注:要放在基类的定义体中)

virtual 函数原型=0;

声明了纯虚函数的类,称为抽象类。

  • 抽象类中可以有多个纯虚函数

  • 不能声明抽象类的对象,但可以声明指向抽象类的指针变量和引用变量

  • 抽象类也可以定义其他非纯虚函数

  • 如果派生类中没有重新定义基类中的纯虚函数,则在派生类中必须再将该虚函数声明为纯虚函数

  • 从抽象类可以派生出具体或抽象类,但不能从具体类派生出抽象类

  • 在一个复杂的类继承结构中,越上层的类抽象程度越高,有时甚至无法给出某些成员函数的实现,显然,抽象类是一种特殊的类,它一般处于类继承结构的较外层

  • 引入抽象类的目的,主要是为了能将相关类组织在一个类继承结构中,并通过抽象类来为这些相关类提供统一的操作接口

明天将会跟大家分享关于静态多态的一些东西;

0 0