为什么我们需要虚拟表?

时间:2010-06-09 09:19:59

标签: c++

我正在寻找有关虚拟桌的一些信息,但找不到任何易于理解的内容 有人可以给我提供很好的例子(不是来自维基),附带解释或链接吗?

5 个答案:

答案 0 :(得分:9)

如果没有虚拟表,您将无法使运行时多态性工作,因为所有对函数的引用都将在编译时绑定。一个简单的例子

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表指针找到),然后它调用表条目指向的函数。该可能Base::f,但它也可能指向其他内容 - 例如Derived::f

这意味着由于虚拟表,您可以在运行时具有多态性,因为调用的实际函数是在运行时通过查找虚拟表中的函数指针然后通过该指针调用函数来确定的 - 而不是直接调用函数(非虚函数的情况)。

答案 1 :(得分:5)

要回答您的标题问题 - 您没有,并且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 ++程序员来说坦率地说并不感兴趣 - 我在回答这样的问题时只会考虑它。

答案 2 :(得分:4)

虚函数表是一个实现细节 - 它是编译器在类中实现多态方法的方式。

考虑

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()方法,编译器添加代码来获取第三个函数指针并使用它。然后,这将指向适当的实现。

使用非虚方法,这不是必需的,因为被调用的实际函数可以在编译时确定 - 只有一个可能的函数可以被调用非虚拟调用。

答案 3 :(得分:1)

简答:虚函数调用basePointer-&gt; f(),根据basePointer的历史记录意味着不同的东西。如果它指向Really是派生类的东西,则会调用另一个函数。

为此,编译器执行一个简单的函数指针游戏。要为不同类型调用的函数的地址存储在虚拟表中。

虚拟表不仅用于函数指针。 RTTI机制将它用于运行时类型信息(获取由其中一种基类型的地址引用的对象的实际类型)。

一些新的/删除实现会将对象大小存储在虚拟表中。

Windows COM编程使用虚拟表破解并将其作为接口推送。

答案 4 :(得分:0)

假设PlayerMonster从定义虚拟Actor操作的抽象基类name()继承。进一步假设你有一个函数要求演员提供他的名字:

void print_information(const Actor& actor)
{
    std::cout << "the actor is called " << actor.name() << std::endl;
}

在编译时不可能推断演员是真的是玩家还是怪物。由于它们具有不同的name()方法,因此决定调用哪个方法必须延迟到运行时。编译器向每个actor对象添加了附加信息,允许在运行时做出此决定。

在我所知道的每个编译器中,这个附加信息是一个指针(通常称为 vptr )指向一个特定于该函数指针的函数指针表(通常称为 vtbl )。具体课程。也就是说,所有玩家对象共享相同的虚拟表,其中包含指向所有玩家方法的指针(对于怪物来说也是如此)。在运行时,通过从应该调用该方法的对象的vptr指向的vtbl中选择方法,找到正确的方法。