What about sharing code, the other reason for inheritance? In C++, it is possible to use member functions of a base class in its derived class. (You can also share data between a base class and derived classes, but this is a bad idea for reasons I'll discuss later.)
Suppose that I wanted to add a new member function, NumberPushed(), to both implementations of Stack. The ArrayStack class already keeps count of the number of items on the stack, so I could duplicate that code in ListStack. Ideally, I'd like to be able to use the same code in both places. With inheritance, we can move the counter into the Stack class, and then invoke the base class operations from the derived class to update the counter.
class Stack {
public:
virtual ~Stack(); // deallocate data
virtual void Push(int value); // Push an integer, checking for overflow.
virtual bool Full() = 0; // return TRUE if full
int NumPushed(); // how many are currently on the stack?
protected:
Stack(); // initialize data
private:
int numPushed;
};
Stack::Stack() {
numPushed = 0;
}
void Stack::Push(int value) {
numPushed++;
}
int Stack::NumPushed() {
return numPushed;
}
We can then modify both ArrayStack and ListStack to make use the new behavior of Stack. I'll only list one of them here:
class ArrayStack : public Stack {
public:
ArrayStack(int sz);
~ArrayStack();
void Push(int value);
bool Full();
private:
int size; // The maximum capacity of the stack.
int *stack; // A pointer to an array that holds the contents.
};
ArrayStack::ArrayStack(int sz) : Stack() {
size = sz;
stack = new int[size]; // Let's get an array of integers.
}
void
ArrayStack::Push(int value) {
ASSERT(!Full());
stack[NumPushed()] = value;
Stack::Push(); // invoke base class to increment numPushed
}
There are a few things to note:
ArrayStack::ArrayStack(int sz) : Stack()
The same thing applies to destructors. There are special rules for which get called first -- the constructor/destructor for the base class or the constructor/destructor for the derived class. All I should say is, it's a bad idea to rely on whatever the rule is -- more generally, it is a bad idea to write code which requires the reader to consult a manual to tell whether or not the code works!
Note however that I made Stack's data member private, not protected. Although there is some debate on this point, as a rule of thumb you should never allow one class to see directly access the data in another, even among classes related by inheritance. Otherwise, if you ever change the implementation of the base class, you will have to examine and change all the implementations of the derived classes, violating modularity.
ArrayStack *s = new ArrayStack(17);
ASSERT(s->NumPushed() == 0); // should be initialized to 0
Stack *s = new ArrayStack(17);
if (!s->Full()) // ArrayStack::Full
s->Push(5); // ArrayStack::Push
void ArrayStack::Push(int value)
{
...
Stack::Push(); // invoke base class to increment numPushed
}
Of course, if we just called Push() here (without prepending Stack::, the compiler would think we were referring to ArrayStack's Push(), and so that would recurse, which is not exactly what we had in mind here.
Whew! Inheritance in C++ involves lots and lots of details. But it's real downside is that it tends to spread implementation details across multiple files -- if you have a deep inheritance tree, it can take some serious digging to figure out what code actually executes when a member function is invoked.
So the question to ask yourself before using inheritance is: what's your goal? Is it to write your programs with the fewest number of characters possible? If so, inheritance is really useful, but so is changing all of your function and variable names to be one letter long -- "a", "b", "c" -- and once you run out of lower case ones, start using upper case, then two character variable names: "XX XY XZ Ya ..." (I'm joking here.) Needless to say, it is really easy to write unreadable code using inheritance.
So when is it a good idea to use inheritance and when should it be avoided? My rule of thumb is to only use it for representing shared behavior between objects, and to never use it for representing shared implementation. With C++, you can use inheritance for both concepts, but only the first will lead to truly simpler implementations.
To illustrate the difference between shared behavior and shared implementation, suppose you had a whole bunch of different kinds of objects that you needed to put on lists. For example, almost everything in an operating system goes on a list of some sort: buffers, threads, users, terminals, etc.
A very common approach to this problem (particularly among people new to object-oriented programming) is to make every object inherit from a single base class Object, which contains the forward and backward pointers for the list. But what if some object needs to go on multiple lists? The whole scheme breaks down, and it's because we tried to use inheritance to share implementation (the code for the forward and backward pointers) instead of to share behavior. A much cleaner (although slightly slower) approach would be to define a list implementation that allocated forward/backward pointers for each object that gets put on a list.
In sum, if two classes share at least some of the same member function signatures -- that is, the same behavior, and if there's code that only relies on the shared behavior, then there may be a benefit to using inheritance. In Nachos, locks don't inherit from semaphores, even though locks are implemented using semaphores. The operations on semaphores and locks are different. Instead, inheritance is only used for various kinds of lists (sorted, keyed, etc.), and for different implementations of the physical disk abstraction, to reflect whether the disk has a track buffer, etc. A disk is used the same way whether or not it has a track buffer; the only difference is in its performance characteristics.