Я не могу понять, почему компилятор не вызвал функцию экземпляра класса?
Это то, что делает компилятор - гарантирует, что ваша программа вызывает функцию класса, к которому принадлежит экземпляр. Ключевым словом здесь является пример: знание класса экземпляра недоступно во время компиляции.
Рассмотрим простой пример:
struct Dude {
virtual void howdy() = 0;
};
struct Bob : public Dude {
virtual void howdy() { cout << "Hi, Bob!" << endl; }
};
struct Moe : public Dude {
virtual void howdy() { cout << "Hi, Moe!" << endl; }
};
// Note the pass by reference below: passing by reference or by pointer,
// but not by value, is important for achieving polymorphic behavior.
void say_hi(Dude& dude) {
dude.howdy(); // <<== Here is the tricky line
}
int main(int argc, char* argv[]) {
Bob b;
Moe m;
Dude *d = rand() & 1 ? (Dude*)&b : &m;
say_hi(*d);
}
Примечание хитрая строки: компилятор имеет экземпляр, но он не знает класс. На самом деле, он знает класс, но не самый конкретный. Знания, которые компилятор имеет во время компиляции, достаточны, чтобы знать, что существует функция, называемая howdy
, но недостаточно для определения того, какая из нескольких возможностей будет той, которую нужно вызвать во время выполнения.
Это то, где на помощь приходят vtables: компилятор знает, что в подклассах Dude
будет указатель на их функцию howdy
, встроенную где-то в их таблицу vtable. Это все, что им нужно знать во время компиляции! Они вставляют виртуальный вызов, который ищет указатель функции во время выполнения, достигая ожидаемого поведения (причудливым словом для такого поведения является «полиморфизм»). Вот demo of this program работает на ideone.
Причина использования vtable заключается в том, что когда производный класс повторно реализует виртуальную функцию, во время конструктора производного класса vtable обновляется, указывая на производную реализацию вместо базы. – Borgleader
Два слова: * динамическая отправка *. Несмотря на то, что ниже приведены хорошие объяснения, вам необходимо хотя бы услышать правильную терминологию. –