EECS 211
How to read Chapters 12 and 13

Chapters 12 and 13 are about class hierarchies and inheritance. Chapter 12 takes a long time and a lot of code to make a few points. Their goal was to show how classes can be used to refactor shared code in several classes into superclasses. But while this demonstration might work interactively, as static text is obscures rather than reveals the key ideas.

Here's the C++ class hierarchy for input output streams. It's very abbreviated. It omits some types of streams and only shows a few example member functions for each stream, with no parameters or return types. A much more detailed version of this hierarchy can be found at the cplusplus.com web site.

stream hierarchy

In this hierarchy, ios is a superclass of istream and ostream. Since istream is a superclass of ifstream, by transitivity, ios is also a superclass of ifstream. We can also say that istream is a subclass of ios, and so on. Notice that iostream has two superclasses. This is called multiple inheritance. It allowed in C++ and Lisp, but not in languages like Smalltalk and Java.

How do you say one class is a superclass of another? Like this:

class istream : public ios { ... }

(The above is a lie. Streams are actually template classes, so things are more complicated. Ditto for examples below.)

Inheritance

What are hierarchies good for? One of their primary uses is to share code among classes. In the case of streams, all I/O streams will need functions for keeping track of whether the stream is still in a usable (good) state. Rather than define such methods in every kind of stream, the C++ library designers created the ios class to hold code for this and other common operations. Similarly, all input streams will need member functions for reading, and all output streams will need member functions for writing, so they created classes for those streams.

A subclass inherits the members of all its superclasses that are declared public or protected. Therefore, even though it looks like iostream has no member functions, it in fact has all the member functions of all its superclasses.

The point of protected is to let you define member functions can only be accessed by the class and its subclasses.

If a subclass defines its own code for a member function defined in a superclass, that definition overrides the inherited function. The subclass code can call the superclass version of the function using the scope operator, e.g., the code in istream::eof() could call ios::eof() to run the superclass version of that function.

Constructors and Destructors

When an instance of a class is constructed, the superclasses are constructed first. This is to make sure that the subclass constructor has all the parts it needs before adding its own parts.

When an instance of a class is destroyed, its destructor is called first, and then the destructors of its superclasses are called. In other words, destructors are called in the opposite order of constructors.

When C++ constructs a class, it will call the default (no argument) constructors of its superclasses, unless the class constructor explicitly calls a different superclass constructor in the initialization list. For example:

istringstream::istringstream( const char * s ) : istream(), ... { ... }

Polymorphism

You can store an instance of a class in a variable whose type is either that class or a superclass. Thus you could have an instance of an ifstream in a variable of type istream. Let's call that variable in. This raises the question of what member functions can you call using in. Can we write in.open() because it's really an ifstream?

In C++, the default answer is no. With a variable of type T you can only access the members defined (or inherited) by T, even if the variable actually holds a more specific object. This is called static dispatch

You could override this by using typecasting, but there's an alternative.

Then inPtr->get() will call istringstream::get(), not istream::get(). That is, the member function called will be determined at run-time by the type of the instance. Normally the function called is determined at compile-time by the type of the variable.

That may sound complicated but it's the normal default behavior in languages like Smalltalk, Java, and C#. In those languages, all variables with instances actually have pointers to instances, and any call to a member function refers to the one defined for the object's actual type, if present.

Polymorphism is very useful. It allows us to have an array of pointers to instances of objects, say objects in a video game, and tell each object to draw() itself, and each object will call the appropriate drawing code, even though the type of the array is something very generic, like Shape * objects[10].