编译器如何知道vtable中的哪个表项对应于虚函数?< / h1 >
How does the compiler know which entry in vtable corresponds to a virtual function?
假设父类和派生类中有多个虚函数。在父类和派生类的虚函数表中,将为这些虚函数创建一个虚函数表。
编译器如何知道虚函数表中的哪个条目对应于哪个虚函数?
的例子:
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_age
和animal_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
的名称以及传递给函数的任何其他参数。
然后,从静态类型构造虚函数表的类型名。(##
是预处理器的令牌粘贴操作符。例如,如果STYPE
是animal
,那么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
。静态/重新解释类型转换将使我们得到"真正的"表,并且我们可以轻松访问额外的函数指针。
- 从 c++ 中派生类的析构函数调用虚函数
- 模板类可以有纯虚函数和虚运算符吗?
- 指向普通函数和虚函数的基类指针
- 纯虚函数覆盖虚函数
- 使用抽象基类的回调函数和虚函数有什么区别?
- 调用非虚函数的虚函数
- 对象生存期内显式构造函数和虚函数调用
- 我是否需要使用继承对象(相对于基对象)覆盖我的虚函数?
- 编译器如何知道vtable中的哪个表项对应于虚函数?< / h1 >
- 如何使这个析构函数为虚函数
- 从构造函数调用虚函数
- 为什么在析构函数中将虚表设置回该级别
- 它被称为抽象类,用于实现虚函数.它适用于四个类似实现中的三个
- 为什么当类A的析构函数为虚函数或非虚函数时,B的类成员n具有不同的值
- 构造函数中虚函数的奇怪行为
- 挂钩(热补丁)类成员函数.修改虚函数表项
- 基类析构函数中虚函数调用的多态能力
- 删除对象时正则函数和虚函数的行为
- 覆盖非虚函数和虚函数的区别是什么?
- 检测特定虚函数的虚值表偏移量(使用Visual c++)