Вітаю вас з подоланням найдовшого розділу цього туторіалу. Якщо у вас не було попереднього досвіду в програмуванні, то цей розділ, скоріш за все, був для вас найскладнішим з усіх попередніх. Однак, якщо ви дійшли до цього моменту, то все добре — ви впоралися! Так тримати!
Гарна новина полягає в тому, що наступний розділ буде легшим, і дуже скоро ми доберемося до самого серця цього туторіалу — об’єктно-орієнтованого програмування!
Теорія
Масиви дозволяють зберігати і отримувати доступ до багатьох змінних одного і того ж типу даних через один ідентифікатор. Доступ до елементів масиву здійснюється за допомогою оператора індексу []
. Будьте обережні з діапазоном масиву, не допускайте індексації елементів поза діапазоном. Масиви можна ініціалізувати за допомогою списку ініціалізаторів або 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
Підказка: Ви можете змінити алгоритм сортування масиву методом вибору для сортування вашого динамічного масиву. Якщо ви напишете сортування масиву окремою функцією, то масив повинен передаватися по адресу (як вказівник).
Відповідь №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() можливість перетасовки і виведення вже оновленої (перетасованої) колоди карт.
Підказки:
Для генерації випадкових чисел дивіться урок №74.
Не забудьте в початку функції 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 очко»). Якщо ви не знайомі з цією грою і її правилами, то ось посилання на статтю у Вікіпедії про Блекджек.
Правила нашої версії гри “Блекджек” наступні:
спочатку дилер отримує одну карту (в реальності дилер отримує дві карти, але одна лицьовою стороною вниз, тому на даному етапі це не має значення);
потім гравець отримує дві карти;
гравець починає;
гравець може або “взяти” (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); |