10.x – Резюме к главе 10 и небольшой тест

Добавлено 12 июня 2021 в 08:21
Глава 10 – Массивы, строки, указатели и ссылки  (содержание)

Слова ободрения

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

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

Краткое резюме

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

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

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

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

Массивы можно использовать для создания строк в стиле C. Обычно вам следует избегать этого и использовать вместо этого std::string_view и std::string.

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

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

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

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

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

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

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

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

int value{ 5 };
const int *ptr{ &value }; // ok, ptr указывает на "const int"

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

int value{ 5 };
int *const ptr{ &value }; // ptr является константным, но *ptr не является константой

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

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

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

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

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

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

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

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

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

Чтобы сделать тесты немного проще, мы должны ввести пару новых алгоритмов.

std::reduce применяет функцию, по умолчанию оператор +, ко всем элементам в списке, в результате чего получается одно значение. Когда мы используем оператор +, результатом является сумма всех элементов в списке. Обратите внимание, что существует также std::accumulate. std::accumulate нельзя распараллелить, потому что он применяет функцию слева направо. std::reduce сегментирует список, что означает, что функция применяется в неизвестном порядке, что позволяет распараллелить операцию. Если мы хотим суммировать список, нас не волнует порядок, и мы используем std::reduce.

Примечание автора


std::reduce в настоящее время реализован не полностью во всех основных стандартных библиотеках. Если он не сработает, вернитесь к std::accumulate.

std::shuffle принимает список и случайным образом меняет порядок его элементов.

#include <algorithm> // std::shuffle
#include <array>
#include <ctime>
#include <iostream>
#include <numeric>  // std::reduce
#include <random>
 
int main()
{
  std::array arr{ 1, 2, 3, 4 };
 
  std::cout << std::reduce(arr.begin(), arr.end()) << '\n'; // 10
 
  // Если вы не можете использовать std::reduce, используйте std::accumulate.
  // 0 - начальное значение результата: 0 + (((1 + 2) + 3) + 4)
  std::cout << std::accumulate(arr.begin(), arr.end(), 0) << '\n'; // 10
 
  std::mt19937 mt{ static_cast<std::mt19937::result_type>(std::time(nullptr)) };
  std::shuffle(arr.begin(), arr.end(), mt);
 
  for (int i : arr)
  {
    std::cout << i << ' ';
  }
 
  std::cout << '\n';
 
  return 0;
}

Возможный вывод программы

10
10
2 1 4 3

Вопрос 1

Представьте, что вы пишете игру, в которой игрок может держать в руках 3 типа предметов: зелья здоровья (health potion), факелы (torch) и стрелы (arrow). Создайте перечисление для идентификации различных типов элементов и массив std::array для хранения количества единиц каждого элемента, который несет игрок (перечислители используются в качестве индексов массива). Игрок должен начать с 2 зельями здоровья, 5 факелами и 10 стрелами. Напишите функцию countTotalItems(), которая возвращает общее количество элементов, которые есть у игрока. Заставьте вашу функцию main() распечатать выходное значение countTotalItems(), а также количество факелов.

#include <array>
#include <numeric> // std::reduce
#include <iostream>
 
// Мы хотим использовать ItemTypes для индексации массива. Используем enum, а не enum class.
enum ItemTypes
{
  item_health_potion,
  item_torch,
  item_arrow,
  max_items
};
 
using inventory_type = std::array<int, ItemTypes::max_items>;
 
int countTotalItems(const inventory_type& items)
{
  return std::reduce(items.begin(), items.end());
}
 
int main()
{
  inventory_type items{ 2, 5, 10 };
 
  std::cout << "The player has " << countTotalItems(items) << " item(s) in total.\n";
 
  // Мы можем получить доступ к отдельным элементам с помощью перечислителей:
  std::cout << "The player has " << items[ItemTypes::item_torch] << " torch(es)\n";
 
  return 0;
}

Вопрос 2

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

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

Joe
82
Terry
73
Ralph
4
Alex
94
Mark
88

Вывод должен выглядеть так:

Alex got a grade of 94
Mark got a grade of 88
Joe got a grade of 82
Terry got a grade of 73
Ralph got a grade of 4

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

#include <algorithm> // std::sort
#include <cstddef> // std::size_t
#include <iostream>
#include <string>
#include <vector>
 
struct Student
{
  std::string name{};
  int grade{};
};
 
int getNumberOfStudents()
{
  int numberOfStudents{};
 
  do
  {
    std::cout << "How many students do you want to enter? ";
    std::cin >> numberOfStudents;
  } while (numberOfStudents <= 0);
 
  return numberOfStudents;
}
 
std::vector<Student> getStudents()
{
  using vector_type = std::vector<Student>;
 
  int numberOfStudents{ getNumberOfStudents() };
 
  // Создаем вектор с количеством элементов, равным numberOfStudents.
  vector_type students(static_cast<vector_type::size_type>(numberOfStudents));
 
  int studentNumber{ 1 };
 
  for (auto& student : students)
  {
    std::cout << "Enter name #" << studentNumber << ": ";
    std::cin >> student.name;
    std::cout << "Enter grade #" << studentNumber << ": ";
    std::cin >> student.grade;
 
    ++studentNumber;
  }
 
  return students;
}
 
// Передаем по ссылке, чтобы избежать медленного копирования.
bool compareStudents(const Student& a, const Student& b)
{
  return (a.grade > b.grade);
}
 
int main()
{
  auto students{ getStudents() };
 
  std::sort(students.begin(), students.end(), compareStudents);
 
  // Печатаем все имена
  for (const auto& student : students)
  {
    std::cout << student.name << " got a grade of " << student.grade << '\n';
  }
 
  return 0;
}

Вопрос 3

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

Используйте параметры-ссылки

void swap(int& a, int& b)

#include <iostream>
 
// Используем параметры-ссылки, чтобы
// мы могли изменять значения переданных аргументов
void swap(int& a, int& b)
{
  // Временно сохраняем значение a
  int temp{ a };
 
  // Помещаем значение b в a
  a = b;
  // Помещаем предыдущее значение a в b
  b = temp;
}
 
int main()
{
  int a{ 6 };
  int b{ 8 };
  swap(a, b);
 
  if (a == 8 && b == 6)
    std::cout << "It works!\n";
  else
    std::cout << "It's broken!\n";
 
  return 0;
}

Вопрос 4

Напишите функцию для посимвольной печати строки в стиле C. Используйте указатель для перехода по символам строки и печати текущего символа. Остановитесь, когда вы встретите нулевой терминатор. Напишите функцию main, которая проверяет эту функцию со строковым литералом "Hello, world!".

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

const char* str{ "Hello, world!" };
std::cout << *str; // H
++str;
std::cout << *str; // e
// ...

#include <iostream>
 
// str будет указывать на первую букву строки в стиле C.
// Обратите внимание, что str указывает на const char,
// поэтому мы не можем изменить значения, на которые он указывает.
// Однако мы можем заставить str указывать на что-нибудь еще.
// Это не меняет значения аргумента.
void printCString(const char* str)
{
  // Пока мы не встретили нулевой терминатор
  while (*str != '\0')
  {
    // выводим текущий символ
    std::cout << *str;
 
    // и переводим str на следующий символ
    ++str;
  }
}
 
int main()
{
  printCString("Hello world!");
 
  std::cout << '\n';
 
  return 0;
}

Вопрос 5

Что не так с каждым из этих фрагментов кода, и как это исправить?

a)

int main()
{
  int array[]{ 0, 1, 2, 3 };
 
  for (std::size_t count{ 0 }; count <= std::size(array); ++count)
  {
    std::cout << array[count] << ' ';
  }
 
  std::cout << '\n';
 
  return 0;
}

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

b)

int main()
{
  int x{ 5 };
  int y{ 7 };
 
  const int* ptr{ &x };
  std::cout << *ptr << '\n';
  *ptr = 6;
  std::cout << *ptr << '\n';
  ptr = &y;
  std::cout << *ptr << '\n';
 
  return 0;
}

ptr – указатель на const int. Вы не можете присвоить ему значение 6. Вы можете исправить это, сделав ptr неконстантным.

c)

void printArray(int array[])
{
  for (int element : array)
  {
    std::cout << element << ' ';
  }
}
 
int main()
{
  int array[]{ 9, 7, 5, 3, 1 };
 
  printArray(array);
 
  std::cout << '\n';
 
  return 0;
}

array распадается на указатель, когда он передается в printArray(). Циклы for на основе диапазона не могут работать с указателем на массив, потому что размер массива неизвестен. Одно из решений – добавить параметр длины к функции printArray() и использовать обычный цикл for. Лучшее решение – использовать std::array вместо встроенных фиксированных массивов.

d)

int* allocateArray(const int length)
{
  int temp[length]{};
  return temp;
}

temp – это фиксированный массив, но length не является константой времени компиляции, поэтому мы не можем использовать length для создания массива в стиле C. Переменная temp также выйдет из области видимости в конце функции, возвращаемое значение будет указывать на что-то недопустимое. temp должен использовать динамическое выделение памяти или быть std::vector.

e)

int main()
{
  double d{ 5.5 };
  int* ptr{ &d };
  std::cout << ptr << '\n';
 
  return 0;
}

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


Вопрос 6

Представим, что мы пишем карточную игру.

a) В колоде 52 уникальных карты (13 рангов карт по 4 масти). Создайте перечисления для рангов карт (2, 3, 4, 5, 6, 7, 8, 9, 10, валет (Jack), дама (Queen), король (King), туз (Ace)) и мастей (трефы (clubs), бубны (diamonds), червы (hearts), пики (spades)). Эти перечислители не будут использоваться для индексации массивов.

enum class CardSuit
{
    suit_club,
    suit_diamond,
    suit_heart,
    suit_spade,
 
    max_suits
};
 
enum class CardRank
{
    rank_2,
    rank_3,
    rank_4,
    rank_5,
    rank_6,
    rank_7,
    rank_8,
    rank_9,
    rank_10,
    rank_jack,
    rank_queen,
    rank_king,
    rank_ace,
 
    max_ranks
};

b) Каждая карта будет представлена структурой с именем Card, которая содержит ранг (rank) и масть (suit). Создайте структуру.

struct Card
{
  CardRank rank{};
  CardSuit suit{};
};

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

Используйте оператор switch.

void printCard(const Card& card)
{
    switch (card.rank)
    {
    case CardRank::rank_2:      std::cout << '2';   break;
    case CardRank::rank_3:      std::cout << '3';   break;
    case CardRank::rank_4:      std::cout << '4';   break;
    case CardRank::rank_5:      std::cout << '5';   break;
    case CardRank::rank_6:      std::cout << '6';   break;
    case CardRank::rank_7:      std::cout << '7';   break;
    case CardRank::rank_8:      std::cout << '8';   break;
    case CardRank::rank_9:      std::cout << '9';   break;
    case CardRank::rank_10:     std::cout << 'T';   break;
    case CardRank::rank_jack:   std::cout << 'J';   break;
    case CardRank::rank_queen:  std::cout << 'Q';   break;
    case CardRank::rank_king:   std::cout << 'K';   break;
    case CardRank::rank_ace:    std::cout << 'A';   break;
    default:
        std::cout << '?';
        break;
    }
 
    switch (card.suit)
    {
    case CardSuit::suit_club:       std::cout << 'C';   break;
    case CardSuit::suit_diamond:    std::cout << 'D';   break;
    case CardSuit::suit_heart:      std::cout << 'H';   break;
    case CardSuit::suit_spade:      std::cout << 'S';   break;
    default:
        std::cout << '?';
        break;
    }
}

d) В колоде 52 карты. Создайте массив (используя std::array) для представления колоды карт и инициализируйте его по одной карте каждого типа. Сделайте это в функции с именем createDeck и вызовите createDeck из main. createDeck должен вернуть колоду в main.

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

#include <array>
 
// Нам это понадобится еще много раз, создайте псевдонимы.
using deck_type = std::array<Card, 52>;
using index_type = deck_type::size_type;
 
deck_type createDeck()
{
  deck_type deck{};
 
  // Мы могли бы инициализировать каждую карту отдельно,
  // но это было бы проблемно. Воспользуемся циклом.
 
  index_type card{ 0 };
 
  auto suits{ static_cast<int>(CardSuit::max_suits) };
  auto ranks{ static_cast<int>(CardRank::max_ranks) };
 
  for (int suit{ 0 }; suit < suits; ++suit)
  {
    for (int rank{ 0 }; rank < ranks; ++rank)
    {
      deck[card].suit = static_cast<CardSuit>(suit);
      deck[card].rank = static_cast<CardRank>(rank);
      ++card;
    }
  }
 
  return deck;
}
 
int main()
{
  auto deck{ createDeck() };
 
  return 0;
}

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

2C 3C 4C 5C 6C 7C 8C 9C TC JC QC KC AC 2D 3D 4D 5D 6D 7D 8D 9D TD JD QD KD AD 2H 3H 4H 5H 6H 7H 8H 9H TH JH QH KH AH 2S 3S 4S 5S 6S 7S 8S 9S TS JS QS KS AS

Если вы использовали другие символы, это нормально.

void printDeck(const deck_type& deck)
{
  for (const auto& card : deck)
  {
    printCard(card);
    std::cout << ' ';
  }
 
  std::cout << '\n';
}

f) Напишите функцию с именем shuffleDeck для перетасовки колоды карт с помощью std::shuffle. Обновите свою функцию main, чтобы перетасовать колоду и распечатать ее уже перетасованной.

Напоминание: инициализируйте генератор случайных чисел только один раз.

#include <algorithm> // для std::shuffle
#include <ctime>     // для std::time
#include <random>    // для std::mt19937
 
// ...
 
void shuffleDeck(deck_type& deck)
{
  // mt статический, поэтому инициализируется только один раз.
  static std::mt19937 mt{ static_cast<std::mt19937::result_type>(std::time(nullptr)) };
 
  std::shuffle(deck.begin(), deck.end(), mt);
}
 
int main()
{
  auto deck{ createDeck() };
 
  shuffleDeck(deck);
 
  printDeck(deck);
 
  return 0;
}

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

int getCardValue(const Card& card)
{
  // Обрабатываем ранг от 2 до 10. Мы могли бы сделать это
  // с помощью switch, но это было бы долго.
  if (card.rank <= CardRank::rank_10)
  {
    // RANK_2 это 0 (значение 2)
    // RANK_3 это 1 (значение 3)
    // и т.д.
    return (static_cast<int>(card.rank) + 2);
  }
 
  switch (card.rank)
  {
  case CardRank::rank_jack:
  case CardRank::rank_queen:
  case CardRank::rank_king:
    return 10;
  case CardRank::rank_ace:
    return 11;
  default:
    // Не должно случиться. Если всё-таки случилось,
    // значит, в нашем коде есть ошибка.
    return 0;
  }
}

Вопрос 7

а) Хорошо, время бросить вызов! Напишем упрощенную версию блэкджека. Если вы еще не знакомы с блэкджеком, в Википедии есть статья с кратким описанием.

Вот правила нашей версии блэкджека:

  • для начала дилер получает одну карту (в реальной жизни дилер получает две, но одна закрыта, поэтому на данном этапе это не имеет значения);
  • для начала игрок получает две карты;
  • игрок ходит первым.
  • игрок может многократно сказать «еще» (hit) или сказать «достаточно» (stand);
  • если игрок говорит «достаточно», его ход заканчивается, и его счет рассчитывается на основе карт, которые ему были розданы;
  • если игрок говорит «еще», он получает еще одну карту, и значение этой карты добавляется к его общему счету;
  • туз обычно засчитывается как 1 или 11 (в зависимости от того, что лучше для общего счета); для простоты мы будем считать здесь 11;
  • если у игрока больше 21 очка, он сразу же проигрывает;
  • дилер ходит после игрока;
  • дилер несколько раз тянет карты, пока не наберет 17 или более очков, после чего он останавливается;
  • если у дилера больше 21 очков, он проигрывает, и игрок немедленно выигрывает;
  • в противном случае, если у игрока больше очков, чем у дилера, игрок выигрывает. В противном случае игрок проигрывает (для простоты мы будем рассматривать ничью как выигрыш дилера);
  • в нашей упрощенной версии блэкджека мы не будем отслеживать, какие именно карты были сданы игроку и дилеру. Мы будем отслеживать только сумму значений карт, которые были розданы игроку и дилеру. Это упрощает работу.

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

  • принимать в качестве параметра перетасованную колоду карт;
  • реализовывать партию блэкджека, как описано выше;
  • возвращать true, если выиграл игрок, и false, если он проиграл.

Также напишите функцию main() для одиночной игры в блэкджек.

#include <algorithm> // std::shuffle
#include <array>
#include <ctime>     // std::time
#include <iostream>
#include <random>    // std::mt19937
 
enum class CardSuit
{
    suit_club,
    suit_diamond,
    suit_heart,
    suit_spade,
 
    max_suits
};
 
enum class CardRank
{
    rank_2,
    rank_3,
    rank_4,
    rank_5,
    rank_6,
    rank_7,
    rank_8,
    rank_9,
    rank_10,
    rank_jack,
    rank_queen,
    rank_king,
    rank_ace,
 
    max_ranks
};
 
struct Card
{
    CardRank rank{};
    CardSuit suit{};
};
 
struct Player
{
    int score{};
};
 
using deck_type = std::array<Card, 52>;
using index_type = deck_type::size_type;
 
// Максимальный счет до проигрыша.
constexpr int maximumScore{ 21 };
 
// Минимальный счет, который должен иметь дилер.
constexpr int minimumDealerScore{ 17 };
 
void printCard(const Card& card)
{
    switch (card.rank)
    {
    case CardRank::rank_2:      std::cout << '2';   break;
    case CardRank::rank_3:      std::cout << '3';   break;
    case CardRank::rank_4:      std::cout << '4';   break;
    case CardRank::rank_5:      std::cout << '5';   break;
    case CardRank::rank_6:      std::cout << '6';   break;
    case CardRank::rank_7:      std::cout << '7';   break;
    case CardRank::rank_8:      std::cout << '8';   break;
    case CardRank::rank_9:      std::cout << '9';   break;
    case CardRank::rank_10:     std::cout << 'T';   break;
    case CardRank::rank_jack:   std::cout << 'J';   break;
    case CardRank::rank_queen:  std::cout << 'Q';   break;
    case CardRank::rank_king:   std::cout << 'K';   break;
    case CardRank::rank_ace:    std::cout << 'A';   break;
    default:
        std::cout << '?';
        break;
    }
 
    switch (card.suit)
    {
    case CardSuit::suit_club:       std::cout << 'C';   break;
    case CardSuit::suit_diamond:    std::cout << 'D';   break;
    case CardSuit::suit_heart:      std::cout << 'H';   break;
    case CardSuit::suit_spade:      std::cout << 'S';   break;
    default:
        std::cout << '?';
        break;
    }
}
 
int getCardValue(const Card& card)
{
    // Обрабатываем ранг от 2 до 10. Мы могли бы сделать это
    // с помощью switch, но это было бы долго.
    if (card.rank <= CardRank::rank_10)
    {
        // RANK_2 это 0 (значение 2)
        // RANK_3 это 1 (значение 3)
        // и т.д.
        return (static_cast<int>(card.rank) + 2);
    }
 
    switch (card.rank)
    {
    case CardRank::rank_jack:
    case CardRank::rank_queen:
    case CardRank::rank_king:
        return 10;
    case CardRank::rank_ace:
        return 11;
    default:
        // Не должно случиться. Если всё-таки случилось,
        // значит, в нашем коде есть ошибка.
        return 0;
    }
}
 
void printDeck(const deck_type& deck)
{
    for (const auto& card : deck)
    {
        printCard(card);
        std::cout << ' ';
    }
 
    std::cout << '\n';
}
 
deck_type createDeck()
{
    deck_type deck{};
 
    // Мы могли бы инициализировать каждую карту отдельно,
    // но это было бы проблемно. Воспользуемся циклом.
 
    index_type card{ 0 };
 
    auto suits{ static_cast<int>(CardSuit::max_suits) };
    auto ranks{ static_cast<int>(CardRank::max_ranks) };
 
    for (int suit{ 0 }; suit < suits; ++suit)
    {
        for (int rank{ 0 }; rank < ranks; ++rank)
        {
            deck[card].suit = static_cast<CardSuit>(suit);
            deck[card].rank = static_cast<CardRank>(rank);
            ++card;
        }
    }
 
    return deck;
}
 
 
void shuffleDeck(deck_type& deck)
{
    static std::mt19937 mt{ static_cast<std::mt19937::result_type>(std::time(nullptr)) };
 
    std::shuffle(deck.begin(), deck.end(), mt);
}
 
// Возвращает true, если игрок хочет еще карту. В противном случае - false.
bool playerWantsHit()
{
    while (true)
    {
        // «еще» - hit (h), «достаточно» - stand (s)
        std::cout << "(h) to hit, or (s) to stand: ";
 
        char ch{};
        std::cin >> ch;
 
        switch (ch)
        {
        case 'h':
            return true;
        case 's':
            return false;
        }
    }
}
 
// Возвращает true, если у игрока «перебор». В противном случае - false.
bool playerTurn(const deck_type& deck, index_type& nextCardIndex, Player& player)
{
    while (true)
    {
        if (player.score > maximumScore)
        {
            // Это может произойти даже до того, как у игрока был выбор,
            // если он взял 2 туза.
            std::cout << "You busted!\n";
            return true;
        }
        else
        {
            if (playerWantsHit())
            {
                int cardValue { getCardValue(deck[nextCardIndex++]) };
                player.score += cardValue;
                std::cout << "You were dealt a " << cardValue 
                          << " and now have " << player.score << '\n';
            }
            else
            {
                // У игрока нет перебора
                return false;
            }
        }
    }
}
 
// Возвращает true, если у дилера «перебор». В противном случае - false.
bool dealerTurn(const deck_type& deck, index_type& nextCardIndex, Player& dealer)
{
    // Тянем карты, пока не достигнем минимального значения
    while (dealer.score < minimumDealerScore)
    {
        int cardValue{ getCardValue(deck[nextCardIndex++]) };
        dealer.score += cardValue;
        std::cout << "The dealer turned up a " << cardValue 
                  << " and now has " << dealer.score << '\n';
 
    }
 
    // Если счет дилера слишком высок, у него перебор
    if (dealer.score > maximumScore)
    {
        std::cout << "The dealer busted!\n";
        return true;
    }
    
    return false;
}
 
bool playBlackjack(const deck_type& deck)
{
    // Индекс карты, которая будет вытянута следующей.
    // Обойти массив невозможно, потому что игрок
    // проиграет до того, как будут использованы все карты.
    index_type nextCardIndex{ 0 };
 
    // Создаем дилера и даем ему 1 карту.
    Player dealer{ getCardValue(deck[nextCardIndex++]) };
 
    // Карта дилера открыта, игрок ее видит.
    std::cout << "The dealer is showing: " << dealer.score << '\n';
 
    // Создаем игрока и даем ему 2 карты.
    Player player{ getCardValue(deck[nextCardIndex]) + getCardValue(deck[nextCardIndex + 1]) };
    nextCardIndex += 2;
 
    std::cout << "You have: " << player.score << '\n';
 
    if (playerTurn(deck, nextCardIndex, player))
    {
        // У игрока перебор.
        return false;
    }
 
    if (dealerTurn(deck, nextCardIndex, dealer))
    {
        // У дилера перебор, игрок выигрывает.
        return true;
    }
 
    return (player.score > dealer.score);
}
 
int main()
{
    auto deck{ createDeck() };
 
    shuffleDeck(deck);
 
    if (playBlackjack(deck))
    {
        std::cout << "You win!\n";
    }
    else
    {
        std::cout << "You lose!\n";
    }
 
    return 0;
}

Пройдя тест, обратите внимание на некоторые из наиболее распространенных ошибок:

Генерация случайных чисел

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

void shuffleDeck(deck_type& deck)
{
  // неслучайный
  /* static */ std::mt19937 mt{ static_cast<std::mt19937::result_type>(std::time(nullptr)) }; 
  // случайный
  static std::mt19937 mt{ static_cast<std::mt19937::result_type>(std::time(nullptr)) }; 
 
  std::shuffle(deck.begin(), deck.end(), mt);
}

Магические числа

  • Если ваш код содержит числа 10, 11, 17, 21 или 52 внутри тела функции, у вас есть магические числа, которые следует удалить.
  • Если вы использовали 10 или 11, вероятно, вы не использовали getCardValue для получения значения карты. Кроме того, чтобы проверить, является ли карта тузом, проверяйте не ее значение, а ее ранг.
  • Если вы использовали 17 или 21, они должны быть переменными constexpr, чтобы можно было быстро изменить конфигурацию игры и упростить чтение кода.
  • Если вы использовали 52, вы должны использовать вместо него deck.size().

b) Дополнительное задание: время критического мышления: опишите, как вы можете изменить приведенную выше программу, чтобы справиться со случаем, когда тузы могут быть равны 1 или 11.

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

Один из способов – отслеживать, сколько тузов было сдано игроку и дилеру (в структуре Player как целое число). Если у игрока или дилера больше 21, а его счетчик тузов больше нуля, вы можете уменьшить его счет на 10 (преобразовать туз с 11 очков в 1) и «удалить» один туз из счетчика. Это можно делать сколько угодно раз, пока счетчик тузов не достигнет нуля.

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

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

enum class BlackjackResult
{
    player_win,
    dealer_win,
    tie
};
 
BlackjackResult playBlackjack(const deck_type& deck);

Теги

arrayC++ / CppLearnCppstd::arraystd::vectorSTL / Standard Template Library / Стандартная библиотека шаблоновАлгоритмВекторДля начинающихИтераторМассивОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.