OO Concept: Constructors & Destructors

Description

In the OO concept Objects & Classes it was explained that objects are miniature robust programs with a high degree of autonomy. The object takes responsibility for everything that happens to it, from the cradle to the grave. At its birth, a special member function called a constructor, is called, and at its demise, a second member function called a destructor is called.

Constructors

Preparing an Object for Action

A constructor is responsible for preparing the object for action, and in particular establishing initial values for all its data, i.e. its data members. Although it plays a special role, the constructor is just another member function, and in particular can be passed information via its argument list that can be used to initialise it. The name of the constructor function is the name of the class; that's how C++ knows its a constructor.

In Objects & Classes we started to develop a Track class. Suppose we want to be able to create track objects specifying mass and energy, then the first thing to be done is to add the constructor to the class definition file (i.e. the header file):-

class Track {

  Float_t fEnergy;
  Float_t fMass;
  Float_t fMomentum;

  Track(Float_t mass, Float_t energy);
  Float_t GetEnergy();
  void  SetEnergy( Float_t energy);

};
Note the function is called Track. The other odd thing is there is no return value, the user cannot directly call the constructor function. Having declared it, we can now add its definition to the implementation file:-
 
#include "Track.h"

  Float_t Track::GetEnergy() { 
    return fEnergy; 
  }

  void  Track::SetEnergy( Float_t energy ) {
    fEnergy = energy;
    fMomentum = sqrt( fEnergy*fEnergy - fMass*fMass );
  }

  Track::Track(Float_t mass, Float_t energy) {
    fMass = mass;
    SetEnergy(energy);
  }

Remember the Track:: stuck at the front of each function is just telling the compiler that this belongs to (or is in the scope of) Track. The constructor function starts with:-
 
    fMass = mass;
so sets the track's mass. The next line deserves a bit more attention:-
 
    SetEnergy(energy);
SetEnergy is another of the particle's member functions. So in effect the object is sending itself a message. People talking to themselves is generally regarded as a bad thing, but with objects is often a sign of good class design. In this simple case, having already solved the problem of keeping momentum, mass and energy self consistent, we reuse the solution. Its another part of the OO ethic: Only solve a problem once. As the above example shows, when making internal calls like this, the member functions just looks like a normal function. Only if one object wants to call one of its siblings does it have to designate the target object, thus requiring the full object.function syntax.

Calling Constructors

Having defined a constructor, we can now make track objects. The syntax that was illustrated in Objects & Classes:-
Track MyTrack;
Track *MyTrackPtr = new Track;
now becomes:-
Track MyTrack(.135, 1.0);
Track *MyTrackPtr = new Track(.135, 1.0);
Of course we cannot send a object its constructor message, so the argument list is either supplied behind the identifier or the class name. As has been mentioned before,

Member Initialisation

C++ tries to make the syntax of user defined data types (i.e. classes) identical to the built in types where possible. The first form above is allowed for built in types. For example, instead of writing:-
  Int MyInt = 3;
we can write:-
  Int MyInt(3);
so we can treat int as having a constructor that takes a single argument that is an int. Although this notation isn't normally used like that it is often used in constructors. We could write:-
 
  Track::Track(Float_t mass, Float_t energy) {
    fMass = mass;
    SetEnergy(energy);
  }
as:-
 
  Track::Track(Float_t mass, Float_t energy): fMass(mass) {
    SetEnergy(energy);
  }
the assignment has become an initialisation. The colon after the argument list signals this construction. If more than one variable is initialised like this they must be separated by commas. Although the syntax is not exactly intuitive, it does allow the compiler to optimise the code.

The Default Constructor

C++ requires that all classes have a default constructor, that is to say a constructor that can be used without arguments. This is a requirement in case it has to create a temporary object as part of the evaluation of some expression. Remember classes are just an extension of the language, the compiler must have a default way of creating any data type, including user defined ones. If we don't provide a default the compiler will supply one. We could write our own that just creates a null track:-
 
  Track::Track(): fMass(0.), fMomentum(0.), fEnergy(0.) {}
The empty argument list shows this to be the default constructor. All the data members are initialised to zero leaving nothing for the function to do. Odd though this is, it is not an uncommon to see constructors like this. We have glossed over one point. We now have two constructors. Why doesn't the compiler complain - it certainly would if it were FORTRAN. The answer is that, so long as the compiler can decide which to use, its happy. In the examples we have seen in our Track class, its quite easy to tell which is the right one, just by looking at the size argument list. This duplication is called Overloading and is the subject of a later OO concepts topic.

Destructors

Tidying Up

When an object is no longer needed it has to be deleted. Objects created within functions as local variables, i.e. using the form:-
Track MyTrack;
are deleted automatically, once control leaves the innermost compound statement ( i.e. the innermost {..}) that contains the statement. For objects created dynamically:-
Track *MyTrackPtr = new Track;
the object remains until explicitly deleted, although, in the above statement, the pointer MyTrack is a local and will get deleted automatically. To delete a single object the delete operator is applied to a pointer to it:-
delete MyTrack;
To delete an array:-
delete [] MyTracks;
note that the array [] really is empty - the compiler takes care to record the array size.

Whenever an object is deleted its destructor member function is called. Its understandable why constructors are so important, objects must be properly initialised before they can be used, but is it really necessary to have a special member function that gets called when the object is about to disappear? In many cases, including our Track class as developed so far, the answer is no, we could leave the compiler to invent a default no-op one. However suppose our Track object contained a list of detector hits from which it was built. Without going into detail, its likely that this would be some kind of dynamic object owned by the Tack object and accessed via a pointer. Now when it comes time to delete the Track object, we want this list to be deleted, but probably not the hits that it points to! The compiler cannot possibly know, when it comes across a pointer in an object whether it points to something owned by the object and to be deleted as well, or simply something related to, but independent of, the object. So the rule is If an object, during its lifetime, creates other dynamic objects, it must have a destructor that deletes them afterwards. Failure to tidy up like this can leave to orphan objects that just clog up the memory, something that is called a memory leak. Even when a default is acceptable, its a good idea to define a destructor, so we should add the following line to our class definition:-

~Track() {};
As with the constructor, the function does not return a value. The function is called the class name but with a leading ~, which is the C++ complement operator - a bleak reminder that this function is the complement of creation! Currently it does nothing, as shown by the empty {}.

Object Ownership

Object ownership, and knowing when to delete an object is one of the knotty problems in OO. In the FORTRAN memory manager ZEBRA, the rule is very clear, each bank has a single parent, deleting the parent deletes it as well. In OO, objects lead a far more independent life. Its perfectly normal for an object to survive after its "parent", i.e. creator, has died.

Object Lifetimes

Its worth pressing home the point about the range of object lifetimes. If, outside any function, so not local to any, we make the definition:-
Track MyTrack;
then this object is brought into existence at the start of the program, even before the main function starts to execute. It persists until the main function exits. On the other hand, were we to write:-
{ Track MyTrack;}
within a function, then the {..} would ensure that the object would be created and destroyed all within this single line. This means that objects can take every possible role in a program play, from a walk-on part, to a lead character. As examples of these two extremes:-
Go Back to the The C++ Crib Top Page


If you have any comments about this page please send them to Nick West