class circle : public shape { \fbox{circle}
public:
circle( int x, int y, int r) : shape(x,y), _radius(r) { }
void draw() { cout << "C:" << _x << _y << _radius; }
protected:
int _radius;
};
class rectangle : public shape { \fbox{rectangle}
public:
rectangle( int x, int y, int l, int r ) : shape(x,y), _l(l), _r(r) { }
void draw() { cout << "R:" << _x << _y << _l << _r; }
protected:
int _l,_r;
};
slide: Concrete shapes
For a circle we need to define, apart from its origin, a
radius.
And, similarly, for a rectangle we need to define the length of the sides.
Both circle and rectangle inherit the origin and the member function move
from the shape class.
Instantiating the inherited part takes place, as indicated
after the colon, before evaluating the function body of the constructor.
Unlike the initialization of instance variables,
which may be assigned
a value in the body of the constructor,
the initialization of the inherited
parts must be done in this way.
An explicit initializer is required unless
a default constructor is available.
The difference between the initialization of a data member
immediately after the colon or in the function body of the constructor is
quite subtle. In the latter case, a default constructor will
be applied
to create the data member and the subsequent assignment in the
function body may lead to the creation of another instance.
Generally, it is safer and more efficient to initialize data members
immediately after the colon.
Unfortunately, it is not always possible to initialize
data in the colon-list.
Also, there is no way in which to communicate between the initializers,
which may result in repeated computations when there is
a dependency between the initial values of the data members.
A concrete shape class must necessarily (re)define the member function draw,
since an abstract shape cannot possibly know how to draw itself.
A code fragment illustrating the use of concrete shapes looks
as follows:
circle c(1,1,2); rectangle r(2,2,1,1);
c.draw(); r.draw();
Note that calling draw is for both kinds of shapes the same.
The difference between the two distinct shapes, however, becomes visible when calling
the function draw. The function draw specified for circle overrides
the specification given for the abstract shape,
and similarly for rectangle.
Dynamic binding
The reuse of code is one of the most important aspects of inheritance.
The principle underlying the efficient reuse of code (by employing inheritance)
may be characterized as {\em "programming by stating the difference,"}
which means that one has to (re)define the features of the derived class
that are added to or different from what is provided by the base class.
To fully exploit this principle we need virtual
functions, that is functions for which dynamic binding applies.
Operationally, dynamic binding may be regarded as a dispatching mechanism
that acts like a case statement to select (dynamically) the appropriate
procedure in response to a message.
In many procedural programs, such a case statement often occurs (explicitly)
when a kind of polymorphism is introduced by means of an explicit tag
(as, for example, in combination with a union or a variant-record).
The use of such tags may become a nightmare when modifying
the informal type system, since each case statement then needs to be updated.
Using inheritance with dynamic binding,
such case statements are, so to speak,
implicitly inserted by the compiler or interpreter.
The obvious advantage of such a feature, apart from reducing the amount
of code that must be written, is that maintenance is greatly
facilitated.
A possible disadvantage, however, might be that program understanding
becomes more difficult since many of the choices are now implicitly
made by the dispatching mechanism
instead of being written out explicitly.
To illustrate the power of virtual functions (and dynamic binding)
we will add a compound shape to our hierarchy of shapes.
See slide [2-compound].
class compound : public shape { \ifsli{}{\fbox{compound}
public:
compound( shape* s = 0 ) : fig(s) { next = 0; }
void add( shape* s ) {
if (next) next->add(s); else next = new compound(s);
}
void move(int x, int y) {
if (fig) fig->move(x,y);
if (next) next->move(x,y);
}
void draw() {
if (fig) fig->draw();
if (next) next->draw();
}
private:
shape* fig; compound* next;
};
slide: Compound shapes
A compound shape is actually a linked list of shapes.
To add shapes to the list, the class compound
extends the class shape with a member function add.
Both the member functions move and draw are redefined
in order to manipulate the list of shapes in the appropriate way.
The list is traversed by recursively invoking
the function for the objects stored in the next
pointer unless next is empty, which indicates the end of the list.
The class compound is made a subclass of shape to allow
a compound shape to be treated as a shape.
As an example of the use of a compound shape,
consider the following fragment:
compound s;
s.add( new circle(1,1,2) );
s.add( new rectangle(2,2,3,5) );
s.draw(); s.move(7,7); s.draw();
After creating an empty compound shape, two shapes, respectively
a circle and a rectangle, are added.
The compound shape is asked to draw itself, it is moved,
and then asked to draw itself again.
The compound shape object, when moving and drawing the list of shapes, has
no knowledge of what actual shapes are contained in the list, which may
be compound shapes themselves.
This illustrates how we may achieve polymorphic behavior
by using inheritance.
A more explicit example of the polymorphic behavior of shapes is given by the following
code fragment.
shape* fig[3];
fig[0] = &s; the compound shape
fig[1] = new circle(3,3,5);
fig[2] = new rectangle(4,4,5,5);
for( int i = 0; i < 3; i++ )
fig[i]->draw();
After storing some actual shapes, including a compound shape,
in an array of (pointers to) shapes,
a simple loop with a uniform request for drawing is sufficient
to display
all the shapes contained in the array, independent of their actual type.
This example is often used to demonstrate that when adopting an
object-oriented approach the programmer no longer needs to include
lengthy case statements to choose between the various drawing operations
on the basis of an explicit type tag.
The careful reader may have noted that the absence of the declaration virtual for
the member function move may lead to problems.
Indeed, this leads to erroneous behavior since moving only the origin of
the compound shape will not do.
In our slightly wasteful implementation of a compound shape,
the member variables inherited from shape play no role.
Instead, each shape in the list must be moved.
This could be repaired either by declaring the function