A constructor is a special type of function on a class. A constructor gets called automatically when an object of that class is created. Because of this, it is the ideal place to put any sort of initialisation.
To let C++ know that a function is a constructor, it needs to have two properties:
Monster
class, any function that is a constructor is also called Monster
void
)We can see an example of a constructor below:
class Monster {
public:
Monster() {
Health = 150;
}
int GetHealth() { return Health; }
private:
int Health;
};
We should ensure that the constructors we create are in the public:
section of our classes.
This will not be enforced by the compiler, as there are advanced scenarios where a non-public constructor is useful, but those are beyond the scope of this course. Therefore, for now, we just have to be mindful to always ensure our constructors are public.
What is a constructor?
Imagine we had this simple class:
class Car {};
What would be a valid prototype for a constructor for this class?
Above, we added a constructor to our class. As that constructor does not require any arguments, it is considered the default constructor.
This constructor will be called for us automatically, without making any changes to the code we had previously written. A simple Monster Harry;
statement will cause this constructor to be called.
The following code will log out `"Ready for battle!"
#include <iostream>
using namespace std;
class Monster {
public:
Monster() {
cout << "Ready for battle!";
}
};
int main() {
Monster Harry;
}
However, because constructors are functions, they can also accept arguments:
class Monster {
public:
Monster(int InitialHealth = 150) {
Health = InitialHealth;
cout << "Ready for battle!";
}
int GetHealth() { return Health; }
private:
int Health;
};
With this constructor's only argument being optional, it will still act as a default constructor. That means creating a Monster using the code Monster Harry;
will still call this constructor.
However, we now have the ability to give our Monster objects an initial Health
value when creating them.
Just like we were able to pass values to the int
data type to set an initial value:
int Level { 5 };
We can now do the same with our Monster
data type
// This still works
Monster Harry;
Harry.GetHealth(); // 150
// We can now pass values to the constructor
Monster Julia { 300 };
Julia.GetHealth(); // 300
Like with any other function, constructors can have multiple arguments. For example:
class Monster {
public:
Monster(
int InitialHealth = 150,
bool isInitiallyEnraged = false
) {
Health = InitialHealth;
isEnraged = isInitiallyEnraged;
}
private:
int Health;
bool isEnraged;
}
// 150 health, not enraged
Monster SmallCalmGoblin;
// 300 health, enraged
Monster BigAngryGoblin { 300, true };
We can have multiple constructors in our classes. The only requirement is that each constructor must have sufficiently unique signatures.
For example, we can have a constructor that accepts a boolean, and a different one that accepts an integer.
In this example, we have 4 constructors:
class Monster {
public:
Monster() {
cout << "Using default constructor!";
}
Monster(int InitialHealth) {
Health = InitialHealth;
}
Monster(bool isInitiallyEnraged) {
isEnraged = isInitiallyEnraged;
}
Monster(int InitialHealth, bool isInitiallyEnraged) {
Health = InitialHealth;
isEnraged = isInitiallyEnraged;
}
private:
int Health { 150 };
bool isEnraged { false };
};
// Uses first constructor
Monster DefaultGoblin;
// Uses second constructor
Monster BigGoblin { 400 };
// Uses third constructor
Monster AngryGoblin { true };
// Uses fourth constructor
Monster BigAngryGoblin { 400, true };
As long as the parameter types are sufficiently unique, we can create as many constructors as we want.
The specific constraint is that, any time we create an object, it must be unambiguously clear which constructor is to be used.
That is, when the compiler receives a statement to create an object, there must be only one constructor that can handle the arguments that were provided in that statement.
This means that the following class would be problematic, as there are some argument combinations that could be handled by either constructor:
class Monster {
public:
Monster(int Level, int InitialHealth = 150) {
Level = Level;
Health = InitialHealth;
}
Monster(int InitialHealth) {
Health = InitialHealth;
}
private:
int Level { 1 };
int Health { 150 };
};
Whilst the above code would compile, issues can arise when we try to construct objects of that class. For example, were we to try to create a Monster in the following way:
Monster Harry { 100 };
It is not clear which constructor to call - either could be used. We might be using the top constructor to set Level
to 100
, or the bottom constructor to set Health
to 100.
Therefore, our code would not compile, with an error message such as "call to constructor of Monster
is ambiguous".
What is the maximum number of constructors a class can have?
Consider this code:
class Car {
public:
Car(int Speed, int HorsePower, int Torque) {}
Car(int Speed, int HorsePower) {}
Car(int Speed) {}
Car(int Speed = 50) {}
};
Car A { 100, 200, 300 };
Car B { 100, 200 };
Car C { 100 };
Car D;
Which of the above Cars cannot be constructed?
In a previous lesson, we gave an example of how functions can be implemented outside of the body of the class, with the Monster::TakeDamage
example. This also applies to constructors:
class Monster {
public:
// The prototype
Monster(int InitialHealth);
private:
int Health { 150 };
};
// The implementation
Monster::Monster(int InitialHealth) {
Health = InitialHealth;
}
In addition to constructors, our class can also define a destructor.
A destructor returns nothing, has no parameters, and has the same name as the class, but with a ~
prepended. We’ll see some exceptions later but, in almost all cases, a destructor should be public
:
class SomeClass {
public:
// Destructor
~SomeClass(){
// ...
}
};
A destructor automatically runs when our objects are destroyed. This makes them the ideal place to do any cleanup we might need.
The object life cycle is something that will become more important as we learn more, and we’ll discover ways to control it. For now, we can note that the objects we’re creating are automatically destroyed when the scope they were created in is destroyed.
This means objects created in the global scope will be destroyed just before our program ends.
For objects created in a block scope, such as between the {
and }
of a function, the object will be destroyed when execution reaches the end of the block.
#include <iostream>
using namespace std;
class Monster {
public:
// Constructor
Monster(){
cout << "Monster Created" << endl;
}
// Destructor
~Monster(){
cout << "Monster Destroyed" << endl;
}
};
void SomeFunction(){
Monster Goblin;
}
int main(){
cout << "Hello World" << endl;
SomeFunction();
cout << "Goodbye!";
}
Hello World
Monster Created
Monster Destroyed
Goodbye!
In this chapter, we introduced the concept of classes. Classes are the main tool we use to implement the 4 key principles of object oriented programming. We also saw what the first two of those principles are:
Abstraction is the ability to group our objects into categories - conceptual models of what it means to be that type of thing. We grouped all individual monsters under the category, or class, of Monster
.
The Monster
class is the abstraction - each individual Monster object is an example, or an instance, of a Monster
.
We also looked at how we can use encapsulation to restrict access to parts of our objects, making them easier to use and maintain.
Encapsulation is the ability to hide implementation details under the hood.
Our objects expose only the controls that we intend to be used by external code - code that is not part of our class.
We make these things public
. Implementation details - things the outside world doesn't need to know about, are made private
.
In the next chapter, we will finish out our initial exploration of object oriented programming by progressing our design further. We will encounter some new problems, and see how we can use the other two big ideas of object oriented programming.
Those principles are inheritance and polymorphism, and will allow us to create more and more complex software in an effective and organised way.
— Added section on destructors
— First Published
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way