要记住的第一点是免责声明:这些标准实际上都没有保证。该标准说明了代码需要的外观以及它应该如何工作,但实际上并没有明确说明编译器需要如何实现这一点。
也就是说,基本上所有的C++编译器在这方面的工作非常相似。
因此,让我们从非虚函数开始。它们分为两类:静态和非静态。
两者中较简单的是静态成员函数。一个静态成员函数几乎就像一个全局函数,它是该类的一个friend
,除了它还需要类的名称作为函数名称的前缀。
非静态成员函数稍微复杂一些。它们仍然是直接调用的正常函数 - 但它们传递了一个隐藏的指针,指向它们被调用的对象的实例。在函数内部,您可以使用关键字this
来引用该实例数据。所以,当你打电话给a.func(b);
时,生成的代码与你得到的代码非常相似func(a, b);
现在让我们考虑一下虚拟功能。这里是我们进入vtable和vtable指针的地方。我们有足够的间接性,可能最好绘制一些图表来看看它是如何布置的。这几乎是最简单的例子:有两个虚函数一类的一个实例:
所以,对象包含其数据和一个指向虚函数表。 vtable包含一个指向由该类定义的每个虚函数的指针。但是,它可能并不是显而易见的,为什么我们需要这么多的间接性。要理解的是,让我们看看接下来的(非常轻微)更复杂的情况:这个类的两个实例:
注意类的每个实例如何有自己的数据,但他们都共享相同的vtable和相同的代码 - 如果我们有更多的实例,他们仍然会在同一个类的所有实例中共享一个vtable。
现在,我们来考虑派生/继承。举个例子,让我们将现有的类重命名为“Base”,并添加一个派生类。由于我感觉想象力丰富,我将其命名为“派生”。如上所述,基类定义了两个虚函数。派生类覆盖的那些中的一个(而不是其他):
当然,我们可以将二者结合起来,使每个基体的多个实例和/或派生类:
现在我们来深入了解一下更详细的内容。关于派生的有趣之处在于,我们可以将派生类的对象的指针/引用传递给为接收基类的指针/引用而编写的函数,它仍然有效 - 但如果调用虚函数,你会得到实际类的版本,而不是基类。那么,这是如何工作的?我们如何将派生类的实例视为它是基类的一个实例,并且仍然有效?为此,每个派生对象都有一个“基类子对象”。例如,让我们考虑这样的代码:
struct simple_base {
int a;
};
struct simple_derived : public simple_base {
int b;
};
在这种情况下,当您创建的simple_derived
一个实例,你就会得到包含两个int
秒的对象:a
和b
。 a
(基类部分)位于内存中对象的开始处,并且b
(派生类部分)紧跟其后。因此,如果将对象的地址传递给期望基类实例的函数,则它将使用基类中存在的部分,编译器将相同的偏移量放置在对象中, d在基类的一个对象中,所以函数可以在不知道它处理派生类的对象的情况下操纵它们。同样,如果你调用一个虚拟函数,所有它需要知道的是vtable指针的位置。就它而言,类似Base::func1
的东西基本上只意味着它遵循vtable指针,然后在指定的偏移量处使用指向函数的指针(例如,第四个函数指针)。
至少现在,我将忽略多重继承。它增加了相当复杂的图片(特别是当涉及到虚拟继承时),你根本没有提到它,所以我怀疑你真的很在乎。
至于访问任何的这种,或者使用比简单地调用虚函数以外的任何方式:你可以拿出一些特定的编译器 - 但不要指望它是便携式的。虽然像调试器这样的东西经常需要看这样的东西,但涉及的代码往往非常脆弱并且特定于编译器。
[Wikipedia](https://en.wikipedia.org/wiki/Virtual_method_table) – Barmar
这个vtable是依赖于实现的,没有标准的方法来访问它。 – Barmar
因此,@Barmar有什么方法可以访问vtable或查看使用了多少内存vtable? –