我正在寻找关于虚拟表的信息,但是没有找到容易理解的内容。
有人可以给我一些带有解释的好例子吗?
没有虚拟表,你不能使运行时多态性起作用,因为所有对函数的引用都将在编译时绑定。以下是一个简单的例子:
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
指向什么。
因此,需要使用所谓的“虚表”,它基本上是一张函数指针表。每个具有虚函数的对象都有一个“虚表指针”,指向其类型的对象的虚表。
上述 callF
函数中的代码只需要在虚表中查找Base::f
的条目(根据对象中的虚表指针找到虚表),然后调用该表项指向的函数。那可能是Base::f
,但也有可能指向其他东西-例如Derived::f
。
这意味着由于虚表,您能够在运行时实现多态性,因为通过在虚表中查找函数指针并通过该指针调用函数,从而确定实际被调用的函数-而不是像非虚函数一样直接调用函数。
Base::f
的方式:对于所有类型兼容的类,指向函数 f()
的指针始终存储在相同的偏移量上(根据 维基百科)。例如,如果在 Base
的虚函数表中,指向 Base::f()
的指针存储在偏移量 +4
处,则在 Derived
的虚函数表中,指向 Derived::f()
的指针也存储在偏移量 +4
处。 - kafman虚函数表是编译器实现类中多态方法的一个细节。
考虑以下示例:
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]
当我们有一个Animal
的实例时,我们不知道调用哪个talk()
方法。我们通过查看虚拟表并获取第三个条目来找到它,因为编译器知道它对应于talk
指针(编译器知道Animal的虚拟方法,因此知道vtable中指针的顺序)。
给定一个Animal,为了调用正确的talk()方法,编译器添加代码来获取第3个函数指针并使用它。这样就可以将其指向适当的实现。
对于非虚拟方法,这是不必要的,因为可以在编译时确定要调用的实际函数——对于非虚拟调用只有一个可能被调用的函数。
struct A {
virtual ~A() {}
virtual void f() {}
};
struct B : public A {
void f() {}
};
A * p = new B;
p->f();
需要调用B类的f函数而不是A类的f函数。虚函数表是实现这一点的一种方式,但对于普通的C++程序员来说并不重要 - 只有在回答这样的问题时才会考虑它。
Player
和Monster
都继承自一个抽象基类Actor
,该类定义了一个虚拟的name()
操作。进一步假设您有一个函数,用于向演员询问他的名字:void print_information(const Actor& actor)
{
std::cout << "the actor is called " << actor.name() << std::endl;
}
name()
方法,因此必须推迟到运行时决定调用哪个方法。编译器向每个演员对象添加附加信息,以允许在运行时做出此决策。