我一直在寻找有关虚拟表的一些信息,但找不到任何易于理解的信息。
有人可以给我解释的好例子吗?
如果没有虚拟表,您将无法使运行时多态性工作,因为对函数的所有引用都将在编译时绑定。一个简单的例子
struct Base {
virtual void f() { }
};
struct Derived : public Base {
virtual void f() { }
};
void callF( Base *o ) {
o->f();
}
int main() {
Derived d;
callF( &d );
}
在函数内部callF
,你只知道o
指向一个Base
对象。但是,在运行时,代码应该调用Derived::f
(因为Base::f
是虚拟的)。在编译时,编译器无法知道调用将执行哪些代码,o->f()
因为它不知道o
指向什么。
因此,您需要一个称为“虚拟表”的东西,它基本上是一个函数指针表。每个具有虚函数的对象都有一个“v-table 指针”,它指向该类型对象的虚表。
上面函数中的代码callF
只需要Base::f
在虚拟表中查找条目(它根据对象中的 v-table 指针找到),然后调用表条目指向的函数。这可能是Base::f
,但也有可能它指向其他东西 -Derived::f
例如。
这意味着由于虚拟表,您可以在运行时具有多态性,因为被调用的实际函数是在运行时通过在虚拟表中查找函数指针然后通过该指针调用函数来确定的 - 而不是调用直接调用函数(与非虚函数一样)。
虚函数表是一个实现细节——它是编译器在类中实现多态方法的方式。
考虑
class Animal
{
virtual void talk()=0;
}
class Dog : Animal
{
virtual void talk() {
cout << "Woof!";
}
}
class Cat : Animal
{
virtual void talk() {
cout << "Meow!";
}
}
现在我们有了
A* animal = loadFromFile("somefile.txt"); // from somewhere
animal->talk();
我们如何知道talk()
调用的是哪个版本?动物对象有一个表,该表指向与该动物一起使用的虚拟功能。例如,talk
可能在第 3 个偏移量,如果有其他两个虚拟方法:
dog
[function ptr for some method 1]
[function ptr for some method 2]
[function ptr for talk -> Dog::Talk]
cat
[function ptr for some method 1]
[function ptr for some method 2]
[function ptr for talk -> Cat::Talk]
当我们有 的实例时Animnal
,我们不知道talk()
要调用哪个方法。我们通过查看虚拟表并获取第三个条目来找到它,因为编译器知道对应于talk
指针(编译器知道 Animal 上的虚拟方法,因此知道 vtable 中指针的顺序。)
给定一个 Animal,为了调用正确的 talk() 方法,编译器添加代码来获取第三个函数指针并使用它。然后,这指向适当的实现。
对于非虚拟方法,这不是必需的,因为可以在编译时确定被调用的实际函数 - 只有一个可能的函数可以被调用用于非虚拟调用。
要回答您的标题问题-您没有,并且 C++ 标准没有指定必须为您提供一个。你想要的是能够说:
struct A {
virtual ~A() {}
virtual void f() {}
};
struct B : public A {
void f() {}
};
A * p = new B;
p->f();
并调用 B::f 而不是 A::f。虚函数表是实现这一点的一种方式,但坦率地说,普通 C++ 程序员并不感兴趣——我只在回答这样的问题时才会考虑它。
简短的回答:虚函数调用,basePointer->f(),根据 basePointer 的历史有不同的含义。如果它指向真正是派生类的东西,则会调用不同的函数。
为此,编译器做了一个简单的函数指针游戏。不同类型要调用的函数的地址存储在虚拟表中。
虚拟表不仅仅用于函数指针。RTTI 机器将它用于运行时类型信息(获取由一种基类型的地址引用的对象的实际类型)。
一些新的/删除的实现会将对象大小存储在虚拟表中。
Windows COM编程使用虚拟表来破解它并将其作为接口推送。
假设Player
并Monster
继承自Actor
定义虚拟name()
操作的抽象基类。进一步假设你有一个函数询问演员的名字:
void print_information(const Actor& actor)
{
std::cout << "the actor is called " << actor.name() << std::endl;
}
在编译时不可能推断出演员实际上是玩家还是怪物。由于它们具有不同name()
的方法,因此必须将调用哪个方法的决定推迟到运行时。编译器将附加信息添加到每个参与者对象,以允许在运行时做出此决定。
在我知道的每个编译器中,这个附加信息是一个指针(通常称为vptr ),指向特定于具体类的函数指针表(通常称为vtbl )。也就是说,所有玩家对象共享同一个虚拟表,其中包含指向所有玩家方法的指针(怪物也是如此)。在运行时,通过从应该调用该方法的对象的 vptr 指向的 vtbl 中选择方法来找到正确的方法。