10.2.1. Building Inheritance

Time: 00:04:00 | Download: Large, Large (CC), Small | Streaming (CC) | Slides (PDF)

The following section builds extensively on previously introduced concepts and terms. Please review the following as needed:

Three UML class symbols connected by inheritance: Person at the top, <kbd>Actor</kbd> in the middle, and Star at the bottom. Person has one attribute: -name:string, and one constructor: +Person(n:string). <kbd>Actor</kbd> has one attribute: -agent:string, and one constructor: +Agent(n:string, a:string). Star has one attribute: -balance:double, and one constructor: +Star(n:string, a:string, b:double).
A simple inheritance hierarchy. Inheritance hierarchies may be arbitrarily tall, and they may be arbitrarily wide at any level. In tall hierarchies, a class may be both a subclass (Actor is a subclass of Person) and a superclass (Actor is a superclass of Star).

Building a complete, working inheritance relationship requires two steps. First, programmers must establish the inheritance relationship between a pair of classes, and second, they must initialize the objects created by instantiating the subclass. Implementing inheritance takes place as a part of the subclass specification. Initialization means storing the first or initial values in the objects' member variables. If the initial value is a constant or otherwise known at compile-time, assign it to the member variable in the class specification. In more complex or dynamic situations, object initialization occurs through constructor calls. The Actor example at the right will provide a concrete context to illustrate these elements.

Implementing Inheritance

The notation for implementing inheritance consists of three parts:

  1. a colon
  2. the keyword "public"
  3. the name of the superclass

The keywords "protected" and "private" can replace "public," but this is never done in practice because a whole-part relationship is more clear and better achieves the same results.

Inheritance is unidirectional, from the subclass to the superclass - the subclass "knows about" the superclass. Consequently, the syntax implementing inheritance appears in subclasses but not superclasses. In terms of the Actor example, only the Actor and Star classes require any inheritance syntax, highlighted in the following figure.

class Actor : public Person {...} class Star : public Actor {...}
Implementing inheritance in C++. The C++ syntax implementing an inheritance relationship is part of the class specification.
  1. Actor is a subclass of Person - an Actor inherits all of the features of a Person
  2. Star is a subclass of Actor, which means that Star is also a subclass of Person - a Star inherits all the features of an Actor and all the features of a Person
The ellipses denote code omitted for simplicity.

The full class specification for a superclass must precede the specification for any subclass. Although uncommon, programmers can put all class specifications in the same file. In that case, the specifications must appear in UML inheritance hierarchy order - superclass at the top and subclasses below (the reverse direction of the inheritance arrows). The Actor 1 example demonstrates this ordering. More commonly, programmers put each class specification in a separate header file. In that case, they must #include the superclass header file above the subclass specification, appropriately "protected" by a #pragma or #ifndef. The Actor 2 example demonstrates this common practice.

Initializing Inheritance

In all but the most simple classes, initializing a newly constructed object is the task of a constructor function. When a program instantiates a subclass object, it simultaneously instantiates a superclass object, which is initialized or constructed by a constructor function. So, how do we call the superclass constructor when creating an instance of a subclass? There are two possible situations, each requiring a different answer to the original question.

Inheritance With Default Constructors

The first and most simple case is where the superclass has a default constructor that appropriately initializes the superclass object for the inheritance relationship. In this case, the subclass constructor automatically and implicitly calls the superclass default constructor. Although this special case is simple, it does have two options.

Superclass ConstructorSubclass Constructor
class Alpha
		Alpha() { . . . }

class Beta : public Alpha
		int length;
		Beta() : Alpha(), length(20) {}	// (a)
		Beta() : length(20) {} 		// (b)
Calling a superclass default constructor. There is an inheritance relationship between Alpha and Beta, where Beta is a subclass of Alpha. The second code fragment shows two alternative (i.e., mutually exclusive) ways of writing the Beta constructor. (The two functions are mutually exclusive because they are not properly overloaded - their argument lists are identical - but either will work.)
  1. The Beta constructor clearly and explicitly calls the Alpha constructor (in yellow). While this notation is correct, programmers don't typically use it because the compiler automatically calls default constructors.
  2. The Beta constructor implicitly calls the Alpha default constructor. This notation is the most commonly used.

Inheritance With Non-Default Constructors

The second case deals with the more common and more general situation where the superclass either does not have a default constructor or another constructor is in some way better suited to the problem than the default. In this case, initializing inheritance requires explicitly calling a parameterized constructor. C++ does this through the subclass's initializer list. The notation creates a chain of constructor calls that pass some argument data from the subclass constructor to the superclass constructor.

Person Constructor
Person(string n) : name(n) {}
Two arrows. One arrow points from the Person constructor, through the Actor constructor, down to the Star constructor. The arrow indicates that the constructors are defined downward from the superclass to the last subclass. The second arrow points upwards from the Star constructor to the Person constructor, suggesting that the constructor calls run from the last subclass to the superclass.
Actor Constructor
Actor(string n, string a) : Person(n), agent(a) {}
Star Constructor
Star(string n, string a, double b) : Actor(n, a), balance(b) {}
Constructor call chains. The three constructors and their initializer lists for the Actor example. The bullets list the constructors in definition order: However, the constructor function calls run in the opposite direction: The Star constructor calls the Actor constructor, and the Actor constructor calls the Person constructor. Notice that the highlighted code fragments are function calls, so the type and the number of arguments in the calls must match the type and number of parameters in the function signatures.
InheritanceInitializer List
class Actor : public Person Actor(string n, string a) : Person(n), agent(a) {}
Inheritance vs. initializer lists. The syntax used to implement inheritance is similar to the syntax used to introduce an initializer list: both begin with a colon. You can distinguish the two by noting that inheritance is a part of the class specification - when it is present, it is specified right after the keyword class. On the other hand, initializer lists are always part of a constructor function. Furthermore, when an initializer list calls a superclass constructor, the call must be the first element in the list:
: Actor(n, a), balance(b)

Variable Names

Constructors and setters are special functions used, at least in part, to initialize or update member variables. Naming the function parameters after the member variables they initialize is convenient. A standard naming convention adds the letter 'a' at the beginning of the member variable name to form the corresponding parameter name - the letter 'a' distinguishes the parameter (or argument) from the member variable with the same name when both are in the same scope. By chaining constructor calls, programs can pass data from subclass constructors to superclass constructors. If there is no scope conflict between the parameter and the member names, they may be the same.

Person Constructor
Person(string a_name) : name(a_name) {}
Actor Constructor
Actor(string name, string a_agent) : Person(name), agent(a_agent) {}
Star Constructor
Star(string name, string agent, double a_balance) : Actor(name, agent), balance(a_balance) {}
Constructor parameter names. The variable names appearing in a constructor's initializer list are not arbitrary. Programmers may choose the constructor parameters' names, but once chosen, programmers must use the names consistently throughout the function, including the initializer list. Review the classes in Figure 1 and notice that Person has a member variable called "name" while Actor has a member called "agent." The Actor constructor illustrates the consistent use of variable names: I often use single-character variable names in the textbook examples to simplify them and save space on the page. But, when writing "real" code, choosing names that better suggest what the variable does is good practice.

Data Flow And Constructor Calls

Classes and their constructors are specified downward from the top of the inheritance hierarchy (see Figure 1). However, constructors are called and run from the subclass upward in the hierarchy when the program instantiates a subclass. Each subclass constructor calls its superclass constructor. The call can pass some of its arguments to the superclass while retaining some for its class's member variables. The following figure illustrates the data passing sequence.

A picture illustrating how data flows through a sequence of chained constructor calls. The example lists the constructors in specification order - from top to bottom - followed by a call to the Star constructor. An arrow pointing upwards indicates that the constructors run upward from the subclass to the superclass. The execution sequence begins when a program instantiates an object from the Star class, triggering a call to the Star constructor. Arrows illustrate data beginning as three arguments in the Star constructor function call. The Star constructor calls the Actor constructor from an initializer list. Arrows from the Star constructor denote it passing two parameters to the Actor constructor's parameters. The Actor constructor calls the Person constructor from an initializer list. A single arrow indicates the Actor constructor passing one parameter to the Person constructor, initializing the Star's balance.
Data flow in chained constructor calls. The arrows indicate the direction that data, which originates in the function call at the bottom (1), flows upwards through the constructors, the initializer lists, and finally into the member variables.
  1. The chain begins when the program instantiates an instance of Star (big_star) and passes three values into the constructor.
  2. The Star constructor uses one parameter, b, to initialize its member variable, balance, and passes the values in the remaining two parameters, n and a, to the Actor constructor
  3. The Actor constructor uses one parameter, a, to initialize its member variable, agent, and passes the value in the remaining parameter, n, to the Person constructor.
  4. The Person constructor uses the parameter, n, to initialize its member variable, name.