When programming in C, it is common to view problem solutions from a
top-down approach: functions and actions of the program are defined in
terms of sub-functions, which again are defined in sub-sub-functions, etc.. This
yields a hierarchy of code: main() at the top, followed by a level
of functions which are called from main(), etc..
In C++ the dependencies between code and data can also be defined in terms of classes which are related to other classes. This looks like composition (see section Composition ), where objects of a class contain objects of another class as their data. But the relation which is described here is of a different kind: a class can be defined by means of an older, pre-existing, class; which leads to a situation in which a new class has all the functionality of the older class, and additionally introduces its own specific functionality. Instead of composition, where a given class contains another class, we mean here derivation, where a given class is another class.
Another term for derivation is inheritance: the new class inherits the functionality of an existing class, while the existing class does not appear as a data member in the definition of the new class. When speaking of inheritance the existing class is called the base class, while the new class is called the derived class.
Derivation of classes is often used when the methodology of C++ program development is fully exploited. In this chapter we will first address the syntactical possibilities which C++ offers to derive classes from other classes. Then we will address the peculiar extension to C which is thus offered by C++.
As we have seen the object-oriented approach to problem solving in the introductory chapter (see section OOP ), classes are identified during the problem analysis, after which objects of the defined classes can be declared to represent entities of the problem at hand. The classes are placed in a hierarchy, where the top-level class contains the least functionality. Each derivation and hence descent in the hierarchy adds functionality in the class definition.
In this chapter we shall use a simple vehicle classification system to build
a hierarchy of classes. The first class is Vehicle, which
implements as its functionality the possibility to set or retrieve the weight of
a vehicle. The next level in the object hierarchy are land-, water- and air
vehicles.
The initial object hierarchy is illustrated in the following figure.
The relationship between the proposed classes representing different kinds of
vehicles is now further illustrated. The figure shows the object hierarchy in
vertical direction: an Auto is a special case of a
Land vehicle, which in turn is a special case of a
Vehicle.
The class Vehicle is thus the `greatest common denominator' in
the classification system. For the sake of the example we implement in this
class the functionality to store and retrieve the weight of a vehicle:
class Vehicle
{
public:
// constructors
Vehicle ();
Vehicle (int wt);
// interface
int getweight () const;
void setweight (int wt);
private:
// data
int weight;
}
Using this class, the weight of a vehicle can be defined as soon as the corresponding object is created. At a later stage the weight can be re-defined or retrieved.
To represent vehicles which travel over land, a new class Land
can be defined with the functionality of a Vehicle, but in addition
its own specific information. For the sake of the example we assume that we are
interested in the speed of land vehicles and in their weight. The
relationship between Vehicles and Lands could of
course be represented with composition, but that would be awkward: composition
would suggest that a Land vehicle contains a vehicle, while
the relationship should be that the Land vehicle is a
special case of a vehicle.
A relationship in terms of composition would also introduce needless code.
E.g., consider the following code fragment which shows a class Land
using composition (only the setweight() functionality is
shown):
class Land
{
public:
void setweight (int wt);
private:
Vehicle v; // composed Vehicle
};
void Land::setweight (int wt)
{
v.setweight (wt);
}
Using composition, the setweight() function of the class
Land would only serve to pass its argument to
Vehicle::setweight(). Thus, as far as weight handling is concerned,
Land::setweight() would introduce no extra functionality, just
extra code. Clearly this code duplication is redundant: a Land
should be a Vehicle, and not: a Land should
contain a Vehicle.
The relationship is better achieved with inheritance: Land is
derived from Vehicle, in which Vehicle is the
base class of the derivation.
class Land: public Vehicle
{
public:
// constructors
Land ();
Land (int wt, int sp);
// interface
void setspeed (int sp);
int getspeed () const;
private:
// data
int speed;
};
By postfixing the class name Land in its definition by
public Vehicle the derivation is defined: the class
Land now contains all the functionality of its base class
Vehicle plus its own specific information. The extra functionality
consists here of a constructor with two arguments and interface functions to
access the speed data member. (public. C++ also implements
private derivation, which is not often used and which we will
therefore leave to the reader to uncover.
To illustrate the usage of the derived class Land consider the
following example:
Land
veh (1200, 145);
int main ()
{
printf ("Vehicle weighs %d\n"
"Speed is %d\n",
veh.getweight (), veh.getspeed ());
return (0);
}
This example shows two features of derivation. First,
getweight() is no direct member of a Land;
nevertheless it is used in veh.getweight(). This member function is
an implicit part of the class, inherited from its `parent' vehicle.
Second, although the derived class Land now contains the
functionality of Vehicle, the private fields of
Vehicle remain private in the sense that they can only be accessed
by member functions of Vehicle itself. This means that the member
functions of Land must use the interface functions
(getweight(), setweight()) to address the
weight field; just as any other code outside the
Vehicle class. This restriction is necessary so that the aspect of
data hiding thus remains ensured. The class Vehicle could, e.g., be
recoded and recompiled, after which the program could be relinked. The class
Land itself could remain unchanged.
In this example we assume that the class Auto, which represents
automobiles, should be able to represent the weight, speed and name of a car.
This class is therefore derived from Land:
class Auto: public Land
{
public:
// constructors
Auto ();
Auto (int wt, int sp, char const *nm);
// copy constructor
Auto (Auto const &other);
// assignment
Auto const &operator= (Auto const &other);
// destructor
~Auto ();
// interface
char const *getname () const;
void setname (char const *nm);
private:
// data
char const *name;
};
In the above class definition, Auto is derived from
Land, which in its turn is derived from Vehicle. We
speak here of nested derivation: Land is Auto's
direct base class, while Vehicle is the indirect base class.
Note the presence of a destructor, a copy constructor and overloaded
assignment function in the class Auto. Since this class uses a
pointer to address allocated memory, these tools are needed.
As mentioned previously, a derived class inherits the functionality of its base class. In this section we shall describe the effects of the inheritance on the constructor of a derived class.
As can be seen from the definition of the class Land, a
constructor exists to set both the weight and the
speed of an object. The poor-man's implementation of this
constructor could be:
Land::Land (int wt, int sp)
{
setweight (wt);
setspeed (sp);
}
This implementation has the following disadvantage. The C++ compiler will generate code to call the default constructor of a base class from each constructor in the derived class, unless explicitly instructed otherwise. This can be compared to the situation which arises in composed objects (see section Composition ).
The result in the above implementation is therefore that (a) the default
constructor of a Vehicle is called, which probably initializes the
weight of the vehicle, and that (b) subsequently the weight is redefined by
calling setweight().
The better solution is of course to directly call the constructor of
Vehicle which expects an int argument. The syntax to
achieve this, is to place the constructor to be called (supplied with an
argument) following the argument list of the constructor of the derived
class:
Land::Land (int wt, int sp)
: Vehicle (wt)
{
setspeed (sp);
}
The actions of all functions which are defined in a base class (and which are therefore also available in derived classes) can be redefined. This feature is illustrated in this section.
Let's assume that the vehicle classification system should be able to
represent trucks, which consist of a two parts: the front engine, which pulls a
trailer. Both the front part and the trailer have their own weight; but the
getweight() function should return the combined weights.
The definition of a Truck therefore starts with the class
definition, derived from Auto but expanded to hold one more
int field to represent additional weight information. Here we
choose to represent the weight of the front part of the truck in the
Auto class and to store the weight of the trailer as the additional
field:
class Truck: public Auto
{
public:
// constructors
Truck ();
Truck (int engine_wt, int sp, char const *nm,
int trailer_wt);
// interface: to set two weight fields
void setweight (int engine_wt, int trailer_wt);
// and to return combined weight
int getweight () const;
private:
// data
int trailer_weight;
};
// example of constructor
Truck::Truck (int engine_wt, int sp, char const *nm,
int trailer_wt)
: Auto (engine_wt, sp, nm)
{
trailer_weight = trailer_wt;
}
Note that the class Truck now contains two functions which are
already present in the base class:
setweight() is already defined in
Vehicle. The redefinition in Truck poses no problem:
this functionality is simply redefined to perform actions which are specific
to a Truck object.
The definition of a new version of setweight() in the class
Truck will hide the version of Vehicle: for a
Truck only a setweight() function with two
int arguments can be used.
getweight() is also already defined in
Vehicle, with the same argument list as in Truck. In
this case, the class Truck redefines this member function.
The following code fragment presents the redefined function
getweight():
int Truck::getweight () const
{
return
( // sum of:
Auto::getweight () + // engine part plus
trailer_weight // the trailer
);
}
Note that in this function the call Auto::getweight() explicitly
selects the getweight() function of the class Auto. An
implementation like
return (getweight () + trailer_weight);
would not be correct: this statement would lead to infinite recursion, and hence to an error in the program execution.
In the previously described derivations, a class was always derived from one base class. C++ also implements multiple derivation, in which a class is derived from several base classes and hence inherits the functionality of more than one `parent' at the same time.
For example, let's assume that a class Engine exists with the
functionality to store information about an engine: the serial number, the
power, the type of fuel, etc..:
class Engine
{
public:
// constructors and such
Engine ();
Engine (char const *serial_nr, int power,
char const *fuel_type);
// tools needed 'cuz we have pointers in the class
Engine (Engine const &other);
Engine const &operator= (Engine const &other);
~Engine ();
// interface to get/set stuff
void setserial (char const *serial_nr);
char const *getserial () const;
void setpower (int power);
int getpower () const;
void setfueltype (char const *type);
char const *getfueltype () const;
private:
// data
char const *serial_number, *fuel_type;
int power;
};
To represent an Auto but with all information about the engine,
a class MotorCar can be derived from Auto and
from Engine; as is illustrated in the below listing. By using
multiple derivation, the functionality of a Auto and of an
Engine are swept into a MotorCar:
class MotorCar: public Auto, public Engine
{
public:
// constructors
MotorCar ();
MotorCar (int wt, int sp, char const *nm,
char const *ser, int pow, char const *fuel);
};
MotorCar::MotorCar (int wt, int sp, char const *nm,
char const *ser, int pow, char const *fuel)
: Engine (ser, pow, fuel), Auto (wt, sp, nm)
{
}
A few remarks concerning this derivation are:
public is present both before the classname
Auto and before the classname Engine. This is so
because the default derivation in C++ is private: the
keyword public must be repeated before each base class
specification.
MotorCar introduces no `extra'
functionality of its own, but only combines two pre-existing types into one
aggregate type. Thus, C++ offers the possibility to simply sweep
multiple simple types into one more complex type.
This feature of C++ is very often used. Usually it pays to develop `simple' classes each with its strict well-defined functionality. More functionality can always be achieved by combining several small classes.
Note also the syntax of the constructor: following the argument list, the two
base class constructors are called, each supplied with the correct arguments. It
is also noteworthy that the order in which the constructors are called is
defined by the derivation, and not by the statement in the
constructor of the class MotorCar. This means that:
Auto is called, since
MotorCar is first of all derived from Auto;
Engine is called,
MotorCar itself are
executed (in this example, none). Lastly, it should be noted that the multiple derivation in this example may
feel a bit awkward: the derivation implies that MotorCar is
an Auto and at the same time is an Engine. A
relationship `a MotorCar has an Engine' would
be expressed as composition, by including an Engine object in the
data of a MotorCar. But using composition, consider the unnecessary
code duplication in the interface functions for an Engine (here we
assume that a composed object engine of the class
Engine exists in a MotorCar):
void MotorCar::setpower (int pow)
{
engine.setpower (pow);
}
int MotorCar::getpower () const
{
return (engine.getpower ());
}
// etcetera, repeated for set/getserial(),
// and set/getfueltype()
Clearly, such simple interface functions are better avoided by using
derivation. Alternatively, when insisting on the has relationship and
hence on composition, the interface functions could be avoided using
inline functions.
When inheritance is used in the definition of classes, it can be said that an object of a derived class is at the same time an object of the base class. This has important consequences which shall be discussed in this section.
We define two objects, one of a base class and one of a derived class:
Vehicle
v (900); // vehicle with weight 900 kg
Auto
a (1200, 130, "Ford"); // automobile with weight 1200 kg,
// max speed 130 km/h, make Ford
The object a is now initialized with its specific values.
However, an Auto is at the same time a Vehicle, which
makes the assignment from a derived object to a base object possible:
v = a;
The effect of this assignment is that the object v now receives
the value 1200 as its weight field. A Vehicle has
neither a speed nor a name field; these data are
therefore not assigned.
The conversion from a base object to a derived object poses however problems: what data should a statement like
a = v;
substitute for the fields speed and name, which are
missing in the right-hand side Vehicle? Such an assignment is
therefore not accepted by the compiler.
The following general rule applies: when assigning related objects, an assignment where some data are dropped is legal. An assignment where data would have to be left blank is however not legal. This rule is a syntactic one: it also applies when the classes in question have their overloaded assignment functions.
The conversion of an object of a base class to an object of a derived class can of course be explicitly defined, if needed. E.g., to achieve the correct working of a statement
a = v;
the class Auto would need an assignment function accepting a
Vehicle as its argument. It would then be the programmer's
responsibility to decide what to do with the missing data:
Auto const &Auto::operator= (Vehicle const &veh)
{
setweight (veh.getweight ());
.
. code to handle other fields should
. be supplied here
.
}
We define the following objects and one pointer variable:
Land
l (1200, 130);
Auto
a (500, 75, "Daf");
Truck
t (2600, 120, "Mercedes", 6000);
Vehicle
*vp;
Subsequently we can assign vp to the addresses of the three
objects of the derived classes:
vp = &l;
vp = &a;
vp = &t;
Each of these assignments is perfectly legal. However, an implicit conversion
of the type of the derived class to a Vehicle is made, since
vp is defined as a pointer to a Vehicle. Hence, when
using vp only the member functions which manipulate the
weight can be called; this is the only functionality of a
Vehicle and thereby the only functionality which can be accessed by
using a pointer to a Vehicle.
The restriction in functionality has furthermore an important effect for the
class Truck. After the statement vp = &t,
vp points to a Truck; nevertheless,
vp->getweight() will return 2600; and not 8600 (i.e., the
combined weight of the cabin and of the trailer, 2600+6000) which
t.getweight() would return.
When a function is called via a pointer to an object, then the type of the pointer and not the object itself determines which member function is available and executed. In other words, C++ always implicitly converts the object which is pointed to to the type of the pointer.
There is of course a way around the implicit conversion, which is an explicit type cast:
Truck
truck;
Vehicle
*vp;
vp = &truck; // vp now points to a truck object
.
.
.
Truck
*trp;
trp = (Truck *) vp;
printf ("Make: %s\n", trp->getname ());
The second to last statement of the code fragment above specifically casts a
Vehicle* variable to a Truck* in order to assign the
value to the pointer trp. This code will only work if
vp indeed points to a Truck and hence a function
getname() is available; otherwise unexpected behavior of the
program may be the result.
The fact that pointers to a base class can be used to address derived classes can be used to develop general-purpose classes which can process objects of the derived types. A typical example of such processing is the storage of objects, be it in an array, a list, a tree or whichever storage method may be appropriate. Classes which are designed to store objects of other classes are therefore often called container classes. The stored objects are then contained in the container class.
As an example we present here the class VStorage, which is used
to store pointers to Vehicles. The actual pointers may be addresses
of Vehicles themselves, but also may refer to derived types such as
Autos.
The definition of the class is the following:
class VSTorage
{
public:
// constructors, destructor
VStorage ();
VSTorage (VStorage const &other);
~VStorage ();
// overloaded assignment
VStorage const &operator= (VStorage const &other);
// interface:
// add Vehicle* to storage
void add (Vehicle *vp);
// retrieve first Vehicle*
Vehicle *getfirst (void) const;
// retrieve next Vehicle*
Vehicle *getnext (void) const;
private:
// data
Vehicle **storage;
int nstored, current;
};
Concerning this class definition we remark the following:
Vehicle* to the storage, one to retrieve the first
Vehicle* from the storage, and one to retrieve next pointers
until no more are in the storage.
The class could therefore be used as is illustrated in the following example:
Land
l (200, 20); // weight 200, speed 20
Auto
a (1200, 130, "Ford"); // weight 1200 , speed 130,
// make Ford
VStorage
garage; // the storage
garage.add (&l); // add to storage
garage.add (&a);
Vehicle
*anyp;
int
total_wt = 0;
for (anyp = garage.getfirst (); anyp; anyp = garage.getnext())
total_wt += anyp->getweight ();
printf ("Total weight: %d\n", total_wt);
This example demonstrates how derived types (one Auto and one
Land) are implicitly converted to their base type (a
Vehicle), so that they can be stored in a VStorage.
Base-type objects are then retrieved from the storage; the function
getweight(), defined in the base type, is the greatest common
denominator and hence can be used to compute the combined weight.
VStorage furthermore contains all the tools to
ensure that two VStorage objects can be assigned to one another
etc.. These tools are the overloaded assignment function and the copy
constructor.
private section is seen. The class VStorage
maintains an array of pointers to Vehicles and needs two
ints to store how many objects are in the storage and which the
`current' index is, to be returned by getnext(). The class VStorage shall not be further elaborated; similar
examples shall appear in the next chapters. It is however very noteworthy
that by providing class derivation and base/derived conversions, C++
presents a powerful tool: these features of C++ allow the processing of
all derived types by one generic class.
The above class VStorage could even be used to store all types
which may be derived from a Vehicle in the future. It seems a bit
paradoxical that the class should be able to use code which isn't even there
yet, but there is no real paradox: VStorage uses a certain
protocol, defined by the Vehicle and obligatory for all
derived classes.
The above class VStorage has just one disadvantage: when we add
a Truck object to a storage, then a code fragment like:
Vehicle
*any;
VStorage
garage;
.
.
any = garage.getnext ();
printf ("%d\n", any->getweight ());
will not print the truck's combined weight of the cabin and the
trailer. Only the weight stored in the Vehicle portion of the truck
will be returned via the function any->getweight().
There is, of course, also a remedy to this slight disadvantage. This will be discussed in the next chapter.
Next Chapter, Previous Chapter, Home