Страницы

Поиск по вопросам

понедельник, 10 февраля 2020 г.

Порядок работы с таблицами виртуальных методов

#cpp #полиморфизм


Добрый вечер. 

Если в классе объявлен виртуальный метод, то компилятор создает таблицу виртуальных
методов, объявленных в определении этого класса. 
Производный класс "получает" эту таблицу при наследовании и записывает туда адреса
переопределенных (своих реализаций) виртуальных методов.

В момент создания объекта (наследника через указатель на базовый класс), в базовом
классе (совместно с созданием VMT) объявляется виртуальный табличный указатель объекта
или vptr.

Сначала vptr объекта наследуется всеми производными классами, до тех пор пока не
будет инициализирован адресом VMT того произодного класса, которому принадлежит созданный
объект.

При вызове виртуального метода, компилятор через vptr достает фактический адрес виртуального
метода из VMT нужного производного класса  

То есть компилятор генерирует «скрытый» код в конструкторе каждого класса для инициализации
vpointer'ов объектов класса адресами соответствующей VMT.


сначала вызывается конструктор базового класса, инициализирующий vptr адресом VMT
базового класса, 
затем вызывается конструктор производного класса, который перезаписывает значение
vptr адресом VMT производного класса. (и так до тех пор пока vptr не будет инициализирован
адресом VMT того производного класса, которому принадлежит созданный объект)


Вопросы в следующем:

1) Что подразумевается под инициализацией vptr адресом VMT производного класса? Понятно,
что у виртуальных методов есть адреса, которые хранятся в таблице, разве адрес есть
непосредственно у VMT класса?

2) В каком порядке при запуске программы происходит создание VMT базового класса,
создание VMT производного класса (которая получает VMT базы при наследовании и инициализирует
ее элементы адресами своих методов), создание vptr и его инициализация таблицами базового
и производных классов?

3) При вызове виртуального деструктора значение vptr тоже инициализируется сначала
адресом VMT производного, а затем базового класса?
    


Ответы

Ответ 1



Если в классе объявлен виртуальный метод, то компилятор создает таблицу виртуальных методов, объявленных в определении этого класса. Производный класс "получает" эту таблицу при наследовании и записывает туда адреса переопределенных (своих реализаций) виртуальных методов. Это верно, с той только оговоркой, что все это делается на этапе компиляции. То есть вышепроцитированное - это то, как может "рассуждать" компилятор в процессе формирования VMT для очередного класса. 1) Что подразумевается под инициализацией vptr адресом VMT производного класса? Понятно, что у виртуальных методов есть адреса, которые хранятся в таблице, разве адрес есть непосредственно у VMT класса? VMT - это таблица в памяти. У нее есть адрес. Вот ее адрес и хранится в указателе vptr каждого объекта. В указателе vptr у объекта типа SomeClass хранится указатель на VMT класса SomeClass. А уж в таблице хранятся указатели на виртуальные методы класса SomeClass (или его предков). 2) В каком порядке при запуске программы происходит создание VMT базового класса, создание VMT производного класса (которая получает VMT базы при наследовании и инициализирует ее элементы адресами своих методов), Вопрос не совсем корректно поставлен. Таблицы VMT для всех классов как правило инициализированы статически, то есть их содержимое известно на стадии компиляции. При запуске программы они уже лежат готовенькие к использованию (в т.наз. сегменте инициализированных данных). Таблицы VMT формирует линкер (хотя возможно и более "позднее" формирование загрузчиком). В любом случае, когда программа фактически запустилась, все VMT уже сформированы. Порядок их формирования никакой роли не играет, ибо от него ничего не зависит. При создании объектов класса идет работа только с указателями на VMT. Сами VMT при этом никто уже не модифицирует (и не "создает"). создание vptr и его инициализация таблицами базового и производных классов? Вы сами совершенно правильно описали этот процесс в последовательности вызова конструкторов создаваемого объекта, от базовых к производным. Последним отрабатывает "самый производный" конструктор - конструктор полного объекта - в результате чего в vptr остается правильный указатель на VMT полного объекта. 3) При вызове виртуального деструктора значение vptr тоже инициализируется сначала адресом VMT производного, а затем базового класса? Деструкция происходит в порядке, обратном конструкции. Сначала отрабатывает тело деструктора полного объекта, а в конце он вызывает деструкторы всех своих базовых подобъектов. Каждый деструктор первым делом ставит vptr на VMT своего класса. Так работает любой деструктор, поэтому не совсем ясно, зачем вы упоминаете вызов именно виртуального деструктора. Когда деструктор уже начал работу, его виртуальность уже не имеет никакого значения.

Ответ 2



В популярных реализациях С++, таблицы виртуальных функций константны. В классе есть скрытый член, который хранит указатель на таблицу виртуальных функций. В констурукторе и деструкторе этот член заменяется на таблицу текущего типа. Допустим есть следующие классы struct A { A(); virtual void f(); virtual ~A(); }; struct B : A { B(); void f() override; virtual void g(); ~B() override; }; Для них будут сгенерированы две таблицы виртуальных функций: void* A_vft[] { &A::~A, &A::f, }; void* B_vft[] { // функции унаследованные от A &B::~B, &B::f, // новые функции &B::g, }; В объектах будет находиться член-указатель на таблицу вирт. функций. struct A { void** vft; }; struct B : A { }; Конструкторы будут сгенерированы примерно следующим образом: A::A() { this->vft = A_vft; // тело к-тора A в коде } B::B() { A::A(); // к-тор родителя this->vft = B_vft; // тело к-тора B в коде } Деструкторы выглядят так же A::~A() { this->vft = A_vft; // тело д-тора A в коде } B::~B() { this->vft = B_vft; // тело д-тора B в коде A::~A(); // д-тор родителя } Вызов A* a; a->f(); компилируется следующим образом: void* f = a->vft[1]; f(a); Вызов delete a; компилируется практически так же (при множественном наследовании всё сложнее): void* dtor = a->vft[0]; dtor(a); operator delete(a); Такая схема позволяет добавлять новые классы без перекомпиляции старых. Для класса C : B надо просто добавить его таблицу виртуальных функций, конструктор и деструктор.

Комментариев нет:

Отправить комментарий