Я понимаю почему, но как именно виртуальные функции / VTables позволяют получить доступ к правильным функциям через указатели? - PullRequest
0 голосов
/ 11 марта 2019

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

(пример, приведенный в этой теме Зачем нам нужны виртуальные функции в C ++? ):

class Animal
{
public:
    void eat() { std::cout << "I'm eating generic food."<<endl; }
};

class Cat : public Animal
{
public:
    void eat() { std::cout << "I'm eating a rat."<<endl; }
};

void func(Animal *xyz) { xyz->eat(); }

Итак, у нас есть функция и производная функция, которая былапереопределены.

Cat *cat = new Cat;
Animal *animal = new Animal;


animal->eat(); // Outputs: "I'm eating generic food."
cat->eat();    // Outputs: "I'm eating a rat."
func(animal);  // Outputs: "I'm eating generic food."
func(cat);     // Outputs: "I'm eating generic food."

Таким образом, мы не можем получить доступ к желаемой функции, если она не является виртуальной функцией.Но почему именно?

Если работает следующее:

Cat cat;
Animal animal;

animal.eat(); // Outputs: "I'm eating generic food."
cat.eat();    // Outputs: "I'm eating a rat."

Тогда, предположительно, в памяти уже есть две разные функции eat без использования vtable.

Поэтому, когда мы делаем виртуальную функцию, каждый класс теперь получает свою собственную таблицу vTable со своими собственными функциями.Итак ... Мы просто храним функции в другом месте в памяти.Так что же происходит с указателем между тем, когда он вызывает обычную функцию через объект, и когда он вызывает виртуальную функцию через объект?

Как и в чем разница между: Animal-> eat ();// Вызов виртуальной функции и Animal-> eat ();// Вызов обычной функции

Когда мы объявляем виртуальную функцию, TutorialsPoint говорит

На этот раз компилятор смотрит на содержимое указателя, а не на его тип

Да, но как?Почему он не мог сделать это раньше?Предположительно, он просто хранится в памяти так же, как обычная функция.Это как-то связано с указателем Vtable в начале объекта?

Я просто ищу подробности, чтобы понять, почему.Я не хочу звучать так, будто я увяз в чем-то совершенно бессмысленном.Просто интересно по академическим соображениям.

Ответы [ 2 ]

2 голосов
/ 11 марта 2019

Рассмотрим этот код:

void Function(Animal *foo)
{
    foo->eat();
}

Если eat не является виртуальной функцией-членом, она просто вызывает Animal::eat. Не имеет значения, на что указывает foo.

Если eat является виртуальной функцией-членом, это примерно равно *(foo->eatPtr)();. Вы можете думать о Animal и всех его производных классах как о переменной-члене, указывающей на функцию eat. Так что если foo фактически указывает на Bear, то foo->eatPtr() получит доступ к Bear::eatPtr и вызовет функцию eat класса Bear.

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

Эта дополнительная переменная-член класса, которая указывает на vtable для класса, объясняет, почему размер экземпляра класса или его указателей (в зависимости от реализации) обычно увеличивается на размер одного указателя при добавлении первого virtual функция для этого класса.

0 голосов
/ 11 марта 2019

Я считаю, что лучше всего реализовать это, чтобы понять это.

struct ifoo;
struct ifoo_vtable{
  void(*print)(ifoo const*);
};

struct ifoo{
  ifoo_vtable const* vtable;
  void print()const{ vtable->print(this); }
};

struct fooa:ifoo{
  void print_impl()const{ std::cout<<"fooa\n"; }
  fooa(){
    static const ifoo_vtable mytable={
      +[](ifoo const* self){
        static_cast<fooa const*>(self)->print_impl();
      }
    };
    vtable=&mytable;
  }
};
struct foob:ifoo{
  void print_impl()const{ std::cout<<"foob\n"; }
  foob(){
    static const ifoo_vtable mytable={
      +[](ifoo const* self){
        static_cast<foob const*>(self)->print_impl();
      }
    };
    vtable=&mytable;
  }
};

теперь нет нуля использования virtual выше.Но:

fooa a;
foob b;
ifoo* ptr = (rand()%2)?&a;&b;
ptr->print();

будет случайным образом вызывать либо метод fooa, либо метод print_impl для foob.

vtable - это структура указателей на функции.Вызов виртуального methid Actual запускает небольшую заглушку, которая ищет метод в vtable, а затем запускает его.

Код, написанный для вас в конструкторах классов реализации, заполняет эту vtable указателями функций, указывающими на переопределения.

Теперь есть детали, которые здесь не рассматриваются - соглашения о вызовах, деструкторы, девиртуализация, множественное наследование интерфейса, динамическое приведение и т. Д. - но ядро ​​очень похоже на то, как каждый основной компилятор C ++ реализует виртуальные методы.

По стандарту это не детализировано;только поведение есть.Но этот тип vtable появился еще до того, как C ++ стал языком, и я полагаю, что именно такие vtable имели в виду разработчики языка C ++, когда указывали поведение виртуальных функций в C ++.

Обратите внимание, что этодалеко не единственный способ сделать это.Карты сообщений MFC, объективные сообщения C / small talk, python, lua и многие другие языки имеют другие способы сделать это с преимуществами и недостатками по сравнению с решением C ++.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...