Виртуальные функции с реферат

Обновлено: 04.07.2024

Полиморфизм времени исполнения обеспечивается за счет использования производных классов и виртуальных функций. Виртуальная функция — это функция, объявленная с ключевым словом virtual в базовом классе и переопределенная в одном или в нескольких производных классах. Виртуальные функции являются особыми функциями, потому что при вызове объекта производ­ного класса с помощью указателя или ссылки на него С++ определяет во время исполнения про­граммы, какую функцию вызвать, основываясь на типе объекта. Для разных объектов вызываются разные версии одной и той же виртуальной функции. Класс, содержащий одну или более вир­туальных функций, называется полиморфным классом (polymorphic class).

Виртуальная функция объявляется в базовом классе с использованием ключевого слова virtual. Когда же она переопределяется в производном классе, повторять ключевое слово virtual нет не­обходимости, хотя и в случае его повторного использования ошибки не возникнет.

В качестве первого примера виртуальной функции рассмотрим следующую короткую программу:

Программа выдаст следующий результат:

Base
First derivation
Second derivation

Проанализируем подробно эту программу, чтобы понять, как она работает.

Как можно видеть, в объекте Base функция who() объявлена как виртуальная. Это означает, что эта функция может быть переопределена в производных классах. В каждом из классов first_d и second_d функция who() переопределена. В функции main() определены три переменные. Первой является объект base_obj, имеющий тип Base. После этого объявлен указатель р на класс Base, затем объекты first_obj и second_obj, относящиеся к двум производным классам. Далее указателю р при­своен адрес объекта base_objи вызвана функция who(). Поскольку эта функция объявлена как виртуальная, то С++ определяет на этапе исполнения, какую из версий функции who() употребить, в зависимости от того, на какой объект указывает указатель р. В данном случае им является объект типа Base, поэтому исполняется версия функции who(), объявленная в классе Base. Затем указате­лю р присвоен адрес объекта first_obj. (Как известно, указатель на базовый класс может быть ис­пользован для любого производного класса.) После того, как функция who() была вызвана, С++ снова анализирует тип объекта, на который указывает р, для того, чтобы определить версию фун­кции who(), которую необходимо вызвать. Поскольку р указывает на объект типа first_d, то ис­пользуется соответствующая версия функции who(). Аналогично, когда указателю р присвоен адрес объекта second_obj, то используется версия функции who(), объявленная в классе second_d.

Наиболее распространенным способом вызова виртуальной функции служит использование параметра функции. Например, рассмотрим следующую модификацию предыдущей программы:

Эта программа выдаст следующий результат:

Base
First derivation
Base

Надо иметь в виду, что характеристики наследования носят иерархический характер. Чтобы проиллюстрировать это, предположим, что в предыдущем примере класс second_d порожден от класса first_d вместо класса Base. Когда функцию who() вызывают, используя указатель на объект типа second_d (в котором функция who() не определялась), то будет вызвана версия функции who(), объявленная в классе first_d, поскольку этот класс — ближайший к классу second_d. В общем случае, когда класс не переопределяет виртуальную функцию, С++ использует первое из определений, которое он находит, идя от потомков к предкам.

В предыдущем уроке, посвященном указателям и ссылкам базового класса на объекты производных классов, мы рассмотрели ряд примеров, в которых использование указателей или ссылок базового класса могло упростить код. Однако в каждом случае мы сталкивались с проблемой, заключающейся в том, что указатель или ссылка базового класса могли вызывать только базовую версию функции, а не ее производную версию.

Вот простой пример такого поведения:

Этот пример выводит следующий результат:

Поскольку rBase является ссылкой типа Base , она вызывает Base::getName() , даже если на самом деле ссылается на часть Base объекта Derived .

В этом уроке мы покажем, как решить эту проблему с помощью виртуальных функций.

Виртуальные функции и полиморфизм

Виртуальная функция – это особый тип функций, которая при вызове преобразуется в версию функции, которая принадлежит самому дочернему классу из имеющихся функций в базовом и производном классах. Эта возможность известна как полиморфизм. Производная функция считается совпадающей, если она имеет ту же сигнатуру (имя, типы параметров и является ли она константной) и возвращаемый тип, что и базовая версия функции. Такие функции называются переопределениями.

Чтобы сделать функцию виртуальной, просто поместите перед объявлением функции ключевое слово virtual .

Вот приведенный выше пример, переписанный с использованием виртуальной функции:

В этом примере выводится следующий результат:

Поскольку rBase является ссылкой на часть Base объекта Derived , при вычислении rBase.getName() вызов обычно преобразуется в Base::getName() . Однако Base::getName() является виртуальной, что указывает программе посмотреть, есть ли еще производные версии этой функции, доступные в Base и Derived . В этом случае вызов будет преобразован в Derived::getName() !

Давайте посмотрим на более сложный пример:

Как вы думаете, что будет выводить эта программа?

Давайте посмотрим, как это работает. Сначала мы создаем экземпляр объекта класса C . rBase – это ссылка типа A , которую мы установили для ссылки на часть A объекта C . Наконец, мы вызываем rBase.getName() . rBase.getName() вычисляется как A::getName() . Однако A::getName() является виртуальной, поэтому компилятор вызовет наиболее дочернюю совпадающую функцию между классами A и C . В данном случае это C::getName() . Обратите внимание, что он не будет вызывать D::getName() , потому что наш исходный объект был C , а не D , поэтому рассматриваются функции только между A и C .

В результате наша программа выводит:

Более сложный пример

Давайте еще раз посмотрим на пример с животными, с которым мы работали в предыдущем уроке. Вот исходный код классов и тестовый код:

Этот код печатает:

А вот эквивалентные классы с виртуальной функцией speak() :

Эта программа дает следующий результат:

При вычислении animal.speak() программа замечает, что Animal::speak() является виртуальной функцией. В случае, когда animal ссылается на часть Animal объекта Cat , программа просматривает все классы между Animal и Cat , чтобы узнать, может ли она найти более дочернюю функцию. В этом случае она находит Cat::speak() . В случае, когда animal ссылается на часть Animal объекта Dog , программа вычисляет этот вызов функции как Dog::speak() .

Обратите внимание, что мы не сделали виртуальной Animal::getName() . Это связано с тем, что getName() никогда не переопределяется ни в одном из производных классов, поэтому в этом нет необходимости.

Точно так же следующий пример массива теперь работает правильно:

Что дает следующий результат:

Несмотря на то, что в этих двух примерах используются только Cat и Dog , любые другие классы, производные от Animal , также будут работать с нашей функцией report() и массивом animals без дальнейших изменений! Это, пожалуй, самое большое преимущество виртуальных функций – возможность структурировать код таким образом, чтобы новые производные классы автоматически работали со старым кодом без его изменения!

Предупреждение: чтобы можно было использовать функцию производного класса, сигнатура функции производного класса должна точно соответствовать сигнатуре виртуальной функции базового класса. Если функция производного класса имеет другие типы параметров, программа, скорее всего, всё равно будет компилироваться нормально, но вызов виртуальной функции не будет разрешен должным образом.

Использование ключевого слова virtual

Если функция помечена как виртуальная, все совпадающие переопределения также считаются виртуальными, даже если они явно не отмечены как таковые. Однако наличие ключевого слова virtual в производных функциях не повредит и служит полезным напоминанием о том, что данная функция является виртуальной функцией, а не обычной. Следовательно, обычно в производных классах рекомендуется использовать ключевое слово virtual для виртуализированных функций, даже если это не является строго необходимым.

Типы возвращаемых значений виртуальных функций

В обычных условиях тип возвращаемого значения виртуальной функции и ее переопределения должны совпадать. Рассмотрим следующий пример:

В этом случае Derived::getValue() не считается совпадающим переопределением для Base::getValue() , и компиляция завершится ошибкой.

Не вызывайте виртуальные функции из конструкторов или деструкторов

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

Помните, что когда создается объект производного класса Derived , сначала создается часть базового класса Base . Если бы вы вызывали виртуальную функцию из конструктора класса Base , когда часть класса Derived еще даже не была создана, было бы невозможно вызвать версию этой функции из Derived , потому что еще нет объекта класса Derived , с которым функция из Derived могла бы работать. В C++ вместо этого вызывается версия из класса Base .

Аналогичная проблема существует и для деструкторов. Если вы вызываете виртуальную функцию в деструкторе базового класса, она всегда будет преобразовываться в версию функции базового класса, потому что часть производного класса к этому моменту уже будет уничтожена.

Лучшая практика

Никогда не вызывайте виртуальные функции из конструкторов или деструкторов.

Обратная сторона виртуальных функций

Поскольку в большинстве случаев вы хотите, чтобы ваши функции были виртуальными, почему бы просто не сделать все функции виртуальными? Ответ в том, что это неэффективно – разрешение вызова виртуальной функции занимает больше времени, чем разрешение вызова обычной функции. Кроме того, компилятор также должен выделить дополнительный указатель для каждого объекта класса, который имеет одну или несколько виртуальных функций. Мы поговорим об этом подробнее в будущих уроках данной главы.

Небольшой тест

Вопрос 1

Что печатают следующие программы? Это упражнение должно выполняться путем проверки в уме, а не путем компиляции примеров с помощью компилятора.

rBase – это ссылка типа A , указывающая на объект C . Обычно rBase.getName() вызывает A::getName() , но A::getName() является виртуальной, поэтому вместо этого вызывается наиболее дочерняя совпадающая функция между классами A и C . Это B::getName() , которая печатает B .

Это довольно просто, поскольку C::getName() является наиболее дочерним совпадающим вызовом между классами B и C .

Поскольку getName() в A не является виртуальной, при вызове rBase.getName() вызывается A::getName() .

Несмотря на то, что getName() в B и C не отмечены как виртуальные функции, A::getName() является виртуальной, а B::getName() и C::getName() переопределяют ее. Следовательно, B::getName() и C::getName() неявно считаются виртуальными, и поэтому вызов rBase.getName() преобразуется в C::getName() , а не в B::getName() .

Это немного сложнее. rBase – это ссылка типа A на объект C , поэтому rBase.getName() обычно вызывает A::getName() . Но A::getName() является виртуальной, поэтому вызывается наиболее производная версия функции между A и C . И это A::getName() . Поскольку B::getName() и C::getName() не являются константными, они не считаются переопределениями! Следовательно, эта программа печатает A .

Еще один хитрый пример. Когда мы создаем объект C , сначала создается часть A . Когда для этого вызывается конструктор A , он вызывает виртуальную функцию getName() . Поскольку части B и C класса еще не созданы, этот вызов преобразуется в A::getName() .

Виртуальные функции — специальный вид функций-членов класса. Виртуальная функция отличается об обычной функции тем, что для обычной функции связывание вызова функции с ее определением осуществляется на этапе компиляции. Для виртуальных функций это происходит во время выполнения программы.


Для объявления виртуальной функции используется ключевое слово virtual . Функция-член класса может быть объявлена как виртуальная, если

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


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

Определение того, какой экземпляр виртуальной функции вызывается по выражению вызова функции, зависит от класса объекта, адресуемого указателем или ссылкой, и осуществляется во время выполнения программы. Этот механизм называется динамическим (поздним) связыванием или разрешением типов во время выполнения .

Указатель на базовый класс может указывать либо на объект базового класса, либо на объект порожденного класса. Выбор функции-члена зависит от того, на объект какого класса при выполнении программы указывает указатель, но не от типа указателя. При отсутствии члена порожденного класса по умолчанию используется виртуальная функция базового класса.

В каждом случае выполняется различная версия функции print() . Выбор динамически зависит от объекта, на который ссылается указатель.

Пример : выбор виртуальной функции

Чистая виртуальная функция

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

Чистая виртуальная функция — это метод класса, тело которого не определено.

В базовом классе такая функция записывается следующим образом:

Чистая виртуальные функции используются для того, чтобы отложить решение задачи о реализации функции на более поздний срок. В терминологии ООП это называется отсроченным методом . Класс, имеющий по крайней мере одну чистую виртуальную функцию, называется абстрактным базовым классом . Для иерархии типа полезно иметь абстрактный базовый класс. Он содержит общие свойства иерархии типа, но каждый порожденный класс реализует эти свойства по-своему.

Для рассмотренного выше примера (класс Фигура) функцию вычисления площади целесообразно задать чистой виртуальной функцией, которую переопределяет каждый наследуемый класс.
Строка 9 при этом будет иметь вид:

Спасибо очень, чётко и просто объяснили в отличие от других сайтов, где авторы статей, словно школьники, которых поймали, когда те курили за гаражами, уходящие от ответа на вопрос: "Зачем?".

Нормальная статья. Вирутуальная функция - это просто указатель на функцию, которую каждый класс потомок может изменить на свою. Это если бы мы писали на С

Виртуальные методы — один из важнейших приёмов реализации полиморфизма . Они позволяют создавать общий код, который может работать как с объектами базового класса, так и с объектами любого его класса-наследника. При этом базовый класс определяет способ работы с объектами и любые его наследники могут предоставлять конкретную реализацию этого способа.

Сюда же можно было бы про виртуальный деструктор написать, я думаю. Статья годная, первый пример не очень удачный. С абстрактными классами можно было бы связать второй пример. У Вас там как раз описывается некая "фигура", т.е. тоже абстрактное понятие, для которого нельзя описать метод вычисления площади или чего-то там. Это был бы удачный пример, т.к. это:

не совсем верно. Формально, фигура - это множество точек и оно может не иметь площади, возвращать ноль - не правильно. Описать чисто виртуальный метод - правильно. Термин "отсроченный метод" какой-то странный. Он встречается в классических книгах?

В классе можно определять элементы данных и функции со спецификатором static. В этом случае их можно рассматривать как глобальные переменные или функции в пределах класса, и доступны они не одному объекту, а разделяются между всем классом, не ассоциируются с отдельными представителями класса.

static int count; >

Для элементов, которые объявлены в открытой секции класса, вызов, например, будет выглядеть следующим образом:

Если элементы определены в закрытой секции, то такое обращение некорректно.

Если мы объявили такой элемент в закрытой секции класса, то мы должны определить функции для работы с этими элементами. Обращение к таким функциям выглядит так: Name::Add1();

1) Статические функции и элементы класса не ассоциируются с отдельными представителями класса. Обращение к ним производится выражением вида:

имя класса:: имя элемента

2) Им не передается указатель this, т.к. он ассоциируется с отдельным представителем класса.

3) Статические функции могут вызываться независимо от того, существует ли хотя бы один представитель класса.

4) Статические функции не могут быть виртуальными.

2. Виртуальные функции

Виртуальные функции – это функции-члены класса, вызов которых осуществляется во время выполнения программы (то есть динамически) по типу объекта, с которым она используется.

Например, базовый класс может описывать фигуру на экране без конкретизации ее вида, а производные классы (окружность, эллипс и т.п.) однозначно определяют ее формы и размеры. Если в базовом классе ввести функцию для изображения фигуры на экране, то выполнение этой функции будет возможно только для объектов каждого из производных классов, определяющих конкретные изображения.

Объявление виртуальной функции в классе:

Отметим, что классы, включающие такие функции, играют особую роль в объектно-ориентированном программировании. Именно поэтому они носят специальное название – полиморфные.

Вернемся к упомянутому выше примеру с фигурами. Рассмотрим класс Point(производный от Location).

Пусть в этом классе определена компонентная функция voidShow(). Так как внешний вид фигуры, для которой будет использоваться данная функция, в базовом классе еще не определен, то в каждый из производных классов нужно включить свою функцию voidShow() для формирования изображения на экране. Это не очень удобно, поэтому в таких случаях используют механизм виртуальных функций. Любая нестатическая функция базового класса может быть сделана виртуальной, если в ее объявлении использовать спецификатор virtual.

class Point: public Location

Point (int nx,int ny);

virtual void Show();

virtual void Hide();

virtual void Drag(int by);

void MoveTo (int nx ,int ny); >;

Виртуальными могут быть не любые функции, а только нестатические компонентные функции какого-либо класса. После того как функция определена как виртуальная, ее повторное определение в производном классе (с тем же самым прототипом) создает в этом классе новую виртуальную функцию, причем спецификатор virtual может не использоваться.

В производном классе нельзя определять функцию с тем же именем и с той же сигнатурой параметров, но с другим типом возвращаемого значения, чем у виртуальной функции базового класса. Это приводит к ошибке на этапе компиляции.

Если в производном классе ввести функцию с тем же именем и типом возвращаемого значения, что и виртуальная функция базового класса, но с другой сигнатурой параметров, то эта функция производного класса не будет виртуальной. В этом случае с помощью указателя на базовый класс при любом значении этого указателя выполняется обращение к функции базового класса (несмотря на спецификатор virtual и присутствие в производном классе похожей функции).

Рассмотрим, как будет выглядеть вызов виртуальной функции voidShow() в производном классе:

class Circle : public Point

Circle (int nx,int ny, int nr);

void Expand(int by);

Как мы видим, спецификатор virtual можно уже не указывать.

Механизм виртуального вызова может быть подавлен с помощью явного использования полного квалифицированного имени. Таким образом, при необходимости вызова из производного класса виртуального метода (компонентной функции) базового класса употребляется полное имя.

Все выше сказанное можно объединить в ПРАВИЛА:

1) не объявлять static;

2) Объявления виртуальных функций в производных классах должны иметь ту же сигнатуру, что и в базовом. Указывать спецификатор virtual в этом случае не обязательно.

3) Виртуальная функция должна быть обязательно определена или быть чистой виртуальной функцией.

Чистой виртуальной функцией называется компонентная функция, которая имеет следующее определение:

virtualтип имя_функции (список формальных параметров)=0;

В этой записи конструкция “=0” называется “чистый спецификатор”. Пример описания чистой виртуальной функции:

Чистая виртуальная функция в определении класса показывает, что ее определение откладывается до производных классов. Чистая виртуальная функция “ничего не делает” и недоступна для вызовов. Ее назначение – служить основой для подменяющих ее функций в производных классах и показывать, что данный класс абстрактный.

3. Абстрактные классы

Абстрактные классы могут использоваться только в качестве базовых для других классов. Предполагается, что абстрактный класс содержит одну или несколько чистых виртуальных функций.

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

Как всякий класс, абстрактный класс может иметь явно определенный конструктор. Из конструктора возможен вызов методов класса, но любые прямые или опосредованные обращения из конструктора к чистым виртуальным функциям приведут к ошибкам во время выполнения программы.

Механизм абстрактных классов разработан для представления общих понятий, которые в дальнейшем предполагается конкретизировать. Эти общие понятия обычно невозможно использовать непосредственно, но на их основе можно, как на базе, построить частные производные классы, пригодные для описания конкретных объектов.

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

В качестве иллюстрации выше сказанного рассмотрим пример для расчета площади треугольника и прямоугольника:

На предыдущем уроке мы рассматривали ряд примеров, в которых использование указателей или ссылок родительского класса упрощало логику и уменьшало количество кода.

Виртуальные функции и Полиморфизм

Тем не менее, мы сталкивались с проблемой, когда родительский указатель или ссылка вызывали только родительские методы, а не дочерние. Например:

rParent is a Parent

Поскольку rParent является ссылкой класса Parent, то вызывается Parent::getName(), хотя фактически мы ссылаемся на часть Parent объекта child .

На этом уроке мы рассмотрим, как можно решить эту проблему с помощью виртуальных функций.

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

rParent is a Child

Поскольку rParent является ссылкой на родительскую часть объекта child , то, обычно, при обработке rParent.getName() вызывался бы Parent::getName(). Тем не менее, поскольку Parent::getName() является виртуальной функцией, то компилятор понимает, что нужно посмотреть, есть ли переопределения этого метода в дочерних классах. И компилятор находит Child::getName()!

Рассмотрим пример посложнее:

Как вы думаете, какой результат выполнения этой программы?

Рассмотрим всё по порядку:

Сначала создается объект c класса C.

rParent — это ссылка класса A, которой мы указываем ссылаться на часть A объекта c .

Затем вызывается метод rParent.getName() .

Обратите внимание, компилятор не будет вызывать D::getName(), поскольку наш исходный объект был класса C, а не класса D, поэтому рассматриваются методы только между классами A и C.

Результат выполнения программы:

Более сложный пример

Рассмотрим класс Animal из предыдущего урока, добавив тестовый код:

// Мы делаем этот конструктор protected так как не хотим, чтобы пользователи имели возможность создавать объекты класса Animal напрямую,

Результат выполнения программы:

Matros says .
Barsik says .

А теперь рассмотрим тот же класс, но сделав метод speak() виртуальным:

// Мы делаем этот конструктор protected так как не хотим, чтобы пользователи имели возможность создавать объекты класса Animal напрямую,

Результат выполнения программы:

Matros says Meow
Barsik says Woof

При обработке animal.speak() , компилятор видит, что метод Animal::speak() является виртуальной функцией. Когда animal ссылается на часть Animal объекта cat , то компилятор просматривает все классы между Animal и Cat, чтобы найти наиболее дочерний метод speak(). И находит Cat::speak(). В случае, когда animal ссылается на часть Animal объекта dog , компилятор находит Dog::speak().

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

Аналогично со следующим примером с массивом животных:

Matros says Meow
Barsik says Woof
Ivan says Meow
Tolik says Woof
Martun says Meow
Tyzik says Woof

Несмотря на то, что эти два примера используют только классы Cat и Dog, любые другие дочерние классы также будут работать с нашей функцией report() и с массивом животных, без внесения дополнительных модификаций! Это, пожалуй, самое большое преимущество виртуальных функций — возможность структурировать код таким образом, чтобы новые дочерние классы автоматически работали со старым кодом, без необходимости внесения изменений со стороны программиста!

Предупреждение: Сигнатура виртуального метода дочернего класса должна полностью соответствовать сигнатуре виртуального метода родительского класса. Если у дочернего метода будет другой тип параметров, нежели у родительского, то вызываться этот метод не будет.

Использование ключевого слова virtual

Если функция отмечена как виртуальная, то все соответствующие переопределения тоже считаются виртуальными, даже если возле них явно не указано ключевое слова virtual. Однако, наличие ключевого слова virtual возле методов дочерних классов послужит полезным напоминанием о том, что эти методы являются виртуальными, а не обычными. Следовательно, полезно указывать ключевое слово virtual возле переопределений в дочерних классах, даже если это не является строго необходимым.

Типы возврата виртуальных функций

Типы возврата виртуальной функции и её переопределений должны совпадать. Рассмотрим следующий пример:

В этом случае Child::getValue() не считается подходящим переопределением для Parent::getValue(), так как типы возвратов разные (метод Child::getValue() считается полностью отдельной функцией).

Не вызывайте виртуальные функции в теле конструкторов или деструкторов

Вот еще одна ловушка для новичков. Вы не должны вызывать виртуальные функции в теле конструкторов или деструкторов. Почему?

Помните, что при создании объекта класса Child сначала создается родительская часть этого объекта, а затем уже дочерняя? Если вы будете вызывать виртуальную функцию из конструктора класса Parent при том, что дочерняя часть создаваемого объекта еще не была создана, то вызвать дочерний метод вместо родительского будет невозможно, так как объект child для работы с методом класса Child еще не будет создан. В таких случаях, в языке C++ будет вызываться родительская версия метода.

Аналогичная проблема существует и с деструкторами. Если вы вызываете виртуальную функцию в теле деструктора класса Parent, то всегда будет вызываться метод класса Parent, так как дочерняя часть объекта уже будет уничтожена.

Правило: Никогда не вызывайте виртуальные функции в теле конструкторов или деструкторов.

Недостаток виртуальных функций

Какой результат выполнения следующих программ? Не нужно запускать/выполнять следующий код, вы должны определить результат, без помощи своих IDE.

Читайте также: