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

  Юрий  | 

  |

  Обновл. 24 Сен 2021  | 

 48724

 ǀ   34 

Вот мы и рассмотрели сердце этого туториала — объектно-ориентированное программирование в языке C++. Теперь пора закрепить полученные знания.

Теория

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

Спецификаторы доступа позволяют указать, кто будет иметь доступ к членам класса. Доступ к открытым (public) членам класса имеют все. Доступ к закрытым (private) членам класса имеют только другие члены класса. О protected мы поговорим детально, когда будем рассматривать наследование в языке С++. По умолчанию все члены класса являются private, а все члены структуры — public.

Инкапсуляция — это когда все переменные-члены вашего класса являются закрытыми, и доступ к ним напрямую невозможен. Это защищает ваш класс от неправильного/некорректного использования.

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

Список инициализации членов класса позволяет инициализировать переменные-члены из конструктора (вместо присваивания значений переменным-членам).

В C++11 инициализация нестатических членов класса позволяет напрямую указывать значения по умолчанию для переменных-членов при их объявлении.

До C++11 конструкторы не должны вызывать другие конструкторы (хоть это и скомпилируется, но будет работать не так, как вы ожидаете). В C++11 конструкторам разрешено вызывать другие конструкторы. Этот процесс называется делегированием конструкторов (или «цепочкой конструкторов»).

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

Все методы имеют скрытый указатель *this, который указывает на текущий объект класса (который используется в данный момент). В большинстве случаев вам не нужно напрямую обращаться к этому указателю.

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

Методы класса могут (и должны) быть const, если они не изменяют данные класса. Константные объекты класса могут вызывать только константные методы класса.

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

Аналогично, статические методы класса — это методы, которые не имеют указателя *this. Они имеют доступ только к статическим переменным-членам класса.

Дружественные функции — это внешние функции, которые имеют доступ к закрытым членам класса.

Дружественные классы — это классы, в которых все методы являются дружественными функциями.

Анонимные объекты создаются для обработки выражений или для передачи/возврата значений.

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

Тайминг кода осуществляется через библиотеку chrono и позволяет засечь время выполнения определенного фрагмента кода.

Тест


Задание №1

a) Напишите класс с именем Point. В классе Point должны быть две переменные-члены типа double: m_a и m_b со значениями по умолчанию 0.0. Напишите конструктор для этого класса и функцию вывода print().

Следующая программа:

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

Point(0, 0)
Point(2, 5)

Ответ №1.а)

b) Теперь добавим метод distanceTo(), который будет принимать второй объект класса Point в качестве параметра и будет вычислять расстояние между двумя объектами. Учитывая две точки (a1, b1) и (a2, b2), расстояние между ними можно вычислить следующим образом: sqrt((a1 - a2) * (a1 - a2) + (b1 - b2) * (b1 - b2)). Функция sqrt() находится в заголовочном файле cmath.

Следующая программа:

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

Point(0, 0)
Point(2, 5)
Distance between two points: 5.38516

Ответ №1.b)

c) Измените функцию distanceTo() из метода класса в дружественную функцию, которая будет принимать два объекта класса Point в качестве параметров. Переименуйте эту функцию на distanceFrom().

Следующая программа:

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

Point(0, 0)
Point(2, 5)
Distance between two points: 5.38516

Ответ №1.c)

Задание №2

Напишите деструктор для следующего класса:

Ответ №2

Задание №3

Давайте создадим генератор случайных монстров.

a) Сначала создайте перечисление MonsterType со следующими типами монстров: Dragon, Goblin, Ogre, Orc, Skeleton, Troll, Vampire и Zombie + добавьте MAX_MONSTER_TYPES, чтобы иметь возможность подсчитать общее количество всех перечислителей.

Ответ №3.а)

b) Теперь создайте класс Monster со следующими тремя атрибутами (переменными-членами): тип (MonsterType), имя (std::string) и количество здоровья (int).

Ответ №3.b)

c) Перечисление MonsterType является специфичным для Monster, поэтому переместите его внутрь класса под спецификатор доступа public.

Ответ №3.c)

d) Создайте конструктор, который позволит инициализировать все переменные-члены класса.

Следующий фрагмент кода должен скомпилироваться без ошибок:

Ответ №3.d)

e) Теперь нам нужно вывести информацию про нашего монстра. Для этого нужно конвертировать MonsterType в std::string. Добавьте функцию getTypeString(), которая будет выполнять конвертацию, и функцию вывода print().

Следующая программа:

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

Jack is the orc that has 90 health points.

Ответ №3.e)

f) Теперь мы уже можем создать сам генератор монстров. Для этого создайте статический класс MonsterGenerator и статический метод с именем generateMonster(), который будет возвращать случайного монстра. Пока что возвратом метода пускай будет анонимный объект: (Monster::Orc, "Jack", 90).

Следующая программа:

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

Jack is the orc that has 90 health points.

Ответ №3.f)

g) Теперь MonsterGenerator должен генерировать некоторые случайные атрибуты. Для этого нам понадобится генератор случайного числа. Воспользуйтесь следующей функцией:

Поскольку MonsterGenerator будет полагаться непосредственно на эту функцию, то поместите её внутрь класса в качестве статического метода.

Ответ №3.g)

h) Теперь измените функцию generateMonster() для генерации случайного MonsterType (между 0 и Monster::MAX_MONSTER_TYPES-1) и случайного количества здоровья (от 1 до 100). Это должно быть просто. После того, как вы это сделаете, определите один статический фиксированный массив s_names размером 6 элементов внутри функции generateMonster() и инициализируйте его 6-ю любыми именами на ваш выбор. Добавьте возможность выбора случайного имени из этого массива.

Следующий фрагмент должен скомпилироваться без ошибок:

Ответ №3.h)

i) Почему мы объявили массив s_names статическим?

Ответ №3.i)

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

Задание №4

Настало время для нашего и вашего любимого задания «Blackjack». На этот раз мы перепишем игру «Blackjack», которую написали ранее в итоговом тесте главы №6, но уже с использованием классов! Вот полный код без классов:

Нехило, правда? С чего же начинать? Для начала нам нужна стратегия. Программа «Blackjack» состоит из 4 частей:

   Логика работы с картами.

   Логика работы с колодами карт.

   Логика раздачи карт из колоды.

   Логика игры.

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

Скопируйте вышеприведенный код в вашу IDE, а затем закомментируйте всё, кроме строк, содержащих #include.

a) Начнем с того, что переделаем Card из структуры в класс. Хорошей новостью является то, что класс Card очень похож на класс Monster из предыдущего задания. Алгоритм действий следующий:

   Во-первых, переместите перечисления CardSuit и CardRank внутрь класса Card под спецификатор доступа public (они неотъемлемо связаны с Card, поэтому должны находиться внутри класса).

   Во-вторых, создайте закрытые переменные-члены m_rank и m_suit для хранения значений CardRank и CardSuit.

   В-третьих, создайте открытый конструктор класса Card с инициализацией карт (переменных-членов класса). Укажите параметры по умолчанию для конструктора (используйте MAX_RANKS и MAX_SUITS).

   Наконец, переместите функции printCard() и getCardValue() внутрь класса под спецификатор доступа public (не забудьте сделать их const!).

Примечание: При использовании std::array (или std::vector), где элементами являются объекты класса, класс должен иметь конструктор по умолчанию, чтобы элементы могли быть инициализированы разумными значениями по умолчанию. Если вы этого не сделаете, то получите ошибку попытки ссылаться на удаленную функцию.

Следующий фрагмент кода должен скомпилироваться без ошибок:

Ответ №4.а)

b) Хорошо, теперь давайте работать над классом Deck:

   Во-первых, в Deck должно быть 52 карты, поэтому создайте private-член m_deck, который будет фиксированным массивом с 52-мя элементами (используйте std::array).

   Во-вторых, создайте конструктор, который не принимает никаких параметров и инициализирует каждый элемент массива m_deck случайной картой (используйте код из функции main() с циклами for из вышеприведенного примера, где присутствует полный код). Внутри циклов создайте анонимный объект Card и присваивайте его каждому элементу массива m_deck.

   В-третьих, переместите функцию printDeck() в класс Deck под спецификатор доступа public (не забудьте о const).

   В-четвертых, переместите функции getRandomNumber() и swapCard() в класс Deck в качестве закрытых статических членов.

   В-пятых, переместите функцию shuffleDeck() в класс в качестве открытого члена.

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

Следующий фрагмент должен cкомпилироваться без ошибок:

Ответ №4.b)

c) Теперь нам нужен способ отследить то, какая карта будет раздаваться следующей (в исходной программе для этого используется cardptr):

   Во-первых, добавьте в класс Deck целочисленный член m_cardIndex и инициализируйте его значение 0.

   Во-вторых, создайте открытый метод dealCard(), который будет возвращать константную ссылку на текущую карту и увеличивать m_cardIndex.

   В-третьих, метод shuffleDeck() также должен быть обновлен для сброса m_cardIndex (так как после перетасовки колоды, раздается карта, которая является верхней).

Следующий фрагмент должен скомпилироваться без ошибок:

Ответ №4.c)

d) Почти готово! Теперь немного самостоятельности:

   Добавьте в программу функции getPlayerChoice() и playBlackjack().

   Измените функцию playBlackjack() в соответствие с уже имеющимся классом и методами.

   Удалите лишнее и добавьте нужное в функцию main() (см. полный код выше).

Ответ №4.d)

Ура!

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

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

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

  1. Павел:

    Блин, я не понял почему мы используем статические методы (

    1. Павел:

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

      По сути, это просто две глобальные функции, которые будут работать и так

      А в мейне вызов через

      В класс их заключили вероятно потому что они используются в паре для одной цели.

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

  2. Vlad:

    Здравствуйте! Не могли бы объяснить подробнее, почему мы используем static метод с функцией getRandomNumber( )?

    И для чего в строке "const Card& dealCard( )" — указан "сonst" и ссылочный тип возвращения? Для оптимизации работы функции с классами?

  3. Сергей:

    Так и осталось непонятным почему статическая переменная static std::string s_name[6] обязательно должна быть объявлена в теле функции Monster generateMonster(), а не в теле класса MonsterGenerator. Если объявить эту переменную в теле класса (неважно Private или Public) то вылезет ошибка "E1592 элемент с инициализатором внутри класса должен быть постоянной".

    Использую IDE Visual Studio.

  4. Тимур:

    Нужно ли для однотипных методов в разных классах давать уникальные имена?

    Например:

    При вызове метода от объекта получается:

    Но конструкции:

    Более читаемые. По тому от какого объекта они вызываются, итак понятно какому классу они принадлежат.
    В чем тогда преимущество делать уникальные имена printCard() и printDeck()?

    1. Максим:

      Думаю, уникальные имена для методов нужны. Они обеспечивают интуитивное понимание работы каждого метода.

      У тебя приведен простой пример, где имена объектов отражают суть класса:

      Методы не потеряют своей "понятности", если упростить имена методов:

      Но если имена объектов будут неочевидными:

      А с упрощенными именами методов это будет выглядеть так:

      Здесь уже голову можно сломать.

  5. Юлия:

    Почему в ответе в функции перетасовки карт не используется цикл foreach? Я использовала именно его:

    1. Grave18:

      Можно и так, оба варианта занимают одинаковое время на выполнение.

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

    Есть вопрос по выводу. Почему при m_rank == RANK_10 функция printCard() вместо 10 он выдаёт 12592 (я проверил, что по отдельности 1 и 0 функция printCard() выдаёт именно как 1 и 0)?

    1. Виталий:

      Функция printCard() возвращает тип данных char а он может хранить в себе только 1 символ, глава 2 урок 35. '10' — ошибка тут 2 символа выход за пределы диапазона char и преобразуются в int без расширения знака. Если вы желаете вывести 10 просто поменяйте кавычки на строковые "10".

  7. Андрей:

    В третьем задании выдает ошибку: "MonsterGenerator::m_names": статический элемент элемент данных с инициализатором внутри класса должен иметь неизменяемый целочисленный константный тип или должен быть указан как "inline". Почему? Код точно такой же как в уроке

    1. Юлия:

      У меня было то же самое, пока я не поместила массив s_names ВНУТРЬ функции generateMonster ()

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

    Кто-нибудь может объяснить, почему в 3 задании для генерации монстров создаётся отдельный класс? Почему нельзя обе функции из него сделать в основном классе?

  9. Иван:

    Насколько правильно или неправильно сделать playBlackjack методом класса Deck? Тоже самое для getPlayerChoice, поместил его в private.

    1. Grave18:

      Получается, что колода предназначена только для игры в Blackjack. Конечно так можно сделать, но ведь и другие игры есть.

  10. Анатолий:

    Можно популярно, почему используется именно эта конструкция для генерирования случайного числа?

    И (max-min+1) в VSC подчеркивается и говорит Arithmetic overflow С26451.

    1. Алексей:

      Это выведенная в предыдущих уроках прога, привязанная к системным часам для 100% рандома чисел. Именно поэтому в main добавили

      и

    2. Сергей:

      Может кто-то разжевать как именно работает эта первая функция (не понимаю откуда формулы такие)

      1. Алексей:

        Про:

        Здесь 1.0, по сути, будет делиться на 0.0(переполнение RAND_MAX, при помощи + 1.0, т.к RAND_MAX это int16_t и RAND_MAX всегда равно 32 767), что является небольшой хитростью плюсов, т.к. таким способом мы реализуем double бесконечность, ну а после, возвращаем уже готовую функцию rand() с нужным нам диапазоном(предварительно конвертируя её в int).

        2-ое:
        Если у вас происходит overflow попробуйте написать вместо:

        Вот это:

  11. Дмитрий:

    Во-вторых, создайте конструктор, который не принимает никаких параметров и инициализирует каждый элемент m_deck случайной картой…

    Подскажите пожалуйста, я так и не понял что значит случайной картой? Ведь в ответе инициализация происходит по-порядку… Или я где-то ошибаюсь?

    1. kmish:

      Меня тоже это смутило. Походу опечатка.

    2. Алекс:

      Я это сделал, а потом удивился ответу… Ну, работаю сам в госучреждении, поэтому привык к правилу "Если дали ТЗ — делай по ТЗ. Даже если всё будет криво — можно отмазаться чётко поставленной задачей". Тем более нет возможности переспросить дающего задание о рациональности заполнять массив рандомными картами, учитывая, что среди них 100% попадутся одинаковые.

    3. Виталий:

      1. Виталий:

  12. Владимир:

    //main.cpp

    //Monster.h

    //Monster.cpp

  13. Shom:

    "Во-вторых, создайте конструктор, который не принимает никаких параметров и инициализирует каждый элемент m_deck случайной картой…" — долго соображал, как это можно сделать, но так и не осилил. В итоге инициализировал картами по порядку. Потом, в Ответе увидел, что так-же решено : )

  14. Сергей:

    возникла проблема при создании класса Deck.
    Так как std::array не имеет конструктора по умолчанию, то при попытке создать таковой для класса компилятор ругается. Подскажите, как быть.
    Пы.Сы. Класс вынесен в отдельный хедер.

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

      Во-первых, если вы не создаете конструктор по умолчанию для своего класса, то он создается автоматически компилятором. Во-вторых, std::array как раз имеет конструктор по умолчанию в своей реализации и ваша ошибка может быть из-за того, что вы пытаетесь переопределить конструктор, который уже есть.

      Ваша ошибка скорее всего не из-за конструктора по умолчанию.

  15. Андрей:

    В финальном коде 2 ошибки:
    return (playerTotal > dealerTotal);
    1)gt не объявлен
    2) ";" между playerTotal &gt и dealerTotal

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

      &gt — это код символа >, как-то попало, не усмотрел. Спасибо, исправил.

  16. Герман:

    Действительно все просто, но только после Ваших объяснений! Спасибо!!!!

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

      Пожалуйста 🙂

  17. Герман:

    Уважаемый автор! В 3 задании не совсем понятна 85 строка!

    Нет ли возможности дать пояснения?

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

      Есть два отдельных класса: Monster и MonsterGenerator. В строчке

      Вызывается статический метод generateMonster() класса MonsterGenerator, который возвращает случайного монстра в виде возвращаемого анонимного объекта, который затем и присваивается объекту m класса Monster.

      С помощью метода print() класса Monster сгенерированный монстер выводится на экран.

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

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