https://blog.csdn.net/primeprime/article/details/80776625
“现在,我们声明一个类A的指针p来指向对象bObject。虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象bObject的虚表指针。bObject的虚表指针指向类B的虚表,所以p可以访问到B vtbl。如图3所示。“
解读:基类实例、派生类实例对象的内存地址头指向类的虚函数表,实例对象的内存地址中从低位到高位(栈内存分配从高字节到低字节,然后分配的连续字节是从低字节到高字节方向阅读的)分别存放基类部分、中间派生类派生部分、当前派生类派生部分。但并不是说派生类实例对象的内存地址头就都指向基类的虚表,不是这样的,仍然指向派生类的虚表
每个派生类都会对从基类继承的所有虚函数建立虚函数表项,不管自己是否重载了某个虚函数。这造成派生关系较长时每个派生类都要维护从基类继承来的虚函数在虚函数表中的项,也是一笔不小的内存开支。MFC的消息函数(方法)也是重载的基类(CDialog、CEdit等基类)的虚函数,但消息路由是通过类静态成员的一张路由表来进行的,避免了虚函数表的建立。在台湾哥孙鑫的《深入浅出MFC》中有详细介绍。
关于1、类实例对象的首地址指向虚函数表;2、而虚函数表中虚函数项又是按基类中声明顺序排列的;3、并且虚函数表只是指针数组而不包含其他内容(因为虚函数表中的元素——虚函数指针已经是按照基类中虚函数声明顺序排列的,调用一个虚函数时知道这个虚函数的名字就可以随机计算出其在虚函数表中的下标)。有一例详细的佐证:
https://baike.baidu.com/item/%E8%99%9A%E5%87%BD%E6%95%B0
class A{//虚函数示例代码
public:
virtual void fun(){cout<<1<<endl;}
virtual void fun2(){cout<<2<<endl;}
};
class B : public A{
public:
void fun(){cout<<3<<endl;}
void fun2(){cout<<4<<endl;}
};
#include<iostream>
using namespace std;
//将上面“虚函数示例代码”添加在这里
int main()
{
void(*fun)(A*);
A *p=new B; //基类指针p指向派生类B类实例对象,而首地址是虚函数表,的第一个虚函数B::func()函数地址
long lVptrAddr;
memcpy(&lVptrAddr,p,4); //把虚函数表指向的首地址开始的4个字节的B::fun()函数地址拷贝给整数lVptrAddr
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4); //将整数lVptrAddr存储的B::fun()函数地址拷贝给函数指针fun
fun(p); //执行B::func(),输出3
delete p;
system("pause");
return 0;
}
输出3;