我对虚函数有一些疑问,或者我们可以说运行时多态性.据我说,我假设它的工作方式如下,
将为每个具有至少一个虚拟成员函数的类创建虚拟表(V表).我相信这是静态表,因此它是为每个类创建的,而不是为每个对象创建的.如果我错了,请在此纠正我.
该V表具有虚函数的地址.如果该类有4个虚函数,那么该表有4个条目指向相应的4个函数.
编译器将添加一个虚拟指针(V-Ptr)作为该类的隐藏成员.该虚拟指针将指向虚拟表中的起始地址.
假设我有这样的程序,
class Base
{
virtual void F1();
virtual void F2();
virtual void F3();
virtual void F4();
}
class Der1 : public Base //Overrides only first 2 functions of Base class
{
void F1(); //Overrides Base::F1()
void F2(); //Overrides Base::F2()
}
class Der2 : public Base //Overrides remaining functions of Base class
{
void F3(); //Overrides Base::F3()
void F4(); //Overrides Base::F4()
}
int main()
{
Base* p1 = new Der1; //Believe Vtable will populated in compile time itself
Base* p2 = new Der2;
p1->F1(); //how does it call Der1::F1()
p2->F3(); //how does it call Base::F3();
}
Run Code Online (Sandbox Code Playgroud)
如果V-Table在编译时填充,为什么称它为Run Time Polymorphism?请使用上面的例子解释我有多少vtable和vptr以及它是如何工作的.据我说,3个Vtables将用于Base,Der1和Der2类.在Der1 Vtable中,它具有自己的F1()和F2()的地址,而对于F3()和F4(),地址将指向Base类.另外3个Vptr将作为隐藏成员添加到Base,Der1和Der2类中.如果在编译时确定所有内容,那么在运行时会发生什么?如果我在概念上错了,请纠正我.
它显然是实现定义的,但大多数实现都非常相似,或多或少与您描述的一致.
这是对的.
vtables不仅包含指向函数的指针.通常有一个条目指向RTTI信息,并且通常有一些关于如何在调用函数时修复this指针的信息(虽然这也可以使用trampolines来完成).在虚拟基础的情况下,虚拟基础也可能存在偏移.
这也是正确的.请注意,在构造和销毁期间,编译器将更改vptr对象的动态类型更改,并且在多重继承(有或没有虚拟基础)的情况下,将有多个vptr.(vptr相对于类的基址,它处于固定偏移量,并且在多重继承的情况下,并非所有类都可以具有相同的基址.)
至于你的最后评论:vtable在编译时填充,并且是静态的.但是vptr是在运行时根据动态类型设置的,函数调用使用它来查找vtable并调度调用.
在你的(非常简单的)例子中,有三个vtable,每个类一个.因为只涉及简单的继承,所以每个实例只有一个vptr Base,在派生类之间共享.V表用于Base将包含四个插槽,指向Base::f1,Base::f2,Base::f3和Base::f4.对于虚函数表Der1也将包含四个插槽,指向
Der1::f1,Der1::f2,Base::f3和Base::f4.对于虚函数表Der2将指向Base::f1,Base::f2,Der2::f3和
Der2::f4.构造函数Base将把vptr设置为表的Base; 派生类的构造函数将首先调用基类的构造函数,然后将vptr设置为与其类型对应的vtable.(实际上,在这种简单的情况下,编译器可能能够确定vptr从未在构造函数中使用Base,因此跳过设置它.在更复杂的情况下,编译器无法看到基础的所有行为但是,类构造函数并非如此.)
至于它被称为运行时多态的原因,请考虑一个函数:
void f(Base* p)
{
p->f1();
}
Run Code Online (Sandbox Code Playgroud)
实际调用的函数将有所不同,具体取决于是p指向a Der1还是a Der2.换句话说,它将在运行时确定.