为什么只有在虚函数的情况下才需要虚表?

3

来自http://www.learncpp.com/cpp-tutorial/125-the-virtual-table/,代码如下:

class Base
{
public:
    virtual void function1() {};
    virtual void function2() {};
};

class D1: public Base
{
public:
    virtual void function1() {};
};

class D2: public Base
{
public:
    virtual void function2() {};
};

生成一个类似于以下链接的虚拟表:http://www.learncpp.com/images/CppTutorial/Section12/VTable.gif,如上所示。虚拟表有其合理性,毕竟对象需要一种调用函数的方式,并且需要使用函数指针来查找它们。
我不明白的是为什么只有在使用虚拟函数时才需要这样做?我肯定是遗漏了什么,因为虚拟表并不直接依赖于虚拟函数。
例如,如果正在使用的代码是:
class Base
{
public:
    void function1() {};
    void function2() {};
};

...

Base b;
b.function1();

如果没有虚表(意味着没有指向函数所在位置的指针),那么b.function1()调用会如何解决?


或者说,在这种情况下,我们是否也有一个表,只是它不被称为虚表?如果是这样,那么问题就出现了:为什么我们需要一种新的虚函数表?


1
请注意,vtable是一种实现细节。语言本身并没有对虚拟表的形式、功能甚至存在性做出任何要求。你的编译器可能会使用它,并且它可能看起来像你的图片。但也可能不是这样。 - Robᵩ
当你使用虚继承时,也会得到一个vptr。除此之外,如果不需要,为什么要创建vptr/vtable呢? - PlasmaHH
2个回答

9

如果没有虚拟表(也就是没有指向函数所在位置的指针),那么 b.function1() 的调用该如何解析?

编译器在解析和分析代码时,会有一个“指针”。编译器决定函数生成的位置,因此它知道如何解析对该函数的调用。与链接器协同工作,这一切都在构建过程中整洁地完成。

唯一不能使用这种方法的原因是对于 virtual 函数,调用哪个函数取决于只有在运行时才知道的类型;实际上,相同的函数指针完全存在于虚拟表中,并由编译器写入其中。只是在这种情况下,有多个可供选择的函数,而且要在编译器完全不再参与的很长时间后(可能是几个月甚至几年!)才能进行选择。


1

已经有一个很好的答案了,但我会尝试一个稍微简单一些(虽然更长)的方法:

想象一个形式为非虚拟方法的函数

class A
{
public:
  int fn(int arg1);
};

相当于一个形式为自由函数的内容:

int fn(A* me, int arg1); // overload A

me 对应于方法版本中的 this 指针。

如果现在有一个子类:

class B : public A
{
public:
  int fn(int arg1);
};

这相当于一个类似于以下自由函数的函数:

int fn(B* me, int arg1); // overload B

请注意,第一个参数的类型与我们之前声明的自由函数不同 - 函数在第一个参数的类型上进行了重载。
如果您现在有一些调用fn()的代码,它将根据第一个参数的静态类型(编译时类型)选择重载:
A* p;
B* q;
// ...
// assign valid pointer values to p and q
// ...
int a = fn(p, 0); // will call overload A
int b = fn(q, 0); // will call overload B

编译器可以并且将在每种情况下确定要调用的函数,并且可以发出带有固定函数地址或地址偏移量的汇编代码。在这里,运行时虚拟表的概念是荒谬的。

现在,当我说将方法版本视为等效于自由函数版本时,您会发现在汇编语言级别上,它们是等效的。唯一的区别将是所谓的名称混淆,它在编译后的函数名称中编码类型并区分重载函数。您通过p->fn(0)调用方法的事实,也就是在方法名称之前使用第一个参数,纯粹是语法糖 - 尽管看起来像是在解引用指针p,但实际上并不是。您只是将p作为隐式的this参数传递。因此,继续上面的例子,

p->fn(0); // will always call A::fn()
q->fn(0); // will always call B::fn()

因为fn是一个非虚方法,这意味着编译器会在this指针的静态类型上进行分派,这可以在编译时完成。

虽然虚函数使用与非虚成员函数相同的调用语法,但实际上您正在对对象指针进行解引用;具体来说,您正在对对象类的虚表指针进行解引用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接