Я решил выяснить, как реально создается vtable. Итак, я открыл отладчик и обнаружил некоторые странные вещи. Узел PTR содержит несколько вптр. Я всегда думал, что был только один vptr на объект. Кто-нибудь может мне объяснить, что здесь происходит? (Я имею в виду, когда указатель базового класса указывает на объект производного класса)
#include <iostream>
using namespace std;
class Base
{
int base;
public:
virtual void say()
{
cout << "Hello" << endl;
}
virtual void no()
{
cout << "No" << endl;
}
};class Base2
{
public:
virtual void lol()
{
cout << "lol" << endl;
}
};
class Derv:public Base,public Base2
{
public:
void say()
{
cout << "yep" << endl;
}
};int main()
{
Base* ptr = new Derv();
ptr->say();
ptr = new Base();
ptr->say();
}
Два указателя необходимы, потому что у вас есть два базовых класса с виртуальными функциями.
Давайте пройдемся по шагам:
Вы сначала определяете Base
который имеет виртуальные функции. Поэтому компилятор создаст виртуальную таблицу, которая примерно выглядит следующим образом (индексы приведены в скобках; обратите внимание, что это пример, точная компоновка таблицы будет зависеть от компилятора):
[0] address of Base::say()
[1] address of Base::no()
в Base
макет будет поле __vptr
(или, тем не менее, он называется, если он вообще указан), указывая на эту таблицу. Когда дан указатель pBase
типа Base*
и попросил позвонить say
, компилятор на самом деле вызовет (p->__vptr[0])()
,
Далее вы определяете второй, независимый класс Base2
, чья виртуальная таблица будет выглядеть так:
[0] address of Base2::lol()
Вызов lol
через Base2
указатель теперь будет переведен на что-то вроде (pBase2->__vptr[0])()
,
Теперь, наконец, вы определяете класс Derv
который наследует от обоих Base
а также Base2
, Это особенно означает, что вы можете иметь как Base*
и Base2*
указывая на объект типа Derv
, Теперь, если бы у вас был только один __vptr
, pBase->say()
а также pBase2->lol()
будет вызывать одну и ту же функцию, потому что они оба переводят в (pXXX->__vptr[0])()
,
Однако на самом деле происходит то, что есть два __vptr поля, одно для Base
базовый класс, и один для _Base2
Базовый класс. Base*
указывает на Base
подобъект со своим __vptr
, Base2*
указывает на Base2
подобъект со своим __vptr
, Теперь Derv
виртуальный стол может выглядеть, например, как это:
[0] address of Derv::say()
[1] address of Base::no()
[2] address of Base2::lol()
__vptr
из Base
подобъект указывает на начало этой таблицы, а __vptr
из Base2
подобъект указывает на элемент [2]
, Сейчас звоню pBase->say()
будет переводить на (pBase->__vptr[0])()
и так как __vptr
из Base
подобъект указывает на начало Derv
виртуальная таблица, она в конечном итоге вызывает Derv::say()
как предполагалось. С другой стороны, если вы звоните pBase2->lol()
оно будет переведено на (pBase2->__vptr[0])()
, но с тех пор pBase2
указывает на Base2
подобъект od Derv
будет разыменовано соответствующее __vptr
который указывает на элемент [2]
из Derv
виртуальная таблица, где адрес Base2::lol
хранится. А сейчас Base2::lol()
называется как задумано.
Подумайте о том, что происходит, когда вы приводите указатель к производному указателю на базу, он должен ссылаться на блок памяти, имеющий ту же структуру, что и базовый тип. Когда у вас есть множественное наследование, вы получите один vptr в каждой базе, которая имеет виртуальные функции.