有人可以解释这个不同类的虚拟表是如何存储在内存中的吗?当我们使用指针调用函数时,他们如何使用地址位置调用函数?我们可以使用类指针获取这些虚拟表内存分配大小吗?我想看看一个类的虚拟表使用了多少内存块。我怎么能看到它?
class Base
{
public:
FunctionPointer *__vptr;
virtual void function1() {};
virtual void function2() {};
};
class D1: public Base
{
public:
virtual void function1() {};
};
class D2: public Base
{
public:
virtual void function2() {};
};
int main()
{
D1 d1;
Base *dPtr = &d1;
dPtr->function1();
}
谢谢!提前
答案 0 :(得分:7)
要记住的第一点是免责声明:标准实际上并未保证这一点。标准说明代码需要看起来是什么以及它应该如何工作,但实际上并没有准确指出编译器需要如何实现这一点。
尽管如此,基本上所有C ++编译器在这方面的工作方式都非常相似。
所以,让我们从非虚函数开始。它们分为两类:静态和非静态。
两者中较简单的是静态成员函数。静态成员函数几乎就像是类的friend
的全局函数,除了它还需要类的名称作为函数名的前缀。
非静态成员函数稍微复杂一些。它们仍然是直接调用的普通函数 - 但它们会传递一个隐藏指针,指向调用它们的对象的实例。在函数内部,您可以使用关键字this
来引用该实例数据。因此,当您调用类似a.func(b);
之类的内容时,生成的代码与您为func(a, b);
调用的代码非常相似
现在让我们考虑虚拟功能。这是我们进入vtable和vtable指针的地方。我们有足够的间接性,可能最好绘制一些图表,看看它是如何布局的。这几乎是最简单的情况:一个具有两个虚函数的类的实例:
因此,该对象包含其数据和指向vtable的指针。 vtable包含指向该类定义的每个虚函数的指针。然而,为什么我们需要这么多的间接,可能不会立即显而易见。为了理解这一点,让我们看一下下一个(稍微有点)更复杂的情况:该类的两个实例:
请注意该类的每个实例如何拥有自己的数据,但它们共享相同的vtable和相同的代码 - 如果我们有更多的实例,它们仍然会共享所有实例中的一个vtable。同一个班级。
现在,让我们考虑派生/继承。例如,让我们将现有类重命名为“Base”,并添加派生类。由于我感觉富有想象力,我将它命名为“Derived”。如上所述,基类定义了两个虚函数。派生类会覆盖其中一个(但不是其他):
当然,我们可以将两者结合起来,每个基类和/或派生类都有多个实例:
现在让我们再详细研究一下。有关派生的有趣之处在于,我们可以将派生类的对象的指针/引用传递给编写的函数,以接收对基类的指针/引用,它仍然有效 - 但是如果调用虚函数,你得到的是实际类的版本,而不是基类。那么,这是如何工作的?我们如何将派生类的实例视为基类的实例,并且仍然可以工作?为此,每个派生对象都有一个“基类子对象”。例如,让我们考虑这样的代码:
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指针,然后使用指向某个指定偏移处的函数的指针(例如,第四个函数指针)。
至少目前,我将忽略多重继承。它为图片增加了相当多的复杂性(特别是涉及虚拟继承时)并且你根本没有提到它,所以我怀疑你真的在乎。
至于访问其中的任何一个,或者以简单地调用虚函数之外的任何方式使用:你可能能够为特定的编译器提供一些东西 - 但是不要指望它完全可移植。虽然像调试器这样的东西经常需要查看这些东西,但所涉及的代码往往非常脆弱且特定于编译器。
答案 1 :(得分:4)
虚拟表应该在类的实例之间共享。更确切地说,它生活在"类"级别,而不是实例级别。每个实例都有实际拥有指向虚拟表的指针的开销,如果在它的层次结构中有虚函数和类。
表本身至少是为每个虚函数保存指针所需的大小。除此之外,它是一个实现细节,它是如何实际定义的。请查看here以获取有关此问题的详细信息。
答案 2 :(得分:3)
首先,以下答案包含您想要了解的有关虚拟表的所有内容: https://stackoverflow.com/a/16097013/8908931
如果您正在寻找更具体的内容(使用常规免责声明,这可能会在平台,编译器和CPU架构之间发生变化):
答案 3 :(得分:1)
Jerry Coffin给出的答案很好地解释了虚函数指针如何在C ++中实现运行时多态性。但是,我认为缺乏回答存储器中存储器的内存。正如其他人所指出的那样,这并不是标准所规定的。
然而,Martin Kysel有一个很好的blog post(s),它详细介绍了虚拟表的存储位置。总结博客文章:
答案 4 :(得分:-1)
每个类都有一个指向函数列表的指针,它们对于派生类的顺序相同,然后覆盖的特定函数在列表中的该位置发生变化。
当您使用基指针类型指向时,指向的对象仍具有正确的_vptr。
Base's
Base::function1()
Base::function2()
D1的
D1::function1()
Base::function2()
D2的
Base::function1()
D2::function2()
进一步派生的drom D1或D2只会将新的虚拟函数添加到2当前的列表中。
当调用虚函数时我们只调用相应的索引,function1将是索引0
所以你的电话
dPtr->function1();
实际上是
dPtr->_vptr[0]();