Lesson 3 - A RollingDie in C++ and Constructors
In the previous lesson, First OOP application in C++ - Hello object world, we programmed our first object-oriented application in C++. We are already able to create new classes with fields and parameterized methods with return values. Today, we're going to start working on an arena in which two warriors will battle against each other. The battle will be turn-based and one warrior will decrease the health points of other one based on his attack and on the other warrior's defense. Essentially, we'll be simulating a board game, so we'll add a rolling "die" to our game so as to add a bit of randomness. We're also going to learn to declare custom constructors.
Creating a project
We'll start by creating a new empty application and naming it
Arena
. We'll create a main.cpp
file as usual with the
basic code:
#include <iostream> using namespace std; int main() { cin.get(); return 0; }
We'll also add a new class
to our project and name it
RollingDie
. Let's think about the fields we'll have to add to our
die in order for it to function according to our current needs. We should be
able to choose the number of sides. Typically 6 or 10 sides as is standard in
this type of game. Our die will a single field now: sides_count
of
the int
type. Our class now looks like this:
We've removed the generated #pragma once
, but kept the generated
methods.
RollingDie.h
#ifndef __ROLLINGDIE_H__ #define __ROLLINGDIE_H__ class RollingDie { public: RollingDie(); ~RollingDie(); int sides_count; }; #endif
Сonstructors
Until now, we were unable to set class fields when creating a new instance. We had to write something like this:
RollingDie die;
die.sides_count = 6;
However, we would like to set the number of sides while the instance is being
created. We've already discussed constructors a bit. A constructor is a method
that is called while an object instance is being created. We
use it to set the internal state of the object and perform any necessary
initializations. A simple constructor has already been generated by Visual
Studio. It's written as: RollingDie()
. It has a blank body
({}
) in the RollingDie.cpp
file. However, we'll now
add something to its body. In the constructor, we'll set the number of sides to
a fixed value. The constructor will look like this:
RollingDie.cpp
RollingDie::RollingDie()
{
sides_count = 6;
}
We declare the constructor as a method, but it doesn't have a return
type. Also, the constructor must have the same name as the
class name, which in our case is RollingDie
.
If we create the die now, it'll have the sides_count
field set
to 6
.
main.cpp
We'll print the number of sides to the console, so as to visually confirm
that the value is there. Let's move on to main.cpp
so we could
create our first die and print the number of its sides to the console:
#include <iostream> #include "RollingDie.h" using namespace std; int main() { RollingDie die; cout << die.sides_count << endl; cin.get(); return 0; }
The output:
Console application
6
Now we have visual confirmation that the constructor had been called. However, we'd like to be able to specify how many sides our die will have.
RollingDie.cpp
Let's add a parameter to our constructor (we have to modify the declaration
in RollingDie.h
too to accept an int
parameter):
RollingDie::RollingDie(int _sides_count)
{
sides_count = _sides_count;
}
Notice that we prefixed the parameter name with _
, otherwise
it'd have the same name as the field and cause an error. Let's go back to
main.cpp
and pass a value for this parameter in the
constructor:
RollingDie die(10); // a constructor with a par. 10 is called cout << die.sides_count << endl;
The output:
Console application
10
Note the use of parentheses. If we omit the parentheses (as we have done so far), then the parameterless constructor (the one that was originally generated) is called. If a class has only constructors that accept parameters, then we must create an instance that way. If the class had a parameterless constructor and some other constructors, then the one without parameters would be called when creating an instance without parentheses. The only difference is when creating an instance dynamically where parentheses may be, but don't have to.
RollingDie sixsided; RollingDie sixsided2(); // won't work RollingDie tensided(10); RollingDie* dynamic_sixsided = new RollingDie; RollingDie* dynamic_sixsided2 = new RollingDie(); // will work RollingDie* dynamic_tensided = new RollingDie(10);
Note: If no constructor is defined (as it was in the previous lesson), then the compiler will generate a parameterless empty constructor automatically.
In the constructor, we should ask for all the information the class needs to work properly. This will force programmers using our class to pass these parameters (otherwise they won't be able to create the instance).
However, we assume a 6-sided die is the default, so we'd like to create a six-sided dice automatically if we don't specify the parameter. There are two ways to do this - the first is to use default parameter values:
// RollingDie.h class RollingDie { public: RollingDie(int _sides_count); ~RollingDie(); int sides_count; }; // RollingDie.h RollingDie::RollingDie(int _sides_count = 6) { sides_count = _sides_count; } // main.cpp RollingDie sixsided; RollingDie tensided(10); // a constructor with a par. 10 is called cout << sixsided.sides_count << endl; cout << tensided.sides_count << endl;
The output:
Console application
6
10
Another approach would be to overload the constructor by declaring another parameterless one:
// RollingDie.h class RollingDie { public: RollingDie(int _sides_count); ~RollingDie(); int sides_count; }; // RollingDie.cpp RollingDie::RollingDie() { sides_count = 6; } RollingDie::RollingDie(int _sides_count) { sides_count = _sides_count; } RollingDie::~RollingDie() { }
Such an implementation isn't the best idea, but we'll talk more about it later.
C++ doesn't mind us having two methods with the same name as long as their
parameters are different. We say that the RollingDie()
method,
which in this case is the constructor, is overloaded. This
works for all methods with same names and different parameters, not just
constructors. Many C++ methods have several overloads, try to look at the
getline()
method on the cin
object. Visual Studio
offers overloaded versions of methods when we're typing them. We can scroll
through the method variants with the arrow keys. This "scroll-tool" is called
IntelliSense. It's good to look through method overloads, so you don't end up
programming something what has already been done for you.
Invoking Constructors
In the previous case, we set the value of 6
to the
sides_count
attribute directly in the parameterless constructor.
Let's consider we have 5 attributes like this one and 3 constructors. Setting up
all the attributes in all constructors isn't very wise since we'd repeatedly do
the same thing and we could make a mistake. As a rule, a more specific
constructor (with fewer parameters) calls the more general one (the one with
more parameters). But how do we call a constructor manually, knowing just it's
called automatically when an instance is being created? For this case, C++
introduces delegating constructors. The syntax is to add a colon after the
constructor declaration and then the name of the constructor we want to be
called with its parameters follows. An example will serve the best:
RollingDie.cpp
#include <iostream> #include "RollingDie.h" using namespace std; RollingDie::RollingDie() : RollingDie(6) { cout << "Parameterless constructor called" << endl; } RollingDie::RollingDie(int _sides_count) { cout << "Parametric constructor called" << endl; sides_count = _sides_count; } RollingDie::~RollingDie() { }
And the usage:
RollingDie sixsided;
In the sample we see that both constructors have been called:
Console application
Parametric constructor called
Parameterless constructor called
Note: Delegating constructors are supported since C++ 11, so it may not work for all compilers. Delegating constructors like this is useful for one more thing. Let's consider we want to create a user instance for which we need a name. That's not a problem yet. But we'll need to declare it in another object, in the arena, when we don't yet know the name. Sample classes for players and arena follow:
Player.h
#ifndef __PLAYER__H_ #define __PLAYER__H_ #include <string> using namespace std; class Player { public: string name; Player(string _name); }; #endif
Player.cpp
#include "Player.h"
Player::Player(string _name)
{
name = _name;
}
Arena.h
#ifndef __ARENA_H_ #define __ARENA_H_ #include "Player.h" class Arena { public: Player first; Player second; Arena(); }; #endif
Arena.cpp
#include "Arena.h"
Arena::Arena()
{
}
The project should not compile and report the following message (for Visual Studio):
error C2512: 'Player': no appropriate default constructor available
The reason is simple. The constructor takes care of creating an instance. This means that when the constructor finishes, the class must be ready for use. However, this doesn't work for us because the player needs a name. We could try the following code:
Arena::Arena() { first = Player("Carl"); second = Player("Paul"); }
But we still get the same error. Before any method (including the
constructor) is called on an instance, the instance must already be
created correctly. So we have to tell C++ somehow how to create a player before
the Arena
itself is created. The syntax is the same as for the
delegating constructor, using the variable name instead of the class name:
#include "Arena.h" Arena::Arena(string name_first, string name_second) : first(name_first), second(name_second) { }
#ifndef __ARENA_H_ #define __ARENA_H_ #include "Player.h" class Arena { public: Player first; Player second; Arena(string name_first, string name_second); }; #endif
#ifndef __PLAYER__H_ #define __PLAYER__H_ #include <string> using namespace std; class Player { public: string name; Player(string _name); }; #endif
#include "Player.h" Player::Player(string _name) { name = _name; }
#include <iostream> #include "Arena.h" using namespace std; int main() { Arena arena("Carl", "Paul"); cout << arena.first.name << " and " << arena.second.name << " fight" << endl; cin.get(); return 0; }
Console application
Carl and Paul fight
That would be all for today's lesson. In the next lesson, Introducing destructors and more about constructors in C++, we'll talk about the main reason for the existence of constructors, describe the destructors, and finish our rolling die.
Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.
Download
By downloading the following file, you agree to the license terms
Downloaded 4x (455.76 kB)
Application includes source codes in language C++