Once upon a time I was going to, and even
promised to write about the mechanism of virtual functions relative to destructors. Now I finally have free time and I decided to make this venture a reality. In fact, this mini-article serves as a “prologue” to my
next article . But I tried to explain in a clear and understandable way the main points on the current topic. If you feel that you have not yet understood the mechanism of virtual calls, then perhaps you should first read my
previous article .
Immediately, as usual, I’ll make a reservation that: 1) my article does not claim to be a complete presentation of the material; 2) mega-programmers will not learn anything new here; 3) the material is not new and has been described in many books for a long time, but if you don’t read about it explicitly and don’t specifically think about it yourself, you may not even be aware of some points (for a while). I also apologize for the contrived examples :)')
Virtual Destructors
If you already know and know how to use virtual functions, then you just have to know when and why virtual destructors are needed. Otherwise, the following text was written just for you.
The basic rule is:
if you have at least one virtual function in a class, the destructor should also be made virtual . At the same time, we should not forget that the default destructor will not be virtual, therefore it should be declared explicitly. If you do not do this, your program will almost certainly have memory leaks. To understand why, again, a lot of mind is not necessary. Consider a few examples.
In the first case, create a derived class object on the stack:
#include <cstdlib>
#include <iostream>
using std :: cout ;
using std :: endl ;
class A {
public :
A ( ) { cout << "A()" << endl ; }
~A ( ) { cout << "~A()" << endl ; }
} ;
class B : public A {
public :
B ( ) { cout << "B()" << endl ; }
~B ( ) { cout << "~B()" << endl ; }
} ;
int main ( )
{
B b ;
return EXIT_SUCCESS ;
}
It is clear to everyone that the output of the program will be as follows:
A ()
B ()
~ B ()
~ A ()
because the base part of the class is first constructed, then the derivative, and when it is destroyed, the reverse is done — first the destructor of the derived class is called, which, after its work ends, calls the base destructor along the chain. This is right and it should be.
Let us now try to create the same object in dynamic memory, using the pointer to the base class object (the code of the classes has not changed, therefore I only quote the function code main ()):
int main ( )
{
A * pA = new B ;
delete pA ;
return EXIT_SUCCESS ;
}
This time, the object is constructed as it should, and when destroyed, a memory leak occurs, because the destructor of the derived class is not called:
A ()
B ()
~ A ()
This happens because the removal is done through a pointer to the base class and the compiler uses early binding to call the destructor. The destructor of the base class cannot call the destructor of the derivative, because it knows nothing about it. As a result, part of the memory allocated for the derived class is irretrievably lost.
To avoid this, the destructor in the base class must be declared as virtual:
#include <cstdlib>
#include <iostream>
using std :: cout ;
using std :: endl ;
class A {
public :
A ( ) { cout << "A()" << endl ; }
virtual ~A ( ) { cout << "~A()" << endl ; }
} ;
class B : public A {
public :
B ( ) { cout << "B()" << endl ; }
~B ( ) { cout << "~B()" << endl ; }
} ;
int main ( )
{
A * pA = new B ;
delete pA ;
return EXIT_SUCCESS ;
}
Now we get the desired call order:
A ()
B ()
~ B ()
~ A ()
This happens because from now on a late binding is used to call the destructor, that is, when an object is destroyed, a pointer to the class is taken, then the address of the destructor we need is determined from the virtual function table, and this is the destructor of the derived class, which after its work, as expected, causes destructor base. The result: the object is destroyed, the memory is freed.
Virtual Functions in Destructors
Let's first consider the situation with calling virtual functions inside a class. Suppose we have a cat who asks to eat meowing, and then starts the process :) Many cats come in this way, but not Cheshire! Cheshire, as you know, not only smiles forever, is also quite talkative, so we will teach him to speak by overriding the speak () method:
#include <cstdlib>
#include <iostream>
using std :: cout ;
using std :: endl ;
class Cat
{
public :
void askForFood ( ) const
{
speak ( ) ;
eat ( ) ;
}
virtual void speak ( ) const { cout << "Meow! " ; }
virtual void eat ( ) const { cout << "*champing*" << endl ; }
} ;
class CheshireCat : public Cat
{
public :
virtual void speak ( ) const { cout << "WTF?! Where \' s my milk? =) " ; }
} ;
int main ( )
{
Cat * cats [ ] = { new Cat, new CheshireCat } ;
cout << "Ordinary Cat: " ; cats [ 0 ] - > askForFood ( ) ;
cout << "Cheshire Cat: " ; cats [ 1 ] - > askForFood ( ) ;
delete cats [ 0 ] ; delete cats [ 1 ] ;
return EXIT_SUCCESS ;
}
The output of this program will be as follows:
Ordinary Cat: Meow! * champing *
Cheshire Cat: WTF ?! Where's my milk? =) * champing *
Consider the code in more detail. There is a Cat class with a pair of virtual methods, one of which is redefined in the CheshireCat derivative. But all the most interesting happens in the askForFood () method of class Cat.
As you can see, the method only contains calls to the other two methods, however, the speak () construction in this context is equivalent to this-> speak (), that is, the call is made through a pointer, which means the late binding will be used. That's why when calling the askForFood () method through a pointer to CheshireCat, we see what we wanted: the virtual function mechanism works properly even though the call to the virtual method itself occurs inside another class method.
And now the most interesting thing: what happens if you try to use this in the destructor? We are upgrading the code so that during destruction our pets say goodbye, who knows how:
#include <cstdlib>
#include <iostream>
using std :: cout ;
using std :: endl ;
class Cat
{
public :
virtual ~Cat ( ) { sayGoodbye ( ) ; }
void askForFood ( ) const
{
speak ( ) ;
eat ( ) ;
}
virtual void speak ( ) const { cout << "Meow! " ; }
virtual void eat ( ) const { cout << "*champing*" << endl ; }
virtual void sayGoodbye ( ) const { cout << "Meow-meow!" << endl ; }
} ;
class CheshireCat : public Cat
{
public :
virtual void speak ( ) const { cout << "WTF?! Where \' s my milk? =) " ; }
virtual void sayGoodbye ( ) const { cout << "Bye-bye! (:" << endl ; }
} ;
int main ( )
{
Cat * cats [ ] = { new Cat, new CheshireCat } ;
cout << "Ordinary Cat: " ; cats [ 0 ] - > askForFood ( ) ;
cout << "Cheshire Cat: " ; cats [ 1 ] - > askForFood ( ) ;
delete cats [ 0 ] ; delete cats [ 1 ] ;
return EXIT_SUCCESS ;
}
It can be expected that, as in the case of the call to the method speak (), late binding will be performed, but this is not so:
Ordinary Cat: Meow! * champing *
Cheshire Cat: WTF ?! Where's my milk? =) * champing *
Meow-meow!
Meow-meow!
Why? Yes, because when calling virtual methods from the destructor, the compiler uses no later than, and early binding. If you think about why he does this, everything becomes obvious: you just need to consider the order of construction and destruction of objects. Everyone remembers that the construction of the object occurs, starting with the base class, and the destruction goes in a strictly reverse order. Thus, when we create an object of type CheshireCat, the order of calls to constructors / destructors will be as follows:
Cat ()
CheshireCat ()
~ CheshireCat ()
~ Cat ()
If we want to make a virtual call to the sayGoodbye () method inside the ~ Cat () destructor, then we will actually try to contact the part of the object that has already been destroyed.
Moral:
if you have thoughts in your head, select some sort of “stripping” algorithm into a separate method, redefined in derived classes, and then virtually call it in the destructor, nothing will work out for you.