编译器如何知道vtable中的哪个条目对应于虚函数?

Ash*_*win 7 c++ compiler-construction virtual-functions vtable

假设我们在父类和派生类中有多个虚函数.对于父派生类,将在vtable中为这些虚函数创建一个vtable.

编译器如何知道vtable中的哪个条目对应于哪个虚函数?

例:

class Animal{
public:
 void fakeMethod1(){}
 virtual void getWeight(){}
 void fakeMethod2(){}
 virtual void getHeight(){}
 virtual void getType(){}
};

class Tiger:public Animal{
public:
 void fakeMethod3(){}
 virtual void getWeight(){}
 void fakeMethod4(){}
 virtual void getHeight(){}
 virtual void getType(){}
};
main(){
Animal a* = new Tiger();
a->getHeight(); // A  will now point to the base address of vtable Tiger
//How will the compiler know which entry in the vtable corresponds to the function getHeight()?
}
Run Code Online (Sandbox Code Playgroud)

我的研究中没有找到确切的解释 -

/sf/answers/6953901/ =

"该表用于解析函数调用,因为它包含该类的所有虚函数的地址."

该表用于解析函数调用的具体方式是什么?

/sf/answers/14219551/ =

"因此在运行时,代码只使用对象的vptr来定位vtbl,并从那里找到实际被覆盖函数的地址."

我无法理解这一点.Vtable保存虚函数的地址而不是实际重写函数的地址.

5go*_*der 8

我将稍微修改你的例子,以便它显示面向对象的更多有趣方面.

假设我们有以下内容:

#include <iostream>

struct Animal
{
  int age;
  Animal(int a) : age {a} {}
  virtual int setAge(int);
  virtual void sayHello() const;
};

int
Animal::setAge(int a)
{
  int prev = this->age;
  this->age = a;
  return prev;
}

void
Animal::sayHello() const
{
  std::cout << "Hello, I'm an " << this->age << " year old animal.\n";
}

struct Tiger : Animal
{
  int stripes;
  Tiger(int a, int s) : Animal {a}, stripes {s} {}
  virtual void sayHello() const override;
  virtual void doTigerishThing();
};

void
Tiger::sayHello() const
{
  std::cout << "Hello, I'm a " << this->age << " year old tiger with "
            << this->stripes << " stripes.\n";
}

void
Tiger::doTigerishThing()
{
  this->stripes += 1;
}


int
main()
{
  Tiger * tp = new Tiger {7, 42};
  Animal * ap = tp;
  tp->sayHello();         // call overridden function via derived pointer
  tp->doTigerishThing();  // call child function via derived pointer
  tp->setAge(8);          // call parent function via derived pointer
  ap->sayHello();         // call overridden function via base pointer
}
Run Code Online (Sandbox Code Playgroud)

我忽略了一个好的建议,即virtual函数成员的类应该有一个virtual析构函数用于本例的目的.无论如何我要泄漏物体.

让我们看看我们如何将这个例子转化为没有成员函数的好老C,不要单独使用virtual.以下所有代码都是C,而不是C++.

struct animal很简单:

struct animal
{
  const void * vptr;
  int age;
};
Run Code Online (Sandbox Code Playgroud)

除了age成员之外,我们还添加了一个vptr指向vtable的指针.我正在使用一个void指针,因为无论如何我们都必须做丑陋的演员表并使用void *一点点来减少丑陋.

接下来,我们可以实现成员函数.

static int
animal_set_age(void * p, int a)
{
  struct animal * this = (struct animal *) p;
  int prev = this->age;
  this->age = a;
  return prev;
}
Run Code Online (Sandbox Code Playgroud)

注意附加的第0个参数:this在C++中隐式传递的指针.同样,我正在使用void *指针,因为它将在以后简化操作.请注意,任何成员函数内部,我们总是静态地知道this指针的类型,因此强制转换是没有问题的.(在机器级别,它根本不会做任何事情.)

sayHello除了this指针这次被限定之外,同样定义了该成员const.

static void
animal_say_hello(const void * p)
{
  const struct animal * this = (const struct animal *) p;
  printf("Hello, I'm an %d year old animal.\n", this->age);
}
Run Code Online (Sandbox Code Playgroud)

时间为动物vtable.首先,我们必须给它一个类型,这是直截了当的.

struct animal_vtable_type
{
  int (*setAge)(void *, int);
  void (*sayHello)(const void *);
};
Run Code Online (Sandbox Code Playgroud)

然后我们创建一个vtable的单个实例,并使用正确的成员函数进行设置.如果Animal有一个纯virtual成员,相应的条目将有一个NULL值,最好不要取消引用.

static const struct animal_vtable_type animal_vtable = {
  .setAge = animal_set_age,
  .sayHello = animal_say_hello,
};
Run Code Online (Sandbox Code Playgroud)

请注意animal_set_ageanimal_say_hello声明static.这是onkay因为它们永远不会被引用,而只能通过vtable引用(而vtable只能通过vptr它来引用static).

我们现在可以实现构造函数Animal...

void
animal_ctor(void * p, int age)
{
  struct animal * this = (struct animal *) p;
  this->vptr = &animal_vtable;
  this->age = age;
}
Run Code Online (Sandbox Code Playgroud)

......和相应的operator new:

void *
animal_new(int age)
{
  void * p = malloc(sizeof(struct animal));
  if (p != NULL)
    animal_ctor(p, age);
  return p;
}
Run Code Online (Sandbox Code Playgroud)

关于唯一有趣的vptr是在构造函数中设置的行.

让我们继续老虎.

Tiger继承自从Animal它获得一个struct tiger子对象.我这样做是通过放置一个struct animal作为第一个成员.至关重要的是,这是第一个成员,因为它意味着该对象的第一个成员vptr- 具有与我们的对象相同的地址.我们稍后会在做一些棘手的演员时需要这个.

struct tiger
{
  struct animal base;
  int stripes;
};
Run Code Online (Sandbox Code Playgroud)

我们也可以简单地struct animal在定义的开头复制词法成员,struct tiger但这可能更难维护.编译器不关心这样的风格问题.

我们已经知道如何实现老虎的成员函数.

void
tiger_say_hello(const void * p)
{
  const struct tiger * this = (const struct tiger *) p;
  printf("Hello, I'm an %d year old tiger with %d stripes.\n",
         this->base.age, this->stripes);
}

void
tiger_do_tigerish_thing(void * p)
{
  struct tiger * this = (struct tiger *) p;
  this->stripes += 1;
}
Run Code Online (Sandbox Code Playgroud)

请注意,我们正在this指向struct tiger此时的指针.如果调用tiger函数,this指针最好指向老虎,即使我们通过基指针调用.

vtable旁边:

struct tiger_vtable_type
{
  int (*setAge)(void *, int);
  void (*sayHello)(const void *);
  void (*doTigerishThing)(void *);
};
Run Code Online (Sandbox Code Playgroud)

请注意,前两个成员与完全相同animal_vtable_type.这是必不可少的,基本上是你问题的直接答案.如果我把struct animal_vtable_type第一个成员作为第一个成员,那么它可能会更明确.我想强调的是,对象布局将完全相同,只是在这种情况下我们无法播放我们讨厌的演员技巧.同样,这些是C语言的各个方面,不存在于机器级别,因此编译器不会为此烦恼.

创建一个vtable实例:

static const struct tiger_vtable_type tiger_vtable = {
  .setAge = animal_set_age,
  .sayHello = tiger_say_hello,
  .doTigerishThing = tiger_do_tigerish_thing,
};
Run Code Online (Sandbox Code Playgroud)

并实现构造函数:

void
tiger_ctor(void * p, int age, int stripes)
{
  struct tiger * this = (struct tiger *) p;
  animal_ctor(this, age);
  this->base.vptr = &tiger_vtable;
  this->stripes = stripes;
}
Run Code Online (Sandbox Code Playgroud)

老虎构造函数做的第一件事是调用动物构造函数.记住动物的构造如何设置vptr&animal_vtable?这就是为什么virtual从基类构造函数中调用成员函数会让人惊讶的原因.只有在基类构造函数运行之后,我们才重新分配vptr到派生类型,然后进行自己的初始化.

operator new 只是样板.

void *
tiger_new(int age, int stripes)
{
  void * p = malloc(sizeof(struct tiger));
  if (p != NULL)
    tiger_ctor(p, age, stripes);
  return p;
}
Run Code Online (Sandbox Code Playgroud)

我们完成了.但是我们如何调用虚拟成员函数?为此,我将定义一个辅助宏.

#define INVOKE_VIRTUAL_ARGS(STYPE, THIS, FUNC, ...)                     \
  (*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS, __VA_ARGS__ )
Run Code Online (Sandbox Code Playgroud)

现在,这很难看.它的作用是将静态类型STYPE,this指针THIS和成员函数的名称FUNC以及传递给函数的任何其他参数.

然后,它从静态类型构造vtable的类型名称.(这##是预处理程序的标记粘贴操作符.例如,如果STYPEanimal,STYPE ## _vtable_type则会扩展为animal_vtable_type.)

接下来,THIS指针被转换为指向刚刚派生的vtable类型的指针的指针.这是有效的,因为我们已确保将每个对象中vptr第一个成员放在一起,因此它具有相同的地址.这很重要.

完成后,我们可以取消引用指针(获取实际值vptr)然后请求其FUNC成员并最终调用它.(__VA_ARGS__扩展为附加的可变参数宏参数.)注意,我们还将THIS指针作为第0个参数传递给成员函数.

现在,实际的事实是我必须为不带参数的函数再次定义一个几乎相同的宏,因为预处理器不允许可变参数宏参数包为空.它应该是.

#define INVOKE_VIRTUAL(STYPE, THIS, FUNC)                               \
  (*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS )
Run Code Online (Sandbox Code Playgroud)

它有效:

#include <stdio.h>
#include <stdlib.h>

/* Insert all the code from above here... */

int
main()
{
  struct tiger * tp = tiger_new(7, 42);
  struct animal * ap = (struct animal *) tp;
  INVOKE_VIRTUAL(tiger, tp, sayHello);
  INVOKE_VIRTUAL(tiger, tp, doTigerishThing);
  INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);
  INVOKE_VIRTUAL(animal, ap, sayHello);
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

你可能想知道发生了什么

INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);
Run Code Online (Sandbox Code Playgroud)

呼叫.我们正在做的是调用通过指针引用的对象上的非重写setAge成员.该指针首先被隐式地转换为指针,因此作为指针传递给指针.该函数然后将其转换为指针.它是否正确?它是因为我们小心地把它作为第一个成员,因此对象的地址与子对象的地址相同.这是我们玩的同一个技巧(只有一个级别).AnimalTigerstruct tigervoidthisanimal_set_agestruct animalstruct animalstruct tigerstruct tigerstruct animalvptr