The following section builds on previously introduced concepts and terms. Please review the following as needed:
An inheritance relationship captures some aspects of a real-world problem. But, when we use inheritance in a computer program, it becomes genuinely beneficial. First, let's see what inheritance tells us when we model a real-world problem with object orientation.
A frequently used example is a simple program to draw shapes on a computer screen. As illustrated in the adjacent figure, we could easily represent those shapes with an inheritance hierarchy or tree. When specifying classes that participate in an inheritance relationship, the best practice is to "hoist" all attributes and operations as high in the hierarchy as possible so that they apply to all of the subclasses. For example, both a Circle and a Rectangle have a rendering color and an x and y location on the screen. So, we hoist or lift those attributes to the Shape class. Now Shape, Circle, and Rectangle all have a color and an x, y location - without redefining those attributes in Circle, Rectangle, or any other Shape subclass. In this way, inheritance allows us to reuse code. This practice also means that if we ever need to correct or modify the code, there is only one copy, located in one class, that we must edit, rather than editing code in each subclass.
However, a Rectangle does not have a radius, and a Circle does not have a width or height (yes, we could describe a Circle with a circumscribing rectangle, but we're not going to do that). This observation means that it is not appropriate to hoist radius, width, or height to the Shape class as they do not apply to all Shape subclasses. Inheritance provides a clear statement of how two classes are similar while also clearly stating how those same classes are different. A Circle and a Rectangle are similar in that they both have a color and an x and y location; they are different in that a Circle has a radius while a Rectangle has a width and a height. Similarly, a Shape and a Circle are the same in that a Circle has everything that a Shape has, but a Circle is also distinct in that it has a radius that a Shape does not.
Even though we may not see it just yet, the distinction between which class "owns" some feature is the key to unlocking inheritance for us to use. However, before we turn that key, we must revisit some old concepts, acquire new terms, and clarify their meanings.
Applied to object orientation, encapsulation denotes the practice and mechanisms of packaging data and the functions that operate on it in a way that hides or protects the data from the rest of the program - the only way to access the data is with the packaged functions. In this sense, encapsulation is a synonym for an object (if you have encapsulation, you have an object, and if you have an object, you have encapsulation). Encapsulation also separates the implementation of a function from the class's public interface. In C++, a public interface generally consists of the prototypes for all of the class's public functions (or, more accurately, the class's non-private features). Most C++ documentation includes this information. The documentation and the public interface focus on what a function does but not on how it does it. So, neither generally includes the function's implementation or body.
C++ controls the visibility or accessibility to the parts of a class with three keywords. The first two keywords, public and private
, were introduced in the previous chapter. We delayed our coverage of the protected
keyword until now because it only has meaning in the context of an inheritance relationship.
public
private
private
members, it cannot "see" or access them directly. Instead, the subclass must use the public interface inherited from the superclass.
protected
protected
features are completely invisible and inaccessible to classes that are not subclasses of the defining class (as if the features were private). UML class diagrams denote protected
features with the "#" symbol.Overloaded functions were introduced in chapter 6. Now, we are ready to consider overriding functions, requiring two classes related by inheritance.
A function override takes place when a function in a subclass has exactly the same signature (i.e., the same name, the same argument list, and the same return type) as a function in a superclass. In the case of the Shape and Circle example, we would say that the Circle draw function overrides the Shape draw function. The Circle class inherits everything that the Shape class has, including the draw function, but Circle adds its own draw function, replacing the Shape draw function. When a program calls the draw function through a Circle object, it calls the Circle function instead of the inherited function, but the Circle draw function can call the Shape draw function if needed.
The ability to overload or to override a function is strongly tied to the function's scope. For example, we don't define non-member functions in the scope of any class (although we may declare them in a namespace), so they may be overloaded but not overridden. Requiring overloaded functions to have at least one unique argument provides the compiler with enough information to match a function call with the correct function definition. But when we override functions, we deny the compiler this information because overridden functions must have the same arguments. We'll see a way of overcoming this problem in the polymorphism chapter. The adjacent figure illustrates the main differences between overloading and overriding functions.
Understanding how to chain overridden function calls together is quite simple, but understanding why we want to do so is more difficult. It's easier to understand the purpose of chaining overridden functions if we recall three previous concepts. First, member functions typically use some or all of an object's data to solve some problem. Next, subclasses inherit all the superclasses' features (functions and variables). And finally, subclasses often have member variables that their superclasses do not.
Now, imagine that a superclass has a member function that solves a problem using one or more superclass member variables. Next, imagine that a subclass has an overridden function that solves the same kind of a problem but uses both subclass and superclass variables. Although the subclass inherits its superclass's member variables, it cannot access them if they are private
. One possible way to solve this problem is to make the superclass's variables protected. But, the following example takes a different approach based on using the superclass's public interface by chaining overridden function calls.
SalariedEmployee { private: double salary; public: double calc_pay() { return salary / 24; } }; SalesEmployee : public SalariedEmployee { private: double total_sales; double commission; // percentage of sales private: double calc_pay() { return SalariedEmployee::calc_pay() + total_sales * commission; } }; |
The highlighted expression illustrates how the SalesEmployee calc_pay function calls the SalariedEmployee calc_pay by chaining the function calls. The scope resolution operator specifies the call is to the SalariedEmployee function, avoiding an erroneous recursive call to the SalesEmployee function. The sales employee's commission is added to their salary to calculate their semi-monthly pay. The highlighted expression serves as an example of how to chain together all overridden functions.
Using member functions within an inheritance hierarchy is similar to using them within a single class. The Shape and Circle classes illustrate three "rules" for calling member functions defined in classes related by inheritance.
In summary, a superclass may denote a general kind of thing while its subclasses denote more specialized kinds. For example, a Circle and a Rectangle are specialized Shapes. Following this pattern, a superclass function performs general operations while the subclasses override the function to perform more specific operations. For example, the Shape draw function might set the drawing coordinates and the foreground color, and the subclass draw functions draw a circle or rectangle, respectively. The Circle and Rectangle draw functions call the Shape draw function to perform these initial operations and finish by drawing the specific, specialized shapes. Implementing the subclass draw functions this way eliminates duplicating the general code in the Circle and Rectangle draw functions.