First Steps: Stack & Heap Objects

Overview

Creating and destroying objects on the stack and the heap. Accessing objects via pointers.

The Lesson

Look up in the Glossary if the terms stack or heap are unfamiliar to you. In the lesson Classes when we created an object by:- Quad my_object(1.,2.,-3.); we were creating an object on the stack. A FORTRAN programmer may be familiar with the idea - its not unlike a local variable in a function or subroutine. Although there are still a few old timers who don't know it, FORTRAN is under no obligation to save local variables once the function or subroutine returns unless the SAVE statement is used. If not then it is likely the FORTRAN will place them on the stack and they will "pop off" when the RETURN statement is reached.

To give an object more permanence it has to be placed on the heap. To demonstrate this start up ROOT and load the Quad class:-

root .L Quad.cxx Now type:-
  Quad* my_objptr = new Quad(1., 2., -3.);
Picking this apart:- Quad* my_objptr (which can also be written Quad *my_objptr) Declares a pointer to Quad called my_objptr. From the syntax point of view, this is just like all the other declarations we have seen so far, i.e. this is a stack variable. The value of the pointer is set equal to:- new Quad(1., 2., -3.); new, despite its looks, is an operator and creates an object or variable of the type that comes next, Quad in this case, on the heap. Just as with stack objects it has to be initialised by calling its constructor. The syntax requires that the argument list follows the type. This one statement has brought two items into existence, one on the heap and one on the stack. The heap object will live until the delete operator is applied to it.

There is no FORTRAN parallel to a heap object; variables either come and go as control passes in and out of a function or subroutine, or, like a COMMON block variables, live for the lifetime of the program. However, most people in HEP who use FORTRAN will have experience of a memory manager and the act of creating bank is a good equivalent of a heap object. For those who know systems like ZEBRA, it will come as a relief to learn that objects don't move, C++ does not garbage collect, so there is never a danger that a pointer to an object becomes invalid for that reason. However, having created an object, it is the user's responsibility to ensure that it is deleted when no longer needed, or to pass that responsibility onto to some other object. Failing to do that will result in a memory leak

To send a message to an object via a pointer to it, you need to use the -> operator e.g.:-

my_objptr->Solve(); Although we chose to call our pointer my_objptr, to emphasise that it is a pointer, heap objects are so common in an OO program that pointer names rarely reflect the fact - you have to be careful that you know if you are dealing with an object or its pointer! Fortunately the compiler won't tolerate an attempt to do something like:- my_objptr.Solve(); although this is a permitted CINT Shortcuts - one that you are strongly advised not to follow!

As we have seen, heap objects have to be accessed via pointers, whereas stack objects can be accessed directly. They can also be accessed via pointers:-

  Quad stack_quad(1.,2.,-3.);
  Quad* stack_ptr = &stack_quad;
  stack_ptr->Solve();
Here we have a Quad pointer that has been initialised with the address of a stack object. Be very careful if you take the address of stack objects. As we shall see soon, they get deleted automatically which could leave you with an illegal pointer. Using it will be corrupt and may well crash the program!

It is time to look at the destruction of objects. Just as its constructor is called when it is created, so its destructor is called when it is destroyed. The compiler will provide a destructor that does nothing if none is provided. We will add one to our Quad class so that we can see when it gets called.

If you are running ROOT then exit and then edit the two files Quad.h and Quad.cxx as shown in red

Quad.h

  class Quad {

  public:
    Quad(Float_t a, Float_t b, Float_t c);
   ~Quad();
    Float_t Evaluate(Float_t x) const;
    void Solve() const;

  private:
    Float_t fA;
    Float_t fB;
    Float_t fC;

  };

Quad.cxx

#include <iostream.h>
#include <math.h>
#include "Quad.h"

  Quad::Quad(Float_t a, Float_t b, Float_t c) {
    fA = a;
    fB = b;
    fC = c;
  }

  Quad::~Quad() {
  cout << "deleting object with coeffts: "
       << fA << "," << fB << "," << fC << endl;
  }

  Float_t Quad::Evaluate(Float_t x) const {
    return fA*x*x + fB*x + fC;
  }

  void Quad::Solve() const {

    Float_t temp = fB*fB - 4.*fA*fC;
    
    if ( temp > 0. ) {
      temp = sqrt( temp );
      cout << "There are two roots: "
           << ( -fB - temp ) / (2.*fA)
           << " and "
           << ( -fB + temp ) / (2.*fA)
           << endl;
    }
    else {
      if ( temp == 0. ) {
        cout << "There are two equal roots: " << -fB / (2.*fA) << endl;
      }
      else {
        cout << "There are no roots" << endl;
      }
    }
    
  }
The destructor is named by the class but with the prefix ~ which is the C++ one's complement i.e. bitwise complement, and hence has destruction overtones! We declare it in the .h and define it in the .cxx. It does not do much except print out that it has been called (still a useful debug technique despite today's powerful debuggers!). Now run root, load the Quad class and create a heap object:- root .L Quad.cxx Quad* my_objptr = new Quad(1., 2., -3.); To delete the object:-
  delete my_objptr;
  my_objptr = 0;
You should see the print out from its destructor. Setting the pointer to zero afterwards isn't strictly necessary (and CINT does it automatically), but the object is no more, and any attempt to use the pointer again will, as has already been stated, cause grief.

So much for heap objects, but how do stack objects get deleted? In C++ a stack object is deleted as soon as control leaves the innermost compound statement that encloses it. So it is singularly futile to do something like:-

{ Quad my_object(1.,2.,-3.); } CINT does not follow this rule; if you type in the above line you will not see the destructor message. As explained in the Macros lesson, you can load in compound statements, which would be a bit pointless if everything disappeared as soon as it was loaded! Instead, to reset the stack you have to type:- gROOT->Reset(); which sends the Reset message via the global pointer to the ROOT object which, amongst its many roles, acts as a resource manager. Start ROOT again and type in the following:- .L Quad.cxx Quad my_object(1.,2.,-3.); Quad* my_objptr = new Quad(4., 5., -6.); gROOT->Reset(); You will see that this deletes the first object but not the second. We have also painted ourselves into a corner, as my_objptr was also on the stack, attempting to do:- my_objptr->Solve(); will fail, CINT doesn't no longer knows what my_objptr is. This is a great example of a memory leak; the heap object exists but we have lost our way to access it. As we shall see in the next lesson Inheritance ROOT and CINT have their own way to resolve this dilemma. In general this is not a problem. If any object will outlive the compound statement in which it was created then it will be pointed to by a more permanent pointer, which frequently is part of another heap object.

Summary