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.
- 1.
public members of Book are public members of Ebook;
- 2.
protected members of Book are protected members of Ebook; and
- 3.
private members of Book are hidden from Ebook, and so may not be used by the derived class.
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.
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.
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.
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
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
7.5 Run-Time Polymorphism
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.
The method CalculateBill for this derived class is then implemented using the code below.
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.
7.6 The Abstract Class Pattern
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.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.
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.
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.
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.
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.
- 1.Copy, save, compile and run the above program. The output from the Print method calls in lines 25 and 26 ought to be:
- 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.
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.What happens if you use the code fragment below to instantiate an instance of the abstract class in the main function?
- 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:
- 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.
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
- For the forward Euler method, we set = . For is given by
For the fourth order Runge–Kutta method, we set . For , we calculate using the following formulae:
- 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.
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 as described above, and writes the values of t and y 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.Test the class FowardEulerSolver using the initial value ordinary equationfor the time interval , and with initial condition at . This equation has solution . Investigate how the choice of step size affects the accuracy of the solution.
- 4.
Repeat the two sub-parts above using the fourth order Runge–Kutta method to calculate the values of y.