Глава №12. Итоговый тест

  Юрий  | 

  Обновл. 20 Окт 2020  | 

 14599

 ǀ   20 

Итак, наше путешествие в наследование и виртуальные функции в языке C++ подошло к концу. Пора закрепить пройденный материал.

Теория

Язык C++ позволяет создавать указатели/ссылки родительского класса на объекты дочерних классов. Это полезно при использовании функций или массивов, которые должны работать с объектами дочерних классов.

Без виртуальных функций указатели/ссылки родительского класса на объект дочернего класса будут иметь доступ только к членам родительского класса.

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

Модификатор override используется для обозначения метода переопределением.

Модификатор final запрещает переопределять виртуальную функцию или наследовать определенный класс.

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

Вы можете игнорировать вызов переопределений виртуальной функции, используя оператор разрешения области видимости, чтобы напрямую указать, какую функцию вы хотите вызвать. Например, parent.Parent::GetName().

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

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

   Вызов виртуальных функций занимает больше времени.

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

Виртуальную функцию можно сделать чистой виртуальной/абстрактной функцией, добавив = 0 в конец её прототипа. Класс, содержащий чистую виртуальную функцию, называется абстрактным классом. Объекты абстрактного класса не могут быть созданы. Класс, который наследует чистые виртуальные функции, должен предоставить свои переопределения этих функций, или он также будет считаться абстрактным. Чистые виртуальные функции могут иметь тело (определение, записанное отдельно), но они по-прежнему считаются абстрактными функциями.

Интерфейс (или «интерфейсный класс») — это класс без переменных-членов, все методы которого являются чистыми виртуальными функциями. Имена интерфейсов часто начинаются с I.

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

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

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

Самый простой способ перегрузить оператор вывода << для классов с наследованием — записать перегрузку оператора << в родительском классе, а выполнение операции вывода делегировать виртуальному методу.

Тест


Задание №1

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

Child

a)

Ответ №1.a)

Parent::getName() не является виртуальной функцией, поэтому p.getName() не вызовет Child::getName().

b)

Ответ №1.b)

Хотя метод Child::getName() является константным, метод Parent::getName() не является константным, поэтому Child::getName() не считается переопределением и, следовательно, не вызывается.

c)

Ответ №1.c)

Объект ch присваивается объекту p по значению (а не по ссылке), что приводит к обрезке объекта ch.

d)

Ответ №1.d)

Класс Parent был объявлен как final, поэтому класс Child не может наследовать класс Parent. Результат — ошибка компиляции.

e)

Ответ №1.e)

Child::getName() является абстрактной функцией, хотя имеет тело (записанное отдельно), поэтому класс Child является абстрактным, а объекты абстрактного класса создавать нельзя.

f)

Ответ №1.f)

Эта программа выводит верный результат, но имеет другую проблему. В конце функции main() мы удаляем p, который является указателем класса Parent, но у нас нет виртуального деструктора в классе Parent. Следовательно, удаляется только часть Parent объекта класса ch, а часть Child объекта ch остается в виде утечки памяти.

Задание №2

a) Создайте абстрактный класс Shape. Этот класс должен иметь три метода:

   чистую виртуальную функцию print() с параметром типа std::ostream;

   перегрузку operator<<;

   пустой виртуальный деструктор.

Ответ №2.a)

b) Создайте два класса: Triangle и Circle, которые наследуют класс Shape.

   Triangle должен иметь 3 точки в качестве переменных-членов.

   Circle должен иметь одну центральную точку и целочисленный радиус в качестве переменных-членов.

Перегрузите функцию print(), чтобы следующий код:

Выдавал следующий результат:

Circle(Point(1, 2, 3), radius 7)
Triangle(Point(1, 2, 3), Point(4, 5, 6), Point(7, 8, 9))

Вот класс Point, который вы можете использовать:

Ответ №2.b)

c) Используя код из предыдущих заданий (классы Point, Shape, Circle и Triangle) завершите следующую программу:

Подсказка: Вам нужно добавить метод getRadius() в Circle и выполнить понижающее приведение Shape* в Circle*, чтобы получить доступ к этому методу.

Ответ №2.c)

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (85 оценок, среднее: 4,89 из 5)
Загрузка...

Комментариев: 20

  1. Sem:

    Меня немного беспокоит то, что центр круга задан тремя координатами. Радиус и центр в пространстве описывают сферу, а не круг.

    1. Юрий:

      Этот тест все таки проверяет знания темы "Виртуальные Функции" а не стереометрии, поэтому не вижу причин для беспокойства.
      А автору спасибо большое за интересные задания!

  2. Виктор:

    В последнем задании используется "new" для добавления объектов в вектор, с чем это связано? Разве мы динамически выделяем память? Если да, то насколько я помню урок про векторы, то там как-раз таки говорилось, что преимущество вектора в отсутствии надобности выделения памяти динамически (то есть это делается за нас).

    Собственно мой первый комментарий на этом сайте, т.к. до этого все возникшие ранее вопросы как-то удавалось решить либо через гугл, либо через знакомых, либо через освежение памяти, в этот раз больше ничего не остаётся, как спросить здесь, не хочу, чтобы оставался пробел в знаниях. Ну и, пользуясь случаем, выражаю автора огромную благодарность!

    1. Владимир:

      Ответить коротко вряд ли смогу, зато могу дать намек. Во первых обрати внимание что это массив указателей std::vector<Shape*>, а во вторых еще раз перечитай что такое r-value и l-value данные.
      И тогда твой вопрос разрешится.

    2. Андрей:

      Вектор <Shape*> это массив динамически созданных указателей. Каждый из указателей указывает на динамический объект. При удалении вектора удаляются все указатели, но динамические объекты остаются. Поэтому нужно вручную перебрать все указатели и удалить объекты, на которые они ссылаются.

    3. Grave18:

      Можно обойтись и без new:

    4. AvRu:

      Мы можем добавить объекты без new используя l-value, как сделал Grave18. Мы можем это сделать, потому что при создании объекта мы и так выделяем память, на которую будет указывать указатель в векторе. Но для того чтобы добавить безымянный объект, который является r-value, то есть не имеет адреса, мы должны выделить память при помощи new. Надеюсь все правильно объяснил и у Вас больше не осталось вопросов 🙂

  3. Константин:

    Получилось!
    2С:

  4. Vlad:

    1. Владимир:

      Человек. У тебя в конце утечка памяти, вместо удаления(освобождения) ты просто "занулил" указатели. Зануляют, по правилам хорошего кода, обычно только после удаления(освобождения).

  5. Анастасия:

    Вопрос по заданию 1d:
    1) Из 164 урока мы знаем, что:
    "Модификатор override может использоваться с любым методом, который должен быть переопределением. Достаточно просто указать override в том месте, где обычно указывается const (после скобок с параметрами)." — так что же тогда этот модификатор в данных примерах делает до скобок с параметрами?
    2) Опять же из 164 урока: "Указывается final в том же месте, где и модификатор override" — то есть см. выше, в описании метода, до скобок с параметрами. Так что же он делает в объявлении класса? Или это где-то ещё про него такое было? Посмотрела свои конспекты, не нашла.

    1. Анастасия:

      прошу поместить мой комментарий в следующей редакции, т.к. со скобками это я напутала, override верно расположен:
      Вопрос по заданию 1d:
      Из 164 урока мы знаем, что:
      "Модификатор override может использоваться с любым методом, который должен быть переопределением. Достаточно просто указать override в том месте, где обычно указывается const (после скобок с параметрами)."
      и "Указывается final в том же месте, где и модификатор override" — то есть см. выше, в описании метода, после скобок с параметрами. Так что же он делает в объявлении класса? Или это где-то ещё про него такое было? Посмотрела свои конспекты, не нашла.

    2. Анастасия:

      … и вообще удалите мой комментарий, я и про final для класса нашла))

  6. Khloyan:

    Перегрузите или переопределить? … функцию print() …

  7. koh:

    1 — 7 глава :
    Хм… это очень интересно, я все понимаю. Бывают сложности, но все же, это не проблемно.
    8 — 10 глава.
    Мдэ. ну, впринципе, терпимо.
    10 — 11 глава.
    Я. Что — то. прочитал.

    Спасите

    1. Анастасия:

      Думаю секрет в том, что нужно читать медленней (не больше двух глав в день) и перед каждым следующим уроком самому себе напоминать важнейшие мысли из предыдущего.

    2. Аноним:

      Поделюсь своей методикой обучения:
      Я читаю быстро и вникаю на ходу, если словил затупок — перечитываю, стараясь понять, но без зубрёжки. После завершения темы — пишу конспект по основным, с моей точки зрения, моментам, попутно пересматривая материал ещё один раз. Ну и конечно же, в сложные темы нельзя вкатить сразу)

  8. kmish:

    Мой вариант. Я членами классов Triangle и Circle сделал ссылки на объекты Point, дабы избежать их копирования в объекты классов Triangle и Circle.

    1. Kris:

      Очень опасно так делать. Если кто-то внешне изменит какой-нибудь из point'ов, то у тебя начнутся проблемы, потому что твои поля тоже изменятся. Тут нету других вариантов, кроме как копировать.

      И да, Point не такой уж и "большой", чтобы бояться его скопировать. Это же всего 3 поля! Это пустяки по скорости.

      1. Виктор:

        Не вижу в этом опасности. Сircle и Point, Triangle и Point как единое целое. Point в каждом дочернем классе отдельный конструктор привязанный к определенному классу и имеющий свой уникальный адрес. Может я чего-то не понимаю.

Добавить комментарий для kmish Отменить ответ

Ваш E-mail не будет опубликован. Обязательные поля помечены *