Let's talk more about pointers to objects.
To follow along with this lesson,
you should already be familiar with class polymorphism through inheritance,
which can be used to build a hierarchy of derived classes.
I will quickly go through the classes I have prepared for this demonstration.
The Instrument class is the base class,
and it only defines if the instrument is electric or acoustic.
Default constructor will assume that the instrument is acoustic,
but you can change that by using the second constructor.
The class also has a play member function,
which just prints out a simple message.
Okay, let's move on to derived classes.
The first derived class is the Guitar class,
which only defines one additional property,
the number of strings, and this is strings as in guitar strings,
not the strings of characters.
The default constructor sets the number of strings to six,
but we also have an option to call one of the other
constructors to manually set that number.
You can even pass the Boolean value to the Instrument
constructor to make this guitar electric.
And of course,
there is a special version of the play function for the guitar class,
which will override the play function from the bass Instrument class.
Finally, there is also another derived Synth class,
which has a smaller definition, because we assume that every synthesizer is
electric, so we only have two constructors. But of course,
Synth also has a function with the special message,
which overrides the base play function.
Okay, now that we are familiar with the classes,
I will show you the main function.
I instantiated one instrument, one electric guitar,
one seven‑string guitar, and one synth with 25 keys.
I will make each one of them call their own version of the play function.
If I compile this, everything should work as expected.
All of these messages are printed out correctly for
that specific type of object.
Now let's ruin this harmony by introducing pointers.
Since we will work with pointers,
let's make this example a little bit more realistic by actually
using these pointers for heap memory allocations.
I am still allocating the same objects, but this time
they will be stored on the heap.
All of these variables are now pointers, so we need to
de‑reference them before calling the play function.
In the previous lesson,
we learned that this is easier to do with an arrow operator.
And finally,
let's also call delete on each one of these pointers to prevent memory leaks.
This version of the program will still work as expected,
but what if I want to point to all of these objects with
the pointer to an instrument?
The purpose of this will be shown later.
For now, let's just try it as an experiment.
Let's try to compile this.
And as you can see, only the play function from the Instrument
class is called; the overrides are being ignored.
This is because of something called early binding or compile‑time binding.
The compiler will see that this is a pointer to an instrument, and
it will assume that the object that this pointer is pointing to is
instantiated from the Instrument class.
Since every derived class also contains all of the public parts of the
base class, compiler will make this pointer call the play function from
the Instrument class, because we are making a call from the instrument
pointer. To solve this problem, C++ offers us the concept of virtual functions.
When you make a function virtual,
you are informing the compiler that this specific
function should be bound at runtime,
which will let you implement the dynamic runtime polymorphism. To make the
function virtual, we need to add the virtual keyword before the function
definition inside of the base class. And this is all we need to do. All of
the play function definitions from the derived classes will now override this
base class function, and they will be able to do that dynamically, while the
program is running.
A good practice is to also add an override specifier to the
overridden function of the derived class.
You can do that by placing the override keyword after the list of
parameters inside of the functions definition.
I would encourage you to do that, because if you make a mistake and don't
override the virtual function correctly, the compiler will give you a warning,
because it knows that each overridden function needs to match the signature of
the virtual function from the base class.
Let's compile again after making this function virtual.
And as you can see, the instrument pointer is calling the correct function for
each derived object. As opposed to early binding, virtual functions allow us to
do late binding, which as the name suggests, means that we bind the correct
function to the object at some later time.
So even though all of these are base class pointers,
the program will first check the type of the object itself
before making a call to the correct function.
The exact implementation of this behavior will be explained in the
next lesson. And now the play function works as expected, but we
still didn't address one hidden bug, and this one is really dangerous
because it causes a memory leak.
In one of the previous lessons,
I mentioned that the size of an instantiated object is
equal to the size of all of its members.
Virtual functions will cause an additional overhead, but we will ignore that
for now. This instrument object has only one Boolean member, so its size
should be 1 byte. Guitars and synths have only one integer member, but they
also inherit the Boolean from the base class, so their total size should be
5 bytes, at least in theory. When we call the delete on this pointer to an
instrument object, the memory will be correctly de‑allocated.
However,
all of these other calls to the delete operator will only
delete the base part from each of these objects.
They will only delete the Boolean member.
This is because the compiler will still use early
binding to access these objects.
These dangling members from the derived classes will stay in memory
and cause a serious memory leak. To fix this,
we need to do the same thing we did with the play function.
We need to implement late binding, and we can do that by
making the base destructor virtual.
I don't really have anything special to put into this
destructor, so I will just make it default.
We don't have to explicitly define the overridden destructor for each
derived class, because it's not necessary in this case.
So now that we've fixed this leak,
let's talk about why we even need this feature.
If you have a hierarchy of classes like this one,
you can use the base class as an interface.
I will first remove all of the code from the main function. Let's say that I
want to form a band as an array of pointers to each instrument.
This is a small band for the demonstration purposes. It only has two
guitars and one synth, all of them allocated on the heap. Inheritance
and late binding allow me to group derived classes in a single array
of base class pointers. Since it makes no sense to have a general
instrument object,
I will turn this class into an abstract class by removing this
play function definition and assigning it to 0.
This will make this function into a pure virtual function, and every class that
has a pure virtual function is known as an abstract class,
which is another word for an interface in C++. Basically, abstract
classes cannot be instantiated, which in our case, makes sense, since we
don't need an unspecified instrument. And now, I can do something like
create a for loop to loop through all of the instruments and call the
play function on each one of them.
In this short line of code,
we could have had a big orchestra with hundreds of instruments, because
we don't have to call their play function manually.
And I also want to check which instrument needs electricity.
So I will call this check_electricity function inside of the loop.
Let me quickly define this function.
The function will just take in a pointer to an
instrument, and it will print a special message if the
electric Boolean member is set to true.
So, here is another use case for the instrument interface.
If we didn't have this,
we would have to overload this function for every type of instrument, or we
would at least be forced to make a templated function.
And finally, we cannot forget to create another loop to
delete each one of these instruments from the heap.
Preventing memory leaks is important.
Let's compile this.
If we did everything correctly,
we should see the message from each play function and also
the message which informs us that the electric guitar and
synth need to be plugged in.
So if you ever need virtual functions in your classes,
always define a virtual destructor to prevent memory leaks.