Learning Objectives
- Represent inheritance hierarchies in UML and C++ and decide when to use inheritance and when to use composition
- Explain how to use
public
,protected
andprivate
inheritance for code reuse in C++- Make good use of constructor chaining in C++ inheritance hierarchies
- Explain the meaning of the OOP terms polymorphism and dynamic binding and describe how they are implemented in C++
- Make use of abstract base classes and (pure) virtual functions in C++ inheritance hierarchies
- Make use of member function hiding in C++ inheritance hierarchies
- Define multiple inheritance hierarchies in C++ and use virtual base classes when appropriate.
By using inheritance, we can cause a class to inherit data members and/or member functions from another class. In C++, we refer to the class:
To for an introductory example let’s define a single class to store information and perform calculations about different geometric shapes.
class Shape {
public:
Shape() {};
float GetArea() { return _area; }
float GetPerimeter() { return _perimeter; }
private:
float _area;
float _perimeter;
};
The Shape
case has one default constructor (Shape() {};
) and two public
functions, as well as two private
data members. As this class stands, there is no way of computing the area or perimeter of the shape, since this detail depends on the specific shape being presented. Therefore, this class is a ‘general’ class for describing shapes and we probably wouldn’t want to define any instances of it as it stands.
Now suppose that we want to define classes for specific shapes, such as:
Therefore, two of the data members are the same for both circles and squares. Rather than duplicating the code, it can be reused by inheriting from Shape
when defining the two new ca=lasses, Square
and Circle
. The code can be modified as so:
class Shape {
public:
Shape() {};
float GetArea() const { return _area; }
float GetPerimeter() const
{ return _perimeter; }
protected:
float _area;
float _perimeter;
};
class Circle: public Shape {
public:
Circle(float r=0) {_radius = r;}
void ComputeArea();
void ComputePerimeter();
protected:
float _radius;
};
class Square: public Shape {
public:
Square(float s) {_side = s;}
void ComputeArea();
void ComputePerimeter();
protected:
float _side;
};
The line class Circle: public Shape
is the location where inheritance is defined for the Circle
class. In essence it means “the Circle
class publicly derives from the Shape
class”. Inheritance is indicated by the colon character :
after the derived class name. The same applies to the Square
class.
Note that the two data members in Shape
are now protected
rather than private
. The difference between protected
and private
members is that private
members in a base class are never inherited by the derived class, whereas protected
members are. Apart from this they are identical.
The public
keyword in the inheritance means that we are using public inheritance
. Alternatives to public inheritance are private
or protected
inheritance, but these are not widely used. The precise definitions of the different types of inheritance are:
Inheritance can be represented using UML class diagrams. In the example below, the inheritance from Circle
and Square
to Shape
is indicated by arrows joining the derived class to the base class.
UML DIAGRAM
Public and protected members contained in the base class, although not shown in the derived classes, are available to those classes. A #
symbol is used to denote protected
members in UML (recall that + denotes public
and - denotes private
)
Let’s consider another example, which contains classes for storing information about doctors and consultants at a hospital. The gender, name, title and basic salary information needs to be stored about doctors. For consultants, the same information needs to be stored but in addition to their specialism. There is a clear relationship between doctors and consultants as they share four of the same data members. Rather than duplicating code in the two classes, inheritance can be used to reuse code.
enum GenderType {Male, Female};
class Doctor {
public:
Doctor(GenderType g, string n)
{
_gender = g;
_name = n;
_title = "Dr";
_basicSalary = 30000;
}
void Display () const
{
cout << _title << " " << name << endl;
if (_gender == Male)
cout << "Male" << endl;
else
cout << "Female" << endl;
cout << "Basic salary = "
<< _basicSalary << endl;
}
protected:
GenderType _gender;
string _name;
string _title;
float _basicSalary;
};
In this example, the following Consultant
class inherits (or derives) from Doctor
using public inheritance.
class Consultant : public Doctor
{
public:
Consultant(GenderType g, string n,
string s): Doctor(g, n)
{
_specialism = s;
if (_gender == Male_)
_title = "Mr";
else
_title = "Ms";
_basicSalary = 70000;
}
void Display()
{
Doctor::Display();
cout << "Specialism = "
<< _specialism << endl;
}
protected:
string: _specialism;
};
The new concept constructor chaining is illustrated above, Consultant( ... ) : Doctor( ... )
. After the Consultant
constructor function header, there is a colon followed by a call to the Doctor
constructor. This causes the base class (i.e Doctor) constructor to be called whenever the derived class (i.e . Consultant
) constructor is called. In order words, the calls to the class constructors are ‘chained’ up the inheritance hierarchy. Constructor chaining happens anyway for default constructors (i.e those with no arguments), but if we want it to happen for other constructors (i.e. those with arguments) we must explicitly define constructor chaining like this.
The below UML diagram shows the relationship between the Doctor
and Consultant
classes.
UML DIAGRAM
The earlier example, also illustrated another new concept called member function overriding or member function hiding. See how there are definitions for the Display()
member function in both the Doctor
class and the Consultant
class. Normally, the Doctor Display()
function would be inherited by the Consultant
, but in this case it would be incorrect as it doesn’t display the consultant’s _specialaism
. Therefore Consultant
defines its own version of Display()
, which hides the base class version.
Note that we can still call the base class version from the derived class version using the scope operator ::
, (Doctor::Display();
).
In this chapter there have been two different examples of inheritance from Circle/Sqaure
to Shape
, and from Consultant
to Doctor
. Although clearly the use of inheritance is appropriate in both of these case, they are slightly different. In the Consultant/Doctor
example, it is possible, even likely, that we will want to create instances of both Consultant
and Doctor
, since they are both types of clinician at the hospital. However, for the Shape
example we will never want to create an instance of the Shape
class - it only exists so that we can derive real
specific shapes from it. Based on this distinction, now let’s introduce a couple of new terms:
Shape
class is an example of an abstract
base class)Square
Circle
Doctor
Consultant
classes are all examples of concrete classes)An inheritance hierarchy can be thought of as defining a type/subtype relationship between classes, e.g. a circle is a type of shape. A virtual function defines a type dependent operation within an inheritance hierarchy.
To illustrate the use of virtual functions, let’s modify the shape example introduced earlier:
class Shape {
public:
Shape() {};
float GetArea() const { return _area }
float GetPerimeter() const
{ return _perimeter; }
// pure virtual functions
virtual void ComputeArea() = 0;
virtual void ComputePerimeter() = 0;
protected:
float _area;
float _perimeter;
};
...
The keyword virtual
specifies that the ComputeArea()
and ComputePerimeter()
functions are virtual functions, which represent type dependent operations in the inheritance hierarchy. In fact, these two functions are pure virtual functions. A pure virtual function is specified by adding an assignment to zero after the virtual function signature. A pure virtual function, as well as being a type dependent operation, means that the class that contains it will be an abstract base class, i.e. it will not be possible to create an instance of it. Attempting to create an instance of it will result in a compilation error.
In addition, all classes that derive from a class containing a pure virtual function will also be abstract base classes unless they provide an implementation of the virtual function. In the shapes example, it is now a requirement for both Circle
and Square
to implement ComputeArea()
and ComputePerimeter()
, otherwise it will not be possible to create any Circle
or Square
instances.
The below UML diagram shows how the Shape.h
relationships change now we have defined pure virtual functions. Displaying the member function names in italics in Shape
indicates that these are virtual functions. Furthermore, the text <<abstract>>
above the class name indicates that this is an abstract base class.
UML DIAGRAM
Virtual functions are one way in which the concept of polymorphism is implemented in C++.
Virtual functions provide a way of specifying different implementations for a single member function at different points in an inheritance hierarchy. However, similar functionality can be produced by overriding a member function without using the virtual key word. i.e. if we override an ‘ordinary’ (non-virtual) base class function in the derived class it will hide the base class function.
For example, the two implementations of an inheritance hierarchy are below, the first using virtual functions and the second using member function overriding. In both implementations an instance of the base
or derived
class will call their ‘own’ version of the function fn
.
class base {
...
public:
virtual float fn();
}
class derived {
...
public:
float fn();
}
When a non-virtual member function is overridden the choice of which implementation from the inheritance hierarchy to use is always made at compile-time. With virtual functions this choice can be made at run-time.
class base {
...
public:
float fn();
}
class derived {
...
public:
float fn();
}
The choice of which implementation to use is known as binding. If binding is made at run-time, it is known as dynamic binding. Dynamic binding is used whenever a virtual member function is called through a pointer. The following code illustrates the use of dynamic binding:
Shape *sq = new Square(4.5);
Shape *c = new Circle(2.7);
sq->ComputeArea();
c->ComputeArea();
Note that ->
is shorthand for dereferencing a pointer then selecting a member, e.g. the following are the same: (*sq).ComputeArea()
and sq->ComputeArea()
Here, the instances sq
and c
both have a static (i.e. compile time) of Shape*
. However, since Square
and Circle
both derive from Shape
, the dynamic type of sq
and c
could b either Shape*
, Square*
or Circle*
. Precisely which one of these three possibilities they are will not be decided until run-time. In other word, binding of sq
and c
is dynamic.
Dynamic binding is only used if the virtual function is accessed through a pointer as in the example above. If sq
and c
had types Square
and Circle
rather than Shape*
then dynamic binding would not be used. In this case they would only have a static type and no dynamic binding would be necessary.
Polymorphism is C++ is related to binding and can be split up into two separate terms:
Composition refers to making one class a data member of another class. Inheritance also results in the members of one class (the base class) being made available to another class (the derived class). All the examples in this chapter could have been implemented using composition rather than inheritance, by including a data member of type Shape
in the Circle
and Square
classes, or add a Doctor
member to the Consultant
class.
To decide between using inheritance and composition the following rules can be applied. If the relationship between two classes can be described as an:
For example, in the shapes example, a Triangle
is a Shape
, so inheritance is the best option. Whereas, with the vectors example, we cannot say that a Vector
is a Point
, but we can say that a Vector
has a Point
, so in this case composition is the appropriate mechanism to use.
In all of the examples so far, each class has only inherited from a single base class. This is know as single inheritance. In C++ it is permissible to inherit from multiple base classes: this is referred to as multiple inheritance.
To illustrate this concept of multiple inheritance, let’s use a student database program example, which stores information about both overseas and UK students. First look at the UML class diagram below. There are three base classes: UKCitizen
Student
OverseasCitizen
. Each has their own data members and corresponding mutator and inspector member functions. The UKStudent
class derives from both Student
and UKCitizen
, and OverseasStudent
derives from both Student
and OverseasCitizen
. This is logical since a UK student is both a student and a UK citizen, and a overseas student is both a student and an overseas citizen.
UML DIAGRAM
The Student
class has a pure virtual function called Fees()
: all students must pay fees, but the way the fees are computed varies. Therefore, Student
is an abstract base class. However, UKCitizen
and OverseasCitizen
are not - they are normal base classes. An excerpt from the code that implements this inheritance hierarchy is shown below:
class OverseasStudent: public OverseasCitizen,
public Student {
...
}
class UKStudent: public UKCitizen,
public Student {
...
}
Multiple inheritance is specified by simply listing multiple vase classes when inheriting, and separating them by commas. Using this inheritance hierarchy is an efficient way to represent the different types of information stored about a student in the database, and permits good code reuse.
Sometimes multiple inheritance can cause problems. This can be illustrated by the following example, which contains a number of classes to represent different types of animal.
Look at the UML diagram below. There is a base class called Animal
, which contains a protected
data member called _expectedLifeSpan
. Two other classes derive from Animal:
Predator
Endangered
. Both of these classes inherit the _expectLifeSpan
data member. The SnowLeopard
class multiply inherits from both Predator
and Endangered
, since snow leopards are endangered predators. Therefore, SnowLeopard
would normally inherit two data members called _expectedLifeSpan
. This would make any reference to _expectedLifeSpan
in SnowLeopard
ambiguous, an cause a compilation error.
UML CLASS DIAGRAM
This answer to this problem lies in the use of a virtual base class. If we need to define a hierarchy with such ambiguous inheritance we can define the root base class as a virtual base class, and then only copy one of each multiply-inherited member will be created. To make an Animal
a virtual base class we simply add the keyword virtual
when defining inheritance from it. For example consider the code below:
class Animal
{
public:
Animal() {};
int GetExpectedLifeSpan()
{ return _expectedLifeSpan; }
void SetExpectedLifeSpan(int val)
{ _expectedLifeSpan = val; }
protected:
int _expectedLifeSpan;
};
class Predator: public virtual Animal
{
public:
Predator() {};
string GetMainPrey()
{ return _mainPrey; }
void SetMainPrey(string val)
{ _mainPrey = val; }
protected:
string _mainPrey;
}
class Endangered: public virtual Animal
{
public:
Endangered() {};
int GetNumLeft() { return _numLeft; }
void SetNumLeft(int val) { _numLeft = val; }
protected:
int _numLeft;
}
class SnowLeopard: public Endangered,
public Predator
{
public:
SnowLeopard() {};
}
Note that virtual
is only added to the Animal
base class, not when SnowLeopard
is inheriting from Endangered
and Predator
.
This inheritance problem will only occur if the inheritance hierarchy forms a ‘diamond-like’ shape. Fortunately such cases are quite rare, but it is good to know about this potential problem.