In the previous two editions of Overload, we have looked at encapsulation and overloading of operators. In this tutorial I will look at inheritance and polymorphism, taking one of the most common examples, a graphics class to pieces and show you that bitmaps and line drawings can be manipulated as easy as simple shapes.
Before I start to describe the mechanism of inheritance, I would like to provide a word of warning. Inheritance is a powerful tool and, like all tools must be used in the correct manner. Too much emphasis is placed on inheritance by novice C++ programmers, who tend to want to see their entire program as an inheritance tree, and often struggle to achieve this.
The example source is full of holes, as I try not to be compiler specific. It is your task this issue to fill in your compiler specific bits and send them in to me. One of you will lucky and receive a C++ book for your troubles.
One of the most cited benefits of C++ is the degree of reusable code that can be generated and indeed reused within the program. One of the main reasons for this reuse is that C++ gives you the ability to build and extend classes through inheritance. Each time you derive a child class from its parent, you get all the functionality of the parent without writing extra code. Classes can inherit from more than one parent, this is called multiple inheritance, the child class inherits data and behaviours from both its parents. For the moment we are going to stick to single inheritance.
The parent class is called the base class, the child class is known as the derived class. The base class is usually simpler than the derived class, for the derived class contains everything in the base class and, if it defines its own data or member functions, it is more complex.
The terms used in the design tools for this is the gen-spec
relationship (generalisation-specialisation). The parent class is
general, the child class is a specialisation of its parent. Try
thinking of this in terms of everyday objects. Taking a car as being
our base class (the parent/generalisation) we can create more
specialised child classes such as sports cars, saloons and estate cars
derived from the parent. In the Rumbaugh Object Modelling Technique,
the symbol used for this gen-spec relationship is the triangle, the
point points towards the generalisation as in the following diagram.
So how does a class inherit its parents features. Taking the above example, we can create the following class structure:
class Car {};
class Sports_Car : public Car {};
class Saloon_Car : public Car {};
class LotusElan : public Sports_Car {};
class Mazda_MX5 : public Sports_Car {};
class Ford_Mondeo : public Saloon_Car;
class Vauxhall_Cavalier : public Saloon_Car{};
The method of showing inheritance is the ": public ...". You may choose later to use private or protected inheritance, but not today. OK, so now that we have inherited from our parents what can we do? You have 2 choices, you can replace the features of your parent or you can extend them. Extending your parents features is achieved in the normal way by adding more member functions. Replacement can take the form of restriction or modification and in order to be effective we need a mechanism for replacing our parents member functions with our own ones.
This is achieved through a mechanism known as the virtual functions. By using virtual functions you tell the compiler that it is not to link the function until run-time as it may be replaced. This is known as late-binding, the normal, C type compilations produce early-binding, where the actual function to be called is known at link time.
Take the following example:
class base
{
public:
void write() {cout << "Hello I'm base" << endl;}
void baseWrite() { write(); }
};
class derived : public base
{
public:
void write() {cout << "Hello I'm derived" << endl;}
void derivedWrite() { write(); }
};
Creating an instance of base and calling baseWrite() will, as expected produce the message declaring that it's the base. Creating an instance of derived and calling derivedWrite() will produce a message stating that it is the derived class and calling baseWrite will produce the message that it is the base. In this case, because base has not explicitly stated (by means of the virtual keyword) that the write function can be replaced by any write functions belonging to its children.
If we make the base::write() a virtual function, as follows:
virtual void write() {cout << "Hello I'm base" << endl;}
Now, the instance of base will behave the same as before, but when calling either derivedWrite() or baseWrite() will produce a declaration that it is the derived function being called.
The write() in the derived function is implicitly virtual even though not specified with the virtual keyword, because its parents member function by the same name and parameter list was virtual.
This method of replacing functions is known as polymorphism (many forms) and is strictly a feature of member functions rather than classes as a whole. Polymorphism does not apply to data; you cannot make data virtual!
The extent to which we have seen this polymorphism used is not particularly exciting so far. Its real power comes when you combine this with pointers. We can have several classes derived from a common base, and have a pointer (or array/list/collection of pointers) that can now point to any of the derived types. Calling the common functions in the base class will then go to the replaced function in the appropriate class.
It's about time to start introducing the graphics classes promised earlier to assist with the explanation. A collection of graphics shapes can be manipulated without any regard to the actual shape, if they are all derived from a common base class (called not surprisingly Shape). By using the virtual mechanism to get the derived class to draw itself whenever needed, the base class can quite happily manipulate everything.
The data held by the shape class is to include a centre, the colour, the size and the velocity it moves in both the horizontal and vertical directions.
class shape
{
protected:
int xc, yc; // x centre and y centre
int lc; // line color
int sz; // size
int dx, dy; // direction for move in x and y planes
public:
shape();
void set(int x, int y, int s, int l);
void set(int x, int y);
void speed(int x, int y);
void move(int new_x, int new_y);
virtual void Draw() {}
void travel();
};
//-------------------------------------
// S H A P E M E T H O D S
//-------------------------------------
shape::shape() : xc(0), yc(0), sz(10), lc(WHITE),dx(5),dy(5)
{ }
void shape::set(int x, int y, int s, int l)
{
xc = x; yc = y; sz = s;
lc = l; // set pen to colour lc
draw();
}
void shape::set(int x, int y)
{
xc = x; yc = y;
}
void shape::speed(int x, int y)
{
dx = x;
dy = y;
}
void shape::size(int x)
{
// Set pen to background colour
draw();
sz=x;
// set pen to colour lc
draw();
}
void shape::move(int new_x, int new_y)
{
// Set pen to background colour
draw();
set(new_x,new_y); // Set pen to colour lc
draw();
}
void shape::travel()
{
int x = xc + dx; int y = yc + dy;
if (x<0) {x = 0; dx = -dx;}
if (y<0) {y = 0; dy = -dy;}
if (x>getmaxx()) {x = getmaxx(); dx = -dx;}
if (y>getmaxy()) {y = getmaxy(); dy = -dy;}
move(x,y);
}
We now have a base class of shape, in order to be useful, we must make more specific shapes. (Shape is too general). The only problem is, what happens if the new derived doesn't replace the virtual draw function. The solution to our problem lies in the ability to force the new class to replace the virtual function. This simply involves replacing the virtual draw function with what is called a pure virtual function. virtual void Draw() = 0;
The '= 0' means that any class deriving from shape must replace the draw member function. The shape class is now known as an abstract class. It is incapable of being instantiated as a class in its own right. It can only exist in derived form.
To create a circle class is now easy:
class circle : public shape
{
public:
void Draw() { circle(xc, yc, sz); } // or whatever your compiler uses
};
Easy isn't it! All the functionality needed to use the circle, exists in the abstract shape class, the only thing circle has to do is draw itself on demand. Likewise Squares and Triangles can be created in a similar manner.
class square : public shape
{
public:
void Draw() { rectangle(xc, yc, xc+sz, yc+sz); }
};
class equilateral : public shape
{
void Draw() { /* draw triangle */ }
};
So lets get a little program to drive this lot.
#define NUMSHAPES 30
void main(void)
{
int i;
// Set graphics mode
// you should ideally use a list or collection class
// that has the foreach function
shape ashape[NUMSHAPES];
for (i=0; i< NUMSHAPES; i++)
{
switch (i%3)
{
case 0: ashape[i] = new circle(); break;
case 1: ashape[i] = new square(); break;
case 2: ashape[i] = new equilateral(); break;
};
ashape[i]->set(/* generate 4 random no's for x,y,colour,size*/);
}
for (int j=0; j<100; j++)
{
for (i=0;i<NUMSHAPES;i++)
ashape[i]->travel();
}
// Switch off graphics mode
}
By deriving from shape you can now draw something a lot more complicated, a wire-frame ship, car or plane, or even use shape to put a pixel by pixel drawing based around the X and Y co-ordinates. The choice is yours.
I hope to receive a number of your implementations for the various compilers so that they can be included on future disks.