C++中虚函数的原理和虚函数表

一, 什么是虚函数

简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的做用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差别而采用不一样的策略,虚函数是C++的多态性的主要体现,指向基类的指针在操做它的多态类对象时,会根据不一样的类对象,调用其相应的函数,这个函数就是虚函数。ios

下面咱们从这段代码中来进行分析:数组

 


ide

运行结果很简单函数

I am in class A fun1性能

I am in class A fun2this

I am in class B fun1spa

I am in class B fun2指针

但这是否真正作到了多态性呢?No,多态还有个关键之处就是一切用指向基类的指针或引用来操做对象。那如今就把main()处的代码改一改。code

对象

此次的运行结果

I am in class A fun1

I am in class A fun2

I am in class A fun1

I am in class A fun2

问题来了,ptr明明指向的B的对象,为何调用的倒是A的函数呢?要解决这个问题,就要用到了虚函数,咱们在修改函数

这时候咱们发现运行结果变了

I am in class A fun1

I am in class A fun2

I am in class B fun1

I am in class A fun2

由于fun1是虚函数,B类继承A类的fun1默认也是虚函数,简单总结下,指向基类的指针在操做它的多态类对象时,会根据不一样的类对象,调用其相应的函数,这个函数就是虚函数。

fun2不是虚函数,因此调用的仍旧是A类的fun2函数

二, 虚函数是如何作到的

虚函数是如何作到因对象的不一样而调用其相应的函数的呢?如今咱们就来剖析虚函数

因为这两个类中有虚函数存在,因此编译器就会为他们两个分别插入一段你不知道的数据,并为他们分别建立一个表。那段数据叫作vptr指针,指向那个表。那个表叫作vtbl,每一个类都有本身的vtbl,vtbl的做用就是保存本身类中虚函数的地址,咱们能够把vtbl形象地当作一个数组,这个数组的每一个元素存放的就是虚函数的地址,请看图

能够看到这两个vtbl分别为class A和class B服务。如今有了这个模型以后,咱们来分析下面的代码

A *p=new A;

  p->fun();

毫无疑问,调用了A::fun(),可是A::fun()是如何被调用的呢?它像普通函数那样直接跳转到函数的代码处吗?No,实际上是这样的,首先是取出vptr的值,这个值就是vtbl的地址,再根据这个值来到vtbl这里,因为调用的函数A::fun()是第一个虚函数,因此取出vtbl第一个slot里的值,这个值就是A::fun()的地址了,最后调用这个函数。如今咱们能够看出来了,只要vptr不一样,指向的vtbl就不一样,而不一样的vtbl里装着对应类的虚函数地址,因此这样虚函数就能够完成它的任务。


而对于class A和class B来讲,他们的vptr指针存放在何处呢?其实这个指针就放在他们各自的实例对象里。因为class A和class B都没有数据成员,因此他们的实例对象里就只有一个vptr指针。经过上面的分析,如今咱们来实做一段代码,来描述这个带有虚函数的类的简单模型

用VC或Dev-C++编译运行一下,看看结果是否是输出3,void (*fun)(A*); 这段定义了一个函数指针名字叫作fun,并且有一个A*类型的参数,这个函数指针待会儿用来保存从vtbl里取出的函数地址

A* p=new B; new B是向内存(内存分5个区:全局名字空间,自由存储区,寄存器,代码空间,栈)自由存储区申请一个内存单元的地址而后隐式地保存在一个指针中.而后把这个地址附值给A类型的指针P.

long lVptrAddr; 这个long类型的变量待会儿用来保存vptr的值

memcpy(&lVptrAddr,p,4); 前面说了,他们的实例对象里只有vptr指针,因此咱们就放心大胆地把p所指的4bytes内存里的东西复制到lVptrAddr中,因此复制出来的4bytes内容就是vptr的值,即vtbl的地址

如今有了vtbl的地址了,那么咱们如今就取出vtbl第一个slot里的内容

memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4); 取出vtbl第一个slot里的内容,并存放在函数指针fun里。须要注意的是lVptrAddr里面是vtbl的地址,但lVptrAddr不是指针,因此咱们要把它先转变成指针类型

fun(p); 这里就调用了刚才取出的函数地址里的函数,也就是调用了B::fun()这个函数,也许你发现了为何会有参数p,其实类成员函数调用时,会有个this指针,这个p就是那个this指针,只是在通常的调用中编译器自动帮你处理了而已,而在这里则须要本身处理。

delete p; 释放由p指向的自由空间;

若是调用B::fun2()怎么办?那就取出vtbl的第二个slot里的值就好了

memcpy(&fun,reinterpret_cast<long*>(lVptrAddr+4),4); 为何是加4呢?由于一个指针的长度是4bytes,因此加4。或者memcpy(&fun,reinterpret_cast<long*>(lVptrAddr)+1,4); 这更符合数组的用法,由于lVptrAddr被转成了long*型别,因此+1就是日后移sizeof(long)的长度

 


虚函数表

 

类的虚函数表是一块连续的内存,每一个内存单元中记录一个JMP指令的地址

  注意的是,编译器会为每一个有虚函数的类建立一个虚函数表,该虚函数表将被该类的全部对象共享。类的每一个虚成员占据虚函数表中的一行。若是类中有N个虚函数,那么其虚函数表将有N*4字节的大小。

  虚函数(Virtual Function)是经过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,因此,当用父类的指针来操做一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图同样,指明了实际所应该调用的函数。

  编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——若是有多层继承或是多重继承的状况下)。 这意味着能够经过对象实例的地址获得这张虚函数表,而后就能够遍历其中函数指针,并调用相应的函数。

相关文章
相关标签/搜索