编译器如何知道vtable中的哪个表项对应于虚函数?< / h1 >

How does the compiler know which entry in vtable corresponds to a virtual function?

本文关键字:函数 于虚 gt lt h1 vtable 何知道 编译器      更新时间:2023-10-16

假设父类和派生类中有多个虚函数。在父类和派生类的虚函数表中,将为这些虚函数创建一个虚函数表。

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

的例子:

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()?
}

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

https://stackoverflow.com/a/99341/437894 =

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

表是如何解析函数调用的?

https://stackoverflow.com/a/203136/437894 =

"所以在运行时,代码只是使用对象的vptr来定位从那里得到实际被覆盖函数的地址。"

我不能理解这个。虚表保存虚函数的地址,而不是实际被覆盖函数的地址。

我将稍微修改一下您的示例,以便它显示面向对象的更有趣的方面。

假设我们有以下内容:

#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
}
在本例中,我忽略了具有virtual函数成员的类应该具有virtual析构函数的好建议。无论如何我都要泄漏这个对象。

让我们看看如何将这个例子转换成没有成员函数的老式C语言,更不用说virtual函数了。以下所有代码都是C,而不是c++。

struct animal很简单:

struct animal
{
const void * vptr;
int age;
};

除了age成员,我们还增加了一个vptr成员,它将是指向虚函数表的指针。我使用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;
}

注意额外的第0个参数:在c++中隐式传递的this指针。同样,我使用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);
}

动物变量表时间。首先,我们必须给它一个类型,这很简单。

struct animal_vtable_type
{
int (*setAge)(void *, int);
void (*sayHello)(const void *);
};

然后创建虚函数表的单个实例,并用正确的成员函数对其进行设置。如果Animal有一个纯virtual成员,对应的表项应该有一个NULL值,最好不要解引用。

static const struct animal_vtable_type animal_vtable = {
.setAge = animal_set_age,
.sayHello = animal_say_hello,
};

注意animal_set_ageanimal_say_hello被声明为static。这是没关系的,因为它们永远不会被命名引用,而只能通过虚表(虚表只能通过vptr,所以它也可以是static)。

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

void
animal_ctor(void * p, int age)
{
struct animal * this = (struct animal *) p;
this->vptr = &animal_vtable;
this->age = age;
}
和相应的operator new:
void *
animal_new(int age)
{
void * p = malloc(sizeof(struct animal));
if (p != NULL)
animal_ctor(p, age);
return p;
}

唯一有趣的是在构造函数中设置vptr的那一行。

让我们来看看老虎。

Tiger继承自Animal,因此它获得struct tiger子对象。我通过将struct animal作为第一个成员来做到这一点。它必须是第一个成员,因为它意味着该对象的第一个成员(vptr)与我们的对象具有相同的地址。当我们稍后做一些棘手的选角时,我们会用到它。

struct tiger
{
struct animal base;
int stripes;
};

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

我们已经知道如何实现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;
}

注意,这次我们将this指针强制转换为struct tiger。如果调用虎函数,则this指针最好指向虎函数,即使通过基指针调用也是如此。

虚表旁:

struct tiger_vtable_type
{
int (*setAge)(void *, int);
void (*sayHello)(const void *);
void (*doTigerishThing)(void *);
};

注意,前两个成员与animal_vtable_type完全相同。这是很重要的,基本上是对你问题的直接回答。如果我将struct animal_vtable_type作为第一个成员,可能会更显式。我想强调的是,对象布局应该是与完全相同的,只是在这种情况下我们不能使用令人讨厌的强制转换技巧。同样,这些是C语言的方面,不存在于机器级别,所以编译器不受此困扰。

创建虚表实例:

static const struct tiger_vtable_type tiger_vtable = {
.setAge = animal_set_age,
.sayHello = tiger_say_hello,
.doTigerishThing = tiger_do_tigerish_thing,
};

并实现构造函数:

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;
}

老虎构造函数做的第一件事是调用动物构造函数。还记得动物构造函数是如何将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;
}

做完了。但是我们如何调用虚成员函数呢?为此,我将定义一个helper宏。

#define INVOKE_VIRTUAL_ARGS(STYPE, THIS, FUNC, ...)                     
(*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS, __VA_ARGS__ )

这太难看了。它所做的是接受静态类型STYPE,this指针THIS和成员函数FUNC的名称以及传递给函数的任何其他参数。

然后,从静态类型构造虚函数表的类型名。(##是预处理器的令牌粘贴操作符。例如,如果STYPEanimal,那么STYPE ## _vtable_type将扩展为animal_vtable_type

接下来,将THIS指针强制转换为指向刚派生的虚表类型的指针的指针。这是有效的,因为我们已经确保在每个对象中将vptr作为第一个成员,因此它具有相同的地址。

一旦完成,我们可以解引用指针(得到实际的vptr),然后请求它的FUNC成员,最后调用它。(__VA_ARGS__扩展为附加的可变宏参数。)注意,还将THIS指针作为成员函数的第0个实参传递。

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

#define INVOKE_VIRTUAL(STYPE, THIS, FUNC)                               
(*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS )

#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;
}

你可能想知道

中发生了什么
INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);

调用。我们所做的是在通过struct tiger指针引用的Tiger对象上调用Animal的未覆盖的setAge成员。该指针首先隐式地转换为void指针,然后作为this指针传递给animal_set_age。然后,该函数将其强制转换为struct animal指针。这是正确的吗?这是因为我们小心地将struct animal作为struct tiger的第一个成员,因此struct tiger对象的地址与struct animal子对象的地址相同。这与我们使用vptr时使用的技巧相同(只是少了一级)。

它可以帮助你实现类似的东西。

struct Bob;
struct Bob_vtable {
void(*print)(Bob const*self) = 0;
Bob_vtable(void(*p)(Bob const*)):print(p){}
};
template<class T>
Bob_vtable const* make_bob_vtable(void(*print)(Bob const*)) {
static Bob_vtable const table(+print);
return &table;
}
struct Bob {
Bob_vtable const* vtable;
void print() const {
vtable->print(this);
}
Bob():vtable( make_bob_vtable<Bob>([](Bob const*self){
std::cout << "Bobn";
})) {}
protected:
Bob(Bob_vtable const* t):vtable(t){}
};
struct Alice:Bob {
int x = 0;
Alice():Bob( make_bob_vtable<Alice>([](Bob const*self){
std::cout << "Alice " << static_cast<Alice const*>(self)->x << 'n';
})) {}
};

生活例子。

这里我们有一个显式的虚值表存储在Bob中。它指向一个函数表。非虚成员函数print使用它动态分派给正确的方法。

Bob的构造函数和派生类Alice将虚表设置为与表中值不同的值(在本例中创建为静态局部)。

使用哪一个指针已经包含在Bob::print的定义中——它知道在表中的偏移量。

如果我们在Alice中添加另一个虚函数,这只意味着虚函数表指针实际上指向struct Alice_vtable:Bob_vtable。静态/重新解释类型转换将使我们得到"真正的"表,并且我们可以轻松访问额外的函数指针。

当我们讨论虚继承和虚函数时,事情变得更奇怪了。我没有资格描述它是如何工作的。