Вот пример полиморфизма из http://www.cplusplus.com/doc/tutorial/polymorphism.html (отредактировано для удобства чтения):
// abstract base class
#include <iostream>
using namespace std;
class Polygon {
protected:
int width;
int height;
public:
void set_values(int a, int b) { width = a; height = b; }
virtual int area(void) =0;
};
class Rectangle: public Polygon {
public:
int area(void) { return width * height; }
};
class Triangle: public Polygon {
public:
int area(void) { return width * height / 2; }
};
int main () {
Rectangle rect;
Triangle trgl;
Polygon * ppoly1 = ▭
Polygon * ppoly2 = &trgl;
ppoly1->set_values (4,5);
ppoly2->set_values (4,5);
cout << ppoly1->area() << endl; // outputs 20
cout << ppoly2->area() << endl; // outputs 10
return 0;
}
Мой вопрос: как компилятор узнает, что ppoly1 - это прямоугольник, а ppoly2 - это треугольник, чтобы он мог вызвать правильную функцию area ()? Это можно было выяснить, посмотрев на "Polygon * ppoly1 = & rect;" line и зная, что прямоугольник - это прямоугольник, но это не сработает во всех случаях, не так ли? Что, если бы вы сделали что-то подобное?
cout << ((Polygon *)0x12345678)->area() << endl;
Предполагая, что вам разрешен доступ к этой случайной области памяти.
Я бы проверил это, но я не могу использовать компьютер, на котором сейчас работаю.
(Надеюсь, я не упускаю что-то очевидное ...)





Не обращая внимания на аспекты привязки, на самом деле это определяет не компилятор.
Это среда выполнения C++, которая оценивает с помощью vtables и vpointers, что на самом деле представляет собой производный объект во время выполнения.
Я настоятельно рекомендую книгу Скотта Мейера «Эффективный C++» за хорошее описание того, как это делается.
Даже описывает, как параметры по умолчанию в методе производного класса игнорируются, а любые параметры по умолчанию в базовом классе по-прежнему принимаются! Это обязывает.
Каждый объект (который принадлежит к классу, по крайней мере, с одной виртуальной функцией) имеет указатель, называемый vptr. Он указывает на vtbl его фактического класса (который каждый класс с виртуальными функциями имеет по крайней мере один из; возможно, более одного для некоторых сценариев множественного наследования).
vtbl содержит набор указателей, по одному на каждую виртуальную функцию. Таким образом, во время выполнения код просто использует vptr объекта, чтобы найти vtbl, а оттуда - адрес фактической замещаемой функции.
В вашем конкретном случае у Polygon, Rectangle и Triangle есть vtbl, каждая с одной записью, указывающей на соответствующий метод area. Ваш ppoly1 будет иметь vptr, указывающий на Rectanglevtbl, и ppoly2 аналогично Trianglevtbl. Надеюсь это поможет!
vptr / vtbl. Ралли Не помню в стандарте :-) Указатель на vtable. Если vtable - это структура, определяемая компилятором, это более наглядно.
@Martin: vptr / vtbl - это термины, используемые в книге Бьярна Страуструпа «Язык программирования C++». :-)
Я думаю, что vtable не требуется стандартом, просто так получилось, что большинство компиляторов реализуют полиморфизм, используя его, поэтому он стал более или менее стандартным поведением
vtable тоже не входит в стандарт. Это всего лишь одна реализация из многих. Это тот, который используется почти повсеместно, но он не предусмотрен стандартом. (из любопытства - есть ли доступный компилятор, который делает что-то еще?)
Я согласен, что это деталь реализации, но я обнаружил, что она действительно помогает людям понять, как должны работать виртуальные функции. :-)
Извините, не не согласен с вашим объяснением, я пытался сказать следующее: лексема 'vtbl' не очень информативна, и если вы еще не слышали термин vtable. Когда я обсуждаю эту тему с коллегами, мы используем термин vtable (никто не говорит о vtbl).
Думаю, почти все языки C++ и C++ используют vtables. См. Здесь: en.wikipedia.org/wiki/Vtable - в этом разделе говорится о языках с множественной отправкой, которым требуется что-то более продвинутое.
Другой часто используемый метод - это хеш-таблица имен методов.
Крик! Не хеш-таблица имен методов! Похоже на позднюю привязку IDispatch (или как там она называется, я имею в виду ту, которая ищет указатели функций по именам методов, а не по идентификаторам DispID). Приносим извинения, если вы никогда не играли с OLE Automation и не понимаете, о чем я говорю. :-П
Чтобы ответить на вторую часть вашего вопроса: на этом адресе, вероятно, не будет v-table в нужном месте, и это приведет к безумию. Кроме того, он не определен в соответствии со стандартом.
Крис Джестер-Янг дает основной ответ на этот вопрос.
Википедия имеет более глубокую трактовку.
Если вы хотите знать полную информацию о том, как работает этот тип вещей (и для всех типов наследования, включая множественное и виртуальное наследование), одним из лучших ресурсов является "Внутри объектной модели C++" Стэна Липпмана.
cout << ((Polygon *)0x12345678)->area() << endl;
Этот код - катастрофа, ожидающая своего часа. Компилятор скомпилирует все правильно, но когда дело доходит до времени выполнения, вы не будете указывать на действительную v-таблицу, и, если вам повезет, программа просто выйдет из строя.
В C++ вы не должны использовать старые C-стили вроде этого, вы должны использовать dynamic_cast так:
Polygon *obj = dynamic_cast<Polygon *>(0x12345678)->area();
ASSERT(obj != NULL);
cout << obj->area() << endl;
dynamic_cast вернет NULL, если данный указатель не является допустимым объектом Polygon, поэтому он будет захвачен ASSERT.
Вы не можете использовать dynamic_cast из целого числа! (На самом деле вы также не можете использовать dynamic_cast из void *, вам нужно начать с указателя / ссылки на тип, который имеет какое-то отношение к типу, к которому вы выполняете приведение.)
Итак, я хочу сказать, что со случайным адресом, таким как reinterpret_cast <void *> (0x12345678), вы находитесь в зоне неопределенного поведения, несмотря ни на что. :-П
Я действительно делал такие вещи, чтобы хранить указатели объектов в списке Windows. По общему признанию, я должен написать это так: MYTYPE * obj = dynamic_cast <MYTYPE *> ((MYTYPE *) listbox.GetItemData (item)); Dynamic_cast немного безопаснее, чем прямое приведение.
Если GetItemData возвращает void *, вы можете избежать приведений в стиле C, используя вместо этого static_cast <MYTYPE *>. :-) Некоторые люди, которых я знаю, весьма догматичны в отношении избегания приведений в стиле C, потому что они могут быть довольно грубым инструментом (например, могут вызывать reinterpret_cast в непредвиденных случаях).
С другой стороны, если GetItemData возвращает int, то переинтерпретировать его в указатель не безопасно, и в этом случае я бы, вероятно, использовал map <int, MYTYPE *>. :-)
Таблицы виртуальных функций. То есть, оба ваших объекта, производных от Polygon, имеют таблицу виртуальных функций, которая содержит указатели функций на реализации всех их (нестатических) функций; и когда вы создаете экземпляр Triangle, указатель виртуальной функции для функции area () указывает на функцию Triangle :: area (); когда вы создаете экземпляр Rectangle, функция area () указывает на функцию Rectangle :: area (). Поскольку указатели виртуальных функций хранятся вместе с данными для объекта в памяти, каждый раз, когда вы ссылаетесь на этот объект как на многоугольник, будет использоваться соответствующая область () для этого объекта.
Оффтоп: Почему бы не проголосовать за других людей, которые потратили время на написание полезных ответов для вас?