对于虚函数而言,这篇博客主要侧重于理解虚函数的实现机制,还有就是如何实现的多态?
在C++中,使用virtual关键字声明的函数为虚函数。当类中有虚函数时,编译器会将该类中所有虚函数的首地址保存在一张表中,这张表也称为虚函数地址表(虚表),在我们代码层面可以理解为函数指针数组。同时编译器还会在类中添加一个隐藏数据成员,该成员称为虚表指针,该指针也就是用于保存前面所说的虚表首地址。
好了,下面就先来看一个简单的虚函数类
class CVirtual
{
public:
virtual int GetNumber() //虚函数一
{
return m_nNum;
}
virtual void SetNumber(int num) //虚函数二
{
m_nNum = num;
}
private:
int m_nNum;
};
上面我们说了虚表指针的概念,对于虚表指针而言,其肯定会在对象的首地址处,由于是指针,所以其大小占用四个字节,那么可以说明上面的这个对象大小应该总共应该是8字节(虚表指针+成员字段m_nNum)
int main(int argc)
{
CVirtual obj;
int objSize = sizeof obj;
return 0;
}
下面,我们可以通过调试来看一下其对象内存分布情况
对于上面其内存布局不太清楚的,可以再看一下下面这内存分配图
OK,上面主要说的是编译器对于虚函数而做的额外工作,当然这些工作是不可少的,下面我们来看看虚函数的调用,就可知道这些虚表和虚表指针的用途了。
int main(int argc)
{
CVirtual obj;
CVirtual *pObj = &obj;
pObj->SetNumber(10); //虚函数调用
return 0;
}
对于虚函数的实现调用,这里我们需要来简单的分析一下其汇编代码
pObj->SetNumber(10);
002817EE 6A 0A push 0Ah //压入参数
002817F0 8B 45 E8 mov eax,dword ptr [pObj] //定位到虚表指针 vfptr
002817F3 8B 10 mov edx,dword ptr [eax] //获取虚表
002817F5 8B 4D E8 mov ecx,dword ptr [pObj] //thiscall 需要传递对象首地址,vfptr肯定在对象首地址的前四个字节,所以其也是虚表指针的位置
002817F8 8B 42 04 mov eax,dword ptr [edx+4] //虚表的第二项
002817FB FF D0 call eax //调用成员函数
结合上面的图片观察,可以发现SetNumber函数正是在函数指针数组的第二项。那么对于虚函数的调用,其本质就是查虚表然后获取其函数地址并调用。
对象
首四字节-vfptr
fun1
fun2
1.获取对象的首四个字节(虚表指针)
2.通过该指针定位到虚表
3.获取虚表内的函数地址
4.调用-(比较明显的特性这里是间接调用)
OK,通过上面的分析,应该对虚函数有了一个宏观上的理解了,下面我们来考虑一些问题,用于加深我们的印象
首先,虚函数必须作为成员函数使用吗?
这里答案显然是肯定的,因为对于非成员函数而言,其没有this指针,也就是无法获取虚表指针,而无法定位到虚表,自然也就无法获取虚函数的地址了。
下个问题,对于上面的例子,可以发现其使用的是指针调用?为什么不使用对象调用
这种查虚表调用的情况只有在指针或者引用的情况下才会出现,因为使用对象调用,其目的很明显,无需进行查表调用,直接调用自身的成员函数即可。
虚表是何时产生的?
虚表信息会在编译后链接到对应的可执行文件中(也就是在你按下编译的那一瞬间),好比编译器使用了一个全局变量用于保存这些虚函