虚表的写入时机
虚表的写入时机指的是虚函数指针指向虚函数表的时机,在C++中创造一个对象要分为两步:1.开辟内存 2.调用构造函数 ,也就是说当完成第一个步骤后,对象的内存就已经被开辟完成了,那么内存中的虚函数指针是什么时候指向虚函数表的呢,首先我们假设虚函数指针是在构造函数之前指向虚函数表的。下面我们用一个例子来探讨这个问题。
include <iostream>
class Base
{
public:
Base()
{
std::memset(this, 0, sizeof(this));
}
virtual void Show()
{
std::cout << "Base's show = "<< std::endl;
}
};
int main()
{
Base* pb = new Base;
pb->Show();
return 0;
}
我们先来自己分析一下整个过程,首先开辟内存,接着虚函数指针指向虚函数表,即
接着调用构造函数,我们在构造函数中将pb对象的所有成员赋值为0,即vfptr被赋值为NULL
此时vfptr已经不再指向vftable,那么接下来pb调用虚函数Show(),程序就会崩溃,因为vfptr在vftable中无法找到Show()函数的入口地址。而编译器执行的结果也正如我们所料
所以在这里我们可以得出结论,虚函数表在编辑阶段生成,虚函数指针在构造函数之前指向虚函数表,即虚表的写入是在构造函数之前进行的。
虚表的二次写入
所谓的虚表的二次写入指的是在继承关系中,派生类对象要调用两次构造函数,一次是基类的,另外一次是自己的,因此虚表要写入两次。
#include <iostream>
class Base
{
public:
Base()
{
std::memset(this, 0, sizeof(this));
}
virtual void Show()
{
std::cout << "Base's show "<< std::endl;
}
};
class Derive :public Base
{
public:
Derive(){}
void Show()
{
std::cout << "Derive's show " << std::endl;
}
};
int main()
{
Derive pd;
Base* pb = &pd;
pb->Show();
return 0;
}
首先是在编译间段生成基类和派生类的虚函数表,注意,其实这里的派生类虚表不是真正的派生类的虚表,最终的派生类的虚表是派生类虚表和基类虚表合并以后的产物。
接着是派生类对象首先给从基类中继承下来的成员开辟内存,接着在调用基类的构造函数之前指向基类的虚表,
接下来调用基类的构造函数,将基类的成员全部赋值为0,即将基类的vfptr指向NULL
接着是派生类对象为自己的成员开辟内存
接着在调用派生类的构造函数之前将vfptr指向派生类的虚表,在这之前,要进行虚表合并,生成派生类最终的虚函数表
即
接着在调用派生类的构造函数之前,将派生类的虚函数指针指向派生类的最终虚函数表
最后进行虚函数指针的合并,由外向内合并
所以最后我们通过pb调用Show()函数,实际上是通过vfptr从虚函数表中找到Show()函数的入口地址,接着调用它,发生了动多态。
程序运行结果: