Я думаю, что я должен что-то упустить где-то
Да, и большинство вещей вы получили до самого конца.
Вот напоминание о действительно базовойвещи в C / C ++ (C и C ++: одно и то же концептуальное наследие, поэтому многие общие понятия являются общими, даже если в какой-то момент мелкие детали значительно расходятся).(Это может быть действительно очевидно и просто, но стоит сказать это громко, чтобы почувствовать это.)
Выражения являются частью скомпилированной программы, они существуют во время компиляции;объекты существуют во время выполнения.Объект (вещь) обозначается выражением (словом);они концептуально различны.
В традиционном C / C ++ lvalue (сокращение от left-value ) - это выражение, оценка выполнения которого обозначает объект;разыменование указателя дает значение l (например, *this
).Это называется «левое значение», потому что оператор присваивания слева требует объекта для назначения.(Но не все lvalue могут быть слева от оператора присваивания: выражения, обозначающие объекты const, являются lvalues, и обычно их нельзя назначить.) Lvalue всегда имеют четко определенную идентичность, и у большинства из них есть адрес (только члены struct, объявленные какбитовое поле не может получить свой адрес, но базовый объект хранения все еще имеет адрес).
(В современном C ++ концепция lvalue была переименована в glvalue, и была изобретена новая концепция lvalue (вместо созданияновый термин для новой концепции и сохранение старого термина концепции объекта с идентичностью, которая может или не может быть изменяемой. Это было по моему не столь скромному мнению серьезная ошибка.)
Поведениеполиморфный объект (объект типа класса, по крайней мере, с одной виртуальной функцией) зависит от его динамического типа , от его типа начатой конструкции (имени конструктора объекта, который начал конструировать элементы данных,или вошел в тело конструктора). Во время выполненияв теле конструктора Child
динамический тип объекта, спроектированный с помощью *this
, равен Child
(во время выполнения тела конструктора базового класса динамический тип - это тип выполняемого конструктора базового класса).
Динамическая полиморфная означает, что вы можете использовать полиморфный объект с lvalue, чей объявленный тип (тип, выведенный во время компиляции из правил языка) не совсем тот же тип, носвязанный тип (связанный с наследованием) .В этом весь смысл ключевого слова virtual в C ++, без которого оно было бы совершенно бесполезным!
Если base_array[i]
содержит адрес объекта (поэтому его значение четко определено, а не null), вы можете разыменоватьЭто.Это дает вам lvalue, объявленный тип которого всегда равен Base *
по определению: это объявленный тип, объявление base_array
:
Base (*(base_array[2])); // extra, redundant parentheses
, которое, конечно, можно записать
Base* base_array[2];
если вы хотите написать это таким образом, но дерево разбора, способ декомпозиции объявления компилятором НЕ
{ Base*
} { base_array[2]
}
(использование фигурной скобки жирным шрифтом для символического представления синтаксического анализа)
, но вместо этого
Base {* {{ base_array
} [2]
}}
Я надеюсь, вы понимаете, что фигурные скобки здесь - мой выборМета-язык, а НЕ фигурные скобки, используемые в грамматике языка для определения классов и функций (я не знаю, как здесь рисовать рамки вокруг текста).
Как новичок, важно, чтобы вы "программировали" свою интуициюправильно, всегда читать объявления, как это делает компилятор;если вы когда-либо объявляете два идентификатора в одном и том же объявлении, разница важна int * a, b;
означает int (*a), b;
И НЕ int (*a), (*b);
(Примечание: даже если это может быть вам понятно, ОП, так как этоЯсно, что этот вопрос представляет интерес для начинающих в C ++, так как напоминание о синтаксисе объявления C / C ++ может пригодиться кому-то еще.)
Итак, возвращаясь к проблеме полиморфизма: объект производного типа (имя самого последнего введенного конструктора) может быть обозначен lvalue объявленного типа базового класса.Поведение вызовов виртуальных функций определяется динамическим типом (также называемым реальным типом) объекта, обозначенного выражением, в отличие от поведения вызовов не виртуальных функций;это семантика, определяемая стандартом C ++.
Способ, которым компилятор получает семантику, определенную стандартом языка, является его собственной проблемой и не описывается в стандарте языка, но когда существует только один эффективный способ сделать этовсе компиляторы делают это по существу одинаково (мелкие детали зависят от компилятора) с
- одной таблицей виртуальных функций (" vtable ") на полиморфный класс
- один указатель на vtable (" vptr ") для каждого полиморфного объекта
(Очевидно, и vtable, и vptr являются концепциями реализации, а не понятиями языка, но они настолько распространены, чтокаждый программист C ++ знает их.)
vtable - это описание полиморфных аспектов класса: операции времени выполнения над выражением заданного объявленного типа, поведение которого зависит от динамического типа.Существует одна запись для каждой операции во время выполнения.Vtable похож на структуру (запись) с одним элементом (записью) на операцию (все записи обычно являются указателями одного размера, поэтому многие люди описывают vtable как массив указателей, но я не описываю его какstruct).
vptr - это скрытый элемент данных (элемент данных без имени, недоступный для кода C ++), чья позиция в объекте фиксирована, как и любой другой элемент данных, который может быть прочитанкод времени выполнения, когда вычисляется lvalue типа полиморфного класса (назовите его D для "объявленного типа").Разыменование vptr в D дает вам виртуальную таблицу, описывающую D lvalue , с записями для каждого аспекта времени выполнения lvalue типа D .По определению местоположение vptr и интерпретация vtable (размещение и использование его записей) полностью определяются объявленным типом D .(Очевидно, что никакая информация, необходимая для использования и интерпретации vptr, не может быть функцией типа времени выполнения объекта: vptr используется, когда этот тип неизвестен.)
Семантика vptr - этонабор гарантированных действительных операций времени выполнения на vptr: как можно разыменовать vptr (vptr существующего объекта всегда указывает на действительный vtable).Это набор свойств формы: добавив смещение off к значению vptr, вы получите значение, которое можно использовать «таким образом». Эти гарантии формируют контракт времени выполнения.
Наиболее очевидный аспект времени выполнения полиморфного объекта - это вызов виртуальной функции, поэтому в vtable есть запись для D lvalueдля каждой виртуальной функции, которая может быть вызвана для lvalue типа D , это запись для каждой виртуальной функции, объявленной либо в этом классе, либо в базовом классе (не считая переопределителей, поскольку они одинаковы).Все нестатические функции-члены имеют «скрытый» или «неявный» аргумент, параметр this
;при компиляции он становится обычным указателем.
Любой класс X , производный от D , будет иметь vtable для значений D l.Для эффективности в обычном случае простого (не виртуального) одиночного наследования семантика vptr базового класса (который мы затем называем первичным базовым классом) будет дополнена новыми свойствами, поэтому vtable для X будет расширен: макет и семантика виртуальной таблицы для D будут расширены: любое свойство виртуальной таблицы для D также является свойствомдля vtable для X семантика будет «унаследована»: существует «наследование» vtables параллельно с наследованием внутри классов.
В логическом смысле увеличивается число гарантий: гарантии vptr объекта производного класса сильнее, чем гарантии vptr объекта базового класса.Поскольку это более сильный контракт, весь код, сгенерированный для базового lvalue, все еще действителен.
[В более сложном наследовании это либо виртуальное наследование, либо не виртуальное вторичное наследование (в множественном наследовании - наследование от вторичной базы,это любая база, которая не определена как «первичная база»), расширение семантики vtable базового класса не так просто.]
[Один из способов объяснить реализацию классов C ++ - этоперевод на C (действительно, первый компилятор C ++ собирал на C, а не на сборку).Перевод функции-члена C ++ - это просто функция C, в которой явный неявный параметр this
является нормальным параметром указателя.]
Запись vtable для виртуальной функции для D lvalueэто просто указатель на функцию с параметром в качестве явного теперь this
параметра: этот параметр является указателем на D , он фактически указывает на базовый подобъект D объектакласс, производный от D , или объект фактического динамического типа D .
Если D является первичной базой X , это тот, который начинается с того же адреса, что и производный класс, и где vtable начинается с того же адреса, поэтому значение vptr одинаково, и vptr используется совместно для первичной базы и производного класса.Это означает, что виртуальные вызовы (вызовы lvalue, которые проходят через vtable) к виртуальным функциям в X , которые заменяют одинаково (которые переопределяют с тем же типом возврата), просто следуют тому же протоколу.Виртуальные переопределители могут иметь другой ковариантный тип возврата, и в этом случае может использоваться другое соглашение о вызовах.)
Существуют другие специальные записи vtable:
- Несколько записей виртуальных вызовов дляданная сигнатура виртуальной функции, если переопределитель имеет ковариантный тип возвращаемого значения, который требует и корректирует (не является первичной базой).
- Для специальных виртуальных функций: когда
delete operator
используется на полиморфной основе с виртуальнойдеструктор, это делается через удаление виртуального деструктора, чтобы вызвать правильный operator delete
(заменил delete, если он есть). - Существует также не удаляющий виртуальный деструктор, который используется для явных вызовов деструкторов:
l.~D();
- В vtables хранятся смещения для каждого виртуального базового подобъекта для неявного преобразования в указатель виртуальной базыили для доступа к его элементам данных.
- Существует смещение наиболее производного объекта для
dynamic_cast<void*>
. - Запись для оператора
typeid
, примененная к полиморфному объекту (в частности,name()
класса). - Достаточно информации для операторов
dynamic_cast<X*>
, примененных к указателю на полиморфный объект для навигации по иерархии классов во время выполнения, для поиска заданного базового класса или производного подобъекта (если только X
это не просто базовый класс приведенного типа, как без динамической навигации по иерархии).
Это просто обзор информации, представленной в vtable, и видов vtable.другие тонкости.(Виртуальные базы заметно сложнее, чем не виртуальные базы на уровне реализации.)