Search

8.x — Chapter 8 comprehensive quiz

In this chapter, we explored the meat of C++ -- object-oriented programming! This is the most important chapter in the tutorial series.

Chapter summary

Classes allow you to create your own data types that bundle both data and functions that work on that data. Data and functions inside the class are called members. Members of the class are selected by using the . operator (or -> if you’re accessing the member through a pointer).

Access specifiers allow you to specify who can access the members of a class. Public members can be accessed directly by anybody. Private members can only be accessed by other members of the class. We’ll cover protected members later, when we get to inheritance. By default, all members of a class are private and all members of a struct are public.

Encapsulation is the process of making all of your member data private, so it can not be accessed directly. This helps protect your class from misuse.

Constructors are a special type of member function that allow you to initialize objects of your class. A constructor that takes no parameters (or has all default parameters) is called a default constructor. The default constructor is used if no initialization values are provided by the user. You should always provide at least one constructor for your classes.

Member initializer lists allows you to initialize your member variables from within a constructor (rather than assigning the member variables values).

In C++11, non-static member initialization allows you to directly specify default values for member variables when they are declared.

Prior to C++11, constructors should not call other constructors (it will compile, but will not work as you expect). In C++11, constructors are allowed to call other constructors (called delegating constructors, or constructor chaining).

Destructors are another type of special member function that allow your class to clean up after itself. Any kind of deallocation or shutdown routines should be executed from here.

All member functions have a hidden *this pointer that points at the class object being modified. Most of the time you will not need to access this pointer directly. But you can if you need to.

It is good programming style to put your class definitions in a header file of the same name as the class, and define your class functions in a .cpp file of the same name as the class. This also helps avoid circular dependencies.

Member functions can (and should) be made const if they do not modify the state of the class. Const class objects can only call const member functions.

Static member variables are shared among all objects of the class. Although they can be accessed from a class object, they can also be accessed directly via the scope resolution operator.

Similarly, static member functions are member functions that have no *this pointer. They can only access static member variables.

Friend functions are functions that are treated like member functions of the class (and thus can access a class’s private data directly). Friend classes are classes where all members of the class are considered friend functions.

It’s possible to create anonymous class objects for the purpose of evaluation in an expression, or passing or returning a value.

You can also nest types within a class. This is often used with enums related to the class, but can be done with other types (including other classes) if desired.

Quiz time

1a) Write a class named Point2d. Point2d should contain two member variables of type double: m_x, and m_y, both defaulted to 0.0. Provide a constructor and a print function.

The following program should run:

This should print:

Point2d(0, 0)
Point2d(3, 4)

Show Solution

1b) Now add a member function named distanceTo that takes another Point2d as a parameter, and calculates the distance between them. Given two points (x1, y1) and (x2, y2), the distance between them can be calculated as sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)). The sqrt function lives in header cmath.

The following program should run:

This should print:

Point2d(0, 0)
Point2d(3, 4)
Distance between two points: 5

Show Solution

1c) Change function distanceTo from a member function to a non-member friend function that takes two Points as parameters. Also rename it “distanceFrom”.

The following program should run:

This should print:

Point2d(0, 0)
Point2d(3, 4)
Distance between two points: 5

Show Solution

2) Write a destructor for this class:

Show Solution

3) Let’s create a random monster generator. This one should be fun.

3a) First, let’s create an enumeration of monster types named MonsterType. Include the following monster types: Dragon, Goblin, Ogre, Orc, Skeleton, Troll, Vampire, and Zombie. Add an additional MAX_MONSTER_TYPES enum so we can count how many enumerators there are.

Show Solution

3b) Now, let’s create our Monster class. Our Monster will have 4 attributes (member variables): a type (MonsterType), a name (std::string), a roar (std::string), and the number of hit points (int). Create a Monster class that has these 4 member variables.

Show Solution

3c) enum MonsterType is specific to Monster, so move the enum inside the class as a public declaration.

Show Solution

3d) Create a constructor that allows you to initialize all of the member variables.

The following program should compile:

Show Solution

3e) Now we want to be able to print our monster so we can validate it’s correct. To do that, we’re going to need to write a function that converts a MonsterType into a std::string. Write that function (called getTypeString()), as well as a print() member function.

The following program should compile:

and print:

Bones the skeleton has 4 hit points and says *rattle*

Show Solution

3f) Now we can create a random monster generator. Let’s consider how our MonsterGenerator class will work. Ideally, we’ll ask it to give us a Monster, and it will create a random one for us. We don’t need more than one MonsterGenerator. This is a good candidate for a static class (one in which all functions are static). Create a static MonsterGenerator class. Create a static function named generateMonster(). This should return a Monster. For now, make it return anonymous Monster(Monster::SKELETON, “Bones”, “*rattle*”, 4);

The following program should compile:

and print:

Bones the skeleton has 4 hit points and says *rattle*

Show Solution

3g) Now, MonsterGenerator needs to generate some random attributes. To do that, we’ll need to make use of this handy function:

However, because MonsterGenerator relies directly on this function, let’s put it inside the class, as a static function.

Show Solution

3h) Now edit function generateMonster() to generate a random MonsterType (between 0 and Monster::MAX_MONSTER_TYPES-1) and a random hit points (between 1 and 100). This should be fairly straightforward. Once you’ve done that, define two static fixed arrays of size 6 inside the function (named s_names and s_roars) and initialize them with 6 names and 6 sounds of your choice. Pick a random name from these arrays.

The following program should compile:

Show Solution

3i) Why did we declare variables s_names and s_roars as static?

Show Solution

4) Okay, time for that game face again. This one is going to be a challenge. Let’s rewrite the Blackjack games we wrote in chapter 6 using classes! Here’s the full code without classes:

Holy moly! Where do we even begin? Don’t worry, we can do this, but we’ll need a strategy here. This Blackjack program is really composed of four parts: the logic that deals with cards, the logic that deals with the deck of cards, the logic that deals with dealing cards from the deck, and the game logic. Our strategy will be to work on each of these pieces individually, testing each part with a small test program as we go. That way, instead of trying to convert the entire program in one go, we can do it in 4 testable parts.

Start by copying the original program into your IDE, and then commenting out everything except the #include lines.

4a) Let’s start by making Card a class instead of a struct. The good news is that the Card class is pretty similar to the Monster class from the previous quiz question. First, move the enums for CardSuit, CardRank inside the card class as public definitions (they’re intrinsically related to Card, so it makes more sense for them to be inside the class, not outside). Second, create private members to hold the CardRank and CardSuit (name them m_rank and m_suit accordingly). Third, create a public constructor for the Card class so we can initialize Cards. Forth, make sure to assign default values to the parameters so this can be used as a default constructor (pick any values you like). Finally, move the printCard() and getCardValue() functions inside the class as public members (remember to make them const!).

Important note: When using a std::array (or std::vector) where the elements are a class type, your element’s class must have a default constructor so the elements can be initialized to a reasonable default state. If you do not provide one, you’ll get a cryptic error about attempting to reference a deleted function.

The following test program should compile:

Show Solution

4b) Okay, now let’s work on a Deck class. The deck needs to hold 52 cards, so use a private std::array member to create a fixed array of 52 cards named m_deck. Second, create a constructor that takes no parameters and initializes m_deck with one of each card (modify the code from the original main() function). Inside the initialization loop, create an anonymous Card object and assign it to your deck element. Third, move printDeck into the Deck class as a public member. Fourth, move getRandomNumber() and swapCard() into the Deck class as a private static members (they’re just helper functions, so they don’t need access to *this). Fifth, move shuffleDeck into the class as a public member.

Hint: The trickiest part of this step is initializing the deck using the modified code from the original main() function. The following line shows how to do that.

The following test program should compile:

Show Solution

4c) Now we need a way to keep track of which card is next to be dealt (in the original program, this is what cardptr was for). First, add a int member named m_cardIndex and initialize it to 0. Create a public member function named dealCard(), which should return a const reference to the current card and advance the index. shuffleDeck() should also be updated to reset m_cardIndex (since if you shuffle the deck, you’ll start dealing from the top of the deck again).

The following test program should compile:

Show Solution

4d) Almost there! Now, just fix up the remaining program to use the classes you wrote above. Since most of the initialization routines has been moved into the classes, you can jettison them.

Show Solution

9.1 -- Introduction to operator overloading
Index
8.16 -- Timing your code

278 comments to 8.x — Chapter 8 comprehensive quiz

  • Mike

    Hi, in the 3rd question, I have used the Mersenne Twister algorithm and I don't know if I have used it correctly or not.
    I would appreciate it if someone could check it.

    another interesting thing, I was able to compile my code in CodeBlocks even though I have accidentally dropped the keyword "int" in line 75, and I have all the warnings turned on.

    Thank you for the great lessons!

    • Hi Mike!

      * Line 98: Initialize your variables with brace initializers. You used copy initialization.
      * Line 80, 90: Initialize your variables with brace initializers. You used direct initialization.
      * Line 23, 24, 25, 26: Initialize your variables with brace initializers.
      * Line 29, 62, 78, 87, 88, 90, 91: Limit your lines to 80 characters in length for better readability on small displays.
      * Inconsistent formatting. Use your editor's auto-formatting feature.
      * @getTypeString should return a const char * or an @std::string_view to prevent creating a copy of the string with every call.
      * See my comment here ( https://www.learncpp.com/cpp-tutorial/59-random-number-generation/comment-page-5/#comment-401088 ) about @std::mt19937 construction.
      * Line 91: Magic number: 5. Use @std::size(s_names) and @std::size(s_roars).

      Your usage of the mersenne twister is correct, apart from the construction, but that's not your fault.

  • Arthur

    too a while to get it working and not sure I did things right , but it works. tried some different naming styles for variables and gave descriptive names to avoid commenting, I find a lot of comments make it harder to read, curious to know if it is readable without comment.

    still have to figure out how to separate it into the different files...

    • * Line 98: Initialize your variables with brace initializers. You used copy initialization.
      * Line 94, 109, 226, 227, 395: Initialize your variables with brace initializers.
      * Line 99, 392, 393: Use std::rand instead of its global-namespaced counterpart.
      * Line 392: Use std::srand instead of its global-namespaced counterpart.
      * Line 392: Use std::time instead of its global-namespaced counterpart.
      * @std::time wants a pointer, pass @nullptr.
      * You don't need to return from voids.
      * @getCardValue: Missing return. Enable compiler warnings.
      * Magic numbers: 51, 52.
      * @presentCard, @getDeckIndex should be const.
      * Line 189: Should be else if.
      * Line 199-201: No need for an if-statement. You can [return (yes_no == 'y');]
      * @playerPlay and @dealerPlay are almost identical. Don't repeat code.
      * Inconsistent formatting. Use your editor's auto-formatting feature.
      * @player_turn and @dealer_turn are the inverse of each other, remove one. Add a variable to indicated a game's end.
      * Line 293, 380: Unnecessary. Booleans have only 2 values.
      * You don't need to store the result of @getYesNo in a variable. You can use it directly in the if's condition.

      Your naming is better, but not consistent, eg.
      totalscore aceinhand cardface (All lower case, no delimiter)
      vs
      deal_hand player_turn card_score (All lower case, underscore delimiter)

      > curious to know if it is readable without comment
      @playGame is too long, it should be split into several functions.

      • Arthur

        thanks nascardriver , I addressed much of that , but certain parts I am not sure how to... the part about random numbers , I just try to follow the tutorial if you would point me to some reading I may know how to do what you are suggesting. inconsistent formatting , I am using code::blocks formatting feature and if I don't know what aspect is inconsistent then I cant change it...

        aceinhand & totalscore , I thought would distinguish as struct variables ? cardface and cardsuit I renamed but as one line variables don't seem particularly important in terms of descriptive names.

        @ dealerPlay and playerPlay I tried to make that as one function but couldn't figure it out that way , will have to think on that.

        @player_turn and dealer_turn I would have to run through that and see , if it saved on measuring conditions it would make sense to me otherwise I think it would make the logic harder to follow because "if (dealer_turn)" is clear , where "if (!player_turn)" a little less so , there is also times in game where both are false so they are not always inverse, it was part of writing the code to respond to silly player choices and resulted from me testing by doing things like repeatedly standing or repeatedly hitting then looking at the stream of printed game play for what did not look like real black jack play on the part of the dealer...

        I was thinking that the printed output @playGame could be turned into a function with perhaps a code for which output to print , but if I moved other aspects would I not be creating spaghetti logic? if goto statements are bad because they make the code path hard to follow , shouldn't the logic flow be treated in the same way? thats just my thinking and I'm just learning so could be wrong on that...

        • > random numbers

          > inconsistent formatting
          auto-format should not produce inconsistently formatted code, no matter how it's configured. I don't have code::blocks, I can't test. Your code doesn't look like it was auto-formatted. A few things i spotted on a quick glance:
          Line 204: No space after "if"
          Line 201: conditional code on the same line
          Line 202: Indention
          Line 246: No space after function name

          > distinguish as struct variables
          I've not seen that before, but if you want to, you can do it.

          > in terms of descriptive names
          My comment wasn't about descriptiveness, but about style. Those 2 are all lower case as opposed to camel case for your other variables.

          > logic flow
          Functions enhance logic flow and increase reusability. goto is bad, because it changes the flow inside of a function. A simple start is splitting @playGame into "dealCards" and "gameOver", then keep on splitting "dealCards".

          • Arthur

            thanks, very helpful!
            @ getYesNo , entirely possible I did not use the auto format there, it was the last function I had taken from @gamePlay , I don't think it would do anything to the else return line 201.

            I will have to dig around in the auto format settings to see if it can correct the spacing issues I try to just see it / write in the first place.

            @distinguish struct variables , just an idea , I skimmed some google ideas/rules just for some ideas, I found it helped me to keep what was what straight which will get easier I imagine when I am not wrestling with how to get things to work.

            thanks , Ill look at splitting off dealCards and gameOver after I figure out merging playerPlay and dealerPlay into a single nextPlay function and see where that leads...

          • Arthur

            I see what you mean about logic flow , took a bit of effort to untangle it into separate functions but left a lot less conditionals and is easier to follow. I got rid of a lot of duplicate code and broke it into smaller functions. Code::Blocks formatting options may be a little less intuitive then Microsoft IDE, there are options for padding it will make spaces before brackets unless they are empty , or I can set it to take the spaces away but that looks to jumbled to me.

            hopefully this is looking a bit better

            • * Line 100: Initialize your variables with brace initializers. You used copy initialization.
              * Line 101: Use std::rand instead of its global-namespaced counterpart.
              * Line 146-150: Should be static constexpr, since they're independent of the object.
              * Line 151-158: You have every variable twice, once for the player, once for the dealer. This is an indicator that you should use a class or struct. Using a class might help you to further tidy up your code.
              * Line 401-407: Could be a do {} while (playAgain(...)) loop.

              You're printing a lot of spaces. You can use @std::string to repeat a certain character

              Add another constant for the number of spaces, so that all the lines have the same indention.

  • Steve S

    Hi, can you point me to the chapter that explains how this works.

    8.8 talks about chaining functions, but only when you return *this as part of the function, which isn't done in this example.

    Thanks!

    • Hi Steve!

      Every function that returns a value can be chained.
      (Note: The following is not entirely true, because it ignores const and reference. @decltype is required for a proper example)

      If you want to check back later, this is what line 2 should look like (You don't need to understand this yet)

  • Tristan Danino

    This chapter was super rewarding! Perhaps an additional bonus question could be asking the reader to implement a "Hand" (as in, a card game player's "hand") function, which be used as a playerHand and a dealerHand to store their cards and their current score. Perhaps taking this form, where an "addCard" member function adds cards to the vector of cards and updates the score appropriately.

    It could even handle the logic of checking that the player is not bust, but I think I prefer keeping that with the game logic itself. Overall, it also simplifies the logic of the game, because the score and cards aren't separate, and it makes keeping track of things much easier.

  • Louis Cloete

    Hey Alex. Just a couple of remarks:

    1. The expected output of Question 1a) should not have semicolons at the end.
    2. The variables referred to in Question 3i) could be const as well as static.

  • hassan magaji

    Hi everyone,
    Am I wellcomed to OOP?
    (suggestion ||correction || advise)  && please
    this is my full code for quiz 3 almost thesame with Alex's.