在面向对象的语言里面,封装,继承,多态可谓是在熟悉不过了,当我们每次再去重新认识它们的时候总会有新的发现,为此我也经常感到疑惑,所以在这里和大家一起探讨三个问题,让我们在向多态靠近一点点。
虚表是否真的存在静态区
经常我们都会看见一个问题,虚表到底是存放在哪里的。当我们去往上查阅的时候都能出奇的发现一个答案,虚表是存放在静态区中的。对此我曾感到疑惑,存放数据的地方无非有栈,堆,静态区,常量区,首先我们可以排除堆区,因为系统不会自己去开一块空间来使用。接下来我们先了解一下虚表的存放体制,当一个对象在局部域中被创建出来并且它其中也有一张虚表,当局部对象被销毁后在另一个域类定义同一个对象,会发现它们使用的是同一张虚表。
那么现在还剩下栈区,常量区,静态区,我们可以想一下如果虚表是存放在main函数里面的,那么也是符合条件的,同样,常量区和静态区更不用说。对此我们知道一个观念,相同的数据往往它们存放的地址不会太远,那么我们可以利用这一特征来检验一下看虚表离谁的地址比较近。
通过这样一段代码我们可以清楚的看到,虚表的地址似乎离静态区相隔很远,但是离常量区似乎很近,那么我们是不是能得出,其实虚表是存放在常量区而并非静态区呢。
派生类中没被重写的虚表去哪里了
首先我们看这样一段代码
基类中有一个fun1和fun2的虚函数,而派生类有两个不是重写的虚函数
当我们想去虚表里面找到这个fun3和fun4的时候发现这两个函数并没有在虚表里面,那么问题来了,它们去哪里了呢。我们对此进行更深一步的研究,通过这个虚表去内存里面看看是否能找到消失的它。
通过对比我们似乎还真找到了这两个地址,但是怎么证明这两个地址就是我们要找的小时的那两个函数呢。
对此我们在回过头来先在了解一下什么是虚函数表。简称虚表,它是由一个指针指向一块空间,空间里面存放了一些地址相连的值,并且这个地址是一个指向函数的指针。那么我们可不可以以认为虚表实际上是一个函数指针数组呢。
既然这样我们就可以用一个函数指针来取出虚表里面的指针,然后通过这个指针去找到那个函数,对此可以先在函数里面添加一点用于标识的东西。
我们用这样一段代码来证明我们的猜想。
通过验证确实得出,那两个地址就是我们消失的fun3和fun4,这就说明,其实并不是不是重写的函数消失了,而是编译器通过特别的方式把它隐藏了起来。
多继承中被重写的虚函数会被放在哪个基类
假设由这样一段代码,一个子类继承成两个父类,那么在子类中,没有被重写的那个虚函数会被放在父类里面呢。
最好的办法就是去实践它,我们去运行这段代码就会发现,实际上没有被重写的虚函数会被放在base1类里面
但是我们似乎发现了一个不一样的点,同样是派生类对象D去调用的fun1,为什么调用的地址是不一样的呢。
我们先设置两个基类指针去指向派生类的对象, 然后通过反汇编去看一下具体是什么原因。先通过监视窗口去观察base1的虚函数表是个怎么样的
可以看到,base1里面第一个第一个虚函地址为0x003f103c,然后主要看反汇编去调用这个函数的那句指令
看到的是call了这个eax,那么我们再去看一下这个eax是一个什么
可以看到eax和虚函数表里面第一个地址是一样的,就说明是找到了第一个函数,接下来就要开始调用了,我们按F11(逐语句调试)继续往下一步走
通过这个地址我们找到了fun1,继续往后走
通过jmp我们拿到了fun1的真正的地址,最后我们得出结论,base1中的调用是正常的,接下来我们继续看看base2的调用
通过base2同样来到了call这个地址
eax同样和第一个虚函数同样的,到目前一切为止一切正常,我们继续往下走
同样的是一句jmp,继续往下走
来到这里我们就发现这里和上面不一样,当继续jmp的时候它并没有去到fun1的真正地址,而是执行了sub对ecx(*this指针)减去4,实际上是对*this指针减去了4。我们继续往下走
当我们来到这里就会发现,这个地址好像就是在对base1进行fun1调用的时候出现的地址
来到这一步我们才发现,最终base2也是调到了fun1但是它饶了一大圈。那么问题又来了,为什么base1不需要绕一大圈呢,这里咱们可以画个图理解一下
首先我们要知道我们要调的是derive的fun1,那么要调derive的fun1就要让指针指向derive对象,base1不用进行操作时因为derive恰好指向derive的开始,而base2却不是,所以要对它进行-4让它指向derive之后才真正开始调用fun1。