By nascardriver on December 19th, 2019 | last modified by nascardriver on January 3rd, 2020
Words of encouragement
Congratulations on reaching the end of the longest chapter in the tutorials! Unless you have previous programming experience, this chapter was probably the most challenging one so far. If you made it this far, you’re doing great!
The good news is that the next chapter is easy in comparison. And in the chapter beyond that, we reach the heart of the tutorials: Object-oriented programming!
Chapter summary
Arrays allow us to store and access many variables of the same type through a single identifier. Array elements can be accessed using the subscript operator ([]
). Be careful not to index an array out of the array’s range. Arrays can be initialized using an initializer list or uniform initialization.
Fixed arrays must have a length that is set at compile time. Fixed arrays will usually decay into a pointer when evaluated or passed to a function.
Loops can be used to iterate through an array. Beware of off-by-one errors, so you don’t iterate off the end of your array. Range-based for-loops are useful when the array hasn’t decayed into a pointer.
Arrays can be made multidimensional by using multiple indices.
Arrays can be used to do C-style strings. You should generally avoid these and use std::string_view
and std::string
instead.
Pointers are variables that store the memory address of (point at) another variable. The address-of operator (&
) can be used to get the address of a variable. The dereference operator (*
) can be used to get the value that a pointer points at.
A null pointer is a pointer that is not pointing at anything. Pointers can be made null by initializing or assigning the value nullptr
(before in C++11, 0
) to them. Avoid the NULL
macro. Dereferencing a null pointer can cause bad things to happen. Deleting a null pointer is okay (it doesn’t do anything).
A pointer to an array doesn’t know how large the array it is pointing to is. This means sizeof()
and range-based for-loops won’t work.
The new
and delete
operators can be used to dynamically allocate memory for a pointer variable or array. Although it’s unlikely to happen, operator new
can fail if the operating system runs out of memory. If you’re writing software for a memory-limited system, make sure to check if new
was successful.
Make sure to use the array delete (delete[]
) when deleting an array. Pointers pointing to deallocated memory are called dangling pointers. Using the wrong delete
, or dereferencing a dangling pointer causes undefined behavior.
Failing to delete dynamically allocated memory can result in memory leaks when the last pointer to that memory goes out of scope.
Normal variables are allocated from limited memory called the stack. Dynamically allocated variables are allocated from a general pool of memory called the heap.
A pointer to a const
value treats the value it is pointing to as const
.
|
int value{ 5 }; const int *ptr{ &value }; // this is okay, ptr is pointing to a "const int" |
A const
pointer is a pointer whose value can not be changed after initialization.
|
int value{ 5 }; int *const ptr{ &value }; // ptr is const, but *ptr is non-const |
A reference is an alias to another variable. References are declared using an ampersand (&
const -- they must be initialized with a value, and a new value can not be assigned to them. References can be used to prevent copies from being made when passing data to or from a function.
The member selection operator (->
) can be used to select a member from a pointer to a struct. It combines both a dereference and normal member access (.
).
Void pointers are pointers that can point to any type of data. They can not be dereferenced directly. You can use static_cast
to convert them back to their original pointer type. It’s up to you to remember what type they originally were.
Pointers to pointers allow us to create a pointer that points to another pointer.
std::array
provides all of the functionality of C++ built-in arrays (and more) in a form that won’t decay into a pointer. These should generally be preferred over built-in fixed arrays.
std::vector
provides dynamic array functionality that handles its own memory management and remember their size. These should generally be favored over built-in dynamic arrays.
Quiz time
To make the quizzes a little easier, we have to introduce a couple of new algorithms.
std::reduce
applies a function, by default the +
operator, to all elements in a list, resulting in a single value. When we use the +
operator, the result is the sum of all elements in the list. Note that there’s also std::accumulate
. std::accumulate
cannot be parallelized, because it applies the function left-to-right. std::reduce
segments the list, which means that the function is applied in an unknown order, allowing the operation to be parallelized. If we want to sum up a list, we don’t care about the order and we use std::reduce
.
std::reduce
is currently not fully implemented in all major standard libraries. If it doesn’t work for you, fall back to std::accumulate
.
std::shuffle
takes a list and randomly re-orders its elements.
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
|
#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 // If you can't use std::reduce, use std::accumulate. The 0 is the initial value // of the result: 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; } |
Possible output
10
10
2 1 4 3
Pretend you’re writing a game where the player can hold 3 types of items: health potions, torches, and arrows. Create an
enum
to identify the different types of items, and an
std::array
to store the number of each item the player is carrying (The enumerators are used as indexes of the array). The player should start with 2 health potions, 5 torches, and 10 arrows. Write a function called
countTotalItems()
that returns how many items the player has in total. Have your
main()
function print the output of
countTotalItems()
as well as the number of torches.
Show Solution
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
|
#include <array> #include <numeric> // std::reduce #include <iostream> // We want to use ItemTypes to index an array. Use enum rather than enum class. enum ItemTypes { ITEM_HEALTH_POTION, ITEM_TORCH, ITEM_ARROW, MAX_ITEMS }; using inventory_type = std::array<int, ItemTypes::MAX_ITEMS>; int countTotalItems(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"; // We can access individual items using the enumerators: std::cout << "The player has " << items[ItemTypes::ITEM_TORCH] << " torch(es)\n"; return 0; } |
Write the following program: Create a
struct
that holds a student’s first name and grade (on a scale of 0-100). Ask the user how many students they want to enter. Create a
std::vector
to hold all of the students. Then prompt the user for each name and grade. Once the user has entered all the names and grade pairs, sort the list by grade (highest first). Then print all the names and grades in sorted order.
For the following input:
Joe
82
Terry
73
Ralph
4
Alex
94
Mark
88
The output should look like this:
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
You can assume that names don’t contain spaces and that that input extraction doesn’t fail.
Show Solution
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
|
#include <algorithm> // std::sort #include <cstddef> // std::size_t #include <iostream> #include <string> 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() }; // Create a vector with numberOfStudents elements. 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; } // Pass by reference to avoid slow copies. 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); // Print out all the names for (const auto& student : students) { std::cout << student.name << " got a grade of " << student.grade << '\n'; } return 0; } |
Write your own function to swap the value of two integer variables. Write a
main()
function to test it.
Show Hint
Use reference parameters
|
void swap(int &a, int &b) |
Show Solution
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
|
#include <iostream> // Use reference parameters so we can modify the values of the arguments passed in void swap(int& a, int& b) { // Temporarily save value of a int temp{ a }; // Put value of b in a a = b; // Put previous value of a in 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; } |
Write a function to print a C-style string character by character. Use a pointer to step through each character of the string and print that character. Stop when you hit the null terminator. Write a
main
function that tests the function with the string literal “Hello, world!”.
Show Hint
Use the ++ operator to advance the pointer to the next character.
|
const char* str{ "Hello, world!" }; std::cout << *str; // H ++str; std::cout << *str; // e |
Show Solution
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> // str will point to the first letter of the C-style string. // Note that str points to a const char, so we can not change the values it points to. // However, we can point str at something else. This does <string>not</strong> change the value of the argument. void printCString(const char* str) { // While we haven't encountered a null terminator while (*str != '\0') { // print the current character std::cout << *str; // and point str at the next character ++str; } } int main() { printCString("Hello world!"); std::cout << '\n'; return 0; } |
What’s wrong with each of these snippets, and how would you fix it?
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; } |
Show Solution
The loop has an off-by-one error, and tries to access the array element with index 4, which does not exist. The conditional in the for loop should use < instead of <=.
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; } |
Show Solution
ptr
is a pointer to a const int
. You can’t assign the value 6 to it. You can fix this by making ptr
non-const.
c)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
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; } |
Show Solution
array
decays to a pointer when it is passed to printArray()
. Range-based for-loops can’t work with a pointer to an array because the size of the array isn’t known. One solution is to add a length parameter to function printArray()
, and use a normal for loop. A better solution is to use std::array
instead of built-in fixed arrays.
d)
|
int* allocateArray(const int length) { int temp[length]{}; return temp; } |
Show Solution
temp
is a fixed array, but length
is not a compile-time constant, so we can’t use length
to create a C-style array. Variable temp
will also go out of scope at the end of the function, the return value will be pointing to something invalid. temp
should use dynamic memory allocation or be a std::vector
.
e)
|
int main() { double d{ 5.5 }; int* ptr{ &d }; std::cout << ptr << '\n'; return 0; } |
Show Solution
You can’t make an int
pointer point at a non-int
variable. ptr
should be of type double*
.
Let’s pretend we’re writing a card game.
a) A deck of cards has 52 unique cards (13 card ranks of 4 suits). Create enumerations for the card ranks (2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen, King, Ace) and suits (clubs, diamonds, hearts, spades). Those enumerators will not be used to index arrays.
Show Solution
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
|
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) Each card will be represented by a struct
named Card
that contains a rank and a suit. Create the struct
.
Show Solution
|
struct Card { CardRank rank{}; CardSuit suit{}; }; |
c) Create a printCard()
function that takes a const Card
reference as a parameter and prints the card rank and value as a 2-letter code (e.g. the jack of spades would print as JS).
Show Hint
Use a switch
-statement.
Show Solution
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
|
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) A deck of cards has 52 cards. Create an array (using std::array
) to represent the deck of cards, and initialize it with one of each card. Do this in a function named createDeck
and call createDeck
from main
. createDeck
should return the deck to main
.
Hint: Use static_cast if you need to convert an integer into an enumerated type.
Show Solution
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
|
// We'll need these many more times, create an aliases. using deck_type = std::array<Card, 52>; using index_type = deck_type::size_type; deck_type createDeck() { deck_type deck{}; // We could initialize each card individually, but that would be a pain. Let's use a loop. index_type card{ 0 }; auto suits{ static_cast<index_type>(CardSuit::MAX_SUITS) }; auto ranks{ static_cast<index_type>(CardRank::MAX_RANKS) }; for (index_type suit{ 0 }; suit < suits; ++suit) { for (index_type 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) Write a function named printDeck()
that takes the deck as a const
reference parameter and prints the cards in the deck. Use a range-based for-loop. When you can printDeck
with the deck you generated in the previous task, the output should be
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
If you used different characters, that’s fine too.
Show Solution
|
void printDeck(const deck_type& deck) { for (const auto& card : deck) { printCard(card); std::cout << ' '; } std::cout << '\n'; } |
f) Write a function named shuffleDeck
to shuffle the deck of cards using std::shuffle
. Update your main function to shuffle the deck and print out the shuffled deck.
Reminder: Only seed your random number generator once.
Show Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
#include <algorithm> // for std::shuffle #include <ctime> // for std::time // ... void shuffleDeck(deck_type& deck) { // mt is static so it only gets seeded once. 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) Write a function named getCardValue()
that returns the value of a Card
(e.g. a 2 is worth 2, a ten, jack, queen, or king is worth 10. Assume an Ace is worth 11).
Show Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
int getCardValue(const Card& card) { // Handle rank 2 to 10. We could do this in the switch, but it would be long. if (card.rank <= CardRank::RANK_10) { // RANK_2 is 0 (value 2) // RANK_3 is 1 (value 3) // etc. 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; } // Should never happen. If it does, we have a bug in our code. return 0; } |
a) Alright, challenge time! Let’s write a simplified version of Blackjack. If you’re not already familiar with Blackjack, the Wikipedia article for
Blackjack has a summary.
Here are the rules for our version of Blackjack:
- The dealer gets one card to start (in real life, the dealer gets two, but one is face down so it doesn’t matter at this point).
- The player gets two cards to start.
- The player goes first.
- A player can repeatedly “hit” or “stand”.
- If the player “stands”, their turn is over, and their score is calculated based on the cards they have been dealt.
- If the player “hits”, they get another card and the value of that card is added to their total score.
- An ace normally counts as a 1 or an 11 (whichever is better for the total score). For simplicity, we’ll count it as an 11 here.
- If the player goes over a score of 21, they bust and lose immediately.
- The dealer goes after the player.
- The dealer repeatedly draws until they reach a score of 17 or more, at which point they stand.
- If the dealer goes over a score of 21, they bust and the player wins immediately.
- Otherwise, if the player has a higher score than the dealer, the player wins. Otherwise, the player loses (we’ll consider ties as dealer wins for simplicity).
In our simplified version of Blackjack, we’re not going to keep track of which specific cards the player and the dealer have been dealt. We’ll only track the sum of the values of the cards they have been dealt for the player and dealer. This keeps things simpler.
Start with the code you wrote in quiz #6. Create a function named playBlackjack()
. This function should:
- Accept a shuffled deck of cards as a parameter.
- Implement Blackjack as defined above.
- Returns
true
if the player won, and false
if they lost.
Also write a main()
function to play a single game of Blackjack.
Show Solution
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 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
|
#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; // Maximum score before losing. constexpr int maximumScore{ 21 }; // Minium score that the dealer has to have. 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) { // Handle rank 2 - 10. We could do this in the switch, but it would be long. if (card.rank <= CardRank::RANK_10) { 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: // Should never happen. If it does, we have a bug in our code. 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<index_type>(CardSuit::MAX_SUITS) }; auto ranks{ static_cast<index_type>(CardRank::MAX_RANKS) }; for (index_type suit{ 0 }; suit < suits; ++suit) { for (index_type 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); } bool playerWantsHit() { while (true) { std::cout << "(h) to hit, or (s) to stand: "; char ch{}; std::cin >> ch; switch (ch) { case 'h': return true; case 's': return false; } } } // Returns true if the player went bust. False otherwise. bool playerTurn(const deck_type& deck, index_type& nextCardIndex, Player& player) { while (true) { std::cout << "You have: " << player.score << '\n'; if (player.score > maximumScore) { // This can happen even before the player had a choice if they drew 2 // aces. return true; } else { if (playerWantsHit()) { player.score += getCardValue(deck[nextCardIndex++]); } else { // The player didn't go bust. return false; } } } } // Returns true if the dealer went bust. False otherwise. bool dealerTurn(const deck_type& deck, index_type& nextCardIndex, Player& dealer) { // Draw cards until we reach the minimum value. while (dealer.score < minimumDealerScore) { dealer.score += getCardValue(deck[nextCardIndex++]); } // If the dealer's score is too high, they went bust. return (dealer.score > maximumScore); } bool playBlackjack(const deck_type& deck) { // Index of the card that will be drawn next. This cannot overrun // the array, because a player will lose before all cards are used up. index_type nextCardIndex{ 0 }; // Create the dealer and give them 1 card. Player dealer{ getCardValue(deck[nextCardIndex++]) }; // The dealer's card is face up, the player can see it. std::cout << "The dealer is showing: " << dealer.score << '\n'; // Create the player and give them 2 cards. Player player{ getCardValue(deck[nextCardIndex]) + getCardValue(deck[nextCardIndex + 1]) }; nextCardIndex += 2; if (playerTurn(deck, nextCardIndex, player)) { // The player went bust. return false; } if (dealerTurn(deck, nextCardIndex, dealer)) { // The dealer went bust, the player wins. 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; } |
b) Extra credit: Critical thinking time: Describe how you could modify the above program to handle the case where aces can be equal to 1 or 11.
It’s important to note that we’re only keeping track of the sum of the cards, not which specific cards the user has.
Show Solution
One way would be to keep track of how many aces the player and the dealer got dealt (In the Player
struct
, as an integer). If either the player or dealer go over 21 and their ace counter is greater than zero, you can reduce their score by 10 (convert an ace from 11 points to 1 point) and “remove” one from the ace counter. This can be done as many times as needed until the ace counter reaches zero.
c) In actual blackjack, if the player and dealer have the same score (and the player has not gone bust), the result is a tie and neither wins. Describe how you’d modify the above program to account for this.
Show Solution
playBlackjack()
currently returns
true
if the player wins and
false
otherwise. We’ll need to update this function to return three possibilities: Dealer win, Player win, tie. The best way to do this would be to define an enumeration for these three options, and have the function return the appropriate enumerator:
|
enum class BlackjackResult { BLACKJACK_PLAYER_WIN, BLACKJACK_DEALER_WIN, BLACKJACK_TIE }; BlackjackResult playBlackjack(const deck_type& deck); |
Leave a Reply