Lesson 4 - Introducing destructors and more about constructors in C++
In the previous lesson, A RollingDie in C++ and Constructors, we described the constructor syntax, including advanced constructs such as the delegating constructor and invoking constructors for attributes. Today, we're going to describe destructors and show the main purpose for which constructors and destructors are used.
Destructors
Like the constructor that is called immediately after the instance is
created, the destructor is called automatically before the instance is deleted.
The instance is usually deleted at the end of the block (that is, the end of the
function or the closing brace }
). We write the destructor as a
method that starts with a tilda (~
) followed by the class name. The
destructor never has parameters and does not return a value. Such a basic
destructor has already been generated by Visual Studio for us and is empty (if
we don't provide a custom destructor, the compiler will create a destructor with
an empty body automatically):
RollingDie.h
class RollingDie { public: RollingDie(); RollingDie(int _sides_count); ~RollingDie(); // destructor declaration int sides_count; };
RollingDie.cpp
RollingDie::~RollingDie() // empty destructor
{
}
To see when the destructor is called, we'll print to the console in it:
#include <iostream> // if missing using namespace std; // if missing RollingDie::~RollingDie() { cout << "Calling the destructor for the die with " << sides_count << " sides" << endl; }
In main.cpp
, we'll put the following code, which shows the cases
when the destructor is called.
void function(RollingDie die) { cout << "Function" << endl; } int main() { RollingDie first(1); if (true) { RollingDie second(2); function(second); cout << "Function finished" << endl; } // cin.get(); return 0; }
We see the logs in the application output:
Console application
Parametric constructor called
Parametric constructor called
Function
Calling the destructor for the die with 2 sides
Function finished
Calling the destructor for the die with 1 sides
When we analyze the example, we find that the destructor is called before the closing curly braces and at the end of the function. That's when the variable is no longer needed and C++ will remove it from memory. For a better understanding, let's add comments to the code as well:
void function(RollingDie die) { cout << "Function" << endl; } // die's destructor called int main() { RollingDie first(1); // first constructor if (true) { RollingDie second(2); // second constructor function(second); cout << "Function finished" << endl; } // second's destructor // cin.get(); if we keep this call, we won't see the the first die being deleted return 0; } // first's destructor called
Constructors calls are also printed because we left the code from the previous lesson. You might be surprised that three destructors are called, but only two constructors. In one case, the copying constructor is called, but we'll deal with it in another lesson. For now, we just need to know when the destructor is called.
Constructors for initialization
Now let's look at one case where constructors are useful - class initialization.
Let's define a roll()
method in the RollingDie
that
returns a random number from 1
to the number of sides. It's very
simple, the method will have no parameter and the return value will be
int
. To get a random number, we call the rand()
function from the cstdlib
library.
RollingDie.h
#ifndef __ROLLINGDIE_H__ #define __ROLLINGDIE_H__ class RollingDie { public: RollingDie(); RollingDie(int _sides_count); ~RollingDie(); int roll(); int sides_count; }; #endif
RollingDie.cpp
#include <iostream> #include <cstdlib> #include "RollingDie.h" using namespace std; // ... already defined methods int RollingDie::roll() { return rand() % sides_count + 1; }
rand ()
returns a pseudo random number. To be
within the required range, we need to use % sides_count + 1
. The
number 1
is added to make the random numbers from one and not zero.
A pseudo-random number means that it starts with some number and next numbers
are calculated by some operation with the initial number. This approach has one
disadvantage - put the following code into main.cpp
(you can delete
the original one):
#include <iostream> #include "RollingDie.h" #include "Arena.h" using namespace std; int main() { RollingDie die; for (int i = 0; i < 10; i++) cout << die.roll() << " "; cin.get(); return 0; }
Note that if we run the program several times, it always generates the same
numbers (even though it should generate them randomly). This is because the
initial number is always the same. We need to start with a different number
every time we run the program. We'll do it using the srand()
method
and passing the current time to it. And because it actually initializes the
instance, we'll put that code into the constructor.
Note: In addition to the cstdlib
library, the
ctime
library must also be included.
RollingDie::RollingDie(int _sides_count) { cout << "Parametric constructor called" << endl; sides_count = _sides_count; srand(time(NULL)); }
Now the rolling die always generates different numbers and we are done.
Using Constructors for Memory Management
The second case where we can use the constructor (and the destructor) is
memory management. Since constructors and destructors are called automatically,
we are sure that the code is always executed. So we can allocate memory in the
constructor and delete it in the destructor. Let's take our arena as an example,
where there are currently two warriors. Let's say we want to enter the number of
warriors as a parameter - so we must create an array of the warriors
dynamically. Edit the Arena.h
file as follows:
#ifndef __ARENA_H_ #define __ARENA_H_ #include "Player.h" class Arena { public: Player** players; int players_count; Arena(int _players_count); // the parameter name has been changed ~Arena(); }; #endif
Don't be scared of the two asterisks - it's an array of pointers to
Player
(we cannot create only an array of players because we don't
have the default = parameterless constructor which is needed for that). We
allocate this array in the constructor, and we ask for the names according to
the number of players. Then, in the destructor, we perform the opposite
operation and delete everything. Let's see the code:
Arena.cpp
#include <iostream> #include "Arena.h" using namespace std; Arena::Arena(int _players_count) { players_count = _players_count; // storing the player count players = new Player*[players_count]; // creating an array for the players for (int i = 0; i < players_count; i++) { string name; cout << "Enter a player name: "; cin >> name; players[i] = new Player(name); // creating the player } } Arena::~Arena() { for (int i = 0; i < players_count; i++) delete players[i]; // deleting the players delete[] players; // deleting the array players = NULL; }
If we did not delete the memory, it'd remain allocated and we wouldn't be able to access it anyhow (there would be no pointer to it) and it would also be impossible to delete it later. For example, if we were creating instances in a loop, then the program would start to consume more and more RAM until it'd take it all (and having a gigabyte of RAM for that small application is rather strange). If there's no free RAM memory and the program asks for more memory, the operating system no longer has anything to allocate and will terminate the application. Therefore, if you find your application crashing after some time, try to check how much memory space it takes and if this space is constantly growing, you may not be freeing memory somewhere, causing a memory leak.
main.cpp
So we have finished our arena and can use it in main.cpp
:
#include <iostream> #include "RollingDie.h" #include "Arena.h" using namespace std; int main() { RollingDie die; for (int i = 0; i < 10; i++) cout << die.roll() << " "; cout << endl; Arena arena(4); cin.get(); return 0; }
The result:
Console application
Parametric constructor called
Parameterless constructor called
2 6 1 2 1 6 2 3 1 4
Enter a player name: Paul
Enter a player name: Carl
Enter a player name: George
Enter a player name: Lucas
Calling the destructor for the die with 6 sides
Everything works like a charm. Allocating and freeing memory is the most common thing that happens in constructors and destructors, so I suggest you take a good look at the last example to understand how it works.
That's all for this lesson. The next time, The this pointer in C++, we'll remove those nasty parameter names starting with underscores. The source code of today's lesson is attached for download below the article as always.
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 2x (964.38 kB)
Application includes source codes in language C++