We're always interested in getting feedback. E-mail us if you like this guide, if you think that important material is omitted, if you encounter errors in the code examples or in the documentation, if you find any typos, or generally just if you feel like e-mailing. Send your email to Frank Brokken.Please state the document version you're referring to, as found in the title (in this document: 5.2.0a) and please state the paragraph you're referring to.
All mail received is seriously considered, and new (sub)releases of the Annotations will normally reflect your suggestions for improvements. Except for the incidental case I will not otherwise acknowledge the receipt of suggestions for improvements. Please don't misinterpret this for lack of appreciation.
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 6.4), 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. This 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 some of the possibilities which are thus offered by C++.
As we have seen the object-oriented approach to problem solving in the introductory chapter (see section 2.4), 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 new derivation (and hence descent in the class hierarchy) adds new functionality compared to yet existing classes.
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 figure 12.
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 { unsigned d_weight; public: Vehicle(); Vehicle(unsigned weight); unsigned getweight() const; void setweight(unsigned 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
, while adding its own specific
information and functionality. Assume that we
are interested in the speed of land vehicles and in their weights. The
relationship between Vehicle
s and Land
s 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 { Vehicle d_v; // composed Vehicle public: void setweight(unsigned weight); }; void Land::setweight(unsigned weight) { d_v.setweight(weight); }Using composition, the
setweight()
function of the class Land
only
serves to pass its argument to Vehicle::setweight()
. Thus, as far as
weight handling is concerned, Land::setweight()
introduces no extra
functionality, just extra code. Clearly this code duplication is superfluous:
a Land
should be a Vehicle
; it should not contain
a Vehicle
.
The intended relationship is better achieved using
inheritance:
Land
is derived from Vehicle
, in which Vehicle
is the
base class of the derivation. Here is how such
inheritance is
achieved:
class Land: public Vehicle { unsigned d_speed; public: Land(); Land(unsigned weight, unsigned speed); void setspeed(unsigned speed); unsigned getspeed() const; };By postfixing the class name
Land
in its definition by : public
Vehicle
the
derivation is realized: the class Land
now contains all
the functionality of its base class Vehicle
plus its own specific
information and functionality. The extra functionality consists here of a
constructor with two arguments and interface functions to access the speed
data member. (The derivation in this example mentions the keyword
public
:
public derivation. C++ also implements
private derivation and
protected derivation, both of which are not
often used and which we will therefore leave to the reader to uncover.). To
illustrate the use of the derived class Land
consider the following
example:
Land veh(1200, 145); int main() { cout << "Vehicle weighs " << veh.getweight() << endl << "Speed is " << veh.getspeed() << endl; }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 to enforce the principle of
data hiding. The class Vehicle
could, e.g., be recoded and recompiled, after which the program could be
relinked. The class Land
itself could remain unchanged.
Actually, the previous remark is not quite right: If the
internal organization of Vehicle
changes, then the internal
organization of Land
objects, containing the data of Vehicle
, changes
as well. This means that objects of the Land
class, after changing
Vehicle
, might require more (or less) memory than before the
modification. However, in such a situation we still don't have to worry about
the use of member functions of the parent class Vehicle
in the class
Land
. We might have to recompile the Land
sources, though, as the
relative locations of the
data members within the Land
objects will
have changed due to the modification of the Vehicle
class.
As a rule of thumb, classes which are derived from other classes must be fully recompiled (but don't have to be modified) after changing the data organization of their base classes. As adding new member functions to the base class doesn't alter the data organization, no recompilation is needed after adding new member functions. (A subtle point to note, however, is that adding a new member function that happens to be the first virtual member function of a class results in a hidden pointer to a table of pointers to virtual functions. This topic is discussed further in chapter 14).
In the following example we assume that the class Auto
, representing
automobiles, should contain the weight, speed and name of a car.
This class is therefore derived from Land
:
class Auto: public Land { char const *d_name; public: Auto(); Auto(unsigned weight, unsigned speed, char const *name); Auto(Auto const &other); ~Auto(); Auto const &operator=(Auto const &other); char const *getname() const; void setname(char const *name); };In the above class definition,
Auto
is derived from Land
, which in
turn is derived from Vehicle
. This is called
nested derivation:
Land
is called Auto
's
direct base class, while Vehicle
is
called the
indirect base class.
Note the presence of a
destructor, a
copy constructor and an
overloaded assignment operator in the class Auto
. Since this class
uses a pointer to reach
dynamically allocated memory, these members should
be part of the
class interface.
As will be clear 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 (unsigned weight, unsigned speed) { setweight(weight); setspeed(speed); }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 6.4).
Consequently, in the above implementation the
default constructor of Vehicle
is called, which probably
initializes the weight of the vehicle, only to be redefined immediately
thereafter by the function setweight()
.
A better approach is of course directly to call the constructor of
Vehicle
that expects an unsigned weight
argument. The syntax to
achieve this is to mention the constructor to be called (supplied with an
argument) immediately following the argument list of the constructor of the
derived class itself. The use of such a
base class initializer is shown
below:
Land::Land(unsigned weight, unsigned speed) : Vehicle(weight) { setspeed(speed); }
class Base { public: ~Base(); }; class Derived: public Base { public: ~Derived(); }; int main() { Derived derived; }At the end of the
main()
function, the derived
object ceases to
exists. Hence, its
destructor Derived::~Derived()
is called. However,
since derived
is also a Base
object, the Base::~Base()
destructor
is called as well.
It is this not necessary to call the Base::~Base()
destructor
explicitly from the Derived::~Derived()
destructor.
Constructors
and
destructors are called in a stack-like fashion: when derived
is
constructed, the appropriate Base
constructor is called first, then the
appropriate Derived
constructor is called. When derived
is destroyed,
the Derived
destructor is called first, and then the Base
destructor
is called for that object. In general, a
derived class destructor is called
before a
base class destructor is called.
Let's assume that the vehicle classification system should be able to
represent trucks, consisting of two parts: the front engine, which pulls
a trailer. Both the front engine and the trailer have their own weights,
but the getweight()
function should return the combined weight.
The definition of a Truck
therefore starts with the class definition,
derived from Auto
but it is then expanded to hold one more unsigned
field
representing the 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 in an additional field:
class Truck: public Auto { unsigned d_trailer_weight; public: Truck(); Truck(unsigned engine_wt, unsigned speed, char const *name, unsigned trailer_wt); void setweight(unsigned engine_wt, unsigned trailer_wt); unsigned getweight() const; }; Truck::Truck(unsigned engine_wt, unsigned speed, char const *name, unsigned trailer_wt) : Auto(engine_wt, speed, name) { d_trailer_weight = trailer_wt; }Note that the class
Truck
now contains two functions already
present in the base class Auto
: setweight()
and getweight()
.
setweight()
poses no problems: this
function is simply redefined to perform actions which are specific to a
Truck
object.
setweight()
, however, will
hide
Auto::setweight()
: for a Truck
only the setweight()
function
having two unsigned
arguments can be used.
Vehicle
's setweight()
function remains available for a
Truck
, but it must now be
called explicitly, as Auto::setweight()
is now hidden from view.
This latter function is hidden,
even though Auto::setweight()
has only one unsigned
argument. To
implement Truck::setweight()
we could write:
void Truck::setweight(unsigned engine_wt, unsigned trailer_wt) { d_trailer_weight = trailer_wt; Auto::setweight(engine_wt); // note: Auto:: is required }
Auto
-version of setweight()
is
accessed through the
scope resolution operator. So, if a Truck t
needs
to set its Auto
weight, it must use
t.Auto::setweight(x);
class Truck
:
void setweight(unsigned engine_wt) { Auto::setweight(engine_wt); }Now the single argument
setweight()
member function can be used by
Truck
objects without having to use the scope resolution operator. As the
function is defined inline, no overhead of an extra function call is involved.
getweight()
also is already defined in Auto
, as
it was inherited from Vehicle
. In this case, the class Truck
should
redefine this member function to allow for the extra (trailer) weight in
the Truck
:
unsigned Truck::getweight() const { return ( // sum of: Auto::getweight() + // engine part plus d_trailer_weight // the trailer ); }
Truck
to display several weights:
int main() { Land veh(1200, 145); Truck lorry(3000, 120, "Juggernaut", 2500); lorry.Vehicle::setweight(4000); cout << endl << "Truck weighs " << lorry.Vehicle::getweight() << endl << "Truck + trailer weighs " << lorry.getweight() << endl << "Speed is " << lorry.getspeed() << endl << "Name is " << lorry.getname() << endl; }Note the explicit call of
Vehicle::setweight(4000)
: assuming
setweight(unsigned engine_wt)
is not part of the interface of the class
Truck
, it must be called explicitly, using the Vehicle::
scope
resolution, as the single argument function setweight()
is hidden from
direct view in the class Truck
.
The situation with Vehicle::getweight()
and Truck::getweight()
is
a different one: here the function Truck::getweight()
is a
redefinition of Vehicle::
getweight()
, so in order to reach
Vehicle::getweight()
a scope resolution operation (Vehicle::
) is
required.
How can we construct a `Swiss army knife' in C++? First we need (at
least) two base classes. For example, let's assume we are designing a toolkit
for the layout of a cockpit instrument panel in an aircraft. We design all
kinds of instruments, like an artifical horizon and an altimeter. One of the
components that is often seen in aircraft is a
nav-com set: a combination
of a navigational beacon receiver (the `nav' part) and a radio communication
unit (the `com'-part). To define the nav-com set, we first design the
NavSet
class. For the time being, its data members are omitted:
class NavSet { public: NavSet(Intercom &intercom, VHF_Dial &dial); unsigned getActiveFrequency() const; unsigned getStandByFrequency() const; void setStandByFrequency(unsigned freq); unsigned toggleActiveStandby(); void setVolume(unsigned level); void identEmphasis(bool on_off); };In the class's contructor we assume the availability of the classes
Intercom
, which is used by the pilot to listen to the information that is
transmitted through the navigational beacon, and a class VHF_Dial
which is
used to represent visually what the NavSet
receives.
Next we construct the ComSet
class. Again, omitting the data members:
class ComSet { public: ComSet(Intercom &intercom); unsigned getFrequency() const; unsigned getPassiveFrequency() const; void setPassiveFrequency(unsigned freq); unsigned toggleFrequencies(); void setAudioLevel(unsigned level); void powerOn(bool on_off); void testState(bool on_off); void transmit(Message &message); };In this class we can receive messages, which are transmitted though the
Intercom
, but we can also transmit messages, using a Message
object which is passed to the ComSet
object using its transmit()
member function.
Now we're ready to construct the NavCom
set:
class NavComSet: public ComSet, public NavSet { public: NavComSet(Intercom &intercom, VHF_Dial &dial); };Done. Now we have defined a
NavComSet
which is both a NavSet
and a ComSet
: the possibilities of either
base class are now
available in the
derived class, using
multiple derivation.
With multiple derivation, please note the following:
public
is present before both base class names
(NavSet
and ComSet
). This is so because the default derivation
in C++ is
private
: the keyword public
must be repeated before each
base class specification. The base classes do not have to have the same kind
of derivation: one base class could have public
derivation, another base
class could use
protected
derivation, yet another base class could use
private
derivation.
NavComSet
introduces no
additional functionality of its own, but merely combines two existing
classes into a new
aggregate class. Thus, C++ offers the possibility to
simply sweep multiple simple classes into one more complex class.
This feature of C++ is frequently used. Usually it pays to develop `simple' classes each having a simple, well-defined functionality. More complex classes can always be constructed from these simpler building blocks.
NavComSet
constructor:
NavComSet::NavComSet(Intercom &intercom, VHF_Dial &dial) : ComSet(intercom), NavSet(intercom, VHF_Dial) {}The constructor requires no extra code: Its only purpose is to activate the constructors of its base classes. The order in which the base class initializers are called is not dictated by their calling order in the constructor code, but by the order in which the base classes are specified in the class interface.
NavComSet
class definition needs no extra data members or
member functions: here (and often) the inherited interfaces provide all the
required functionality and data for the multiply derived class to operate
properly.
setVolume()
in the NavSet
class and a function
setAudioLevel()
in the ComSet
class. A bit
cheating, since we could
expect that bits units in fact use a composed object Amplifier
, which
deals with the volume setting. A revised class might then either use a
Amplifier &getAmplifier() const
member function, and leave it to the
application to set up its own interface to the amplifier, or access functions
for, e.g., the volume are made available through the NavSet
and ComSet
classes as, normally, member functions having the same names (e.g.,
setVolume()
). In situations where two base classes use the same member
function
names, special provisions need to
be made to prevent
ambiguity:
NavComSet navcom(intercom, dial); navcom.NavSet::setVolume(5); // sets the NavSet volume level navcom.ComSet::setVolume(5); // sets the ComSet volume level
inline
:
class NavComSet: public ComSet, public NavSet { public: NavComSet(Intercom &intercom, VHF_Dial &dial); void comVolume(unsigned volume) { ComSet::setVolume(volume); } void navVolume(unsigned volume) { NavSet::setVolume(volume); } };
NavComSet
class is obtained from a third party, and should
not be altered, a
wrapper class could be used which does the previous
explicitatioon for us in our own programs:
class MyNavComSet: public NavComSet { public: MyNavComSet(Intercom &intercom, VHF_Dial &dial) : NavComSet(intercom, dial); {} void comVolume(unsigned volume) { ComSet::setVolume(volume); } void navVolume(unsigned volume) { NavSet::setVolume(volume); } };
NavCom
class, introduced in section
13.5 We start by defining two objects, one of a base class and one
of a derived class:
ComSet com(intercom); NavComSet navcom(intercom2, dial2);The object
navcom
is constructed using an Intercom
and a Dial
object. However, a NavComSet
is at the same time a ComSet
, which makes
the
assignment from navcom
(a derived class object) to com
(a base class object) possible:
com = navcom;The effect of this assignment should be that the object
com
will now
communicate with intercom2
. As a ComSet
does not have a VHF_Dial
,
the navcom
's dial
is ignored by the assignment: when assigning a
base class object from a derived class object only the base class data members
are assigned, other data members are ignored.
The assignment from a base class object to a derived class object, however, is problematic: In a statement like
navcom = com;it isn't clear how to reassign the
NavComSet
's VHF_Dial
data
member as they are missing in the ComSet
object com
. Such an
assignment
is therefore refused by the compiler.
The following general rule applies: in assignments in which base class objects and derived class objects are involved, assignments in which data are dropped is legal. However, assignments in which data would remain unspecified is not allowed. Of course, it is possible to redefine an overloaded assignment operator to allow the assignment of a derived class object by a base class object. E.g., to achieve compilability of a statement
navcom = com;the class
NavComSet
must have an overloaded assignment operator
function accepting a ComSet
object for its argument. It would be the
programmer's responsibility to decide what to do with the missing data.
Vehicle
classes, and define the following objects and
pointer variable:
Land land(1200, 130); Auto auto(500, 75, "Daf"); Truck truck(2600, 120, "Mercedes", 6000); Vehicle *vp;Now we can assign the addresses of the three objects of the derived classes to the
Vehicle
pointer:
vp = &land; vp = &auto; vp = &truck;Each of these assignments is acceptable. However, an implicit conversion of the derived class to the base class
Vehicle
is used, 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 as this is the only functionality
of a Vehicle
, which is the object vp
points to, as far as the compiler
can tell.
The same reasoning holds true for
references to
Vehicles
. If, e.g., a function is defined with a Vehicle
reference
parameter, the function may be passed an object of a class that is derived
from Vehicle
. Inside the function, the specific Vehicle
members of the
object of the derived class remain accessible. This analogy between pointers
and references holds true in general. Remember that a reference is nothing but
a
pointer in disguise: it mimics a plain variable, but actually it is a
pointer.
This
restricted functionality furthermore has an important consequence
for the class Truck
. After the statement vp = &truck
, vp
points to
a Truck
object. So, vp->getweight()
will return 2600 instead of
8600 (the combined weight of the cabin and of the trailer: 2600 + 6000),
which would have been returned by t.getweight()
.
When a function is called via a pointer to an object, then the type of the pointer and not the type of the object itself determines which member functions are available and executed. In other words, C++ implicitly converts the type of an object reached via a pointer to the type of the pointer.
If the actual type of the object to which a pointer points is known, an explicit type cast can be used to access the full set of member functions that are available for the object:
Truck truck; Vehicle *vp; vp = &truck; // vp now points to a truck object Truck *trp; trp = reinterpret_cast<Truck *>(vp); cout << "Make: " << trp->getname() << endl;Here, the second to last statement specifically casts a
Vehicle *
variable to a Truck *
. As is usually the case with type casts, this code
is not without risk: it will only work if vp
really points to a
Truck
. Otherwise the program may behave unexpectedly.