Section 2 - Deriving a FloatStack Class

4B.2.1 Extending the Stack Class

Next we derive a FloatStack class from the Stack class.  This has some medium-sized logic in it, but what we do here will be easy to duplicate for other data types like ints, Galaxies, Cards, etc.

4B.2.2 The FloatStack Class Prototype

Here it is.  There is not much to see:

class FloatStack : private Stack
{
public:
  ~FloatStack();  // needed this time
  void push(float x);
  bool pop(float &f);
};

As you see, there is no new data.  We only override the push() and pop() methods of the base class.  And look:  In this higher level class, we are giving our client simple floats to push and pop. Of course!  Our client does not need to know anything about StackNodes or FloatNodes.  So they will push a float and pop a float.  Nice.

This means, however, that it will be our job to create and delete FloatNodes if we are going to make such tasks transparent to our client.  It isn't that hard.  We actually know everything we need to make this work.

About the use of the word private in

class FloatStack : private Stack

Typically we use the word public in this position, but we can use private here to create a firewall.  This means that all members and methods declared public in the base class Stack, will be private in our new class FloatStack; the client that uses a FloatStack object will not be able to gain access to the inherited public methods Stack::push(StackNode*) and Stack::pop(StackNode*).  This seals off the security leak that I spoke of earlier.  Try to get a client to mess up your FloatStack by any combination of type-casts or naughty StackNodes.  You won't be able to do it (and if you can, let me know so I can figure out how plug the hole you found!).  This is one reason why the security concerns of the base class were really non-starters.

4B.2.3 The FloatStack Destructor, push() and pop() Definitions

Here they are:

// FloatStack method definitions --------------------------

void FloatStack::push(float x)
{
  // create a new FloatNode
  FloatNode *fp = new FloatNode(x);

  // push the StackNode onto the stack (base class call)
  Stack::push(fp);
}

bool FloatStack::pop(float &f)
{
  f = 0.0F;      // just in case of trouble

  // pop a node
  FloatNode *fp = (FloatNode *)Stack::pop();
  if (fp == NULL)
    return false;

  // copy the data into the formal parameter
  f = fp->getData();

  delete fp; // delete the node (we're done with it)
  return true;
}

FloatStack::~FloatStack()
{
  // because we are creating our own StackNodes internally, we
  // do have to destroy any lingering nodes here.
  StackNode *np;

  while (np = Stack::pop())
    delete np;
}
// end FloatStack method definitions ---------------------

Let's take them individually.

push()

We are accepting an ordinary float parameter from our client. In order to push it we have to build a new FloatNode around it.  That is the first line of the push() method.  The second, and final, line of that method is to push that node onto the stack.  Our base class StackNode gives us a push() operation already, so we call that one by using the base class name and scope resolution operator so that the compiler knows we mean the base class push (Stack::push()).  It's that simple.

pop()

We need to pass back a float through the formal parameter, f.  Because we might have an error (due to an empty stack), we first prepare f with a 0.0, just in case of a premature return. (Do you know what the F at the end of 0.0F is?  You should remember from CS 2A.  I'll let you look it up.)

Next we pop() a StackNode off the Stack.  Again, we have to call the pop() method of the base class, which will produce a StackNode pointer for us, then type coerce it to a FloatNode pointer.  If that pointer was NULL, then the stack was empty, so we return a false to our client.

Caution

The call, Stack::pop(), while perfectly valid, is not function chaining (or method chaining). For a non-constructor method to demonstrate method chaining, it would have to call the overridden method, which has the same signature as the overriding derived class method. (See the section on Method Chaining, below.) Nevertheless, Stack::pop() is a perfectly good way to reference a base class method from a derived class method. In this case, it happens to be an overloaded (not overriding) method that calls one of its base class overloaded cousins.

Otherwise, we received a valid FloatNode from the pop() operation which means we can read its value from its data member via getData().  Once we copy that value into f (which will be handed back to the client, since it is a reference parameter), we must do one last important thing: We must delete the FloatNode we just popped. Once we pop a node off the stack, it is used up and we have no need for it. If we forget to delete it, we will have a memory leak.

Destructor

In the base class, we did nothing in the destructor (other than declare it virtual, whatever that means!)  The reason, we recall, was that the client was creating the nodes and therefore the class has no business deleting them.  It is bad form to instantiate nodes at the higher level and delete them at the lower level.  In our new, derived, class, FloatNode, the client is not creating nodes - the client has no knowledge of StackNodes or FloatNodes.  The client created a FloatStack and is pushing and popping floats, (not nodes) onto the stack.  That means our FloatStack class is (as you have seen) creating nodes in push() and deleting them in pop().  And what happens if the program ends or the FloatStack goes out-of-scope with lingering FloatNodes still un-popped?  We must delete them or they will be locking up memory - this is a memory leak!  Thus, we have a destructor that loops through the remaining nodes on the stack and deletes them each.

Now we come to the most subtle detail of this method.  We are using a StackNode*, not a FloatNode*, to pop and delete the nodes in the destructor's loop.  We already know that we are allowed to use a base class pointer to point to a derived object.  So this is fine.  What may be less clear is that if we call delete using a StackNode* we will be deleting the full FloatNode (and not just the underlying StackNode).  As long as the base class destructor is declared to be virtual, statements like

   delete np;

will call the FloatNode destructor if np happens to be pointing to a derived class.  (The distinction is not terribly important now, but it will be in future programs.)

This is how we derive a FloatStack from a generic Stack.  This might seem complicated, but the thing to focus on is that you do not have to think about next pointers, linked lists or any details of the push() and pop() operation from the underlying Stack class.  They will just "work" perfectly. That was the Stack designer's job. Our job is to convert the float data to nodes and back for our client.

Finally, we see how the client uses this great new class of ours.

4B.2.4 Non-Constructor Method Chaining

In the case of a derived class method which overrides its base class counterpart, one can chain to the base class method, but not using the "method() : Base::method() { ... }" notation that constructor chaining uses. Rather, we simply make an explicit call to the base class method of the same name and signature. A common examples is a toString() method:

string BaseClass::toString()
{
  return "hi mom.";
}

string SubClass::toString()
{
  return BaseClass::toString() + " hi dad.";
}

The following is not method chaining but merely a demonstration of a derived class method calling a base class method which has a different signature than the derived method calling it:

string BaseClass::toString(string header)
{
  return header + "\nhi mom.";
}

string SubClass::toString()
{
  return BaseClass::toString("My Message ----") + " hi dad.";
}