With built-in types, such as int
and string
, we’ve previously seen how we can set initial values when creating objects:
int Health { 100 };
string Name { "Roderick" };
In this lesson, we’ll also learn how to give our user-defined types this ability:
Monster Goblin { "Basher the Goblin" };
We unlock this by adding constructors to our class.
A constructor in C++ is a special member function of a class that is executed whenever we create new objects of that class.
It initializes the object's properties and can set up essential prerequisites for the object's functionality.
A constructor that takes no arguments is known as a default constructor.
This type of constructor is used when we want to create an object but don't need to specify any initial values for its properties.
As we’ve seen, we’ve already been able to create our objects without any arguments:
Monster Goblin;
This is because our classes are provided with a basic, default constructor as standard. However, we can replace this, to implement any code we need for our use case.
Let's see a basic example of a default constructor in action:
#include <iostream>
using namespace std;
class Monster {
public:
Monster() {
cout << "Ready for Battle!";
}
private:
string mName;
};
int main() {
Monster Goblin;
}
Ready for Battle!
In the previous example, we prefixed our class variables with the letter m
, as in mName
. This is an abbreviation of "member".
Adopting a naming convention for class members, like prefixing with m
, can be beneficial for several reasons.
This is especially useful when we’re going to create constructors that accept parameters, as it prevents confusion between the parameters and the class members they are meant to initialize.
For example, to make our constructor clear for consumers, we’d want a parameter that initializes the object’s name to be called Name
.
But if the class member is also called Name
, that can get quite confusing, especially in the constructor that’s trying to set Name
(in our class) equal to Name
(from the parameter)
So, it’s somewhat common to implement some standard naming conventions for our private internal members, such as prefixing them with m
.
Other common naming conventions include using an underscore (_
) as a prefix (e.g., _name
) or a postfix (e.g., Name_
). These conventions all serve the same purpose
Like other functions, constructors can be designed to take arguments, which we can use as needed within our constructor function body.
Typically, this is used to allow developers to pass values directly to the object's properties. For instance, a constructor that accepts a single argument can be used to set a specific property of a class.
Below, we’ve created a constructor that takes a string
argument, granting the ability for our monster’s name to be set at creation time:
#include <iostream>
using namespace std;
class Monster {
public:
Monster(string Name){
mName = Name;
cout << mName << " Ready for Battle!";
}
private:
string mName;
};
int main(){
Monster Goblin{"Bonker"};
}
Bonker Ready for Battle!
Similar to other types of functions, a constructor can have multiple parameters, separated by a comma.
When calling the constructor (by declaring an object of its type) we also comma-separate the arguments:
#include <iostream>
using namespace std;
class Monster {
public:
Monster(string Name, int Health){
mName = Name;
mHealth = Health;
cout << mName << " Ready for Battle!"
<< "\nHealth: " << mHealth;
}
private:
string mName;
int mHealth;
};
int main(){
Monster Goblin{"Bonker", 150};
}
Bonker Ready for Battle!
Health: 150
Our classes can define multiple constructors, allowing our objects to be created with a variety of different argument lists.
Below, we allow consumers of our class to create our objects either by providing a string
representing the monster’s name, or both a string
and an int
representing their name and initial Health
value:
#include <iostream>
using namespace std;
class Monster {
public:
Monster(string Name) {
mName = Name;
mHealth = 150;
cout << mName << " Ready for Battle!"
<< "\nHealth: " << mHealth;
}
Monster(string Name, int Health) {
mName = Name;
mHealth = Health;
cout << mName << " Ready for Battle!"
<< "\nHealth: " << mHealth;
}
private:
string mName;
int mHealth;
};
int main() {
Monster Bonker{"Bonker"};
cout << '\n';
Monster Basher{"Basher", 250};
}
Bonker Ready for Battle!
Health: 150
Basher Ready for Battle!
Health: 250
Just like other functions, constructors can have optional parameters. This allows multiple argument lists to be supported by a single constructor.
Our previous code can, and should, be simplified to this:
#include <iostream>
using namespace std;
class Monster {
public:
Monster(string Name, int Health = 150){
mName = Name;
mHealth = Health;
cout << mName << " Ready for Battle!"
<< "\nHealth: " << mHealth;
}
private:
string mName;
int mHealth;
};
int main(){
Monster Bonker{"Bonker"};
cout << '\n';
Monster Basher{"Basher", 250};
}
Bonker Ready for Battle!
Health: 150
Basher Ready for Battle!
Health: 250
When defining multiple constructors, we need to ensure that they don’t "overlap". Specifically, any time we create an object, there must be only one constructor that supports the argument list that was provided.
Below, we have two constructors that both accept a single int
parameter.
This is invalid because, if someone tries to instantiate our class with a single int
argument, the compiler has no way of knowing what constructor is supposed to be used:
#include <iostream>
using namespace std;
class Monster {
public:
Monster(int Level){ mLevel = Level; }
Monster(int Health){ mHealth = Health; }
private:
int mLevel;
int mHealth;
};
int main(){
// Which Constructor?
Monster Bonker{10};
}
When we fall foul of this requirement, we will typically get a compiler error at the point where our class is defined:
error: 'Monster::Monster(int)': member function
already defined or declared
In some situations, our class definition may be valid, but we’ll get an error if we try to create an object with an argument list that can be handled by multiple constructors:
error: 'Monster::Monster': ambiguous call to
overloaded function
Previously, we’ve seen how we were able to create an object from our class, providing no arguments at all:
Monster Basher;
This is because, out of the box, our classes come with a default constructor.
However, once we define a custom constructor, the default is automatically deleted.
If we want to allow our objects to be created without arguments, we can simply reimplement the default constructor. We do this by providing a constructor that takes no arguments:
class Monster {
public:
// A default constructor
Monster() {
// ...
};
Monster(string Name, int Health = 150) {
mName = Name;
mHealth = Health;
cout << mName << " Ready for Battle!"
<< "\nHealth: " << mHealth;
}
private:
string mName;
int mHealth;
};
If we don’t need to provide any implementation for this, we can use the = default
syntax to restore the original default constructor:
class Monster {
public:
Monster() = default;
Monster(string Name, int Health = 150) {
mName = Name;
mHealth = Health;
cout << mName << " Ready for Battle!"
<< "\nHealth: " << mHealth;
}
private:
string mName;
int mHealth;
};
Similar to other functions, we can declare and define constructors in different locations. The syntax is exactly the same as we covered in the previous lesson:
class Monster {
public:
// The prototype
Monster(int Health);
private:
int mHealth{150};
};
// The definition
Monster::Monster(int Health){
mHealth = Health;
}
A destructor in is another special member function of a class, complementary to the constructor.
It is called automatically when an object is deleted.
The syntax for a destructor is similar to the constructor but with a tilde (~
) prefix. Here's a simple example:
class Monster {
public:
// Destructor
~Monster() {
// ...
}
};
When objects get destroyed, and the object lifecycle in general, will get more important as we get into more advanced topics.
For now, we can note that one scenario where objects will get destroyed is when the scope they were created in gets destroyed.
We can see an example of this below, where our Goblin
is created within our function, and then automatically destroyed when our function ends:
#include <iostream>
using namespace std;
class Monster {
public:
// Constructor
Monster(){
cout << "Monster Created\n";
}
// Destructor
~Monster(){
cout << "Monster Destroyed\n";
}
};
void SomeFunction(){
Monster Goblin;
}
int main(){
cout << "Hello World\n";
SomeFunction();
cout << "Goodbye!";
}
Hello World
Monster Created
Monster Destroyed
Goodbye!
Imagine we have a class defined as follows:
class Robot {
public:
Robot(string Model, int Year = 2024) {
mModel = Model;
mYear = Year;
}
private:
string mModel;
int mYear;
};
What will be the result of creating a Robot
object with the statement Robot MyRobot("RX100");
?
Given the following class definition:
class Creature {
public:
Creature(string name) {
mName = name;
}
Creature() {
mName = "Unknown";
}
private:
string mName;
};
What happens if you create an object of Creature
class without passing any arguments?
What happens when you define a custom constructor in a class without explicitly defining a default constructor?
Consider the following class definition:
class Weapon {
public:
Weapon(int Durability){
mDurability = Durability;
}
Weapon(int Weight){
mWeight = Weight;
}
private:
int mDurability;
int mWeight;
};
What will be the effect of creating a Weapon
object with the statement Weapon IronSword{500};
?
What is a destructor?
As we conclude this lesson, let's recap the key concepts we've covered:
mName
).= default
.In our next lesson, we will dive into the world of structs and aggregate initialization in C++. Here's what you can expect:
Learn about special functions we can add to our classes, control how our objects get created and destroyed.
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way