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

  Юрий  | 

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

 46815

 ǀ   60 

Поздравляем вас с преодолением самой длинной главы этого туториала. Если у вас не было предыдущего опыта в программировании, то эта глава, скорее всего, была для вас наиболее сложной из всех предыдущих. Однако, если вы дошли до этого момента, то всё хорошо — вы справились! Так держать!

Хорошая новость заключается в том, что следующая глава будет легче этой, и очень скоро мы доберемся до самого сердца этого туториала — объектно-ориентированного программирования!

Теория

Массивы позволяют хранить и получать доступ ко многим переменным одного и того же типа данных через один идентификатор. Доступ к элементам массива осуществляется с помощью оператора индекса []. Будьте осторожны с диапазоном массива, не допускайте индексации элементов вне диапазона. Массивы можно инициализировать с помощью списка инициализаторов или uniform-инициализации.

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

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

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

Массивы используются в создании строк C-style. Избегайте использования строк C-style, вместо них используйте std::string.

Указатели — это переменные, которые хранят адреса памяти (указывают на) определенных переменных. Оператор адреса (&) используется для получения адреса переменной. Оператор разыменования (*) используется для получения значения, на которое указывает указатель.

Нулевой указатель — это указатель, который ни на что не указывает. Указатель можно сделать нулевым, инициализировав или присвоив ему значение 0 (или nullptr в C++11). Избегайте использования макроса NULL. Разыменование нулевого указателя может привести к неожиданным результатам (сбоям). При удалении нулевого указателя ничего плохого не случится.

Указатель на массив не знает длину массива, на который он указывает. Это означает, что оператор sizeof и циклы foreach работать с ним не могут.

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

Обязательно используйте оператор delete[] для удаления динамически выделенного массива. Указатели, указывающие на освобожденную память, называются висячими указателями. Разыменование висячего указателя не приведет ни к чему хорошему.

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

Для обычных переменных память выделяется из ограниченного резервуара — стека. Память для динамически выделенных переменных выделяется из общего резервуара памяти — кучи.

Указатель на константное значение обрабатывает значение, на которое он указывает, как константное:

Константный указатель — это указатель, значение которого не может быть изменено после инициализации:

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

Оператор выбора элемента (->) может использоваться для выбора члена через указатель на структуру. Он сочетает в себе как операцию разыменования, так и обычный доступ к элементам (.).

Указатели типа void — это указатели, которые могут указывать на любой тип данных. Они не могут быть разыменованы напрямую. Вы можете использовать оператор static_cast для преобразования их обратно в исходный тип указателя. Какой уже это будет тип — решать вам.

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

std::array предоставляет весь функционал стандартных обычных фиксированных массивов в языке C++ в форме, которая не будет распадаться в указатель при передаче. Рекомендуется использовать std::array вместо стандартных фиксированных массивов.

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

Тест

Задание №1

Представьте, что вы пишете игру, в которой игрок может иметь 3 типа предметов: зелья здоровья, факелы и стрелы. Создайте перечисление с этими типами предметов и фиксированный массив для хранения количества каждого типа предметов, которое имеет при себе игрок (используйте стандартные фиксированные массивы, а не std::array). У вашего игрока должны быть при себе 3 зелья здоровья, 6 факелов и 12 стрел. Напишите функцию countTotalItems(), которая возвращает общее количество предметов, которые есть у игрока. В функции main() выведите результат работы функции countTotalItems().

Ответ №1

Задание №2

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

Для следующего ввода:

Andre
74
Max
85
Anton
12
Josh
17
Sasha
90

Вывод должен быть следующим:

Sasha got a grade of 90
Max got a grade of 85
Andre got a grade of 74
Josh got a grade of 17
Anton got a grade of 12

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

Ответ №2

Задание №3

Напишите свою функцию, которая меняет местами значения двух целочисленных переменных. Проверку осуществляйте в функции main().

Подсказка: Используйте ссылки в качестве параметров.

Ответ №3

Задание №4

Напишите функцию для вывода строки C-style символ за символом. Используйте указатель для перехода и вывода каждого символа поочерёдно. Остановите вывод при столкновении с нуль-терминатором. В функции main() протестируйте строку Hello, world!.

Подсказка: Используйте оператор ++ для перевода указателя на следующий символ.

Ответ №4

Задание №5

Что не так с каждым из следующих фрагментов кода, и как бы вы их исправили?

a)

Ответ №5.a)

Цикл for имеет ошибку «неучтенной единицы» и пытается получить доступ к элементу массива под индексом 6, которого не существует. В условии цикла for нужно использовать оператор < вместо оператора <=.

b)

Ответ №5.b)

ptr — это указатель на const int. Мы не можем присвоить ему значение 7.

c)

Ответ №5.c)

array распадается в указатель при передаче в функцию printArray(). Цикл foreach не работает с указателем на массив, так как указателю неизвестна длина массива, на который он указывает. Первое из решений — добавить параметр length в функцию printArray() и использовать обычный цикл for. Второе решение — использовать std::array вместо стандартных фиксированных массивов.

d)

Ответ №5.d)

Мы не можем присвоить указателю типа int переменную не типа int. ptr должен быть типа double*.

Задание №6

Предположим, что мы хотим написать карточную игру.

a) В колоде карт находятся 52 уникальные карты: 13 достоинств (2, 3, 4, 5, 6, 7, 8, 9, 10, Валет, Дама, Король, Туз) и 4 масти (трефы, бубны, червы, пики). Создайте два перечисления: первое для масти, второе для достоинств карт.

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

Ответ №6.a)

b) Каждая карта должна быть представлена структурой Card, в которой хранится информация о достоинстве и масти карты (например, 4 бубны, король трефы). Создайте эту структуру.

Ответ №6.b)

c) Создайте функцию printCard(), параметром которой будет константная ссылка типа структуры Card, которая будет выводить значения достоинства и масти определенной карты в виде 2-буквенного кода (например, валет пики будет выводиться как VP).

Ответ №6.c)

d) Для представления целой колоды карт (52 карты) создайте массив deck (используя std::array) и инициализируйте каждый элемент определенной картой.

Подсказка: Используйте оператор static_cast для конвертации целочисленной переменной в тип перечисления.

Ответ №6.d)

е) Напишите функцию printDeck(), которая в качестве параметра принимает константную ссылку на массив deck и выводит все значения (карты). Используйте цикл foreach.

Ответ №6.e)

f) Напишите функцию swapCard(), которая принимает две карты и меняет местами их значения.

Ответ №6.f)

g) Напишите функцию shuffleDeck() для перетасовки колоды карт. Для этого используйте цикл for с итерацией по массиву. Перетасовка карт должна произойти 52 раза. В цикле for выберите случайное число от 1 до 52 и вызовите swapCard(), параметрами которой будут текущая карта и карта, выбранная случайным образом. Добавьте в функцию main() возможность перетасовки и вывода уже обновленной (перетасованной) колоды карт.

Подсказки:

   Для генерации случайных чисел смотрите урок №71.

   Не забудьте в начале функции main() вызвать функцию srand().

   Если вы используете Visual Studio, то не забудьте перед генерацией случайного числа вызвать один раз функцию rand().

Ответ №6.g)

h) Напишите функцию getCardValue(), которая возвращает значение карты (например, 2 значит 2, 3 значит 3 и т.д., 10, валет, королева или король — это 10, туз — это 11).

Ответ №6.h)

Задание №7

Хорошо, настало время для серьезной игры! Давайте напишем упрощенную версию известной карточной игры Blackjack (русский аналог «Очко» или «21 очко»). Если вы не знакомы с этой игрой и её правилами, то вот ссылка на статью в Википедии о Блэкджеке.

Правила нашей версии Blackjack следующие:

   вначале дилер получает одну карту (в реальной жизни, дилер получает две карты, но одна лицевой стороной вниз, поэтому на данном этапе это не имеет значения);

   затем игрок получает две карты;

   игрок начинает;

   игрок может либо «взять» (hit), либо «удержаться» (stand);

   если игрок «удержался», то его ход завершен, и его результат подсчитывается на основе карт, которые у него есть;

   если игрок «берет», то он получает вторую карту, и значение этой карты добавляется к его уже имеющемуся результату;

   туз обычно считается как 1 или как 11. Чтобы было проще, мы будем считать его как 11;

   если у игрока в результате получается больше 21, то он проиграл;

   ход дилера выполняется после хода игрока;

   дилер берет карты до тех пор, пока его общий результат не достигнет 17 или более очков. Как только этот предел достигнут — дилер карт уже не берет;

   если у дилера больше 21-го, то дилер проиграл, а игрок победил;

   если же у дилера и у игрока до 21 очка, то выигрывает тот, у кого результат больше.

В нашей упрощенной версии Blackjack мы не будем отслеживать, какие конкретно карты были у игрока, а какие у дилера. Мы будем отслеживать только сумму значений карт, которые они получили. Так будет проще.

Начнем с кода, который у нас получился в задании №6. Создайте функцию playBlackjack(), которая возвращает true, если игрок побеждает, и false — если он проигрывает. Эта функция должна:

   Принимать перетасованную колоду карт (deck) в качестве параметра.

   Инициализировать указатель на первую карту (имя указателя — cardPtr). Это будет использоваться для раздачи карт из колоды.

   Иметь две целочисленные переменные для хранения результата игрока и дилера.

   Соответствовать правилам, приведенным выше.

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

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

Протестируйте выполнение одиночной игры «Блэкджек» в функции main().

Ответ №7

Дополнительные задания

a) Время для критического мышления. Опишите, как бы вы могли модифицировать программу, приведенную выше, для обработки случаев, когда стоимость тузов может равняться 1 очку или 11 очкам.

Ответ a)

Можно было бы отслеживать, сколько тузов игрок и дилер получили в отдельной целочисленной переменной-счетчике. Если у игрока или дилера результат превысил 21 очко, и их счетчик тузов больше нуля, то тогда уменьшается результат игрока или дилера на 10 (конвертируем туз из 11 очков в 1) и удаляется 1 из счетчика тузов. Продолжаться это будет до тех пор, пока счетчик тузов не достигнет нуля.

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

Ответ b)

Функция playBlackjack() сейчас возвращает true, если игрок выигрывает, и false — если проигрывает. Нужно обновить эту функцию, чтобы было три возможных исхода: победа дилера, победа игрока или ничья. Лучший способ это сделать — создать перечисление для этих 3-х вариантов + чтобы функция возвращала соответствующее значение из этого перечисления:

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

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

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

  1. Аватар Алексей:

    Не понял зачем отслеживать количество тузов ? если мы не шулеры 🙂 то количество их в колоде постоянно.
    Сам туз определяеделяется по значению (11) т.е. нам даже не нужно менять его значение, просто правильно считать при приходе туза в руку:

  2. Аватар Алексей:

    не понял зачем искать тузы и как-то их учитывать, номинала вполне достаточно

  3. Аватар Владислав:

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

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

    Но тогда, почему работает оператор индексации?

    1. Аватар Владислав:

      Я нашел в чем проблема, все дело в приоритете операций.

  4. Аватар Аноним:

    Кому интересно, написал игру с азартом, настоящий симулятор, как в жизни))

  5. Аватар Rock:

    За примерно 12 часов удалось сделать полноценную игру

    Вот сама игра — https://pastebin.com/Bgc8wGk2

    И 2 подключаемых файла

    https://pastebin.com/JY35NBL9 — проверка корректности ввода

    https://pastebin.com/v9BTzbbu — Рандомайзер на вихре Марсена

    пользуйтесь кому нужно будет)

  6. Аватар Vlad:

  7. Аватар Sagynysh:

    Task 1

  8. Аватар Павел:

    Мой вариант решения задачи. Пошёл не стандартным путём)))

  9. Аватар Павел:

    Задача № 1:

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

  10. Аватар cybersatori:

    закончил 7 задание стал карточным шулером и кулхацкером впридачу) клёво спасибо, единственное с наполнением массива картами запарился, громоздкий стейтмент — а так все сделал, работает.)

  11. Аватар Vlad:

    Делал всю программу целых 2 дня, но оно того стоило. Спасибо вам большое за всё.

  12. Аватар Тимур:

    Юрий, планируется ли обновление и дополнение материалов? На learncpp вышло несколько новых уроков, а также некоторые уроки были дополнены.

    1. Юрий Юрий:

      Планируется добавление новых 5 уроков, сейчас над этим как раз работаю)

  13. Аватар Константин:

    Автор, а в задании 5 в пункте С не ошибка ли?

    В коде, в котором нужно найти ошибку написан for, в ответе вы пишите: "Цикл foreach не работает с указателем на массив, так как указателю неизвестна длина массива, на который он указывает. Первое из решений — добавить параметр length в функцию printArray() и использовать обычный цикл for."
    Но ведь там уже for, хотя должно быть слово foreach, или я что-то не так понял?

    1. Юрий Юрий:

      Всё верно, в примере используется цикл foreach. Перечитайте урок №91, отличительной чертой цикла foreach является использование с перечислительными типами данных, где есть диапазон и при этом в цикле foreach используется то же ключевое слово for (а не foreach).

      1. Аватар Константин:

        Точно! Что-то я конкретно затупил с этим, мне казалось что там "foreach". Тогда извиняюсь за беспокойство, спасибо за ваши труды.

        1. Юрий Юрий:

          Ничего страшного, пожалуйста)

  14. Аватар Анастасия:

    Чтобы не занимать много места скину ссылку на мой github. Вот, в общем то, и задание 7.
    https://github.com/Luzinsan/Chapter_6/blob/master/Blackjack

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

    Спасибо автору learncpp и вам, Юрий, что доносите до нас такую полезную информацию, да ещё так предельно понятно. Буду продолжать заниматься, ведь благодаря этой игре энтузиазм к постижению с++ у меня только увеличился))).

  15. Аватар Анастасия Лузинсан:

    Хорошие задания.) 2-е задание: