Виртуальные функции во время строительства. Почему Java отличается от С++
Сегодня у меня был тест, и один из вопросов касался использования виртуального метода в конструкторе С++. Я пропустил этот вопрос, я ответил, что не должно быть никаких проблем, однако после прочтения этого я узнал, что ошибся.
Итак, я понимаю, что причина этого не в том, что производный объект не полностью инициализирован, поэтому вызов его виртуальным методом может привести к недействительным последствиям.
Мой вопрос, как это было решено в Java/С#? Я знаю, что я могу вызвать производный метод в моем базовом конструкторе, я бы предположил, что эти языки имеют точно такую же проблему.
Ответы
Ответ 1
Java имеет очень другую объектную модель из С++. В Java вы не можете иметь переменные, которые являются объектами типа класса, вместо этого вы можете иметь только ссылки на объекты (типа класса). Поэтому все члены класса (которые являются только ссылками) начинаются тривиально как null
, пока весь производный объект не будет настроен в памяти. Только тогда выполняются конструкторы. Таким образом, к тому моменту, когда базовый конструктор вызывает виртуальную функцию, даже если эта функция переопределена, переопределенная функция может, по крайней мере, правильно обращаться к членам производного класса. (Эти члены не могут быть назначены еще, но, по крайней мере, они существуют.)
(Если это помогает, вы также можете считать, что каждый класс без членов final
в Java технически по умолчанию является конструктивным, по крайней мере в принципе: в отличие от С++, Java не имеет таких вещей, как константы или ссылки (которые должны быть инициализируется в С++), и на самом деле списков инициализации вообще нет. Переменные в Java просто не нужно инициализировать. Это либо примитивы, которые начинаются с 0 или ссылки на типы классов, начинающиеся как null
. Исключение исходит от нестатических членов класса final
, которые не могут быть отскок и на самом деле должны быть "инициализированы", имея точно один оператор присваивания где-то в каждом конструкторе [спасибо @josefx за указание этого!).
Ответ 2
понять, что причина не позволяет это потому, что производный объект не полностью инициализирован и поэтому вызов его виртуальным методом может привести к недействительным последствиям.
Неправильно. С++ вызовет реализацию базового класса метода, а не производного класса. Нет "недействительных последствий". Единственная действительная причина для избежания конструкции заключается в том, что поведение иногда приходит как сюрприз.
Это отличается от Java, потому что Java вызывает реализацию производного класса.
Ответ 3
Каждый конструктор Java выглядит следующим образом:
class Foo extends Bar {
Foo() {
super(); // creates Bar
// do things
}
}
Итак, если вы поместите код, работающий с производными методами в do things
, кажется логичным, чтобы этот базовый объект был правильно инициализирован, после вызова его конструктора в super();
Ответ 4
В С++ каждый полиморфный класс (класс, который имеет хотя бы одну виртуальную функцию) имеет скрытый указатель в начале его (обычно называемый v-table или что-то в этом роде), который будет инициализирован виртуальной таблице (массив функций которые указывают на тело каждой виртуальной функции) этого класса, и когда вы вызываете виртуальную функцию С++, просто вызывайте ((v-table*)class)[index of your function]( function-parameters )
, поэтому, если вы вызываете виртуальную функцию в конструкторе v-таблицы базового класса, указываете на виртуальную таблицу базового класса, так как ваш класс является базовым, и ему все еще нужна инициализация, чтобы стать дочерней, и в результате вы вызовите реализацию функции из базы не из дочернего элемента, и если это чистая виртуальная функция, вы получите нарушение доступа.
но в java это не что-то вроде этого, в целом java класс является чем-то вроде std::map<std::string, JValue>
, в этом случае JValue
является некоторым вариантом (например, union или boost::variant
), когда вы вызываете функцию в конструкторе базы он найдет имя функции на карте и вызовет его, это еще не значение от дочернего элемента, но вы все равно можете его вызвать, и если вы изменили его в prototype
, поскольку прототип, созданный до вашего конструктора, вы можете успешно вызвать функцию из child, но если для функции требуется некоторая инициализация из конструктора ребенка, вы по-прежнему получаете ошибку или недопустимый результат.
поэтому в целом не рекомендуется переводить функцию из дочернего элемента (например, виртуальную функцию) в базовый класс. если ваш класс должен это сделать, добавьте метод initialize и вызовите его из конструктора вашего дочернего класса.
Ответ 5
Я думаю, что Java/С# избегает этой проблемы, создавая из производного класса назад, а не в С++ из базового класса вперед.
Java неявно вызывает super() в конструкторе классов, поэтому к моменту, когда первая строка написанного кода в конструкторе производного класса называется, все конструкторы всех унаследованных классов гарантированно вызываются, и поэтому новый экземпляр будет иметь были полностью инициализированы.
Я думаю, что и в С++ новый экземпляр класса начинает жизнь как базовый класс и получает "обновленный" до конечного типа класса, когда мы перемещаемся по цепочке наследования. Это означает, что при вызове виртуальной функции в конструкторе вы фактически вызываете версию этой функции для базового класса.
В Java и предположительно С# новый экземпляр запускает жизнь как необходимый тип класса, поэтому будет вызываться правильная версия виртуального метода.
Ответ 6
Java не полностью устраняет проблему.
Вызывается переопределенный метод, вызываемый из конструктора суперкласса, который зависит от полей подкласса, перед тем как эти поля были инициализированы.
Если вы контролируете всю иерархию классов, вы можете, конечно, просто убедиться, что ваши переопределения не зависят от полей подкласса. Но безопаснее просто не вызывать виртуальные методы из конструкторов.