© Springer International Publishing AG, part of Springer Nature 2017
Joe Pitt-Francis and Jonathan WhiteleyGuide to Scientific Computing in C++Undergraduate Topics in Computer Sciencehttps://doi.org/10.1007/978-3-319-73132-2_7

7. Inheritance and Derived Classes

Joe Pitt-Francis1   and Jonathan Whiteley1
(1)
University of Oxford, Oxford, UK
 
 
Joe Pitt-Francis

In Sect. 6.​1.​1, we explained how object-oriented programming allowed for a more reliable programming paradigm than was possible using modules. One reason for this, which we touched on briefly, is the availability of inheritance . Inheritance allows us to extend the functionality of a class by introducing a new class, known as the derived class , that contains all the features of the original class, known as the base class.

7.1 Inheritance, Extensibility and Polymorphism

Perhaps the most important feature of object-oriented programming is inheritance . This concept allows the functionality of classes to be built into a “family tree”. The data, operation and functionality of a given class (the base class, sometimes called the parent class) may be directly reused, extended and modified in another class (the derived or child class). The operation of one base class can be inherited by several derived classes.1 In turn, these derived classes may become the base classes of further inheritance, giving rise to further generations.

Suppose we have written a class that allows us to solve linear systems. Suppose further that we now want to write a class for solving linear systems that may be used only when the matrix in the linear system is symmetric and positive definite, thus allowing us to solve the system using the very effective conjugate gradient technique discussed in Sect. A.​2.​3. Much of the functionality required—such as specifying the vectors, matrix and tolerance, and providing a function for calculating the scalar product between two vectors—will already be implemented in the class that has been written to solve more general linear systems. Inheritance allows us to write a new class for solving a special category of linear systems that uses—or inherits—all features of the class for solving general linear systems. If we wanted to extend the functionality of the class that uses the conjugate gradient scheme to include Successive Over–Relaxation (SOR),2 we simply inherit again so that the SOR variant is a grandchild derived class of the original.

Inheritance gives rise to two important concepts first mentioned in Sect. 1.​1.​1: extensibility and polymorphism . Extensibility is the idea, not just that the code can be extended, but that it can be extended easily, and without changing any of the original functional behaviour of the base class. Polymorphism is the ability to perform the same operations on a wide variety of different types of objects. So, for example, the Solve method of the generic linear solver outlined above will perform a certain set of operations. This method of the base class is then redefined in a derived class for symmetric, positive definite matrices, without changing its arguments. At run-time, the program is able to detect which object it has and therefore which version of Solve to run. This version of polymorphism is also known as dynamic polymorphism or run-time polymorphism.

7.2 Example: A Class of E-books Derived from a Class of Books

We now demonstrate the basic features of inheritance through extending the class of books developed in Sect. 6.​2. Suppose the owner of a bookshop also runs a website where she not only sells traditional (paper) books, but also electronic e-books. The advantage of the e-book over a traditional book is that it does not need to be parcelled up and sent through the mail. The e-book may be delivered by giving the customer access to a private URL from which they may download it. The bookseller may wish to update her computer system so that a URL attribute is added to each instance of her e-books. She could do this by deriving a class Ebook from the class Book given in Listings 6.​6 and 6.​7. The class Ebook will have the same members as the class Book, but with two differences. The first difference is that the class member format will be set to “electronic”. The second difference is that instances of the class Ebook will have an additional class member hiddenUrl that contains the private URL. The header file for this class is given below.

As the class Ebook is derived from the class Book, we include the header file for the class Book in the header file for the class Ebook below. Line 7 of this listing specifies that the class Ebook is indeed derived from the class Book, and the word “public” in this line has the effect that:
  1. 1.

    public members of Book are public members of Ebook;

     
  2. 2.

    protected members of Book are protected members of Ebook; and

     
  3. 3.

    private members of Book are hidden from Ebook, and so may not be used by the derived class.

     
This is known as public inheritance . We will discuss access privileges for derived classes in more detail in Sect. 7.3.
../images/218075_2_En_7_Chapter/218075_2_En_7_Figa_HTML.gif
Based on the discussion above, all public and protected members of the class Book defined in Listing 6.​6 are available to instances of the class Ebook. This has the possibly unintended effect that the member mYearOfPublication is not directly available to the derived class Ebook, as this member is private and therefore not available to the derived class. This member is, however, still available indirectly through the public methods of the base class SetYearOfPublication and GetYearOfPublication—as these members are public they are available to the derived class, and can be used to access the member mYearOfPublication. The other difference between the derived class and the parent class is that we have declared two additional members in the listing above: an overridden default constructor, and a string member representing the hidden URL.
The overridden default constructor is given below, where the format is set to “electronic” as required. Note the syntax for overridden default constructors below: this allows the default constructor for the base class Book to be called first, setting the author, the title, and the publisher to “unspecified”. The format is then set to “electronic” inside the overridden default constructor for the derived class.
../images/218075_2_En_7_Chapter/218075_2_En_7_Figb_HTML.gif
Example code using the class Ebook is given below. Note that the member format of an instance of the class Ebook is automatically set to electronic.
../images/218075_2_En_7_Chapter/218075_2_En_7_Figc_HTML.gif
Figure 7.1 shows, in schematic form, a representation of how the class Ebook relates to its parent class Book. This representation is given in the Unified Modelling Language (UML) format where each class is shown as a box. Space inside each box is divided into three components: the class name, a list of the data contained in the class and a list of the class methods. A + sign signifies data and methods which are public. Private data or methods (mYearOfPublication in this case) carry a  sign, while protected members would be given a # sign.
../images/218075_2_En_7_Chapter/218075_2_En_7_Fig1_HTML.gif
Fig. 7.1

An inheritance graph, showing that Ebook is derived from the Book base class

The arrow between the boxes shows the child–parent inheritance relationship. The reason for the repetition of “+ Book()” in the base class is to show that Book has three different constructors: the default constructor, the copy constructor and a specialised Book constructor for setting the title attribute. These three constructors were introduced in Sect. 6.​2.​7. Ebook has only one constructor which is the overridden default (no argument) constructor given above which sets the format attribute.

7.3 Access Privileges for Derived Classes

When developing a class, we specify all class members as being public, protected or private members. When a class is derived from this base class, we need to know what access privileges the members of the base class have in the derived class. In the class Ebook that we derived from the class Book in Sect. 7.2, we used public inheritance in line 7 of Listing 7.1. There are two other types of inheritance: protected inheritance; and private inheritance. These three different types of inheritance determine the access privileges of the base class members in the derived class. In Table 7.1, we state these access privileges.
Table 7.1

Access privileges for derived classes

Access privilege in base class

Type of inheritance

Public

Protected

Private

Public

Public

Protected

Private

Protected

Protected

Protected

Private

Private

Hidden

Hidden

Hidden

7.4 Classes Derived from Derived Classes

We may derive classes from classes that are themselves derived classes, as discussed in Sect. 7.1. If Class2 is derived from Class1, we may derive a new class Class3 from Class2 in exactly the same way as in Sect. 7.2, as shown in the header file for Class3 shown below.
../images/218075_2_En_7_Chapter/218075_2_En_7_Figd_HTML.gif

7.5 Run-Time Polymorphism

Polymorphism may be used when a number of classes are derived from the base class, and for some of these derived classes we want to override one—or more—of the methods of the base class. Suppose we have developed a class of guests who stay at a hotel. This class will include members such as name, room type, arrival date, number of nights booked, and a member method that computes the total bill. It is likely that the hotel has negotiated special nightly rates for individuals from particular organisations. To reflect this, the method that computes the total bill must act differently on guests from these organisations. This may be incorporated into software in a very elegant manner through the use of virtual methods where the method does different things for different derived classes. This is implemented by the use of the virtual keyword, shown in the header file for the class of hotel guests shown below. The virtual keyword is a signal to the compiler that a method has the potential to be overridden by a derived class.
../images/218075_2_En_7_Chapter/218075_2_En_7_Fige_HTML.gif
The implementation of the method CalculateBill is given in the listing below, where the total bill is given by multiplying the number of nights that a guest stayed in the hotel by a nightly rate of £50, and adding the telephone bill to this figure. Even though this method is a virtual method, it is written in exactly the same way as if it were not declared as virtual.
../images/218075_2_En_7_Chapter/218075_2_En_7_Figf_HTML.gif
Suppose now that the hotel have negotiated a deal with a company that reduces the room rate to £45 for the first night that a guest stays, and £40 for subsequent nights, and offers free telephone calls. This may be implemented by deriving a class SpecialGuest from the class Guest as shown below.
../images/218075_2_En_7_Chapter/218075_2_En_7_Figg_HTML.gif
The method CalculateBill for this derived class is then implemented using the code below.
../images/218075_2_En_7_Chapter/218075_2_En_7_Figh_HTML.gif
Note that declaring the member method CalculateBill as virtual in the class Guest does not require that the method must be overridden (redefined) in derived classes: it simply gives us the option to override it.
The real power of run-time polymorphism can be seen when we use only pointers to the base class in a family tree of objects. It might not be obvious what the exact type of each object in our program is, but the run-time system is able to find out. In the following code, there are three pointers to Guest objects, but one of them is in actuality a SpecialGuest and therefore has a reduced bill. One might imagine a larger-scale program running over an array of Guest pointers—representing those guests who are checking out—each of which has their own mechanism for calculating the bill. The programmer does not need to be aware which of these Guest objects might be actually be a SpecialGuest.3
../images/218075_2_En_7_Chapter/218075_2_En_7_Figi_HTML.gif

7.6 The Abstract Class Pattern

Suppose we want to write an object-oriented program for calculating the numerical solution of initial value ordinary differential equations of the form
$$\begin{aligned} \frac{{\mathrm{d}y}}{{\mathrm{d}t}} = f\left( {t, y} \right) ,\quad \quad y(T_0 ) = Y_0 , \end{aligned}$$
where f(t,y) is a given function, and T$$_{0}$$, Y$$_{0}$$ are given values. Many methods exist for calculating the numerical solution of equations such as these, for example, the forward Euler method, Heun’s method, various Runge–Kutta methods, and various multistep methods. One way of implementing these numerical methods would be to write a class called AbstractOdeSolver that has members that would be used by all of these numerical methods, such as variables representing the stepsize and initial conditions, a method that represents the function f(t, y) on the right-hand side of the equation above, and a virtual method SolveEquation for implementing one of the numerical techniques described above. We would then implement each of the numerical methods using a class derived from AbstractOdeSolver, and overriding the virtual function SolveEquation. The derived classes would then contain members that allow a specific numerical algorithm to be implemented, as well as the members of the base class AbstractOdeSolver that would be required by all of the numerical solvers.
Using the class structure described above, the base class AbstractOdeSolver would not actually include a numerical method for calculating a numerical solution of a differential equation, and so we would not want to ever create an instance of this class. We can automatically enforce this by making AbstractOdeSolver an abstract class. This is implemented by setting the virtual functions SolveEquation and RightHandSide to be pure virtual functions as shown in lines 15 and 16 of the listing for AbstractOdeSolver.hpp below. We indicate that these functions are pure virtual functions by completing the declaration of these members with “= 0” as shown in the listing below. Should we mistakenly attempt to create an instance of the class AbstractOdeSolver we would get a compilation error. An investigation into pure virtual functions is made in Exercise 7.2.
../images/218075_2_En_7_Chapter/218075_2_En_7_Figj_HTML.gif
A class is an abstract class if it contains one or more pure virtual methods. We do not discuss implementation of the class AbstractOdeSolver or the derived classes further here: these classes are developed in the exercises at the end of this chapter.

7.7 Tips: Using a Debugger

In Sect. 1.​7 we gave a few tips about how to debug your code using simple techniques such as printing information out to the screen, and we also promised to give a little more information on using a debugger to inspect your code. There is a wide-range of open source and commercial tools to support you, should you wish to do this.

The easiest debuggers to use are those which are integrated with your development environment (such as Visual Studio or Eclipse). These integrated debuggers allow you to set breakpoints (places where you wish to temporarily pause execution) by clicking and selecting individual lines of code in your editing window. In the case of Eclipse, the debugging options basically provide a point and click front-end interface on top of a less user-friendly text-based debugger such as gdb.

The next level of sophistication is a graphical standalone debugger. Many of those available are actually a front-end to a text-based debugger, whereas some, such as ups are completely self-contained debuggers. A popular open source graphical front-end debugger is ddd which is a graphical interface to gdb, although it can also interface with a range of low-level debugging tools for a variety of programming languages. There are many other graphical front-end debuggers available such as KDbg and Xxgdb.

The lowest level of sophistication is the text-based debugger. The most widely used of these is the open source GNU debugger gdb, but many commercial compilers offer their own debugging environments.

All the debugging tools mentioned will allow you to walk through the code line by line, function call by function call, or to the next break point. If your program aborts with a segmentation fault, then the debugger will stop at the place where the fault happened, allowing you to see the line which caused the error. At any stage in execution, you will be able to inspect the values of the program variables and classes. You will also be able to inspect the back-trace (or stack) which shows the function calling sequence which led from the main function to a particular line of code.

Our advice is to debug your code with a graphical front-end to gdb, such as the popular ddd. Such tools are easy to download and install. The fact that they have a graphical interface with a built-in help system will allow you to rapidly see what the capabilities are. We also need to stress at this point that debuggers do not cope well with optimised code. Before you load the program into the debugger, you must remember to first compile your code with the “-g” flag (see Sect. 1.​3.​3).

7.8 Exercises

7.1

In this question, we will develop classes to describe the students at a university.

  1. 1.
    Write a class of students at the university that has the following public members:
    • a string for the student’s name;

    • a double precision floating point variable that stores the library fines owed by the student;

    • a double precision floating point variable that stores the tuition fees owed by the student;

    • a method that returns the total money owed by the student, that is, the sum of the library fines and tuition fees associated with a given student;

    • a few constructors that take different arguments.

     
  2. 2.

    The library fines owed by the students must be a nonnegative number. Enforce this by making a student’s library fines a private member of the class. Write one method that allows the user to set this variable only to nonnegative values, and another method that can be used to access this private variable. Both methods should be public members of the class.

     
  3. 3.

    Students at the university are either graduate students or undergraduate students. All undergraduate students are full-time students. Graduate students may be full-time students or part-time students. Derive a class of graduate students from the class of students that you have already written with an additional member variable that stores whether the student is full-time or part-time.

     
  4. 4.

    Graduate students do not pay tuition fees. Use polymorphism to write a method that calculates the total money owed by a graduate student. This will require the method for calculating the total money owed to be a virtual function of the parent class.

     
  5. 5.

    Ph.D. students are a special class of graduate students who do not pay library fines. Derive a class of Ph.D. students from the class of graduate students. Write a method that calculates the total money owed by a Ph.D. student.

     

7.2

This exercise is an investigation into proper use of the virtual keyword and into safe ways of making abstract classes.

The following program presents a small hierarchy of classes using the abstract class pattern described in Sect. 7.6. There is an abstract class AbstractPerson, which is intended never to be instantiated, and two derived classes, Mother and Daughter. The code in the main function demonstrates the power of polymorphic inheritance. It shows that it is possible to have a variety of objects of the same family stored as pointers to a generic abstract type, each of which could be a different concrete class. The AbstractPerson class promises a Print method, but it is only at run-time that the system inspects the class pointed to by p_mother and works out which Print method to invoke.
../images/218075_2_En_7_Chapter/218075_2_En_7_Figk_HTML.gif
  1. 1.
    Copy, save, compile and run the above program. The output from the Print method calls in lines 25 and 26 ought to be:
    ../images/218075_2_En_7_Chapter/218075_2_En_7_Figl_HTML.gif
     
  2. 2.

    Investigate what happens if you remove the public keyword from the inheritance declaration of either derived class (lines 9 and 15). This will make the base class inaccessible from the derived class.

     
  3. 3.

    Investigate what happens if you remove either of the virtual keywords in lines 6 and 12. Also investigate adding the virtual keyword on line 18. How does the output change after each of these changes?

     
  4. 4.
    What happens if you use the code fragment below to instantiate an instance of the abstract class in the main function?
    ../images/218075_2_En_7_Chapter/218075_2_En_7_Figm_HTML.gif
     
  5. 5.
    The preferred method of making an abstract class with a pure virtual method (so that it cannot be instantiated) is to give no implementation of that method in the class. This is done by replacing line 6 with the rather strange syntax which was introduced in the AbstractOdeSolver of Sect. 7.6:
    ../images/218075_2_En_7_Chapter/218075_2_En_7_Fign_HTML.gif
     
  6. 6.

    After making the Print method of AbstractPerson pure virtual as above, repeat the exercise in part 3 of removing the virtual keywords in lines 6 and 12.

     
  7. 7.

    Also after making the method AbstractPerson::Print() pure virtual as above, repeat the exercise in part 4 of attempting to instantiate an instance of the abstract class.

     

7.3

In Sect. 7.6, we discussed how abstract classes could be used to write a library for calculating the numerical solution of initial value ordinary differential equations, i.e. ordinary differential equations of the form
$$\begin{aligned} \frac{{\mathrm{d}y}}{{\mathrm{d}t}} = f\left( {t, y} \right) , \end{aligned}$$
for some user specified function f(t, y), where y = Y$$_{0}$$ at t = T$$_{0}$$ for an initial value Y$$_{0}$$ at some initial time T$$_{0}$$. We want to calculate a numerical solution in the time interval T$$_{0}$$ <t<T$$_{1}$$ where T$$_{1}$$ is the final time. To solve this equation numerically, we require the user to specify an integration step size, which we denote by h. A large variety of numerical methods exist for solving equations such as these and in Sect. 7.6 we explained that, as these methods all required very similar inputs, they could be coded very effectively using an abstract class pattern. We will base the library developed in this exercise on the abstract class in Listing 7.2: you should save this file, and ensure that you understand how the class members relate to the discussion above.
In this exercise, we will develop the library to allow you to solve initial value ordinary differential equations using two methods: the forward Euler method; and a Runge–Kutta method. Using a step size h, we define the points $$\textit{t}_{\textit{i}}, \textit{i}=0,1,2,\ldots ,\textit{N}$$ by
$$\begin{aligned} t_i = T_0 + ih, \end{aligned}$$
where h is chosen so that t$$_{\textit{N}}$$ = T$$_{1}$$. The numerical solution at these points is denoted by $$\textit{y}_{\textit{i}}, \textit{i}=0,1,2,\ldots ,\textit{N}$$. These values of y$$_{\textit{i}}$$ are determined by the numerical technique chosen.
  • For the forward Euler method, we set $$\textit{y}_{0}$$ = $$\textit{Y}_{0}$$. For $$\textit{i} = 1,2,\ldots ,\textit{N}, \textit{y}_{\textit{i}}$$ is given by
    $$\begin{aligned} y_i = y_{i - 1} + h\, f(t_{i - 1} , y_{i - 1} ). \end{aligned}$$
  • For the fourth order Runge–Kutta method, we set $$\textit{y}_{0}=\textit{Y}_{0}$$. For $$\textit{i}=1,2,\ldots ,\textit{N}$$, we calculate $$\textit{y}_{\textit{i}}$$ using the following formulae:

$$\begin{aligned} k_1&amp;= hf(t_{i - 1} , y_{i - 1} ), \\ k_2&amp;= hf\left( {t_{i - 1} + \frac{1}{2}h, y_{i - 1} + \frac{1}{2}k_1 } \right) , \\ k_3&amp;= hf\left( {t_{i - 1} + \frac{1}{2}h, y_{i - 1} + \frac{1}{2}k_2 } \right) , \\ k_4&amp;= hf(t_{i - 1} + h, y_{i - 1} + k_3 ), \\ y_i&amp;= y_{i - 1} + \frac{1}{6}(k_1 + 2k_2 + 2k_3 + k_4 ). \\ \end{aligned}$$
More details on numerical methods for initial value problems may be found in Kreyszig, [2].
  1. 1.

    Write the methods associated with the class AbstractOdeSolver and save these as the file AbstractOdeSolver.cpp. Note that you do not have to write the pure virtual functions, as the “= 0” when they are declared in the file AbstractOdeSolver.hpp means that these are already written.

     
  2. 2.

    Derive a class called FowardEulerSolver that allows the user to specify the function RightHandSide, and contains a method SolveEquation that uses the forward Euler method to calculate the values of y$$_{\textit{i}}$$ as described above, and writes the values of t$$_{\textit{i}}$$ and y$$_{\textit{i}}$$ to file. You may want to refer back to Sect. 5.​7 to remind yourself how to allow a user to specify a function.

     
  3. 3.
    Test the class FowardEulerSolver using the initial value ordinary equation
    $$\begin{aligned} \frac{{\mathrm{d}y}}{{\mathrm{d}t}} = 1 + t, \end{aligned}$$
    for the time interval $$0&lt; t &lt; 1$$, and with initial condition $${y} = 2$$ at $${t} = 0$$. This equation has solution $${y}=({t}^{2}+2{t}+4)/2$$. Investigate how the choice of step size affects the accuracy of the solution.
     
  4. 4.

    Repeat the two sub-parts above using the fourth order Runge–Kutta method to calculate the values of y$$_{\textit{i}}$$.