Почему виртуальная таблица требуется только в случае виртуальных функций? - PullRequest
3 голосов
/ 02 марта 2012

С http://www.learncpp.com/cpp-tutorial/125-the-virtual-table/, код, такой как

class Base
{
public:
    virtual void function1() {};
    virtual void function2() {};
};

class D1: public Base
{
public:
    virtual void function1() {};
};

class D2: public Base
{
public:
    virtual void function2() {};
};

, создает виртуальную таблицу, аналогичную http://www.learncpp.com/images/CppTutorial/Section12/VTable.gif: enter image description here

Виртуальная таблица, как указано выше, имеет смысл,Ведь всем объектам нужен способ вызова функций, и им нужно использовать указатели функций для их поиска.


Что я не понимаю, так это то, почему это требуется только в случае использования виртуальных функций?Я определенно что-то упускаю, так как виртуальная таблица напрямую не зависит от виртуальных функций.

Например, если используемый код был

class Base
{
public:
    void function1() {};
    void function2() {};
};

...

Base b;
b.function1();

и виртуальная таблица отсутствует (то естьнет указателя на то, где находится функция), как бы разрешить вызов b.function1()?


Или в этом случае у нас также есть таблица, просто она не называетсявиртуальный стол?В этом случае возникнет вопрос, зачем нам нужен новый тип таблицы для виртуальных функций?

Ответы [ 2 ]

9 голосов
/ 02 марта 2012

[Если] нет виртуальной таблицы (имеется в виду, что нет указателя на то, где находится функция), как будет разрешаться вызов b.function1()?

В компиляторе есть "указатель", который анализирует и анализирует ваш код. Компилятор решает, где будет сгенерирована функция, поэтому он знает, как должны разрешаться вызовы указанной функции. Вместе с компоновщиком все это происходит аккуратно в процессе сборки.

Причина только , что это не работает для функций virtual, заключается в том, что функция , которую вы вызываете, зависит от типа, известного только во время выполнения; фактически те же самые указатели функций дословно присутствуют в виртуальной таблице, записанной там компилятором. Просто в этом случае есть несколько вариантов выбора, и они не могут быть выбраны до тех пор, пока (читай: возможно, месяцы или даже годы!) После того, как компилятор вообще перестанет участвовать.

1 голос
/ 03 марта 2012

Хороший ответ уже есть, но я попробую немного более простой (хотя и более длинный):

Подумайте о не виртуальном методе вида

class A
{
public:
  int fn(int arg1);
};

как эквивалент свободной функции вида:

int fn(A* me, int arg1); // overload A

, где me соответствует указателю this внутри версии метода.

Если у вас теперь есть подкласс:

class B : public A
{
public:
  int fn(int arg1);
};

это эквивалентно свободной функции, подобной этой:

int fn(B* me, int arg1); // overload B

Обратите внимание, что первый аргумент имеет тип, отличный от свободной функции, которую мы объявили ранее - функция перегружена на тип первого аргумента.

Если у вас теперь есть код, вызывающий fn(), он выберет перегрузку на основе статического типа (тип времени компиляции) первого аргумента:

A* p;
B* q;
// ...
// assign valid pointer values to p and q
// ...
int a = fn(p, 0); // will call overload A
int b = fn(q, 0); // will call overload B

Компилятор может и будет определять функцию, вызываемую во время компиляции в каждом случае, и может генерировать код сборки с фиксированным адресом функции или смещением адреса. Концепция виртуальной таблицы времени выполнения здесь не имеет смысла.

Теперь, когда я сказал думать о версии метода как эквивалентной версии свободной функции, вы обнаружите, что на уровне ассемблера они эквивалентны . Единственным отличием будет так называемое искаженное имя, которое кодирует тип в имени скомпилированной функции и различает перегруженные функции. Тот факт, что вы вызываете методы через p->fn(0), то есть с первым аргументом перед , имя метода - чистый синтаксический сахар - вы не на самом деле разыменование указателя p в примере, даже если оно выглядит так. Вы просто передаете p в качестве неявного this аргумента. Итак, чтобы продолжить приведенный выше пример,

p->fn(0); // will always call A::fn()
q->fn(0); // will always call B::fn()

, поскольку fn - не виртуальный метод, означает, что компилятор отправляет тип this указателя static , что он может делать во время компиляции.

Хотя виртуальные функции используют тот же самый синтаксис вызова , что и не виртуальные функции-члены, вы фактически разыменовываете указатель объекта; в частности, вы разыменовываете указатель на виртуальную таблицу класса объекта.

...