Constructors and Destructors

Learn about special functions we can add to our classes, control how our objects get created and destroyed.

Ryan McCombe
Updated

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.

Constructors

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 constructor with our own logic to implement whatever behaviour we need for our program.

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!

Constructor Parameters

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

Multiple Constructors

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

Ambiguous Constructor Calls

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

Default Constructor

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;
};

Constructor prototypes

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;
}

Destructor

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!

Test your Knowledge

Constructors and Destructors

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?

Summary

As we conclude this lesson, let's recap the key concepts we've covered:

  • Understanding Constructors and Destructors: We've learned about constructors, special functions in a class that are called when objects are created. We've seen how these can be used to initialize objects with specific values and behaviors.
  • Default and Custom Constructors: We explored how default constructors work and how we can create custom constructors to meet specific needs.
  • Naming Conventions for Class Variables: The reason we might want to adopt class member naming conventions like prefixing member variables (e.g., mName).
  • Constructor Parameters: We looked at how constructors can take parameters to initialize objects with specific values.
  • Handling Multiple Constructors: We explored creating classes with multiple constructors, allowing for flexible object creation.
  • Avoiding Ambiguous Constructor Calls: We learned the importance of ensuring constructors are not ambiguous to avoid compilation errors.
  • Implementing and Restoring Default Constructors: We discussed how to implement custom default constructors and how to restore the original default constructor using = default.
  • Destructor Introduction: We introduced destructors, which are called when objects are destroyed, and their role in resource management.
Next Lesson
Lesson 23 of 60

Structs and Aggregate Initialization

Discover the role of structs, how they differ from classes, and how to initialize them without requiring a constructor.

Have a question about this lesson?
Answers are generated by AI models and may not have been reviewed for accuracy