多态

来源:互联网 发布:消费类股票 知乎 编辑:程序博客网 时间:2024/06/08 08:21

什么是多态?

       顾名思义,多态就是多种形态。说得准确一点就是同一种事物在不同环境下的不同形态。

多态分类

       我们知道对象的类型有静态类型和动态类型。静态类型就是对象定义时所采用的类型,在编译期间确定;动态类型就是当前所指的类型,在运行时确定。由此可引出对象的静态绑定和动态绑定。静态绑定又名前期绑定,所引用的函数或属性依赖于对象的静态类型,其发生在程序编译时;动态绑定又名后期绑定,其依赖于对象的动态类型,发生在程序运行期间。

       根据静态绑定与动态绑定,我们可将多态分为静态多态动态多态。其中静态多态又可分为函数重载泛型编程,在此不作介绍,本文来介绍动态多态。

动态多态

       同过上面的介绍,不难猜出动态多态是用动态绑定来实现的。

       首先给出动态绑定的定义:程序执行期间判断所引用的对象的实际类型,根据对象的实际类型执行相应的操作。

动态绑定的条件

       1、要实现动态绑定则调用的必须是虚函数(即用virtual关键字修饰的函数,要实现多态则在基类中函数前必须加virtual,然后在派生类中对其进行重写,派生类中可不加virtual)。

        2、必须用基类的指针或引用进行调用。

动态多态实例

       我们给出如下继承体系:

class Base{public:virtual void FunTest1(){cout << "Base::FunTest1()" << endl;}int _b;};class Derive :public Base{public:virtual void FunTest1(){cout << "Derive::FunTest1()" << endl;}int _d;};

我们给出基类Base和派生类Derive,采用公有继承,两个类成员访问类型均为public,在类Base中定义了成员函数FunTest1,并用virtual声明为虚函数,然后在类Derive中进行重写(返回值类型、函数名和参数列表都必须相同(协变除外),访问类型可不同),当然Derive中可不加virtual,两个类中的函数执行不同的输出操作。此时动态绑定的条件1---虚函数,已经准备好了,再来给出条件2:

void test(Base& b){b.FunTest1();}

在类外我们定义一个test非成员函数,其形参为一个基类的引用,然后用这个引用来调用继承关系中的FunTest1函数。

        现在,条件2也准备好了,现在我们在主函数中调用test函数:

int main(){Base b;test(b);Derive d;test(d);return 0;}

在主函数中定义了一个基类对象和一个派生类对象,分别调用test函数进行传参。我们知道在调用基类和派生类的FunTest1函数会有不同的输出,而在test函数中是用基类Base的一个引用调用FunTest1函数。在运行程序之前我们来对结果作猜测:猜测1、如果两次函数调用都是调用基类Base中的函数,那么会输出两次Base::FunTest1(),猜测2、如果两次调用为分别调用基类和派生类中的函数,那么就会输出一次Base::FunTest()1和一次Derive::FunTest1()。现在我们来看看调用结果:

两次输出结果不同,而且与调用顺序一致!显然与猜测2吻合,程序调用了不同类中的虚函数,实现了动态多态。

        在上面的例子中,我们先满足了动态绑定的两个条件,然后对test函数传不同类型的参数。其中,在用派生类对象作参数时,因为继承的赋值兼容原则----父类的指针或引用可以指向子类对象,因此这样传参是合法的。经过编译后,在程序运行期间,系统对引用所指的对象进行类型解析,根据实际类型进行相应的调用。这就是动态绑定,这样就实现了多态。

       通过了上面的例子及分析,我们知道了动态绑定是怎么执行的,那么,系统对对象的类型解析到底是怎么进行的呢?

单继承中基类及派生类的对象模型

       要知道系统对对象的类型解析,我们必须研究基类及派生类的对象模型。

       我们对上面的继承关系做如下修改:

class Base{public:virtual void FunTest1(){cout << "Base::FunTest1()" << endl;}int _b;};class Derive :public Base{public:virtual void FunTest1(){cout << "Derive::FunTest1()" << endl;}virtual void FunTest2(){cout << "Derive::FunTest2()" << endl;}void FunTest3(){cout << "Derive::FunTest3()" << endl;}int _d;};

在这段代码中,基类Base不变,派生类Derive新增两个函数,分别是虚函数FunTest2和非虚函数FunTest3。在主函数中进行如下操作:

int main(){cout << sizeof(Base) << endl;cout << sizeof(Derive) << endl;return 0;}

分别输出基类Base和派生类Derive的大小。我们可以先来分析一下:类的函数一般都储存在一个公共的区域,不在类对象中,因此类对象的大小一般为类数据成员及内存对齐的总大小,而这里的基类Base中数据成员只有一个int类型的变量_b,派生类中仅新增了一个int类型的变量_d,可以据此推测,基类Base的大小为4个字节,而基类Derive继承了基类的成员,因此有8个字节。是否真的如分析推测一样呢?我们来看:

8和12!why?比我们推测的都大了4个字节!看来对象中不仅仅有数据成员,还多了一些其他的东西。到底多了什么东西呢?

      我们在主函数中进行如下操作:

int main(){Base b;b._b = 1;Derive d;d._b = 2;d._d = 3;return 0;}

定义了一个基类对象和一个派生类对象,并分别对其成员数据进行赋值。首先对基类对象进行取地址操作,查看基类对象在内存中的分布:

我们可以观察到,基类对象b在内存的8个字节空间中前4个字节储存着一些不知名的数据,后4个字节中存放着基类的数据成员_b。这种结构我们前面遇到过------虚继承体系中,派生类对象的前4个字节存放着一张偏移量表的地址,偏移量表中放着偏移量值。而这里的对象为基类对象,是否这里也存在偏移量表呢?我们进入这个地址看一下:

如图,显然这个地址所指向的空间中存放的数据并不是偏移值,也就不可能是偏移量表了,而看着更像是一个地址,那么是什么地址呢?在这里只能猜测是基类中函数的地址,而基类中只有一个虚函数FunTest1,再加上地址旁边提示已经很明显了-----Base::FunTest1()。我们来取出这个地址来调用一下,看输出结果是否和基类中虚函数输出结果相同,给出代码:

typedef void (*pFts)();int main(){Base b;b._b = 1;(*((pFts*)(*(int*)&b)))();return 0;}

首先在全局区域定义一个与FunTest1函数类型相同的函数指针类型,然后取出基类对象前4个字节的数据将其强制转换为自定义类型的函数指针,然后进行函数调用,我们来看调用结果:

调用了基类中的虚函数。显然,基类对象模型前4个字节存放着一个地址,这个地址指向的空间存放着基类中虚函数的地址。基类的对象模型是这样的:


我们将对象前4个字节所存地址所指向的用于存放虚函数的空间叫做虚表。

        现在,我们来分析派生类的对象模型。我们知道派生类Derive中有两个虚函数、一个普通函数及一个int型变量_d和继承自基类的变量_b,利用之前的代码,我们查看派生类对象的内存空间:

和基类一样,派生类对象的前4个字节存放着虚表的地址,然后才存放数据成员。那么又有新的问题出现-----派生类和基类对象中的虚表有什么联系呢?我们知道派生类中有三个函数,分别是虚函数FunTest1、FunTest2和非虚函数FunTest3,其中虚函数FunTest1与基类中的函数构成重写,虚函数FunTest2与非虚函数FunTest3为新增成员,那么派生类的虚表中到底有几个函数呢?接着来探讨。


可以看见当我们进入虚表时出现了派生类FunTest1和FunTest2的函数名,显然虚表中存放着派生类的两个虚函数,我们用下面的代码来调用这两个函数:

typedef void (*pFts)();int main(){Derive d;pFts *pf = (pFts*)(*(int*)&d);(*pf)();pf += 1;(*pf)();d._b = 2;d._d = 3;return 0;}

我们定义一个派生类对象,然后取出虚表的地址,经过一系列的类型转换后调用函数。在前面的观察中,我们调出派生类虚表看到里面注释了存在两个虚函数,因此,这里我们先来调用这两个虚函数,调用结果如下:


成功调用派生类的两个虚函数。那么派生类的第三个函数是否也在虚表里面呢?其实想想也知道不在里面,虚表之所以叫做虚表,是因为是存放虚函数的地方,FunTest3不是虚函数,因此肯定没有放在虚表中,我们怀着科学的态度,来试着调用一下第三个函数:

typedef void (*pFts)();int main(){Derive d;pFts *pf = (pFts*)(*(int*)&d);(*pf)();pf += 1;(*pf)();pf += 1;(*pf)();d._b = 2;d._d = 3;return 0;}

执行结果:


可以看出,在我们试图调用第三个函数时程序出错了,我们分析可以知道,这里访问的内存不属于第三个函数而是一个未知的地址,所以在进行访问时出错了。

        通过上面的分析我们可以知道,所有虚函数都放在虚表中,而非虚函数这没有进入虚表。

        我们知道在继承体系中,派生类的对象模型如下:

那么问题又来了,在上面的观察中,单继承体系基类与派生类的虚表地址都在对象的前四个字节,而且虚表地址都不同,按道理说派生类的前四个字节数据应该与基类相同的,这里为什么不一样呢?基类与派生类的虚表是否有联系呢?

探讨总结:

       其实派生类继承基类的数据后对前四个字节所指向的虚表进行了改写,在虚表中对进行重写的虚函数进行了覆盖,没有重写的虚函数保持原样,在原有虚函数的末尾添加派生类新增的虚函数,而非虚函数则不在虚表中,这样虽然派生类的虚表是继承自基类,但是派生类对其中的内容进行了更改,是独属于派生类的虚表,因此基类的虚表和派生类的虚表不同,其地址也就自然不同。当对虚函数进行访问时,系统根据虚表地址进入虚表调用相应的虚函数,也就是说编译器的类型检查其实质就是,根据所传递的虚表地址对虚表进行访问的过程。这就是动态绑定的原理了,也是动态多态的原理。



原创粉丝点击