Let me use our Stack example to illustrate the first of these. Our Stack implementation above could have been implemented with linked lists, instead of an array. Any code using a Stack shouldn't care which implementation is being used, except that the linked list implementation can't overflow. (In fact, we could also change the array implementation to handle overflow by automatically resizing the array as items are pushed on the stack.)
To allow the two implementations to coexist, we first define an abstract Stack, containing just the public member functions, but no data.
class Stack {
public:
Stack();
virtual ~Stack(); // deallocate the stack
virtual void Push(int value) = 0;
// Push an integer, checking for overflow.
virtual bool Full() = 0; // Is the stack is full?
};
// For g++, need these even though no data to initialize.
Stack::Stack {}
Stack::~Stack() {}
The Stack definition is called a base class or sometimes a superclass. We can then define two different derived classes, sometimes called subclasses which inherit behavior from the base class. (Of course, inheritance is recursive -- a derived class can in turn be a base class for yet another derived class, and so on.) Note that I have prepended the functions in the base class is prepended with the keyword virtual, to signify that they can be redefined by each of the two derived classes. The virtual functions are initialized to zero, to tell the compiler that those functions must be defined by the derived classes.
Here's how we could declare the array-based and list-based implementations of Stack. The syntax : public Stack signifies that both ArrayStack and ListStack are kinds of Stacks, and share the same behavior as the base class.
class ArrayStack : public Stack { // the same as in Section 2
public:
ArrayStack(int sz); // Constructor: initialize variables, allocate space.
~ArrayStack(); // Destructor: deallocate space allocated above.
void Push(int value); // Push an integer, checking for overflow.
bool Full(); // Returns TRUE if the stack is full, FALSE otherwise.
private:
int size; // The maximum capacity of the stack.
int top; // Index of the lowest unused position.
int *stack; // A pointer to an array that holds the contents.
};
class ListStack : public Stack {
public:
ListStack();
~ListStack();
void Push(int value);
bool Full();
private:
List *list; // list of items pushed on the stack
};
ListStack::ListStack() {
list = new List;
}
ListStack::~ListStack() {
delete list;
}
void ListStack::Push(int value) {
list->Prepend(value);
}
bool ListStack::Full() {
return FALSE; // this stack never overflows!
}
The neat concept here is that I can assign pointers to instances of ListStack or ArrayStack to a variable of type Stack, and then use them as if they were of the base type.
Stack *s1 = new ListStack;
Stack *s2 = new ArrayStack(17);
if (!stack->Full())
s1->Push(5);
if (!s2->Full())
s2->Push(6);
delete s1;
delete s2;
The compiler automatically invokes ListStack operations for s1, and ArrayStack operations for s2; this is done by creating a procedure table for each object, where derived objects override the default entries in the table defined by the base class. To the code above, it invokes the operations Full, Push, and delete by indirection through the procedure table, so that the code doesn't need to know which kind of object it is.
In this example, since I never create an instance of the abstract class Stack, I do not need to implement its functions. This might seem a bit strange, but remember that the derived classes are the various implementations of Stack, and Stack serves only to reflect the shared behavior between the different implementations.
Also note that the destructor for Stack is a virtual function but the constructor is not. Clearly, when I create an object, I have to know which kind of object it is, whether ArrayStack or ListStack. The compiler makes sure that no one creates an instance of the abstract Stack by mistake -- you cannot instantiate any class whose virtual functions are not completely defined (in other words, if any of its functions are set to zero in the class definition).
But when I deallocate an object, I may no longer know its exact type. In the above code, I want to call the destructor for the derived object, even though the code only knows that I am deleting an object of class Stack. If the destructor were not virtual, then the compiler would invoke Stack's destructor, which is not at all what I want. This is an easy mistake to make (I made it in the first draft of this article!) -- if you don't define a destructor for the abstract class, the compiler will define one for you implicitly (and by the way, it won't be virtual, since you have a really unhelpful compiler). The result for the above code would be a memory leak, and who knows how you would figure that out!