Understanding C++: An Accelerated Introduction

Part 5: Inheritance

by Marshall Brain , brain@iftech.com
Interface Technologies, Inc.
1-800-224-4965
© Copyright 1995 by Marshall Brain. All rights reserved.
Version 2.01, 4/25/95


Let's say that you have a list class, and now you want to modify it. In the old world of programming you would take the source and start changing things. In the object oriented world of programming you do things differently. What you do instead is leave the existing class alone and then layer your changes on top of it using a process called inheritance. Layering through inheritance lies at the very heart of object oriented programming. It is a totally different way of doing things, but it has several important advantages:

  1. Let's say that you bought the list class from someone else, so you don't have the source code. By leaving the existing class alone and layering your changes on top of it you don't need to have the source.
  2. The existing class is completely debugged and tested. If you modify its source, it has to go through the testing process again to be re-certified. Changes you make might also have side-effects that aren't detected immediately. By layering the changes on top of the existing class, the existing class never changes and therefore remains bug-free. Only the new pieces must be tested.
  3. The layering process forces you to think in a generic-to-specific way. You create a generic class like a list, and then layer specificity on top of it. A nice bonus of this way of thinking is that the generic classes are useful in many different programs. A list, for example, is useful in a lot of places. Each new program layers its own specifics onto the generic list, but the generic list stays the same everywhere.
  4. If the "base class" is improved, all classes built on top of it take advantage of those improvements without modification. For example, say that the list class is changed so that it sorts 10 times faster than it used to. Now every class built on top of the list class sorts 10 times faster as well, without modifying anything.
It is these benefits that get people excited about object oriented programming.

5.1 Inheritance example

Let's look at a specific example to get a feel for how inheritance works. Say you have purchased a simple list manager. It has the ability to insert at a specified location, to get items from the list, and to return the size of the list. The code for this list class is shown below, along with a small piece of test code:

#include <iostream.h> class List { int array[100]; int count; public: List(): count(0) {} ~List() {} void Insert( int n, int location ) { int i; for (i=count; i >= location; i--) array[i+1] = array[i]; array[location]=n; count++; } int Get( int location ) {return array[location];} int Size() { return count; } }; void main() { List list; int i, value; for (i=0; i < 10; i++) list.Insert(i,i); list.Insert(100,5); list.Insert(200,7); list.Insert(300,0); for (i=0; i < list.Size(); i++) cout << list.Get(i) << endl; }

The class contains no error checking to keep it small--obviously you would want to add some if this were a commercial product.

Now let's say that you want to modify this class to add two features. First, you want to have a sorted insertion function so that the class maintains a sorted list. Second, you want to keep track of the total sum of all items in the list. Rather than cycling through all elements in the list each time the sum function is called, you want to keep a running total as each item is inserted.

Obviously one way to do this is to simply modify the List class shown above. In C++ you use inheritance to make the changes instead. We will create a SortedList class by inheriting the List class and modifying it. Let's start by adding the sorted insertion feature:

class SortedList: public List { public: SortedList():List() {} SortedInsert(int n) { int i,j; i=0; do { j = Get(i); if (j < n ) i++; } while (j < n && i < Size()); Insert(n, i); } };

The List class is totally unchanged--we have simply created the SortedList class on top of it. The SortedList class inherits its behavior from the List class--it is derived from the List class. The List class is the base class for SortedList.

The List class is inherited on the fist line:

class SortedList: public List

The colon indicates that we are inheriting something. The word public indicates that we want the public functions and variables in List to remain public in the SortedList class. We could have also used private or protected. In either of these cases any public variables and functions in the inherited class would be converted in the derived class. The use of public here is standard.

The diagram below shows what is happening:

The SortedList class simply extends the List class. Anyone using the SortedList class has access to the functions available in List as well as the new functions available in SortedList.

The constructor for SortedList is also new--we have used a colon here to call the constructor for the inherited class:

SortedList():List() {}

This line says that the constructor named List from the base class should be called, and that the SortedList constructor needs to do nothing of its own.

In the remainder of the SortedList class we simply add the new SortedInsert function into the class. This new function makes use of the old Insert, Get, and Size functions from the List class as needed, but it does not access any of the List class data members directly because it can't--they are private to the List class, so they cannot be seen in the inheriting class.

Say that you wanted to have a variable or a function that seems private to outside users of a class but seems public to classes that inherit the class. For example, say that the SortedList class needed direct access to the array variable in List in order to improve its performance, but we still want to keep normal instances of List and SortedList from accessing the array directly. The word protected: can be used in the same manner as public: or private: to indicate this behavior. By declaring array as a protected member in List, it would be accessible by the derived class SortedList but not by normal instances of List or SortedList.

Now let's add the totaling capability to the SortedList class. To do this we will need to add a new variable, and we will also need to modify the Insert function so that each insertion adds to the total. The code is shown below:

class SortedList: public List { private: int total; public: SortedList():List(), total(0) {} void Insert( int n, int location ) { total = total + n; List::Insert(n, location); } int GetTotal() { return total; } SortedInsert(int n) { int i,j; i=0; do { j = Get(i); if (j < n ) i++; } while (j < n && i < Size()); Insert(n, i); } };

In this version of the SortedList class we have added a new data member named total, a new member function GetTotal to retrieve the current total, and a new function Insert which overrides the existing Insert function. We have also modified the SortedList constructor so that it initializes total. Now whenever the SortedList class is used and the Insert function is called, the new version of the Insert function will be accessed instead of the old version in List. The same goes for the SortedInsert function as well--when it calls Insert it is calling the new version.

The code for the new Insert function is straightforward:

void Insert( int n, int location ) { total = total + n; List::Insert(n, location); }

This function first adds the new value to the total. It then calls the old Insert function inherited from the base class so that the value is inserted in the list properly. The List:: specifies from which class in the hierarchy the Insert function should be chosen. This is only a two-level hierarchy so it is a simple decision here, but in a hierarchy that has several layers of inheritance you can use this technique to choose a specific function from many. It is this layering, and the ability to work and think in a multi-level inheritance hierarchy as shown here, that gives C++ its 3- dimensional feel.

5.2 A More Advanced Example

Let's take what we have learned about inheritance and use it to create a realistic example class. What we would like to do is create a new number class called a "multi- precision integer", or "mint". This integer type will work like a normal integer, but it will have up to 100 digits (for now--later we will see how to extend it to have as many digits as memory will hold using linked lists). A mint allows you to do things like find the actual value for 60!, or find the 300th value in a fibbinocci sequence.

What is a good way to create the new class in a object-oriented programming environment? One way to think about it is to think in a generic-to-specific way. For example, what is a multi-precision integer? It is simply a list of digits. Therefore, you can start by creating a generic list class that has all of the insertion features needed to implement a mint, and then layer the mint functionality on top of it.

How do we decide which features are needed in the list? A good way to do this is to think about what you will have to do with the digits in typical mint operations, and then use those thoughts to create the list class. Alternatively, you would have a list class laying around and you would simply build on top of it. Let's take the first approach since we don't have a good list class laying around.

How do you initialize a mint? The mint will start off containing no digits. We will then add one digit at a time to create the new mint. For the value 4,269 the mint would look like this:

Each square in this diagram represents one element in the list, and each element in the list contains an integer value between 0 and 9. At the list level we need to be able to add digits to the beginning or the end of the list, depending on where the initial value came from.

Now let's look at a simple addition, as shown in the figure below:

In order to implement addition we will want to start with the last digits of the two mints being summed, add them together, and insert the resulting digit in the new mint being formed as the sum. Then we will go to the previous two digits and do the same thing, and so on. We will therefore need an efficient way to move through the lists from end to beginning (for example, GetLast and GetPrevious functions), and we will also need a way to be able to tell when we have hit the beginning of the list (perhaps a return value from GetPrevious can indicate that the action is not possible, or a Size function can indicate how far to go).

From this discussion and our previous work with lists we can surmise that the list will probably need to have the following capabilities:

The code below implements the list:

class List { int array[100]; int count; int pointer; public: List(): count(0), pointer(0) {} ~List() {} void AddToFront(int n) { int i; for(i=count; i >= 1; i--) array[i]=array[i-1]; array[0]=n; count++; } void AddToEnd(int n) { array[count++]=n; } // & n is a reference - see tutor 2 int GetFirst(int & n) { if (count==0) return 1; else { n=array[0]; pointer=0; return 0; } } int GetLast(int & n) { if (count==0) return 1; else { n=array[count-1]; pointer=count-1; return 0; } } int GetPrevious(int & n) { if (pointer-1 < 0) return 1; else { pointer--; n=array[pointer]; return 0; } } int GetNext(int & n) { if (pointer+1 > count-1) return 1; else { pointer++; n=array[pointer]; return 0; } } int Size() { return count; } void Clear() { count = 0; } };

This code should all be fairly straightforward to you at this point. List is simply a generic list of integers. A data member named pointer points to one of the elements in the list and is moved by the four Get... functions. Each of these functions returns 0 on success and 1 on failure (for example, if pointer is not on element 0 of the list then there is a previous element to get and GetPrevious function will return a 0). The two Add... functions add at the beginning and end of the list respectively--they currently contain no error checking. The AddToFront function contains an inherent inefficiency because it must move the entire contents of the array down one element for each insertion.

The Mint class inherits List and uses it to build the actual mint type. It implements two constructors (a default constructor that accepts no parameters and a second constructor that accepts a string and uses it to fill the list), as well as functions that add two mints and print a mint. The code is shown below:

class Mint: public List { public: Mint():List() {} Mint(char *s):List() { char *p; for (p=s; *p; p++) AddToEnd(*p-'0'); } void Add(Mint & a, Mint & b) { int carry, temp; int erra, errb, na, nb; carry=0; Clear(); erra=a.GetLast(na); errb=b.GetLast(nb); while (!erra || !errb) { if (erra) temp=nb+carry; else if (errb) temp=na+carry; else temp=na+nb+carry; AddToFront(temp%10); carry=temp/10; erra=a.GetPrevious(na); errb=b.GetPrevious(nb); } if (carry > 0) AddToFront(carry); } void Print() { int n, err; err=GetFirst(n); while( !err ) { cout << n; err=GetNext(n); } cout << endl; } };

The following main function tests the mint class by adding two numbers and printing the sum:

void main() { Mint a("1234567"); Mint b("1234"); Mint c; c.Add(a,b); c.Print(); }

The constructors and the Print function are simple and straightforward. The Add function may remind you of your grade school days, because it is doing addition the old fashioned way. It starts with the last digits of the two numbers being summed, adds those digits, saves the result in the current mint, and remembers the carry value. It then moves forward through the list. Since it is likely that the two mints will not have an equal number of digits, the code must continually check to make sure that it has not run out of digits in one or the other mint. It does this using erra and errb. As soon as both mints have run out it checks carry and saves one last digit if necessary.

Running the test code you will see that the Mint class works as advertised and can add two numbers of up to 100 digits each. After you use the Mint class for awhile though you begin to see a problem with the Add function--there is no way to say something like "m = m + 1", or in the format necessary here "m.Add(m, one);" where one has been initialized to "1". The reason for this lies in the fact that Add must clear out the destination of the result before it can place a value into it, and this forces the loss of needed data in the case shown here.

The solution to this problem lies in the creation of a temporary holding value for the result during the actual addition. Then at the end of the function, the final result is copied into the current instance. The this pointer is used to solve the problem, as shown below:

void Add(Mint & a, Mint & b) { int carry, temp; int erra, errb, na, nb; Mint x; carry=0; erra=a.GetLast(na); errb=b.GetLast(nb); while (!erra || !errb) { if (erra) temp=nb+carry; else if (errb) temp=na+carry; else temp=na+nb+carry; x.AddToFront(temp%10); carry=temp/10; erra=a.GetPrevious(na); errb=b.GetPrevious(nb); } if (carry > 0) x.AddToFront(carry); *this = x; }

In this version of Add a temporary value named x has been created. The results of the addition are placed into x digit by digit. The last line of the function copies x into the current instance. The this pointer is available in every instance of a class in C++--it points to the current instance. That is, this is a pointer that points to the data members (the structure) that make up the current instance. In this case we use this because it saves code. The alternative would be to replace the last line with:

array = x.array; count = x.count; pointer = x.pointer;

The value *this is the structure pointed to by this, and it is more expedient to copy the whole structure at once.

As a final example of the Mint class, let's use it to implement a fibbinocci number finder. The fibbinocci sequence is as follows:

1, 1, 2, 3, 5, 8, 13, 21, 34, etc.

Each number in the sequence is the sum of the prior two numbers. In order to implement this feature we will need a way to check for equality in mints so that we can make a loop. The following member function can be added to the Mint class to check for equality between two mints:

int Equal(Mint & a) { if (a.Size()!=Size()) return 0; else { int i, na, nb; a.GetFirst(na); GetFirst(nb); for (i=0; i < a.Size(); i++) if (na!=nb) return 0; else { a.GetNext(na); GetNext(nb); } return 1; } }

Given the existence of this function, then the following code will find the 100th number in the fibbinocci sequence:

void main() { Mint max("100"); Mint counter("1"), one("1"); Mint t1("0"), t2("1"); Mint d; do { d.Add(t1,t2); t1=t2; t2=d; counter.Add(counter,one); } while (!counter.Equal(max)); d.Print(); }

The code uses two values t1 and t2 to remember the previous two values. They are added together and then shifted down by one. The counter is then incremented and the loop continues until the counter has reached the desired value. Using this code, the 100th number was found to be 354,224,848,179,261,915,075.

5.3 Conclusion

In this tutorial you have seen how inheritance is used to create class hierarchies, and at how the existence of inheritance tends to favor the development of code using a generic-to- specific style. The Mint class is a perfect example of this phenomena--a generic list was used to build the Mint class because a mint is nothing more than a list of digits.

Although we have accomplished our goal, the Mint class is not very well integrated into the language. We would like to use the "+" operator for addition and the "==" operator to check for equality. We will see how to do this in the next section.