Поздравляю вас с преодолением самой длинной главы этого туториала! Если у вас не было предыдущего опыта в программировании, то эта глава, скорее всего, была для вас наиболее сложной из всех предыдущих. Однако, если вы дошли до этого момента, то всё хорошо — вы справились! Так держать!
Хорошая новость заключается в том, что следующая глава будет легче этой, и очень скоро мы доберемся до самого сердца этого туториала — объектно-ориентированного программирования!
Теория
Массивы позволяют хранить и получать доступ ко многим переменным одного и того же типа данных через один идентификатор. Доступ к элементам массива осуществляется с помощью оператора индекса []
. Будьте осторожны с диапазоном массива, не допускайте индексации элементов вне диапазона. Массивы можно инициализировать с помощью списка инициализаторов или uniform-инициализации.
Фиксированные массивы должны иметь длину, установленную во время компиляции. Фиксированные массивы распадаются в указатели при передаче в функцию.
Циклы используются для итераций по массиву. Остерегайтесь ошибок «неучтенных единиц». Циклы foreach полезны, когда массив не распадается в указатель.
Массивы можно сделать многомерными, используя сразу несколько индексов.
Массивы используются в создании строк C-style. Избегайте использования строк C-style, вместо них используйте std::string.
Указатели — это переменные, которые хранят адреса памяти (указывают на) определенных переменных. Оператор адреса (&
) используется для получения адреса переменной. Оператор разыменования (*
) используется для получения значения, на которое указывает указатель.
Нулевой указатель — это указатель, который ни на что не указывает. Указатель можно сделать нулевым, инициализировав или присвоив ему значение 0
(или nullptr
в C++11). Избегайте использования макроса NULL
. Разыменование нулевого указателя может привести к неожиданным результатам (сбою). При удалении нулевого указателя ничего плохого не случится.
Указатель на массив не знает длину массива, на который он указывает. Это означает, что оператор sizeof и циклы foreach работать с ним не могут.
Операторы new и delete используются для динамического выделения памяти для указателя, переменной или массива и освобождения этой памяти. Хотя подобное случается крайне редко, оператор new может потерпеть крах, если в операционной системе не останется свободной памяти, поэтому не забывайте выполнять проверку того, возвращает ли оператор new нулевой указатель.
Обязательно используйте оператор delete[] для удаления динамически выделенного массива. Указатели, указывающие на освобожденную память, называются висячими указателями. Разыменование висячего указателя не приведет ни к чему хорошему.
Невозможность удалить динамически выделенную память приведет к утечке памяти, когда указатель, указывающий на эту память, выйдет из области видимости.
Для обычных переменных память выделяется из ограниченного резервуара — стека. Память для динамически выделенных переменных выделяется из общего резервуара памяти — кучи.
Указатель на константное значение обрабатывает значение, на которое он указывает, как константное:
1 2 |
int value = 7; const int *ptr = &value; // всё нормально, ptr указывает на "const int" |
Константный указатель — это указатель, значение которого не может быть изменено после инициализации:
1 2 |
int value = 7; int *const ptr = &value; |
Ссылка — это псевдоним для определенной переменной. Ссылки объявляются с использованием амперсанда &
(в этом контексте это не оператор адреса). Для константных ссылок изменить их значения после инициализации нельзя. Ссылки используются для предотвращения копирования данных при их передаче в функцию или из функции.
Оператор выбора элемента (->
) может использоваться для выбора члена через указатель на структуру. Он сочетает в себе как операцию разыменования, так и обычный доступ к элементам (.
).
Указатели типа void — это указатели, которые могут указывать на любой тип данных. Они не могут быть разыменованы напрямую. Вы можете использовать оператор static_cast для преобразования их обратно в исходный тип указателя. Какой уже это будет тип — решать вам.
Указатели на указатели позволяют создать указатель, указывающий на другой указатель.
std::array предоставляет весь функционал стандартных обычных фиксированных массивов в языке C++ в форме, которая не будет распадаться в указатель при передаче. Рекомендуется использовать std::array вместо стандартных фиксированных массивов.
std::vector предоставляет весь функционал динамических массивов, но которые при этом могут самостоятельно управлять выделенной себе памятью и запоминают свою длину. Рекомендуется использовать std::vector вместо стандартных динамических массивов.
Тест
Задание №1
Представьте, что вы пишете игру, в которой игрок может иметь 3 типа предметов: зелья здоровья, факелы и стрелы. Создайте перечисление с этими типами предметов и фиксированный массив для хранения количества каждого типа предметов, которое имеет при себе игрок (используйте стандартные фиксированные массивы, а не std::array). У вашего игрока должны быть при себе 3 зелья здоровья, 6 факелов и 12 стрел. Напишите функцию countTotalItems(), которая возвращает общее количество предметов, которые есть у игрока. В функции main() выведите результат работы функции countTotalItems().
Ответ №1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <iostream> enum ItemTypes { ITEM_HEALTH_POTION, ITEM_TORCH, ITEM_ARROW, MAX_ITEMS }; int countTotalItems(int *items) // нам здесь не нужно передавать длину массива, так как она уже указана членом MAX_ITEMS перечисления ItemTypes { int totalItems = 0; for (int index = 0; index < MAX_ITEMS; ++index) totalItems += items[index]; return totalItems; } int main() { int items[MAX_ITEMS]{ 3, 6, 12 }; // используем uniform-инициализацию для указания стартового количества предметов, которые имеет при себе игрок (C++11) // int items[MAX_ITEMS] = { 3, 6, 12 }; // используйте список инициализаторов, если у вас не поддерживается C++11 std::cout << "The player has " << countTotalItems(items) << " items in total.\n"; return 0; } |
Задание №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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
#include <iostream> #include <string> #include <utility> // include <algorithm>, если не поддерживается C++11 struct Student { std::string name; int grade; }; // Функция сортировки студентов. Поскольку students - это указатель на массив, и он не знает длину массива (на который он указывает), // то мы передаем длину явно, добавив параметр length void sortNames(Student *students, int length) { // Перебираем каждый элемент массива for (int startIndex = 0; startIndex < length; ++startIndex) { // largestIndex - это индекс наибольшего элемента, который мы обнаружили до сих пор int largestIndex = startIndex; // Ищем наибольший элемент среди оставшихся элементов массива (начиная со startIndex+1) for (int currentIndex = startIndex + 1; currentIndex < length; ++currentIndex) { // Если текущий элемент больше нашего предыдущего наибольшего элемента, if (students[currentIndex].grade > students[largestIndex].grade) // то тогда это наш новый наибольший элемент в текущей итерации largestIndex = currentIndex; } // Меняем местами наш стартовый элемент с найденным наибольшим элементом std::swap(students[startIndex], students[largestIndex]); } } int main() { int numStudents = 0; do { std::cout << "How many students do you want to enter? "; std::cin >> numStudents; } while (numStudents <= 1); // Динамически выделяем массив для хранения студентов Student *students = new Student[numStudents]; // Записываем имя и оценку каждого студента for (int index = 0; index < numStudents; ++index) { std::cout << "Enter name #" << index + 1 << ": "; std::cin >> students[index].name; std::cout << "Enter grade #" << index + 1 << ": "; std::cin >> students[index].grade; } // Сортируем студентов sortNames(students, numStudents); // Выводим имена студентов и их оценки for (int index = 0; index < numStudents; ++index) std::cout << students[index].name << " got a grade of " << students[index].grade << "\n"; // Не забываем об освобождении памяти delete[] students; return 0; } |
Задание №3
Напишите свою функцию, которая меняет местами значения двух целочисленных переменных. Проверку осуществляйте в функции main().
Подсказка: Используйте ссылки в качестве параметров.
Ответ №3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#include <iostream> // Используем ссылки в качестве параметров, чтобы иметь возможность изменить значения исходных аргументов void swap(int &x, int &y) { // Временно сохраняем значение переменной x в temp int temp = x; // Помещаем значение y в x x = y; // Помещаем предыдущее значение x в y y = temp; } int main() { int x = 5; int y = 7; swap(x, y); if (x == 7 && y == 5) std::cout << "It works!"; else std::cout << "It's broken!"; return 0; } |
Задание №4
Напишите функцию для вывода строки C-style символ за символом. Используйте указатель для перехода и вывода каждого символа поочерёдно. Остановите вывод при столкновении с нуль-терминатором. В функции main() протестируйте строку Hello, world!
.
Подсказка: Используйте оператор ++
для перевода указателя на следующий символ.
Ответ №4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> // str указывает на первый символ строки C-style. // Обратите внимание, str указывает на const char и мы не можем изменить это значение. // Однако, мы можем заставить str указывать на что-либо другое. Это не приведет к изменению значения исходного аргумента void printCString(const char *str) { // Пока мы не встретили нуль-терминатор while (*str != '\0') { // Выводим текущий символ std::cout << *str; // И переводим указатель str на следующий символ ++str; } } int main() { printCString("Hello, world!"); return 0; } |
Задание №5
Что не так с каждым из следующих фрагментов кода, и как бы вы их исправили?
a)
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> int main() { int array[6] { 0, 2, 4, 7, 9 }; for (int count = 0; count <= 6; ++count) std::cout << array[count] << " "; return 0; } |
Ответ №5.a)
Цикл for имеет ошибку «неучтенной единицы» и пытается получить доступ к элементу массива под индексом 6, которого не существует. В условии цикла for нужно использовать оператор <
вместо оператора <=
.
b)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> int main() { int a = 4; int b = 6; const int *ptr = &a; std::cout << *ptr; *ptr = 7; std::cout << *ptr; ptr = &b; std::cout << *ptr; return 0; } |
Ответ №5.b)
ptr
— это указатель на const int. Мы не можем присвоить ему значение 7
.
c)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> void printArray(int array[]) { for (const int &element : array) std::cout << element << ' '; } int main() { int array[] { 8, 6, 4, 2, 0 }; printArray(array); return 0; } |
Ответ №5.c)
array
распадается в указатель при передаче в функцию printArray(). Цикл foreach не работает с указателем на массив, так как указателю неизвестна длина массива, на который он указывает. Первое из решений — добавить параметр length
в функцию printArray() и использовать обычный цикл for. Второе решение — использовать std::array вместо стандартных фиксированных массивов.
d)
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> int main() { double d(4.7); int *ptr = &d; std::cout << ptr; return 0; } |
Ответ №5.d)
Мы не можем присвоить указателю типа int переменную не типа int. ptr
должен быть типа double*.
Задание №6
Предположим, что мы хотим написать карточную игру.
a) В колоде карт находятся 52 уникальные карты: 13 достоинств (2, 3, 4, 5, 6, 7, 8, 9, 10, Валет, Дама, Король, Туз) и 4 масти (трефы, бубны, червы, пики). Создайте два перечисления: первое для масти, второе для достоинств карт.
Подсказка: Добавьте в каждое перечисление еще по одному элементу, который будет обозначать длину этого перечисления.
Ответ №6.a)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
enum CardSuit { SUIT_TREFU, SUIT_BYBNU, SUIT_CHERVU, SUIT_PIKI, MAX_SUITS }; enum CardRank { RANK_2, RANK_3, RANK_4, RANK_5, RANK_6, RANK_7, RANK_8, RANK_9, RANK_10, RANK_VALET, RANK_DAMA, RANK_KOROL, RANK_TYZ, MAX_RANKS }; |
b) Каждая карта должна быть представлена структурой Card
, в которой хранится информация о достоинстве и масти карты (например, 4 бубны, король трефы). Создайте эту структуру.
Ответ №6.b)
1 2 3 4 5 |
struct Card { CardRank rank; CardSuit suit; }; |
c) Создайте функцию printCard(), параметром которой будет константная ссылка типа структуры Card
, которая будет выводить значения достоинства и масти определенной карты в виде 2-буквенного кода (например, валет пики будет выводиться как VP
).
Ответ №6.c)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
void printCard(const Card &card) { switch (card.rank) { case RANK_2: std::cout << "2"; break; case RANK_3: std::cout << "3"; break; case RANK_4: std::cout << "4"; break; case RANK_5: std::cout << "5"; break; case RANK_6: std::cout << "6"; break; case RANK_7: std::cout << "7"; break; case RANK_8: std::cout << "8"; break; case RANK_9: std::cout << "9"; break; case RANK_10: std::cout << "T"; break; case RANK_VALET: std::cout << "V"; break; case RANK_DAMA: std::cout << "D"; break; case RANK_KOROL: std::cout << "K"; break; case RANK_TYZ: std::cout << "T"; break; } switch (card.suit) { case SUIT_TREFU: std::cout << "TR"; break; case SUIT_BYBNU: std::cout << "B"; break; case SUIT_CHERVU: std::cout << "CH"; break; case SUIT_PIKI: std::cout << "P"; break; } } |
d) Для представления целой колоды карт (52 карты) создайте массив deck
(используя std::array) и инициализируйте каждый элемент определенной картой.
Подсказка: Используйте оператор static_cast для конвертации целочисленной переменной в тип перечисления.
Ответ №6.d)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
int main() { std::array<Card, 52> deck; int card = 0; for (int suit = 0; suit < MAX_SUITS; ++suit) for (int rank = 0; rank < MAX_RANKS; ++rank) { deck[card].suit = static_cast<CardSuit>(suit); deck[card].rank = static_cast<CardRank>(rank); ++card; } return 0; } |
е) Напишите функцию printDeck(), которая в качестве параметра принимает константную ссылку на массив deck
и выводит все значения (карты). Используйте цикл foreach.
Ответ №6.e)
1 2 3 4 5 6 7 8 9 10 |
void printDeck(const std::array<Card, 52> &deck) { for (const auto &card : deck) { printCard(card); std::cout << ' '; } std::cout << '\n'; } |
f) Напишите функцию swapCard(), которая принимает две карты и меняет местами их значения.
Ответ №6.f)
1 2 3 4 5 6 |
void swapCard(Card &a, Card &b) { Card temp = a; a = b; b = temp; } |
g) Напишите функцию shuffleDeck() для перетасовки колоды карт. Для этого используйте цикл for с итерацией по массиву. Перетасовка карт должна произойти 52 раза. В цикле for выберите случайное число от 1 до 52 и вызовите swapCard(), параметрами которой будут текущая карта и карта, выбранная случайным образом. Добавьте в функцию main() возможность перетасовки и вывода уже обновленной (перетасованной) колоды карт.
Подсказки:
Для генерации случайных чисел смотрите урок №71.
Не забудьте в начале функции main() вызвать функцию srand().
Если вы используете Visual Studio, то не забудьте перед генерацией случайного числа вызвать один раз функцию rand().
Ответ №6.g)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
#include <ctime> // для time() #include <cstdlib> // для rand() и srand() // Генерируем случайное число между min и max (предполагается, что функция srand() уже была вызвана) int getRandomNumber(int min, int max) { static const double fraction = 1.0 / (static_cast<double>(RAND_MAX) + 1.0); // Равномерно распределяем генерацию случайного числа в диапазоне значений return static_cast<int>(rand() * fraction * (max - min + 1) + min); } void shuffleDeck(std::array<Card, 52> &deck) { // Перебираем каждую карту в колоде for (int index = 0; index < 52; ++index) { // Выбираем любую случайную карту int swapIndex = getRandomNumber(0, 51); // Меняем местами с нашей текущей картой swapCard(deck[index], deck[swapIndex]); } } int main() { srand(static_cast<unsigned int>(time(0))); // устанавливаем значение системных часов в качестве стартового числа rand(); // если используете Visual Studio, сбрасываем первое сгенерированное рандомное число std::array<Card, 52> deck; // Можно было бы вручную (по отдельности) инициализировать каждую карту, но мы ведь программисты! Цикл нам в помощь! int card = 0; for (int suit = 0; suit < MAX_SUITS; ++suit) for (int rank = 0; rank < MAX_RANKS; ++rank) { deck[card].suit = static_cast<CardSuit>(suit); deck[card].rank = static_cast<CardRank>(rank); ++card; } printDeck(deck); shuffleDeck(deck); printDeck(deck); return 0; } |
h) Напишите функцию getCardValue(), которая возвращает значение карты (например, 2 значит 2, 3 значит 3 и т.д., 10, валет, королева или король — это 10, туз — это 11).
Ответ №6.h)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
int getCardValue(const Card &card) { switch (card.rank) { case RANK_2: return 2; case RANK_3: return 3; case RANK_4: return 4; case RANK_5: return 5; case RANK_6: return 6; case RANK_7: return 7; case RANK_8: return 8; case RANK_9: return 9; case RANK_10: return 10; case RANK_VALET: return 10; case RANK_DAMA: return 10; case RANK_KOROL: return 10; case RANK_TYZ: return 11; } return 0; } |
Задание №7
Хорошо, настало время для серьезной игры! Давайте напишем упрощенную версию известной карточной игры «Blackjack» (русский аналог «Очко» или «21 очко»). Если вы не знакомы с этой игрой и её правилами, то вот ссылка на статью в Википедии о «Блэкджек».
Правила нашей версии «Blackjack» следующие:
вначале дилер получает одну карту (в реальной жизни, дилер получает две карты, но одна лицевой стороной вниз, поэтому на данном этапе это не имеет значения);
затем игрок получает две карты;
игрок начинает;
игрок может либо «взять» (hit
), либо «удержаться» (stand
);
если игрок «удержался», то его ход завершен, и его результат подсчитывается на основе карт, которые у него есть;
если игрок «берет», то он получает вторую карту, и значение этой карты добавляется к его уже имеющемуся результату;
туз обычно считается как 1 или как 11. Чтобы было проще, мы будем считать его как 11;
если у игрока в результате получается больше 21, то он проиграл;
ход дилера выполняется после хода игрока;
дилер берет карты до тех пор, пока его общий результат не достигнет 17 или более очков. Как только этот предел достигнут — дилер карт уже не берет;
если у дилера больше 21-го, то дилер проиграл, а игрок победил;
если же у дилера и у игрока до 21 очка, то выигрывает тот, у кого результат больше.
В нашей упрощенной версии «Blackjack» мы не будем отслеживать, какие конкретно карты были у игрока, а какие у дилера. Мы будем отслеживать только сумму значений карт, которые они получили. Так будет проще.
Начнем с кода, который у нас получился в задании №6. Создайте функцию playBlackjack(), которая возвращает true
, если игрок побеждает, и false
— если он проигрывает. Эта функция должна:
Принимать перетасованную колоду карт (deck
) в качестве параметра.
Инициализировать указатель на первую карту (имя указателя — cardPtr
). Это будет использоваться для раздачи карт из колоды.
Иметь две целочисленные переменные для хранения результата игрока и дилера.
Соответствовать правилам, приведенным выше.
Подсказка: Самый простой способ раздачи карт из колоды — это заставить указатель указывать на следующую карту в колоде (которая будет раздаваться). Всякий раз, когда нам нужно будет раздать карту, мы получаем значение текущей карты, а затем заставляем указатель указывать на следующую карту. Это можно сделать в одной строке кода:
1 |
getCardValue(*cardPtr++); |
Здесь возвращается значение текущей карты (которое затем может быть добавлено к общему результату игрока или дилера) и указатель cardPtr
переходит на следующую карту.
Протестируйте выполнение одиночной игры «Блэкджек» в функции main().
Ответ №7
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
#include <iostream> #include <array> #include <ctime> // для time() #include <cstdlib> // для rand() и srand() enum CardSuit { SUIT_TREFU, SUIT_BYBNU, SUIT_CHERVU, SUIT_PIKI, MAX_SUITS }; enum CardRank { RANK_2, RANK_3, RANK_4, RANK_5, RANK_6, RANK_7, RANK_8, RANK_9, RANK_10, RANK_VALET, RANK_DAMA, RANK_KOROL, RANK_TYZ, MAX_RANKS }; struct Card { CardRank rank; CardSuit suit; }; void printCard(const Card &card) { switch (card.rank) { case RANK_2: std::cout << "2"; break; case RANK_3: std::cout << "3"; break; case RANK_4: std::cout << "4"; break; case RANK_5: std::cout << "5"; break; case RANK_6: std::cout << "6"; break; case RANK_7: std::cout << "7"; break; case RANK_8: std::cout << "8"; break; case RANK_9: std::cout << "9"; break; case RANK_10: std::cout << "T"; break; case RANK_VALET: std::cout << "V"; break; case RANK_DAMA: std::cout << "D"; break; case RANK_KOROL: std::cout << "K"; break; case RANK_TYZ: std::cout << "T"; break; } switch (card.suit) { case SUIT_TREFU: std::cout << "TR"; break; case SUIT_BYBNU: std::cout << "B"; break; case SUIT_CHERVU: std::cout << "CH"; break; case SUIT_PIKI: std::cout << "P"; break; } } void printDeck(const std::array<Card, 52> &deck) { for (const auto &card : deck) { printCard(card); std::cout << ' '; } std::cout << '\n'; } void swapCard(Card &a, Card &b) { Card temp = a; a = b; b = temp; } // Генерируем случайное число между min и max (предполагается, что функция srand() уже была вызвана) int getRandomNumber(int min, int max) { static const double fraction = 1.0 / (static_cast<double>(RAND_MAX) + 1.0); // Равномерно распределяем генерацию случайного числа в диапазоне значений return static_cast<int>(rand() * fraction * (max - min + 1) + min); } void shuffleDeck(std::array<Card, 52> &deck) { // Перебираем каждую карту в колоде for (int index = 0; index < 52; ++index) { // Выбираем любую случайную карту int swapIndex = getRandomNumber(0, 51); // Меняем местами с нашей текущей картой swapCard(deck[index], deck[swapIndex]); } } int getCardValue(const Card &card) { switch (card.rank) { case RANK_2: return 2; case RANK_3: return 3; case RANK_4: return 4; case RANK_5: return 5; case RANK_6: return 6; case RANK_7: return 7; case RANK_8: return 8; case RANK_9: return 9; case RANK_10: return 10; case RANK_VALET: return 10; case RANK_DAMA: return 10; case RANK_KOROL: return 10; case RANK_TYZ: return 11; } return 0; } char getPlayerChoice() { std::cout << "(h) to hit, or (s) to stand: "; char choice; do { std::cin >> choice; } while (choice != 'h' && choice != 's'); return choice; } bool playBlackjack(const std::array<Card, 52> &deck) { // Настраиваем стартовый режим игры const Card *cardPtr = &deck[0]; int playerTotal = 0; int dealerTotal = 0; // Дилер получает одну карту dealerTotal += getCardValue(*cardPtr++); std::cout << "The dealer is showing: " << dealerTotal << '\n'; // Игрок получает две карты playerTotal += getCardValue(*cardPtr++); playerTotal += getCardValue(*cardPtr++); // Игрок начинает while (1) { std::cout << "You have: " << playerTotal << '\n'; // Смотрим, не больше ли 21 очка у игрока if (playerTotal > 21) return false; char choice = getPlayerChoice(); if (choice == 's') break; playerTotal += getCardValue(*cardPtr++); } // Если игрок не проиграл и у него не больше 21 очка, то тогда дилер получает карты до тех пор, пока у него не получится в сумме 17 очков while (dealerTotal < 17) { dealerTotal += getCardValue(*cardPtr++); std::cout << "The dealer now has: " << dealerTotal << '\n'; } // Если у дилера больше 21 очка, то игрок победил if (dealerTotal > 21) return true; return (playerTotal > dealerTotal); } int main() { srand(static_cast<unsigned int>(time(0))); // устанавливаем значение системных часов в качестве стартового числа rand(); // если используете Visual Studio, сбрасываем первое сгенерированное рандомное число std::array<Card, 52> deck; // Можно было бы вручную (по отдельности) инициализировать каждую карту, но мы ведь программисты! Цикл нам в помощь! int card = 0; for (int suit = 0; suit < MAX_SUITS; ++suit) for (int rank = 0; rank < MAX_RANKS; ++rank) { deck[card].suit = static_cast<CardSuit>(suit); deck[card].rank = static_cast<CardRank>(rank); ++card; } shuffleDeck(deck); if (playBlackjack(deck)) std::cout << "You win!\n"; else std::cout << "You lose!\n"; return 0; } |
Дополнительные задания
a) Время для критического мышления. Опишите, как бы вы могли модифицировать программу, приведенную выше, для обработки случаев, когда стоимость тузов может равняться 1 очку или 11 очкам.
Ответ a)
Можно было бы отслеживать, сколько тузов игрок и дилер получили в отдельной целочисленной переменной-счетчике. Если у игрока или дилера результат превысил 21 очко, и их счетчик тузов больше нуля, то тогда уменьшается результат игрока или дилера на 10 (конвертируем туз из 11 очков в 1) и удаляется 1 из счетчика тузов. Продолжаться это будет до тех пор, пока счетчик тузов не достигнет нуля.
b) В реальном «Блэкджек», если у игрока и дилера равное количество очков, то результатом является ничья, и ни один из них не выигрывает. Опишите, как бы вы изменили программу, приведенную выше, для учета такого исхода игры.
Ответ b)
Функция playBlackjack() сейчас возвращает true
, если игрок выигрывает, и false
— если проигрывает. Нужно обновить эту функцию, чтобы было три возможных исхода: победа дилера, победа игрока или ничья. Лучший способ это сделать — создать перечисление для этих 3 вариантов, и чтобы функция возвращала соответствующее значение из этого перечисления:
1 2 3 4 5 6 7 8 |
enum BlackjackResult { BLACKJACK_PLAYER_WIN, BLACKJACK_DEALER_WIN, BLACKJACK_NICHIA }; BlackjackResult playBlackjack(const std::array<Card, 52> &deck); |
Здравствуйте, подскажите почему мы пишем так:
А не так:
То что вы написали, это ссылка на одну структуру Card, а нам нужен контейнер (фиксированный массив) из 52 структур Card
Здравствуйте, может кто подскажет почему этот код не компилируется, в чём ошибка?
Компилятор — Code:Blocks. Тест ошибки: conversion to 'std::array<Card, 52>::size_type' {aka 'long long unsigned int'} from 'int' may change the sign of the result [-Werror=sign-conversion].
Если поменять тип данных переменной x на long long unsigned int, то всё работает, но совершенно неясно почему так. Такое происходит только с std::array. Как получить доступ к элементу массива используя int?
блэк джек
1 задание:
А у вас в колоде 8 тузов :о)
Хотя конечно мы этого не узнаем, потому что карты "на руках" не показали, а только набранные очки. Но по очкам зато всё честно!
И для любителей математики ещё один вариант инициализации массива карт:
Эх.. Забыл поблагодарить. Спасибо, Юрий!
Хотел пропустить тест, но решил всё же порешать задачи. Всем кто будет проходить курс — рекомендую выполнить.
К стати. Попробовал задать игроку стратегию диллера, для автоматизации запуска игр:
Запускал несколько раз тест на 2 млн.игр. Результат минусовый.
Не играйте в Блэкждек! Продуете.
Еще раз спасибо!
p.s. Хотя стратегия игры гораздо шире чем брать или не брать. Надо бы ещё анализировать какая первая карта у диллера. И если играет несколько человек, то какие карты у них. К тому же в deck может быть не одна а четыре колоды!
задание 3.
хочу выложить чтоб показать, что еще есть люди которые интересуются данным проектом и радуются небольшим успехам 🙂
Капец, с блэкджеком вроде и просто, а вроде и замудренно. Мозг не может порой понять, начинает загоняться, что всё, не мое, но не сдаюсь :]
Добрый день.
Подскажите, пож-ста, почему во многих перечислениях последний элементом является MAX_ITEMS, как здесь?
Видимо я что то упустил.
Почему нельзя использовать перечисление без последнего элемента?
Для удобства же. Ответ №1 посмотри внимательнее где и когда используется MAX_ITEMS (// нам здесь не нужно передавать длину массива, так как она уже указана членом MAX_ITEMS перечисления ItemTypes)
Не понял зачем отслеживать количество тузов ? если мы не шулеры 🙂 то количество их в колоде постоянно.
Сам туз определяеделяется по значению (11) т.е. нам даже не нужно менять его значение, просто правильно считать при приходе туза в руку:
не понял зачем искать тузы и как-то их учитывать, номинала вполне достаточно
Вот интересно. Как я понял имя массива является указателем на его первый элемент, так ведь?
Но почему нельзя использовать логику указателей?
Предположим что компилятору неизвестно сколько байт занимает элемент этого массива и он не может перейти таким образом к другому элементу массива.
Но тогда, почему работает оператор индексации?
Я нашел в чем проблема, все дело в приоритете операций.
Кому интересно, написал игру с азартом, настоящий симулятор, как в жизни))