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

  Юрий  | 

  |

  Обновл. 17 Июл 2021  | 

 38112

 ǀ   29 

В этой главе мы рассмотрели наследование в языке C++. Пора закрепить пройденный материал.

Теория

Наследование позволяет моделировать отношения типа «является» между двумя объектами. Объект, который наследует, называется дочерним классом. Объект, которого наследуют, называется родительским классом.

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

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

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

   Вызывается соответствующий конструктор дочернего класса.

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

   Список инициализации дочернего класса инициализирует члены дочернего класса.

   Выполняется тело конструктора дочернего класса.

   Управление возвращается обратно в caller.

Освобождение памяти (уничтожение) происходит в порядке, противоположном построению: от дочерних до родительских классов.

Язык C++ имеет 3 спецификатора доступа: public, private и protected. Спецификатор protected используется для разрешения доступа к членам дружественным классам/функциям и дочерним классам, всем остальным объектам — доступ закрыт.

Есть 3 типа наследования: public, private и protected. Наиболее распространенный тип наследования — public.

Таблица спецификаторов доступа и типов наследования:

Спецификатор доступа в родительском классе Спецификатор доступа при наследовании типа public в дочернем классе Спецификатор доступа при наследовании типа private в дочернем классе Спецификатор доступа при наследовании типа protected в дочернем классе
public public private protected
private Недоступен Недоступен Недоступен
protected protected private protected

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

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

Тест


Задание №1

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

a)

Ответ №1.a)

Сначала инициализируется родительская часть объекта, а затем уже дочерняя. Уничтожение происходит в обратном порядке.

Parent()
Child()
~Child()
~Parent()

b)

Подсказка: Локальные переменные уничтожаются в порядке противоположном определению.

Ответ №1.b)

Сначала выполняется построение ch:

Parent()
Child()

Затем построение p:

Parent()

Затем уничтожение p:

~Parent()

Затем уничтожение ch:

~Child()
~Parent()

c)

Ответ №1.c)

Не скомпилируется. Метод Child::print() не имеет доступа к закрытому члену m_x.

d)

Ответ №1.d)

Результат:

Parent()
Child()
Child: 7
~Child()
~Parent()

e)

Ответ №1.e)

Результат:

Parent()
Child()
D2()
Child: 7
~D2()
~Child()
~Parent()

Задание №2

a) Создайте классы Apple и Banana, которые наследуют класс Fruit. У класса Fruit есть две переменные-члены: name и color.

Следующий код:

Должен выдавать следующий результат:

My apple is red.
My banana is yellow.

Ответ №2.a)

b) Добавьте новый класс GrannySmith, который наследует класс Apple.

Следующий код:

Должен выдавать следующий результат:

My apple is red.
My banana is yellow.
My Granny Smith apple is green.

Ответ №2.b)

Задание №3

Самое любимое! Будем создавать простую игру, в которой вы будете сражаться с монстрами. Цель игры — собрать максимум золота, прежде чем вы умрете или достигнете 20 уровня.

Игра состоит из трех классов: Creature, Player и Monster. Player и Monster наследуют класс Creature.

a) Сначала создайте класс Creature со следующими членами:

   имя (std::string);

   символ (char);

   количество здоровья (int);

   количество урона, которое он наносит врагу во время атаки (int);

   количество золота, которое он имеет (int).

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

   void reduceHealth(int) — уменьшает здоровье Creature на указанное целочисленное значение;

   bool isDead() — возвращает true, если здоровье Creature равно 0 или меньше;

   void addGold(int) — добавляет золото Creature-у.

Следующий код:

Должен выдавать следующий результат:

The orc has 3 health and is carrying 15 gold.

Ответ №3.a)

b) Теперь нам нужно создать класс Player, который наследует Creature. Player имеет:

   переменную-член level, которая начинается с 1;

   имя (пользователь вводит с клавиатуры);

   символ @;

   10 очков здоровья;

   1 очко урона (для начала);

   0 золота.

Напишите метод levelUp(), который увеличивает уровень Player-а и его урон на 1. Также напишите геттер для члена level и метод hasWon(), который возвращает true, если Player достиг 20 уровня.

Допишите в функцию main() код, который спрашивает у пользователя его имя и выводит количество его здоровья и золота:

Enter your name: Anton
Welcome, Anton.
You have 10 health and are carrying 0 gold.

Ответ №3.b)

c) Следующий класс Monster также наследует Creature и у него нет собственных переменных-членов. Но есть перечисление Type, которое содержит 3 перечислителя, они обозначают типы монстров: DRAGON, ORC и SLIME (вам также нужен дополнительный перечислитель MAX_TYPES).

Ответ №3.c)

d) Каждый тип Монстра имеет свое имя, символ, определенное количество здоровья, урона и золота:

Type Name Symbol Health Damage Gold
DRAGON dragon D 20 4 100
ORC orc o 4 2 25
SLIME slime s 1 1 10

Следующий шаг — реализация конструктора класса Monster, с помощью которого можно создавать монстров. Этот конструктор должен принимать перечисление Type в качестве параметра, а затем создавать монстра с соответствующими таблице характеристиками.

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

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

   Шаг №1: Создайте структуру MonsterData внутри класса Monster. Эта структура должна иметь следующие перечислители: name, symbol, health, damage и gold.

   Шаг №2: Объявите статический массив этой структуры с именем monsterData.

   Шаг №3: Добавьте код определения нашей таблицы поиска вне тела класса:

Теперь мы можем искать любые значения, которые нам нужны! Например, чтобы узнать количество золота Dragon, мы можем использовать monsterData[DRAGON].gold.

Используйте эту таблицу поиска для реализации вашего конструктора:

Следующий код:

Должен выдавать следующий результат:

A orc (o) was created.

Ответ №3.d)

e) Наконец, добавьте статический метод getRandomMonster() в класс Monster. Этот метод должен генерировать случайное число от 0 до MAX_TYPES-1 и возвращать (возврат по значению) определенный тип монстра (вам нужно использовать оператор static_cast для конвертации int в Type, чтобы передать его конструктору класса Monster).

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

Следующий код:

Должен сгенерировать 10 рандомных монстров:

A slime (s) was created.
A orc (o) was created.
A slime (s) was created.
A slime (s) was created.
A orc (o) was created.
A orc (o) was created.
A dragon (D) was created.
A slime (s) was created.
A orc (o) was created.
A orc (o) was created.

Ответ №3.e)

f) Готово, теперь нам нужно разобраться с логикой выполнения нашей игры!

Суть:

   Игрок сталкивается с одним случайно выбранным монстром.

   С каждым монстром игрок может либо (R)un, либо (F)ight.

   Если игрок решает Run, то шансы на удачный побег составляют 50%.

   Если игроку удается сбежать, то ​​он благополучно переходит к следующему монстру (его здоровье/урон/золото при этом не уменьшается).

   Если игроку не удается сбежать, то монстр его атакует. Здоровье игрока уменьшается от урона монстра. Затем игрок выбирает свое следующее действие.

   Если игрок выбирает Fight, то он атакует монстра. Здоровье монстра уменьшается от урона игрока.

   Если монстр умирает, то игрок забирает всё золото монстра + увеличивает свой level и урон на 1.

   Если монстр не умирает, то он атакует игрока. Здоровье игрока уменьшается от урона монстра.

   Игра заканчивается, если игрок умер (проигрыш) или достиг 20 уровня (выигрыш).

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

   Если игрок побеждает, то игра должна сообщить игроку, что он выиграл и сколько у него есть золота.

Пример игры:

Enter your name: Anton
Welcome, Anton
You have encountered a orc (o).
(R)un or (F)ight: r
You successfully fled.
You have encountered a slime (s).
(R)un or (F)ight: f
You hit the slime for 1 damage.
You killed the slime.
You are now level 2.
You found 10 gold.
You have encountered a dragon (D).
(R)un or (F)ight: f
You hit the dragon for 2 damage.
The dragon hit you for 4 damage.
(R)un or (F)ight: f
You hit the dragon for 2 damage.
The dragon hit you for 4 damage.
(R)un or (F)ight: f
You hit the dragon for 2 damage.
The dragon hit you for 4 damage.
You died at level 2 and with 10 gold.
Too bad you can't take it with you!

Подсказка: У вас должны быть следующие 4 функции:

   Функция создания Игрока и основной игровой цикл (в функции main()).

   Функция fightMonster(), которая обрабатывает бой между Игроком и Монстром, и спрашивает у игрока, что он хочет сделать: Run или Fight.

   Функция attackMonster(), которая обрабатывает атаку монстра игроком, включая увеличение уровня игрока.

   Функция attackPlayer(), которая обрабатывает атаку игрока монстром.

Ответ №3.f)

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

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

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

  1. Артём:

    Прекрасный опыт! Третье задание очень понравилось. Сделал самостоятельно код грамотно, но в некоторых местах по иной логике (расположение проверки монстра и игрока на наличие жизней), благодарю Вас, Юрий.

    Удалось пройти свою же игру (без восстановления здоровья на новом уровне! с 2 хп и убийством кучи слизней и орков, но ни одним драконом)!!! Весело, реализовал для рандомных чисел кстати свой класс:

  2. Антон:

    Не понял только, зачем нам условие победы при достижении 20 лвл, если монстров всего 10, а значит макс лвл 11

    1. Артём:

      Антон, содержимое main() для разных подпунктов мы используем лишь для тестирования новой порции кода. В итоговой версии main() должен быть бесконечный цикл спавна монстров, значит можно и больше 20 уровня получить и больше 10 монстров встретить)

  3. Сергей:

    Задание 3. Отличное задание на закрепление! Спасибо Юрий!
    Я не сразу понял логику игры, поэтому пришлось немного повозиться…

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

    1. Сергей:

      Вот что в итоге вышло. Коенчно игроку пришлось добавить здоровья. И есть много других идей на развитие логики, ну да это может быть позже…

  4. Сергей:

    Задание 3. Не сразу понял логику игры. Поэтому получилось немного по другому, но зато пришлось повторить/закрепить прошедший материал.

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

    Итак:

  5. Vlad:

    Разве в задании 2.b не лучше было переопределить, а точнее перегрузить метод (поскольку переопределение методов дочерних классов это уже тема виртуальных функций) getName() для GrannySmith, чтобы тот возвращал "Granny Smith " + Apple::getName() ???
    Ведь если класс Apple уже был написан и "протестирован", то безопаснее было бы не менять в нём ничего. Да, разумеется в этом задании масштабы не такие огромные, но всё же нужно приучивать себя писать безопасно (на работе точно не помешает).

    1. Юрий:

      сразу так и написал)

  6. Владимир:

    Добавил в код и прошёл игры со второго раза, так игра выглядит более логичной.

  7. cybersatori:

    с кодом вс понятно, но как пройти эту ляцкую игру!)

    1. cybersatori:

      попытки с 50й прошёл игру, давно так не нервничал, дарксолс нервно курит)

  8. Арбузик❤❤❤:

    Давайте хвастаться кто до какого лвла дошел 😉

    Я дошел до 11 с 4 попытки 😀

    1. Vlad:

      Прошёл с 7 попытки, 295 голды.

    2. Andrey:

      Прошёл с 6 попытки с 310 золота, видимо более 50 часов геймплея MGQ даром не прошли 🙂 Ох, пока делал эту игру, такая ностальгия напала, как будто делаю и прохожу свою версию "Monster Girl Quest" со слаймами, орками и драконами…

  9. Виктор:

    Этот вариант немного отличается от задания.

  10. Александр:

    Здравствуйте ! Вопрос к автору. В какой версии MS Visual Studio компилируются примеры ? Поясню :

    при компиляции в 2008 и 2012 версиях дают ошибку :
    error C2470: Monster::monsterData: выглядит как определение функции, но без списка параметров; пропускается вероятное тело функции. И компилируются только так :

    Без '=' получается ошибка. Это опечатка или новый стантарт инициализации ? Подобные вещи встречаличь и в предыдущих уроках. Желательно упоминать, для каких версий С++ написаны примеры.

    1. Vlad:

      Скорее всего у вас используется стандарт до С++11. Это значит что поддержки uniform-инициализации нету. Например:

      Соотвественно такое не будет работать:

  11. kolka:

    Коллеги! Может кто объяснить, зачем создавать класс Monster. В рамках данной программы. Нельзя ли просто сделать, например, конструктор по умолчанию в классе Creature, который будет создавать случайного монстра? Тогда не надо городить дополнительную структуру и писать длиннющий конструктор для создания таблицы монстров, но при этом и не надо заранее создавать объекты Creature, как у kmish!

    1. Андрей:

      Класс Creature в данной программе служит абстрактным классом для классов Player и Monster, как, например, класс Fruit для классов Apple и Banana из прошлых уроков.
      Если реализовывать в классе Creature конструктор по-умолчанию, который создает монстра, тогда все наследуемые классы (Player в нашем случае) можно считать монстрами по своей сути.
      Я так понимаю эту ситуацию.

      1. kolka:

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

  12. Кирилл:

  13. kmish:

    Моя версия 🙂 Добавил восстановление здоровья при levelUp, иначе победить практически нереально:

  14. kmish:

    По поводу задачи 3d.
    Совершенно излишне городить дополнительную структуру и писать длиннющий конструктор для создания таблицы монстров. У нас уже есть готовый класс (он же конструктор с теми же переменными). Гораздо короче и читабельнее будет:

  15. kmish:

    Мой вариант Задание №2:

  16. dshadov:

    Добрый день!
    Игра, конечно, клевая, прям затягивает. Единственное, что очень сложно пройти. Я еще добавил увеличение здоровья при повышении уровня, чтоб хоть чуть-чуть полегче было:

    Еще у Вас в ответе 3f) у орка здоровье 6 пунктов, хотя до этого везде 4 стояло.
    Мне интересно, Вы возврат строки по константной ссылке использовали из соображений производительности:

    До последнего примера у Вас геттеры были:

  17. dshadov:

    Возник вопрос по инициализации std::string playerName в третьем примере, хотя с этой проблемой сталкивался и раньше. Почему при копирующей std::string playerName = nullptr, прямой std::string playerName(nullptr) и uniform инициализации std::string playerName{ nullptr } срабатывает исключение в строке 212 файла basic_string.tcc Вот эта строка:

    if (__gnu_cxx::__is_null_pointer(__beg) && __beg != __end)
    std::__throw_logic_error(__N("basic_string::"
    "_M_construct null not valid"));

    В то же время, если nullptr заменить на 0, то код работать будет, но только при uniform инициализации, т.е. при std::string playerName{ 0 } исключение не вылетает.

  18. dshadov:

    Еще вопрос по "фруктам". Если в классе Яблоко сделать конструктор по умолчанию:

    то IDE начинает ругаться на запись Apple a("red"); в main. Пишет, что существует более одного экземпляра конструктора Apple, соответствующего списку аргументов. Но ведь второй конструктор, который по порядку первый, по идее не должен быть виден в main вообще, т.к. protected. Или на конструкторы по умолчанию спецификатор доступа не распространяется? Поясните, пожалуйста.

  19. dshadov:

    Здравствуйте!
    В ответе 1b) у Вас говорится о построениях b и d, наверное, имеется ввиду построения p и ch?

    1. Фото аватара Юрий:

      Привет. Спасибо, исправил.

Добавить комментарий

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