我对虚函数有一些疑问,或者我们可以说运行时多态性。根据我的说法,我假设它的工作方式如下,
将为每个至少具有一个虚拟成员函数的类创建虚拟表(V-Table)。我相信这是静态表,因此它是为每个类创建的,而不是为每个对象创建的。如果我在这里错了,请在此纠正我。
此V-Table具有虚拟功能的地址。如果该类有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();
}
如果V-Table在编译时填充,为什么称它为Run Time Polymorphism?请使用上面的例子解释我有多少vtable和vptr以及它是如何工作的。据我说,3个Vtables将用于Base,Der1和Der2类。在Der1 Vtable中,它具有自己的F1()和F2()地址,而对于F3()和F4(),地址将指向Base类。此外,还将在Base,Der1和Der2类中添加3个Vptr作为隐藏成员。如果在编译时确定所有内容,那么在运行时会发生什么?如果我在概念上错了,请纠正我。
答案 0 :(得分:5)
显然是实现定义,但大多数实现 非常相似,或多或少沿着你描述的路线。
这是正确的。
vtables不仅包含指向函数的指针。 通常有一个条目指向RTTI信息,和 通常一些有关如何修复此指针的信息 调用函数时(虽然这也可以使用 蹦床)。在虚拟基地的情况下,也可能存在 虚拟基地的偏移量。
这也是正确的。注意在施工期间
销毁时,编译器会将vptr
更改为动态
对象的类型更改,并且在多个的情况下更改
继承(有或没有虚拟基础),会有更多
不止一个vptr
。 (vptr
处于固定偏移处
尊重该类的基地址,并且在该情况下
多重继承,并非所有类都可以具有相同的基础
地址)。
关于你的最后评论:vtable是在编译时填充的 时间,是静止的。但是vptr是在运行时设置的, 根据动态类型,函数调用使用它 找到vtable并发出电话。
在你的(非常简单的)例子中,有三个vtable,一个用于
每节课。因为只涉及简单的继承
每个实例只有一个vptr,在Base
和Base
之间共享
派生类。 Base::f1
的vtable将包含四个插槽,
指向Base::f2
,Base::f3
,Base::f4
和Der1
。
Der1::f1
的vtable也将包含四个指向的插槽
Der1::f2
,Base::f3
,Base::f4
和Der2
。 vtable
对于Base::f1
,我会指向Base::f2
,Der2::f3
,Der2::f4
和
Base
。 Base
的构造函数将vptr设置为
Base
的表格;派生类的构造函数将
首先调用基类的构造函数,然后设置vptr
到与其类型相对应的vtable。 (在实践中,在这样的
简单的情况下,编译器可能能够确定
vptr从未在void f(Base* p)
{
p->f1();
}
的构造函数中使用,等等
跳过设置它。在更复杂的情况下,编译器所在的位置
无法看到基类构造函数的所有行为,
但事实并非如此。)
至于为什么它被称为运行时多态,请考虑 功能:
p
实际调用的函数将有所不同,具体取决于
是Der1
指向Der2
还是{{1}}。换句话说,它
将在运行时确定。
答案 1 :(得分:4)
C ++标准没有规定必须如何实现虚函数调用,但这是一个普遍接受的方法的简化示例。
从高层来看,v-table看起来像这样:
<强>基强>:
Index | Function Address
------|------------------
0 | Base::F1
1 | Base::F2
2 | Base::F3
3 | Base::F4
<强> Der1 强>:
Index | Function Address
------|------------------
0 | Der1::F1
1 | Der1::F2
2 | Base::F3
3 | Base::F4
<强> DER2 强>:
Index | Function Address
------|------------------
0 | Base::F1
1 | Base::F2
2 | Der2::F3
3 | Der2::F4
当您创建p1
和p2
时,他们会得到一个指针,分别指向Der1
的vtable和Der2
的vtable
对p1->F1
的调用基本上意味着&#34;在p1
的虚拟表&#34;上调用函数0。
vptr[0]
为Der1::F1
,因此会被调用。
它被称为运行时多态,因为将在运行时确定将为特定对象调用的函数(通过在对象的vtable中查找)。
答案 2 :(得分:2)
它是实现定义的。使用C ++进行编程时,唯一值得关注的是,如果声明方法virtual
,则指针或引用后面的对象的运行时内容将决定将调用哪些代码。
也许你应该read about that topic first。 Here是C ++特有的东西。
答案 3 :(得分:0)
我不打算通过四个虚函数和三个派生类型。可以这么说:对于最终的基类,vtable具有指向所有虚函数的基类的版本的指针。对于派生类,vtable具有指向所有派生类的虚函数的指针;当派生类重写基类函数时,该函数的函数指针指向该虚函数的派生类'版本;当派生类继承虚函数时,函数指针指向继承的函数。