This article provides an overview of the elements of C++; specifically, the 'C' portion of C++.
Note how section 2.2 describes tokens as the "minimal chunks of a program". The root goal of programming is solving problems using the 'chunks' of a programming language. Of course, the chunks must be appropriate for the type of problems to be solved. Generally, smaller chunks are applicable to many types of tasks, but involve more effort; larger chunks involve less effort, but are designed for more specific tasks.
Inheritance: reusing the interface
By itself, the idea of an object is a convenient tool. It allows you to package data and functionality together by concept,
so you can represent an appropriate problem-space idea rather than
being forced to use the idioms of the underlying machine. These concepts
are expressed as fundamental units in the programming language by using
the class keyword.
It seems a pity, however, to go to all the trouble to create a class and then be forced to create a brand new one that might have similar functionality. It's nicer if we can take the existing class, clone it, and then make additions and modifications to the clone. This is effectively what you get with inheritance, with the exception that if the original class (called the base or super or parent class) is changed, the modified "clone" (called the derived or inherited or sub or child class) also reflects those changes.
(The
arrow in the above UML diagram points from the derived class to the
base class. As you will see, there can be more than one derived class).
A
type does more than describe the constraints on a set of objects; it
also has a relationship with other types. Two types can have
characteristics and behaviors in common, but one type may contain more
characteristics than another and may also handle more messages (or
handle them differently). Inheritance expresses this similarity between
types using the concept of base types and derived
types. A base type contains all of the characteristics and behaviors
that are shared among the types derived from it. You create a base type
to represent the core of your ideas about some objects in your system.
From the base type, you derive other types to express the different ways
that this core can be realized.
For
example, a trash-recycling machine sorts pieces of trash. The base type
is "trash," and each piece of trash has a weight, a value, and so on,
and can be shredded, melted, or decomposed. From this, more specific
types of trash are derived that may have additional characteristics (a
bottle has a color) or behaviors (an aluminum can may be crushed, a
steel can is magnetic). In addition, some behaviors may be different
(the value of paper depends on its type and condition). Using
inheritance, you can build a type hierarchy that expresses the problem
you're trying to solve in terms of its types.
A second example is the classic "shape" example, perhaps used in a computer-aided design system or game simulation. The base type is "shape," and each shape has a size, a color, a position, and so on. Each shape can be drawn, erased, moved, colored, etc. From this, specific types of shapes are derived (inherited): circle, square, triangle, and so on, each of which may have additional characteristics and behaviors. Certain shapes can be flipped, for example. Some behaviors may be different, such as when you want to calculate the area of a shape. The type hierarchy embodies both the similarities and differences between the shapes.
Casting
the solution in the same terms as the problem is tremendously
beneficial because you don't need a lot of intermediate models to get
from a description of the problem to a description of the solution. With
objects, the type hierarchy is the primary model, so you go directly
from the description of the system in the real world to the description
of the system in code. Indeed, one of the difficulties people have with
object-oriented design is that it's too simple to get from the beginning
to the end. A mind trained to look for complex solutions is often
stumped by this simplicity at first.
When
you inherit from an existing type, you create a new type. This new type
contains not only all the members of the existing type (although the private ones
are hidden away and inaccessible), but more importantly it duplicates
the interface of the base class. That is, all the messages you can send
to objects of the base class you can also send to objects of the derived
class. Since we know the type of a class by the messages we can send to
it, this means that the derived class is the same type as the base class.
In the previous example, "a circle is a shape". This type equivalence
via inheritance is one of the fundamental gateways in understanding the
meaning of object-oriented programming.
Since
both the base class and derived class have the same interface, there
must be some implementation to go along with that interface. That is,
there must be some code to execute when an object receives a particular
message. If you simply inherit a class and don't do anything else, the
methods from the base-class interface come right along into the derived
class. That means objects of the derived class have not only the same
type, they also have the same behavior, which isn't particularly
interesting.
You have two ways to differentiate your new derived class from the original base class. The first is quite straightforward: You simply add brand new functions to the derived class. These new functions are not part of the base class interface. This means that the base class simply didn't do as much as you wanted it to, so you added more functions. This simple and primitive use for inheritance is, at times, the perfect solution to your problem. However, you should look closely for the possibility that your base class might also need these additional functions. This process of discovery and iteration of your design happens regularly in object-oriented programming.
Although inheritance may sometimes imply that you are going to add new functions to the interface, that's not necessarily true. The second and more important way to differentiate your new class is to change the behavior of an existing base-class function. This is referred to as overriding that function.
To override a function, you simply create a new definition for the function in the derived class. You're saying, "I'm using the same interface function here, but I want it to do something different for my new type".
Is-a vs. is-like-a relationships
There's a certain debate that can occur about inheritance: Should inheritance override only base-class
functions (and not add new member functions that aren't in the base
class)? This would mean that the derived type is exactly the same
type as the base class since it has exactly the same interface. As a
result, you can exactly substitute an object of the derived class for an
object of the base class. This can be thought of as pure substitution, and it's often referred to as the substitution principle.
In a sense, this is the ideal way to treat inheritance. We often refer
to the relationship between the base class and derived classes in this
case as an is-a relationship, because you can say "a circle is a shape".
A test for inheritance is to determine whether you can state the is-a
relationship about the classes and have it make sense.
There are times when you must add new interface elements to a derived type, thus extending the interface and creating a new type. The new type can still be substituted for the base type, but the substitution isn't perfect because your new functions are not accessible from the base type. This can be described as an is-like-a relationship; the new type has the interface of the old type but it also contains other functions, so you can't really say it's exactly the same. For example, consider an air conditioner. Suppose your house is wired with all the controls for cooling; that is, it has an interface that allows you to control cooling. Imagine that the air conditioner breaks down and you replace it with a heat pump, which can both heat and cool. The heat pump is-like-an air conditioner, but it can do more. Because the control system of your house is designed only to control cooling, it is restricted to communication with the cooling part of the new object. The interface of the new object has been extended, and the existing system doesn't know about anything except the original interface.
Of
course, once you see this design it becomes clear that the base class
"cooling system" is not general enough, and should be renamed to
"temperature control system" so that it can also include heating – at
which point the substitution principle will work. However, the diagram
above is an example of what can happen in design and in the real world.
When you see the substitution principle it's easy to feel like this approach (pure substitution) is the only way to do things, and in fact it is nice if your design works out that way. But you'll find that there are times when it's equally clear that you must add new functions to the interface of a derived class. With inspection both cases should be reasonably obvious.