Обрисую как это устроено. Любой полиморфный класс (то есть содержащий виртуальные функции) содержит внутреннюю переменную - указатель на таблицу виртуальных функций.
Код:
class E
{
private: virtual void mem_fun()
{
// do something
}
public: virtual bool L(int)
{
// do something
}
}
// кодируется как (условный код):
struct E_vtbl
{
int base_offset = 0;
mem_fun_address = &mem_fun; // адрес кода mem_fun
L_address = &L; // адрес кода L
}
struct E_class
{
E_vtbl* _vtbl = &E_vtbl;
}
// таким образом вызов виртуальной функции идёт так:
// p->mem_fun();
(*p->_vtbl->mem_fun_address)(p + p->_vtbl->base_offset);
// p->L(1);
(*p->_vtbl->L_address)(p + p->_vtbl->base_offset, 1);
У чистых виртуальных функций считается что указатель в виртуальной таблице является нулём (оттуда и запись), но на самом деле бывает что такой таблицы вообще нет, а бывает, что там находится код, который выдаёт сообщение об ошибке "pure virtual function called". Таким образом:
Код:
class E
{
int a;
public: virtual bool L(int) = 0;
}
class E1 : public E
{
int b;
public: virtual bool L(int)
{
cout << "E1::L\n";
}
}
class E2 : public E
{
double c;
public: virtual bool L(int)
{
cout << "E2::L\n";
}
}
// кодируется как:
struct E1_vtbl
{
base_offset = 0;
L_address = &E1::L // E_vtbl, заполненная правильным адресом E1::L
}
struct E1_vtbl_E
{
base_offset = -4;
L_address = &E1::L // E_vtbl, заполненная правильным адресом E1::L
}
struct E2_vtbl
{
base_offset = 0;
L_address = &E2::L // E_vtbl, заполненная правильным адресом E2::L
}
struct E2_vtbl_E
{
base_offset = -4;
L_address = &E2::L // E_vtbl, заполненная правильным адресом E2::L
}
struct E1_class
{
// общие для всех
E1_vtbl* _vtbl = &E1_vtbl; // таблица E1
// составляющий класс E
E_vtbl* _vtbl1 = &E1_vtbl_E; // таблица E
int a; // данные E
int b; // общие данные
}
struct E2_class
{
// общие для всех
E2_vtbl* _vtbl = &E2_vtbl; // таблица E2
// составляющий класс E
E_vtbl* _vtbl1 = &E2_vtbl_E; // таблица E
int a; // данные E
double c; // общие данные
}
1. преобразование указателя к E - вычисление адреса, где находится составляющий класс: E_ptr = E2_ptr + 4;
2. вызов E::L - из виртуальной таблицы (*E_ptr->_vtbl->L)(E_ptr+E_ptr->_vtbl->base_offset, 1). В результате, если посчитать, вызов преобразуется в E2_ptr->E::L2
Для чего нужен base_offset? Дело в том, что в C++ классы обладают множественным виртуальным наследием. Чтобы программа могла при реализации вызова могла преобразовать адрес к наследнику от любого базового класса (который может и не "знать", в состав чего входит). Иногда base_offset вводят в тело функции. Идёт вызов кода, который вносит изменения в указатель this и делает прыжок на функцию класса-наследника