【问题标题】:Why do we need a virtual table?为什么我们需要一个虚拟表?
【发布时间】:2011-03-01 13:49:23
【问题描述】:

我一直在寻找有关虚拟表的一些信息,但找不到任何易于理解的信息。

谁能给我很好的例子和解释?

【问题讨论】:

  • 不需要它,但编译器需要。

标签: c++ vtable


【解决方案1】:

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

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

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

【讨论】:

  • 也许值得一提的是,在 v-table 中查找 Base::f 是如何工作的:对于所有类型兼容的类,指向函数 f() 的指针始终存储在相同的偏移量处(根据Wikipedia)。例如,如果在Base 的v-table 中指向Base::f() 的指针存储在偏移量+4 中,那么在Derived 的v-table 中指向Derived::f() 的指针也存储在偏移量+4 处。
【解决方案2】:

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

考虑

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 中指针的顺序。) /p>

给定一个 Animal,为了调用正确的 talk() 方法,编译器添加代码来获取第三个函数指针并使用它。然后这会指向适当的实现。

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

【讨论】:

  • A* 动物 = ....; // 来自某处 动物->talk();在这一行中,您可以更具体一些,谢谢
  • 当然 - 假设动物是从文件中加载的,或者根据用户输入创建不同的动物。目的是编译器无法知道我们拥有什么类型的动物。
【解决方案3】:

假设PlayerMonster 继承自定义虚拟name() 操作的抽象基类Actor。进一步假设你有一个函数询问演员的名字:

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

在编译时不可能推断出演员实际上是玩家还是怪物。由于它们具有不同的name() 方法,因此必须将调用哪个方法的决定推迟到运行时。编译器向每个参与者对象添加附加信息,以便在运行时做出此决定。

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

【讨论】:

    【解决方案4】:

    要回答您的标题问题 - 您没有,而且 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++ 程序员并不感兴趣——我只在回答此类问题时才会考虑它。

    【讨论】:

    • 作为替代示例,python 将方法与属性一起存储在对象中。因此,它在不使用virtual 表的情况下完成了此行为,尽管它非常相似。
    • @Matthieu M,python 做的事情与 c++ 完全相同——它存储了对方法对象的一些引用(在 c-python 中用指针实现),而 c++ 存储方法的地址。区别主要在于python表是按对象而不是按类存储的,因为python允许在运行时添加属性和方法。
    • 我实际上不同意:在 C++ 中,虚拟表没有关于存储的函数指针的实际信息,编译器只知道所需的方法位于给定的索引处。另一方面,在 python 中,属性和方法(通常)存储在字典中,您可以按名称查找。这是实现上的一个主要区别,它以性能为代价为 python 提供了更大的灵活性。
    【解决方案5】:

    简答:虚函数调用,basePointer->f(),根据 basePointer 的历史有不同的含义。如果它指向真正是派生类的东西,则会调用不同的函数。

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

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

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

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

    【讨论】:

      猜你喜欢
      • 2021-04-28
      • 2013-10-21
      • 1970-01-01
      • 1970-01-01
      • 2022-08-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多