Professional Documents
Culture Documents
Book Number: 16
All rights reserved. No part of the contents of this book may be reproduced or transmitted in
any form or by any means without the written permission of the copyright owners.
004.42(075.8)
Published by
Univerzitetska knjiga Mostar, 2005
Edicions de la Universitat de Leida, 2005
Josep Maria Ribó
Ismet Maksumić
Siniša Čehajić
Introduction to OOP
with C++
Published by
Univerzitetska knjiga Mostar
Edicions de la Universitat de Leida,
July, 2005
Table of contents
-1-
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
-2-
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
• In order to model a concept with a type we do not consider all its aspects, we
just focus on those issues which are of interest for its users and forget the
rest. For example, if we define the type Student for the administrative
software application of a university, we may provide operations to get and
set his/her name, id and subjects in which he/she has enrolled, but we will
not be interested in his/her parents’ name and the name of his/her friends.
• When we define a type as a set of operations which describe the behaviour of
its entities, we forget about how that behaviour will be internally
implemented.
This abstract approach to define and use new types constitutes an excellent
way to deal with complexity: we reduce it by separating the services offered
by the type (what) from the type implementation (how). The implementation
issues are deferred to a later time or are provided by somebody else (e.g., the
authors of a library of types). As a result, we only have to focus on one
aspect at a time. This issue is usually referred to as separation of concerns.
Since we define these new high-level types from an abstract point of view, they
may be called abstract types.
-3-
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
which precise manner fuel is mixed with air and gets to the engine; which
mechanical elements are involved in the transmission of the energy produced by the
engine to the wheels, etc.). That is, abstraction is a good way to deal with
complexity (both in everyday life and in software construction).
This idea of abstraction (more precisely, data abstraction) is one of the most
fundamental issues in object orientation and, in general, in software construction.
Example
-4-
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Example
The operation of concatenating two instances a and b of the type String can be
implemented by creating a new array, copying to it (from the beginning) all the
characters of the array that represents a and, after that and in subsequent positions,
all the characters of the array that represents b. If we have chosen to represent the
end of the string by means of a character mark, we should include this character
mark in the new array that contains the concatenation of both strings, right after the
last character of the concatenation. If a count integer has been chosen, then this
integer should be appropriately set.
♦♦♦
Most object-oriented programming languages provide a construct that allows:
• the abstract definition of types in terms of their operations
• the representation of the type in terms of lower-level elements and
• the implementation of those operations.
This construct is called class and can be defined in the following way [Meyer1997]:
Definition (Class)
A class is an abstract type together with its representation and
implementation.
A class may provide a partial type representation and/or implementation. In
that case we call it an abstract or deferred class. A class which is not
deferred is said to be effective.
That is:
• A class is an abstraction used to model a (usually high-level) concept in an
O.O. programming language defining the behaviour of that concept (in terms
of a set of operations), an internal representation (or structure) for the
instances of the concept and a specific implementation of its operations in
terms of the representation that has been chosen.
-5-
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
• Effective classes will have their structure defined and their operations
specified and completely implemented. Deferred or abstract classes may
have some parts of their structure not yet represented and/or some operations
not yet implemented.
Example
In the academic application we may need to use the notion of date. Therefore, we
can define a class Date to deal with that concept. This class should offer services
such as creating a date, getting/setting the day/month/year of a date, calculating the
number of days between two dates, etc.
A preliminary class definition could be made in C++ in the following way:
class Date{
public:
void create(int pday, int pmonth, int pyear, bool& err);
int getDay();
int getMonth();
int getYear();
void setDay(int pday, bool& err);
void setMonth(int pmonth, bool& err);
void setYear(int pyear, boole& err);
void copy(Date d);
void addDays(int ndays);
unsigned int daysinBetween(Date d);
private:
unsigned int day;
unsigned int month;
unsigned int year;
};
-6-
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
The definition of the class Date is made so that the separation between the services
offered by the class (what) and the implementation of those services (how) is kept
(recall that this separation of concerns was one of the main aspects of the O.O.
approach). Indeed the definition of the class Date has been done in two separate
layers:
• Class specification (or class behaviour)
It is stated in the public part of the class definition in terms of a list of
operation headers. These are the operations that any user of this class can
apply to its instances.
Notice that the fact that we enumerate the services (operations) supplied by
the class does not mean that we are aware of what they do (i.e., their
behaviour cannot be precisely inferred from their name and parameters 1 ):
we need a precise specification of these operations. We will discuss this
issue in section 1.4.
• Class implementation
In its turn, class implementation is further split in two aspects:
- Class representation (also called class structure):
It follows the private label. The items day, month and year are
the class attributes and they constitute the class representation. Class
users cannot access the elements defined in the private part of the
class.
- Operation implementation:
This implementation is usually given out of the class block (often in
another file, as presented below). The name of each operation is
preceded by the name of the class to which it belongs. This helps the
compiler to associate each operation implementation to its class.
Notice that if today is an instance of the class Date we may apply to it the
operations of this class. For instance, we may apply to today the operation
create(...) in the following way:
today.create(12,11,2004,err);
By no means can a user of the class Date access an element of the private part:
today.day=9; //ERROR!!!! invalid access to the private part
//of Date
♦♦♦
What we have learned from the previous example can be stated more precisely with
the following definition:
1
In this book we will use indistinctly the terms parameter and argument
-7-
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
-8-
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
The link between both roles is provided by the class contract: The class user
commits him/herself to using the class operations in the precise way stated by the
contract. On the other hand, the class implementer commits him/herself to
implementing the class so that it provides the services stated by the contract in the
exact way in which they have been stated (see section 1.4).
It should be clear by now that making such a separation and, furthermore, keeping
both levels completely separated is important for, at least, two good reasons:
1. Complexity reduction in application development
The user of the class Date can use the high-level services offered by this
class (e.g., addDays(ndays)) without having to bother, at the same time,
about how those services are implemented (i.e., how should we add a certain
number of days to a date and which new date would be obtained when doing
so? ). In other words, abstraction of the low-level implementation details
helps in managing complexity.
-9-
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Definition (Object)
An object is a run-time instance of some class [Meyer1997].
This essentially means that an object has the structure defined by the class and it is
possible to apply to it the operations defined by the class interface.
Example
In C++ we can create an object of the class Date as follows:
Date birthday;
The object birthday is an instance of the class Date.
This means that it has the attributes day, month and year associated to and that
we may apply to it the Date operations.
This is done (in C++) in the following way:
birthday.create(10,11,1990,err);
This applies the operation create with the parameters provided to the object
birthday. As a consequence of this application, birthday is now initialized to
the date 10-nov-1990 (see fig. 1.2).
- 10 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
The object birthday may have, at a given instant, the following state: day=12,
month=10; year=2002. The state of an object may change as a result of the
application of an operation on that object.
1.3.2. Behaviour
class Date{
public:
int getDay() const;
...
private:
...
};
Private operations
In general, not all class operations belong to the class interface (and, thus, are
available to class users). Some operations may be used only to support the
implementation of other operations and will not be offered to class users as class
operations. We will refer to the former as public operations and to the latter, as
private operations.
1.3.3. Identity
- 12 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
It is important to make the difference between class and object (or class instance)
clear.
A class is the description of the structure and behaviour that are shared by all the
objects which are instances of that class. The objects are the real entities (allocated
in the computer memory) which have the structure described by the class and to
which class operations can be applied. Each object has a separated identity.
For the sake of an example, we may say that the difference between a class and an
object is similar to that between a car model description and a specific car of that
model. The car model description defines the set of features that characterize the
cars of that model (type of engine, lamps, specific design, etc.).
A car model description is not a real car. It is only some verbose document with a
long list of features and, may be, some pictures or designs. A real car is made out of
metal, plastic and wires. However, we can expect to find in an actual car of a
specific model, all the features that have been described in the car model
description.
Metaclasses
There are some functionalities which cannot be provided by the approach we have
presented so far. For instance, given an object: how a program can know the name
of the class of that object or the operations defined by that class. Even more, how is
it possible to program the application of an operation to an object whose class is
only known at execution time.
In order to answer these questions appropriately it is necessary to consider classes,
themselves, as objects to which it is possible to apply operations like getName()
(which gets the name of the class), getOperations(), (which gets the list of
operations of the class), etc.
- 13 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
If we consider a class as an object (call it c), another question arises: of which class
is c an instance? (i.e., which is the class of a class? ). A metaclass is a class whose
instances are, themselves, classes. Some languages (like Java) define a class in their
APIs which has the role of a metaclass (in the case of Java, this class is called
Class).
Metaclasses are beyond the scope of this book. However, more information can be
found in Appendix A.
it should be clear by now that a program designed using the object orientation
programming paradigm can be described as a collection of objects, instances of
different classes, that collaborate among them by calling the operations defined in
the classes of which they are instances.
An object o1 of class C1 that calls some operation of another object o2 of class C2
is a client or user of the class C2 (we may also say that class C1 is a client or user of
class C2).
On the other hand, class C2 is a provider of some services for class C1.
A class may be, at the same time, client of a another class and provider of a third
one.
Example:
The class Student is a “client” of the class Date because it uses the class Date in
order to store/access the birthdate of a student.
In its turn, the class Student is a provider for the class UniversityStudents. This
latter class is intended to store all the students enrolled in the university. These
relationships are shown in figure 1.3
Figure 1.3
- 14 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
The specification layer of the class definition involves the detailed specification of
the class behaviour, which is usually referred to as class contract.
As we have mentioned above, the class contract represents the meeting point
between the class user (client) and the class implementer. The idea is the following:
- 15 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
In the following sections we will provide some insight on each one the elements
that constitute a class contract.
1.4.2. Preconditions
Definition (Precondition)
A precondition is an assertion attached to a class operation which establishes
those constraints which should be true at the beginning of the execution of
that operation.
This assertion must be guaranteed by the client of the class, not by the
operation to which that operation has been attached.
- 16 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Example
We may choose between two different preconditions for the operation
create(d,m,y):
• Precondition: void .
This means that the client may produce a call today.create(d,m,a)
without any special checking of the three parameters.
In this case, the implementation of create(d,m,y) will be responsible for
ensuring that the three parameters constitute a correct date. If this is not the
case, it should manage the error in some way:
- By means of a (boolean) error parameter (err) which becomes true if
d-m-y are not a correct date: today.create(d,m,y,err) or
- Throwing an exception object (exception management mechanisms will
be presented in Chapter 7).
The criterion to decide which kind of precondition should be applied may be the
following:
In the example, it is clear that the Date class is the expert who has the knowledge
to decide whether a date is correct or not. Since this class does not offer a public
operation to check the correctness of a date, the most natural approach is that this
correctness is checked by the implementation of the Date operations. Hence, we
associate a void precondition to the operation create and we make it responsible
for managing the error: create(d,m,y,err). On the other hand, doing this way,
we get a robust operation that yield controlled results in any circumstances.
In most occasions, a void precondition is preferable since it leads to a more robust
solution (as we have seen in the previous example). However, sometimes, non-void
preconditions are more natural. We present next various examples of the two kinds:
- 17 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Example
• The class IntegerStack models a collection of elements of type integer, so
that the insertions and deletions of integers in the collection are performed at
the top of it (i.e., the last element to get into the stack is the first to get out).
One of the operations of the class IntegerStack is the following:
void pop();
This operation removes from the stack its uppermost element. It is an error to
try to apply the pop() operation to an empty stack. How should we deal with
this error?
- A void precondition and the error is managed by the implementation of
the operation by returning a boolean (output parameter) which is true if it
has been attempted to remove an element from an empty stack (i.e., void
pop(bool& err); ).
- A precondition which states that the stack is not empty
In this case, the second alternative is more natural because most of the
algorithms that work with stacks never try to pop an element from an empty
stack. Usually, these algorithms follow a pattern similar to the following:
void someStackAlgorithm(IntegerStack p)
{
//....
while (!p.empty()){
//....
p.pop(); //the stack p is not empty
}
//...
}
Thus, using a void precondition would lead to check the emptiness of the
stack twice: at the loop condition and within the implementation of pop().
Furthermore, the implementation of pop() will become easier.
• The classPhoneDirectory provides an operation long
getPhoneNumber(char namePerson[]) which obtains the telephone
number of a specific person (identified by name). We may have:
1. Pre: Void (better solution)
2. Pre: namePerson is in the directory (worse solution)
In the case 1, if the namePerson is not in the directory, the caller should be
warned in some way (e.g., returning 0; by means of an output boolean
parameter:
phone=getPhoneNumber(name, notFound);
- 18 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
or launching an exception –see Chapter 7–). Hence, the user can call this
operation without having to bother about whether the person name is in the
directory or not.
In general, in case 2, before calling this operation, the user will have to look
for namePerson in the directory (with an operation of the kind: bool
existsPerson(char namePerson[])) and call
getPhoneNumber(...) , only in the affirmative case. However, this will
probably generate two searches of namePerson within the directory: one in
existsPerson(...) and the other one in getPhoneNumber(...), since,
in order to get the number, this operation will probably have to locate first the
person. Therefore its efficiency will drop.
The option 1 is preferred.
• The class PhoneDirectory provides an operation void
insertPhone(char namePerson[], long phoneNb) which inserts the
pair (namePerson, phoneNb) into the directory. However, the directory
may be full and this insertion may not be possible. We may have:
1. Pre: Void (better solution)
2. Pre: the directory is not full (worse solution)
The fact that the directory is full or not is an implementation detail. A public
operation to make the user aware of this fact is not advisable in this case
(even if that operation were provided, the information yielded by it could not
be trusted if the directory was to be updated concurrently). It seems clear that
it is a responsibility of the operation insertPhone(...) to warn the caller
if the insertion cannot be done because the directory is full. The option 1 will
be preferred. An error parameter should be included or an exception should
be launched (see Chapter 7).
• The class BinarySearch has been defined to encapsulate the algorithm of
binary search on an array of integers. It offers an operation: bool
searchinteger(int v[], int n, int i) which searches the integer
i within v[0..n-1] and returns true if the integer is found and false
otherwise. Recall that a binary search can only be performed on an array if
that array has been sorted. We may have:
1. Pre: void (worse solution)
2. Pre: the array v is sorted in ascending order and has been initialized in
the range of indexes [0..n-1] (better solution)
The case 1 forces the operation to check whether the array v is sorted.
However, this checking compromises the performance of the operation
(which should be O(log n) and now will be O(n)). Furthermore, how does the
operation know if v[0..n] has been properly initialized?
Choice 2 is clearly the best in this case.
♦♦♦
- 19 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Some authors advocate for using (almost) exclusively void preconditions and,
hence, get robust operations [Liskov].
We prefer the use of the naturality criterion. If in application of such criterion, the
non-void precondition is chosen (and hence, a non-robust operation is obtained), it
is always possible to add another operation which mimics the first one but with a
void precondition. This second operation would be used in the small number of
cases in which the first one is not appropriate and a robust operation is required.
1.4.3. Postconditions
Definition (Postcondition)
A postcondition is an assertion associated to a class operation which
establishes those properties that should hold at the end of the execution of
that operation, provided that the precondition held at the beginning of its
execution.
This assertion must be guaranteed by the class implementer.
One important issue implied by this definition is that the operation implementation
is only committed to reaching the postcondition at the end of the operation
execution if the precondition held at the beginning of that execution. Therefore, as
we have mentioned above, the operation implementation can rely on the fact that
the precondition holds at the beginning and should not check it.
The postcondition of an operation will show usually two different forms according
to the type of the operation:
• Creators and modifiers: The postcondition should contain the way in which
the object to which the operation has been applied has been modified as a
result of such operation.
It is important to notice that the postcondition should focus on which changes
have taken place on the object rather than on how these changes have been
implemented.
• Consultors: The postcondition should indicate which part of the object state
is obtained by the operation and where it is obtained. In addition, it should
indicate that the state of the object to which the operation has been applied
has not changed with respect to its precondition.
For both kinds of operations the postcondition should also explain what the
operation will do in the case that some error is encountered. Two habitual
behaviours used by the operations to react to this case are the following:
- 20 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
• Use a parameter or a returned result to inform the caller about the error
situation or
• Raise an exception object. This is the preferred way to face error situations
by O.O. environments. We will present exceptions it in Chapter 7.
A postcondition may contain the following items:
• The public members of the class to which the operation is associated
• The operation parameters.
The postcondition should reference all the output and input/output
parameters of the operation (including its returned result, if it is a function).
Usually, it will also reference all the input parameters (however, this is not
strictly compulsory).
• The object to which the operation is applied.
In the postcondition of an operation op applied to an object x (x.op(a,b))
we may reference the state of the object x right before the beginning of the
execution of op and after it. The state of x before the operation execution
will be notated x@pre. This notation is taken from the OCL standard
notation to write constraints [OCL].
Example:
void addDays(int ndays);
Call: x.addDays(ndays)
Pre: void
Post: The resulting date x is the date x@pre plus ndays days.
• Nothing else should come up in a postcondition.
The objective of class invariants is to state explicitly all those properties that should
always be true (except for the transition instants of operation execution) in order to
ensure the integrity of the class objects. For that reason, the class invariant should
be satisfied immediately after the application of the creation operation to an object
and should not be broken by any other public operation applied to it.
Let us consider some examples:
Example
• The class invariant associated to the class Date may be the following:
Inv: Any object of the class should model a valid date posterior or equal to
1-01-1900
• The class invariant associated to the class IntegerStack may be the
following:
Inv: Any object of the class IntegertStack should model a stack which
contains a number of integers between 0 and N
• The class invariant associated to the class PhoneDirectory may be the
following:
Inv: Any object of the class PhoneDirectory should contain a collection
of pairs (name, phone), where name is a string and phone, a long.
There cannot be two pairs with the same name.
♦♦♦
Some remarks should be made concerning invariants, which introduce some aspects
that will be presented in later chapters:
• If an error occurs during the execution of an operation, that error should be
detected and the state of the class objects that have been involved with the
- 22 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
error should be left in a controlled state (i.e., a state that satisfies the class
invariant). If this is not possible, the program should stop immediately or the
uncontrolled objects should be destroyed. Using a boolean parameter to
signal these error situations is a poor way to deal with them. We have
mentioned several times that Chapter 7 will provide a better solution based
on raising exceptions.
• Notice that there exist a period between the declaration of an object and its
creation (by means of the create operation) in which the invariant does not
hold, thus, the state of that object is uncontrolled:
bool err;
Date d; //object declaration
//mo=d.getMonth(); //INCORRECT!!!!
- 23 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
- 24 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
- 25 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
C++ is a O.O. programming language which allows us to define classes using the
two-layer approach that we have presented in section 1.2 above. In this section we
will discuss the different aspects of class definition in C++. First, we introduce an
example with a complete class definition. Then, the rest of the section is devoted to
present the most relevant aspects of the example.
This section is intended just for introductory purposes. Some of these aspects will
be presented in a more detailed way in successive chapters.
Example
This example shows a complete definition and use of a class in C++. This definition
is split in three files. In addition we show another file with a program that uses the
class (this is called client or user program).
• File Date.txt
It contains the class contract as it has been presented in section 1.4.6
• File Date.h
#ifndef DATA_H
#define DATA_H
#include <iostream>
class Date{
private:
unsigned int day;
unsigned int month;
unsigned int year;
- 26 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
• File Date.cpp
#include <iostream>
#include "Date.h"
- 27 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
void Date::copy(Date d)
{
day=d.day;
month=d.month;
year=d.year;
}
• File User.cpp
#include "Date.h"
#include <iostream>
- 28 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
int main()
{
bool err;
unsigned int d,m,y;
Date today;
cin>>d>>m>>y;
today.create(d,m,y,err);
if (!err) cout<<today.getDay()<<"-"<<today.getMonth()<<"-"
<<today.getYear()<<endl;
else cout<<"error"<<endl;
return 0;
}
♦♦♦
The different aspects of this example are discussed in the following sections.
2
there is, still, the possibility of protected members, which are presented in
section 5.3.1.
3
Actually, they are also visible from the so-called friend actions, as we will see in
section 2.3.
- 29 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
- After the brace (};) which concludes the definition of the class. In this
case, we have to indicate that the operation belongs to a specific class,
prefixing its name by the class name followed by ::, as is shown next:
If the second alternative is chosen, the operation declaration comes up in the .h file
(e.g., date.h) while the operation implementations can be usually found in the .cpp
file (e.g., date.cpp); see the example. Afterwards we will go back to this
distribution.
- 30 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Automatic objects
Pointers to objects
Definition (Pointer)
A pointer is a program entity that may have as value one of the following:
• The memory address where an object of a certain type is stored.
• The value 0 (or NULL), which means that it is not associated to any
object at the moment.
• An undefined value (when a pointer is declared).
The declaration of a pointer states to which type (or class) of objects it may
point.
Date* pbirthday;
This sentence declares the program entity named pbirthday as a pointer to some
object of the class Date. However, it does not make it point to any particular object.
It has an undefined value.
It is important to state clearly that this sentence does not create an object of the
class Date (i.e., pbirthday is not an object).
- 31 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Date birthday;
pbirthday=&birthday;
Date day;
day=*pbirthday;
Dynamic objects
void f(int n)
{
Date* v;
- 32 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
More than one pointer may point to a dynamic object. If zero pointers point to a
dynamic object, then it becomes garbage (see Chapter 2).
Figure 1.4. shows graphically the operations that have been presented.
References to objects
Definition (Reference)
A reference is a program entity such that:
• It is associated to a specific object
• It is an alternative way to refer to the object to which it is associated. In
fact, it is an alias of that object.
• It is associated to an object since the creation of the reference. It cannot be
associated to any other object within the definition block of the reference
and it must be associated to an object at all times.
- 33 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
reftoday is a reference to the object today. From now on, reftoday is an alias
for the object today;
today.create(10,11,2004,err);
and
reftoday.create(10,11,2004,err);
are equivalent.
aux=a;
a=b;
b=aux;
}
int main()
{
int x=10;
int y=20;
exchange(x,y);
//x=20, y=10
return 0;
}
- 34 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
void f(List l)
{
// Procedure implementation
}
int main()
{
List li;
this parameter pass may create l as a copy the entire list li. This would be
very time and space consuming. It can be avoided by passing a reference to
li:
void f(List& l)
{
// Procedure implementation
}
int main()
{
List li;
- 35 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Additional remark:
On the other hand, the declaration of the parameter as const also allows the
call to the function f(l) with a constant parameter, which, otherwise, is not
possible. In particular, the following calls are correct if the parameter of f()
has been defined as const:
int main()
{
List l1;
//......Fill in l1
const List l2=l1;
return 0;
}
imostrecent=0;
for (i=0;i<n;i++){
if (moreRecent(v[i],v[imostrecent])) imostrecent=i;
}
return v[imostrecent];
}
Date recent;
recent=getMostRecentDate(v,n);
- 36 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Date today;
today.create(10,12,2004,err);
getMostRecentDate(v,n)=today;
The object date to which the result of the function refers gets the value of
today.
Notice that this would have not been possible with a non-reference return.
Notice that a function can never return a reference to a local variable (since a
reference must always refer to an existing object and the local variable will
be destroyed when the execution reaches the end of the function).
Object construction deserves much more attention. Chapter 2 is devoted to this
issue. In this chapter we will review and extend some of the aspects we have
introduced here.
As we already know a program may consult and modify the state of an object x
which is an instance of the class C by means of applying to x the members defined
at the public part of the class C.
We will present in this section how we may apply a member to the different kinds
of objects defined in C++.
Application of a member to an automatic object
Date birthday;
bool err;
birthday.create(10,12,2003,err);
This notation implies that we are invoking the operation create with the
parameters 10, 12 and 2003 on the automatic object called birthday.
- 37 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Date* pbirthday;
pbirthday=&birthday;
(*pbirthday).setMonth(11,err);
pbirthday->setMonth(11,err);
Warning:
It is an error to apply an operation to a pointer which is not pointing to any
particular object:
Date* px;
px->create(10,11,2002,err);
//ERROR. px does not point to any object
rbirthday.setMonth(11,err);
birthday.setMonth(11,err); //Both are equivalent
Date birthday;
birthday.setMonth(11,err);
- 38 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
The entity this can be used within the implementation of a class operation
and it is defined as a pointer to the object to which this operation is applied
at execution time.
The object itself can be accessed by *this.
Example
We may add an operation void copy2(Date dat2) to the class Date such that,
when it is applied to an object dat:
dat.copy2(dat2);
it copies the object dat into the parameter dat2.
The implementation of copy2 could be the following:
This refers to the object on which copy2 has been called (in this example dat). An
alternative implementation for this operation is dat2.day=this->day;
♦♦♦
- 39 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
It is important not to get confused between both concepts. We summarize next the
differences:
• References are used as aliases to refer to objects. That is, we can access to an
object using indistinctly the reference or the object identifier. However, we
have just one object.
Date x;
Date& y=x;
x.create(10,11,2004,err);
y.create(10,11,2004,err); //Both are equivalent
Date x;
Date* y;
y=&x;
x.create(10,11,2004,err);
y->create(10,11,2004,err);
(*y).create(10,11,2004,err); //the three of them are equivalent
Date x;
Date& y; //INCORRECT
- 40 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
If we want to define a class called Name we will create the following files:
• File Name.h (or header file). It contains the class definition:
class Name{..};
with:
- The attributes that constitute its class structure (in its private part)
- The headers of its private and public operations
If we write the operation implementation right after its declaration (which is,
in general, not encouraged), the file name.h will also contain the
implementation of the operations.
• File Name.cpp (or implementation file). It contains:
- #include "name.h" (i.e., the inclusion of the class definition).
- The implementation of the private and public class operations.
• File Name.txt (or documentation file). It contains the class contract as it has
been presented in 1.5. This is a text file (it can have any other extension; in
particular, it may be an html file).
• File User.cpp (or client/user file). It contains:
- #include "Name.h"
- Some functions (including a main(){...} function) that use the class
Name.
This file is not a part of the class definition.
This structure of files is a bit different if the defined class is a template class (see
Chapter 3). In that case, there is no file name.cpp. Its contents are inserted into the
file Name.h. This will be presented in Chapter 3.
1. Compile Date.cpp
Using the GNU C compiler, this could be done by issuing the instruction:
g++ -c Date.cpp
This instruction generates an object file called date.o. This file contains the
machine code for the class and the operation implementations. However, it is
not directly executable since it has no main function.
2. Compile User.cpp
- 41 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Using the GNU C compiler, this could be done by issuing the instruction:
g++ -c User.cpp
This generates an object file called user.o. This file contains the machine
code for the main program (user of the class Date). However, it is not
directly executable since it has several calls to operations whose code is not
known by user.o (e.g., create(...)).
- 42 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
- 43 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
- 44 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
class MyString{
public:
void create();
void create(char st[]);
char getIthChar(unsigned int pos, bool& err) const;
void putChar(char c, int pos, bool& err);
unsigned int getLength() const;
unsigned int substring(const MyString& c);
bool equals(const MyString& c);
void copy(const MyString& c);
bool lowerOrEqual(const MyString& c);
void concat(char* c);
1.7.2. Solution
File MyString.txt
This file contains the specification of the class MyString. It may look like the
following:
Class MyString
This class models a string of characters with its usual operations
*Operations:
*Creator:
Call: s.create(st);
Pre: st is an array of chars that ends in '\0'
Post: s is a MyString that contains the characters of st.
void create();
Call: MyString s;
Pre: void
Post: s is a MyString with no characters ("")
*Modifiers:
Call: s.putChar(pos,c,error);
Pre: void
Post: s is the same MyString as s@pre except for the character at
- 45 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Call: s.concat(st);
Pre: void
Post: s contains s@pre followed by st.
Call: s.get(is,c);
Pre: void
Post: s contains the next sequence of characters that have been read
from the
input stream is. The character c is the character that acts as final
mark for the string.
void reverse();
...
*Consultors
File MyString.h
This file contains the representation of the class MyString and the operation
headers.
#ifndef MYSTRING_H
#define MYSTRING_H
#include <iostream>
class MyString{
char* st;
public:
void create();
void create(char st[]);
- 46 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
#endif
Remarks:
• We have chosen to represent a string by means of an array reserved
dynamically with a mark (\0) to signal its end.
Notice that, it is crucial to know at which array position the string finishes
(i.e., how many chars in the array are valid). This can be signalled in two
ways: by means of a final mark (the way we have chosen) or by means of a
counter that indicates the number of characters of the string.
File MyString.cpp
This file contains the implementation of the operations declared in MyString.h.
We use the operations provided by the library cstring, which we include.
#include "MyString.h"
#include <iostream>
#include <cstring>
void MyString::create()
{
st=new char[1];
st[0]='\0';
}
st=new char[strlen(pst)+1];
strcpy(st,pst);
}
- 47 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
i++;
}
return (st[i]<=c.st[i]);
}
void MyString::concat(char* c)
{
int i,j,l1,l2;
char* aux;
aux=new char[getLength()+strlen(c)+1];
i=0;j=0;
while (st[i]!='\0'){
aux[i]=st[i];
i++;
}
- 48 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
while(c[j]!='\0'){
aux[i]=c[j];
j++; i++;
}
aux[i]='\0';
st=aux;
}
while (!end){
c.get(aux[0]);
i=0;
while (aux[i]!=test && i<NCAR-1){
i++;
c.get(aux[i]);
}
if (aux[i]==test){
aux[i]='\0';
end=true;
}
else aux[i+1]='\0';
this->concat(aux);
}
Remarks:
• Notice how the create operations reserve memory dynamically:
• If we use the operation create(), then, at least we reserve one character for
’\0’
- 49 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
#include "MyString.h"
int main()
{
MyString ss1, ss2, ss3;
ss2.create("kjkj");
ss1.create();
ss3.create();
ss3.get(cin,'\n');
return 0;
}
- 50 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
class WordCounter
{
void create();
void countWord(MyString& w, bool& error);
int getNumbOccurrences(MyString& w) const;
int getNumbOccurrences(int j) const;
void getWord(int i, MyString& w) const;
int getNumbWords() const;
void getListOfWords(MyString c[], int& numbWords) const;
};
Notice that, in order to design the class WordCounter, we have reused the class
MyString. In fact, we model a word as an object of the class MyString.
Class reuse is one of the main issues in object-oriented programming.
- 51 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Goal:
Operations:
//Creator:
void create();
Call: wc.create();
Pre: void
Post: wc is an empty instance of the class WordCounter
(i.e., it does not contain any word).
//Modifiers
Call: wc.countWord(st,error);
Pre: Void
Post: wc contains the same words as wc@pre
and the word st counted once more.
If the word st leads to an overflow on wc then
wc=wc@pre and error=true
//Consultors
Call: nb=wc.getNbOccurrences(st);
Pre: Void
Post: nb is the number of occurrences of the word st in wc.
- 52 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
#include "MyString.h"
class CountInfo{
public:
MyString word;
int noccur;
};
class WordCounter{
CountInfo counter[N+1];
int size;
int searchWord(MyString& w) const;
public:
void create();
void countWord(MyString& w, bool& error);
int getNumbOccurrences(MyString& w) const;
int getNumbOccurrences(int j) const;
void getWord(int i, MyString& w) const;
int getNumbWords() const;
void getListOfWords(MyString c[], int& numbWords) const;
};
void WordCounter::create()
{
size=0;
}
error=false;
i=searchWord(w);
if (i==size && size<N){ //We do not have an overflow
(counter[i].word).copy(w);
counter[i].noccur=1;
size++;
}
else if (i<size){ //w has been found
counter[i].noccur++;
}
else{ error=true;} //overflow!!
}
- 53 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
/***********************************
int WordCounter::searchWord (MyString& w) const
(counter[size].word).copy(w);
while (!(w.equals(counter[i].word))){
i++;
}
return i;
}
#endif
- 54 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
Remarks:
• The word counter has been represented by means of an array counter of
(word, numb-of-occurrences) pairs. Those pairs have been encapsulated in a
class called CountInfo. Since it is a very simple class which will be used
exclusively by WordCounter we make public its attributes and we do not
provide operations to get/set them. It would have also been correct to hide
the attributes and provide operations.
counter[i] (0<= i <size) holds the information of a word
(counter[i].word) and its number of occurrences
(counter[i].noccur). counter is not sorted in any way, therefore, new
words will be added at the end (at the index size)
• A very useful tip to program class operations in an elegant and
comprehensible way is the following: identify the existing subfunctionalities
within the implementation of a class operation f(...) and encapsulate them
into new private class operations which will be called from f(...). In the
proposed solution, the operation searchWord(..) has been created using
this criterion. Doing this way, we have achieved two goals:
- The operations countWord(..) and getNumbOccurrences(..) call
the private operation searchWord(..) instead of implementing directly
its functionality. In this way, they have a more elegant and
comprehensible design (clearly, the insertion of the implementation of
searchWord(...) into those couple of operations would have led to a
more difficult-to-understand solution).
- We avoid code redundancy. Notice that without the operation
searchWord(...) we should have repeated virtually the same code in
both operations.
In any case, note that searchWord(...) should be a private operation,
since the clients of WordCounter do not need to know about it.
• We have used the operations copy and equals to assign and compare
MyString objects, respectively
(e.g., (counter[size].word).copy(w);, see countWord(...) ).
This has been a good option since the class MyString defines both
operations for these purposes.
It is worth mentioning that the C++ compiler provides an operator = for any
user-defined class, therefore, it is possible to program instructions of the sort:
counter[i].word=w;. However, the implementation provided by the
compiler may not be the one we need. For this reason, Chapter 2 explains
how these operators (=, ==, «, etc.) can be redefined.
• In the implementation of searchWord(..) we use the sentry technique
(i.e., we add the element that we are looking for at the end of the array so
- 55 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
- 56 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
How many changes have you had to do? Specifically, have you had to do
any change to the class WordCounter?
Are you convinced of the goodness of the solution (b)?
#include "WordCounter.h"
#include "MyString.h"
class WordCounterInterface{
public:
void showMenu(WordCounter& wc);
};
MyString w,wread,w1,w2,w3,w4;
w1.create("1");
- 57 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
w2.create("2");
w3.create("3");
w4.create("4");
do
{
cout<<"\n\n\n*************************"<<endl;
cout<<"Word counter........"<<endl;
cout<<"1. Count a word"<<endl;
cout<<"2. Show the counter of a word"<<endl;
cout<<"3. Show the counter of all words"<<endl;
cout<<"4. Exit\n\n"<<endl;
cout<<"****Enter a choice"<<endl;
wread.get(cin,'\n');
if (wread.equals(w1))
{
cout<<"****Enter a word to be counted"<<endl;
w.get(cin,'\n');
wc.countWord(w);
}
else if (wread.equals(w2))
{
cout<<"****Enter the word which counter "
<<should be shown"<<endl;
w.get(cin,'\n');
no=wc.getNumbOccurrences(w);
cout<<"The number of occurrences of the word "<<w
<<" has been "<<no<<endl;
}
else if (wread.equals(w3))
{
for (j=0;j<wc.getNumbWords();j++){
wc.getWord(j,w);
cout<<"The word "<<w<<" has had "
<<wc.getNumbOccurrences(j)<<" occurrences "<<endl;
}
}
else if (wread.equals(w4)) exit=true;
}
while (!exit);
}
int main()
{
WordCounterInterface wci;
WordCounter wc;
- 58 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
wci.showMenu(wc);
return 0;
}
class WordCounterInterface2{
public:
void readWords(WordCounter& wc);
void showResult(WordCounter& wc);
};
w.get(cin,' ');
while (!(w.equals(wfi))){
wc.countWord(w);
w.get(cin,' ');
}
}
for (j=0;j<wc.getNumbWords();j++){
wc.getWord(j,w);
cout<<"The word "<<w<<" has had "<<
wc.getNumbOccurrences(j)<<" occurrences "<<endl;
}
}
- 59 -
CHAPTER 1: PRINCIPLES OF OOP. CLASSES. OBJECTS
- 60 -
Chapter 2
Special members: constructors,
destructors, overloaded operators
This chapter will be devoted to present in detail three kinds of class members which
are somehow special: constructors, destructors and overloaded operators.
Some of the issues we will present in this chapter have been introduced or
motivated in Chapter 1. What we will do here will be to review the already
presented issues and to take a deeper insight of them. This is the case of
constructors. Although their need has been introduced in Chapter 1, we have
delayed to this chapter the presentation of constructors themselves, since we feel
that this is an important topic which deserves much more attention.
Destructors have to do with dynamic objects and garbage. Both things have been
mentioned in Chapter 1, but now they will be presented in far more detail.
Overloaded operations have not at all the same importance as constructors or
destructors. However, they constitute an interesting feature provided by C++ and its
presentation is necessary.
This chapter also introduces the notion of friend functions and friend classes. They
are not actually special class members (in fact, they are not class members at all),
but this chapter seemed the correct place to introduce them: on the one hand, they
bear certain similarity with class members and, on the other hand, they are essential
to overload certain operators.
2.1. Constructors
- 61 -
CHAPTER 2: SPECIAL MEMBERS
Definition (Constructor)
A constructor is a class operation which creates an object as an instance of an
specific class. This object creation involves:
• Allocate memory to store the structure of that new object.
• Initialize its attributes in a proper way (so that the class invariant holds for
the created object).
- 62 -
CHAPTER 2: SPECIAL MEMBERS
Example
In the class Date presented in 1.6, we may transform the creation operation
(create(...)) into a constructor in the following way:
class Date{
private:
//As before
//...
public:
If the parameters passed to the constructor do not constitute a valid date, the forth
parameter (error) becomes true. This error parameter should be checked after each
call to the constructor. This is not an elegant way to deal with error situations. As it
has been mentioned before, Chapter 7 will present a much better way which can be
easily applied to constructors and other class operations.
This constructor can be used in order to create a new date in the following way:
bool err;
Date today(10,12,2003,err);
The previous instruction generates a call to the constructor of the class Date with
the given parameters. As a result of this call, the object today is initialized to 10-
12-2003 and err=false.
As usual, the constructor code may also come up within the class definition:
class Date
{
private:
//As before......
- 63 -
CHAPTER 2: SPECIAL MEMBERS
public:
Date(unsigned int pday, unsigned int pmonth,
unsigned int pyear, bool& error)
{
if (correctDate(pday, pmonth, pyear)){.....}
else{....}
}
♦♦♦
class Date{
private:
//As before......
public:
Date(){day=1; month=1; year=1900;}
Date(unsigned int pday, unsigned int pmonth,
unsigned int pyear, bool& error)
{
//As before......
}
♦♦♦
- 64 -
CHAPTER 2: SPECIAL MEMBERS
class Date{
private:
//As before......
public:
Date(unsigned int pday=1, unsigned int pmonth=1,
unsigned int pyear=1900)
{
//......
}
};
The instruction
Date d; will initialize d=1-1-1900.
On the other hand, the instruction:
Date d(1,10,2001); is still possible and will initialize d=1-10-2001.
- 65 -
CHAPTER 2: SPECIAL MEMBERS
class Date{
private:
//As before......
public:
Date(const Date& pd)
{
day=pd.day; month=pd.month; year=pd.year;
}
//.....
};
Notice that we copy all the attributes of the object that we are creating (the one on
which the copy constructor is called) with the corresponding attributes of the
parameter. In this way we make the object that we are creating a copy of the
parameter.
Notice also that within the implementation of the copy constructor we may access
the private part of the class Date, thus pd.day, pd.month, pd.year are
correct.
♦♦♦
Although it is not required, the argument of a copy constructor is often a const
reference. This has two effects:
• The copy constructor cannot change the value of the argument (the argument
has been declared as const and, hence, it is read-only).
• It is possible to call the copy constructor with a const argument, like in the
following example:
- 66 -
CHAPTER 2: SPECIAL MEMBERS
Date d1(10,10,1990);
Date d2(d1); //Call to the copy constructor of Date
//(d2=10-10-1990)
- 67 -
CHAPTER 2: SPECIAL MEMBERS
The class Person does not have a user-defined copy constructor. The compiler
provides a default copy constructor with the behaviour shown in fig. 2.3. As a
result, the following program will destroy the attribute name of p:
int main()
{
Person p2("John",20,'m');
Person p(p2);
p2.setName("Ann");
....
}
This would not happen if a copy constructor would have been defined by the class
designer:
Person::Person(const Person& p){
age=p.age;
sex=p.sex;
name=new char[strlen(p.name)+1];
strcpy(name, p.name);
}
♦♦♦
- 68 -
CHAPTER 2: SPECIAL MEMBERS
Date today(10,12,2000,err);
Date d(today);
void f(Date x)
{....}
int main()
{
Date a(....);
......
f(a);
}
In this example the copy constructor of the class Date is used to copy the
value of a onto x (i.e., internally, the following operation is called: Date
x(a);)
• Objects returned by functions
If a function returns an object x of class C, a temporal object of class C is
created and initialized with the state of x by means of a call to the copy
constructor of C.
Date f()
{Date x;
.....
return x;
}
int main()
{
Date a;
.....
a=f();
}
- 69 -
CHAPTER 2: SPECIAL MEMBERS
In this example, the object x of class Date returned by f() is copied into a
temporal object by means of the copy constructor of the class Date. This
temporal object is copied to the object a by means of the = operator (more
details in 2.1.7). This process is illustrated in figure 2.4.
Notice that if the function returns a reference to an object, then there is no
need of copy constructor, since no new object is created.
class B{
....
};
class A{
B x;
.....
};
When this happens, the creation of an object of class A involves the creation
of another object of class B and this may be achieved by means of a call to
the copy constructor of the class B. This issue will be presented more
thoroughly in Chapter 4.
• Exception handling
This topic will be presented in Chapter 7.
- 70 -
CHAPTER 2: SPECIAL MEMBERS
Date today(10,9,2001,err);
Date today;
today= Date(10,9,2001,err);
However, in this case, the process of construction of the object today is different.
In particular, it involves the creation of a temporary object. The process is as
follows:
1. Creation of an object called today of the class Date using the default
constructor.
2. Creation of a temporary object of the class Date which has no identifier
associated with the value 10-9-2001.
3. Call to the assignment operator (=) which assigns the temporary object to
today.
Other situations which involve the creation of a temporary object include the
following:
• Call to a constructor to create an actual parameter within the call to a
function
- 71 -
CHAPTER 2: SPECIAL MEMBERS
void f(Date d)
{.....}
int main()
{
....
f(Date(10,9,2001,err));
}
Date f()
{
Date d(10,11,2001,err);
....
return d;
}
Date* pd1;
pd1=new Date(1,1,2000,err);
pd1 is a pointer that may refer to an object of class Date. However, it points to an
undefined place when it is created: Date* pd1;.
- 72 -
CHAPTER 2: SPECIAL MEMBERS
The operator new creates a new object of class Date dynamically (by means of a
call to the constructor of the class Date with four arguments) and returns a pointer
to this newly created object (see figure 2.5).
The operator new may be called without parameters. In particular, the sentence:
pd1 = new Date;
creates dynamically a new object of class Date by means of a call to the default
constructor of the class.
Date* vd;
vd=new Date[100];
void f(int n)
{
Date* vd=new Date[n];
}
Recall that the following is not correct, since the dimension of an automatic
array must be a constant expression.
void f(int n)
{
Date vd[n]; //INCORRECT. DIMENSION SHOULD BE
//A CONSTANT EXPRESSION
}
- 73 -
CHAPTER 2: SPECIAL MEMBERS
• The class of the array components (in this example, Date) must have a
default constructor. Otherwise, it is not possible to create each one of the
array components.
2.1.10. An example
#include <cstring>
class Person{
char* name
int age;
char sex;
public:
Person(){}
Person(char* pname, int page, char psex)
{
name=new char[strlen(pname)+1];
strcpy(name,pname);
age=page;
sex=psex;
}
void setName(char* pname){...}
void setSex(char psex){...}
void setAge(int page){...}
//......
};
int main()
{
Person* vp;
vp[0].setName("John");
vp[0].setAge(20);
vp[0].setSex('m');
//.....
}
- 74 -
CHAPTER 2: SPECIAL MEMBERS
2.2. Destructors
Definition (Destructors)
Destructors are class operations which are responsible for deallocating
objects of that class when those objects are not to be used anymore.
Definition (Garbage)
We call ’garbage’ to a memory space which has been reserved dynamically
(by means of the new operator), which has become unreachable and which
cannot be reallocated.
Date* p;
Date d1(10,12,2000,err); //(1)
p=&d1; //(4)
- 75 -
CHAPTER 2: SPECIAL MEMBERS
In this example, instructions (3) and (4) generate garbage (see fig. 2.7):
(3) A new object of class Date is allocated dynamically with the state 4-4-
2003. p points to this new object. As a consequence, the Date object to
which p pointed before (the one with state 1-1-2001) has become
unreachable.
(4) Now, p points to the automatic object d1. Again, the dynamic object created
at (3) becomes unreachable.
Therefore, both dynamic objects created in this example have become unreachable
in the end. Both dynamically created objects have become garbage.
class Person{
char* name
int age;
char sex;
public:
- 76 -
CHAPTER 2: SPECIAL MEMBERS
//......
};
void f()
{
int i=1;
Person p("John",20,'m'); //(1)
....
}
The execution of the function f() creates an automatic object p of class Person.
No dynamic object seems to have been created. However, the Person constructor
allocates dynamically an array of characters. When the execution control reaches
the end of the function f(), the non-dynamic part of p is deallocated automatically
in the same way as the integer local variable i is deallocated. You have probably
guessed that the dynamic part of p (i.e., its name) will remain in the memory as
garbage (no pointer will refer to it). This process is shown in figure 2.8.
The ecologists (system managers) warns us about the serious problems caused by
the destruction of the environment (the memory) in our lives (our programs). As a
consequence, they advocate for a policy of recycling and posterior reuse of the
generated garbage. This policy can be carried out following two different strategies:
1. Periodically, a process called garbage collector is launched with the
responsibility for detecting and deallocating all the garbage existing in the
memory. This policy is followed by some O.O. programming languages (like
Java or Smalltalk).
The problem of this approach is that the system performance decline as a
consequence of the periodical execution of the garbage collector.
2. It is the programmer who deallocates the dynamic memory which is not to be
used anymore just before becoming garbage. This strategy is far more
efficient but it is also lower level since the programmer cannot abstract the
memory management.
- 77 -
CHAPTER 2: SPECIAL MEMBERS
int* pi;
Sentence (2) deallocates the integer created dynamically by (1). Without it, this
integer would have become garbage.
The operator delete can also be used to deallocate an dynamic array in the
following way:
int* pi;
The compiler records how many integers it has allocated by the instruction (1). The
sentence (2) deallocates that number of integers starting at the integer pointed to by
pi.
- 78 -
CHAPTER 2: SPECIAL MEMBERS
We have stated that the destructor of the class C is called when the scope of an
automatic object of class C (e.g., called x) is about to end. This may happen, among
others, in the following cases:
• x is a local variable in the function f(..) and the execution control has
reached the end of f(..).
• x is a passed-by-value argument of the function f(C x....) and the
execution control has reached the end of f(..).
• x is an aggregated object of another object y of class D and y is being
destroyed (this is presented in Chapter 4).
We remark two additional aspects concerning destructors:
• There can only be one destructor per class
• If the class definition does not provide any destructor, the compiler provides
a default one, void.
Example
In this example we show how the class Person could add a destructor in order to
deallocate the attribute name.
class Person{
char* name
int age;
char sex;
public:
Person(char* pname, int page, char psex)
{
name=new char[strlen(pname)+1];
strcpy(name,pname);
age=page;
sex=psex;
}
~Person()
{
delete [] name;
}
//......
};
- 79 -
CHAPTER 2: SPECIAL MEMBERS
void f()
{
Person p("John", 20, 'm');
....
}
When the execution control reaches the end of f() the destructor of Person is
called automatically. This destructor is responsible for deallocating the piece of
memory allocated by the operator new of the constructor ( see (1) in fig. 2.9). After
this implicit call, the non-dynamic part of the object p (see (2) in figure 2.9) is also
deallocated in the same way as any other local variable.
void f()
{
Person* pp;
- 80 -
CHAPTER 2: SPECIAL MEMBERS
int main()
{
- 81 -
CHAPTER 2: SPECIAL MEMBERS
*p2=p; //(3)
Fig. 2.11 shows the situation after the execution of the instructions (1), (2) and (3).
Notice that p and *p2 shares a part of its identity (the array of characters pointed to
by name). Fig. 2.11 also shows what happens after the deletion of *p2: p also loses
the array pointed at by name.
♦♦♦
- 82 -
CHAPTER 2: SPECIAL MEMBERS
This example shows that the sharing of identity between objects may have harmful
consequences. If it is required for a specific application, we should work cautiously
in order to avoid loss of information.
Last, in the presented example, we could have avoided the problem overloading the
assignment operator (’=’) so that it copies also the dynamic part (name) of the
object. Section 2.4 is devoted to operator overloading.
Example
class C
{
private:
int x
public:
.....
friend void f(...);
};
void f(...)
{
C c;
c.x=10;
}
- 83 -
CHAPTER 2: SPECIAL MEMBERS
class C{
private:
int x
public:
.....
friend class F;
};
class F{
public:
};
Friend classes and functions violate one of the most important rules of object
orientation. Thus, they should not be used unless it is absolutely necessary.
In general the use of friend functions and classes will be restricted to the
following cases:
• If a couple of classes are to be designed so that they are mutually
dependent (both at specification and at implementation levels). The
visibility of the other class that these classes require may not be naturally
achieved by means of public operations. In that case, each class can be
made friend of the other.
An example of this situation is the class List, which models a sequence
of elements and the class ListIterator, which models a way of
traversing the list and getting its elements.
This situation may also happen between a class and a function, which is
not a member of the class but it depends on it. In that case, the function
may be declared as a friend of that class.
• To overload some operators. In the following few sections we will
discover that several operators may be overloaded by means of a friend
function, while some others can be overloaded exclusively by means of a
friend function.
For this reason we have introduced friend functions and classes right
before presenting operator overloading.
- 84 -
CHAPTER 2: SPECIAL MEMBERS
Function overloading (and also late binding) are the basis of polymorphism, which
is a very important concept of O.O. programming and is presented in Chapter 7.
In the following few sections we will show how to overload the most frequent C++
operators (=, ==, (), [], <<, >>).
2.4.1. Operator =
The assignment operator (’=’) is used to assign an object of a given class to another
one (of the same class). It works in the following way:
- 85 -
CHAPTER 2: SPECIAL MEMBERS
p2=p1; //(1):Assignment
The statement (1) applies the assignment operator to the object p2 using p1 as
argument. As a result, the state of p2 becomes ("John", 20, ’m’).
The assignment operator is generated by default by the compiler. That is, when a
class is defined, the assignment operator can be applied to its objects. However, it is
likely that the default definition does not suit our interests.
The default definition of the assignment operator copies the non-dynamic parts of
the object, but it does not make a copy of its dynamically generated parts (recall
that in section 2.1.5, we mentioned that the same situation happens with the default
copy constructor).
Example
The default assignment operator for the class Person does something similar to the
following:
return *this;
}
- 86 -
CHAPTER 2: SPECIAL MEMBERS
class Person{
//....as before.....
public:
//.....as before....
Person& operator=(const Person& p)
{
delete [] name;
return *this;
}
};
♦♦♦
Notice the following aspects in the example:
• We delete the previous contents of the attribute name, so that we do not
generate any garbage when we assign to the object the new name.
• We avoid the identity sharing by replicating the dynamic attribute (name).
Both objects do not share it anymore (see fig. 2.12 (b) ).
• By forcing that the argument p is constant, we make it read-only and, thus, it
cannot be modified within the operator definition.
• By forcing that the result of the operator is not void but a reference to
Person we imply two things:
- We can compose assignments or apply another operation directly to the
result of an assignment:
p1=p2=p3; //O.K.
This would have not been correct if the return type had been void.
- The (temporary) object we are returning is not a copy of the object that
receives the assignment but just a reference (alias) to it.
- 87 -
CHAPTER 2: SPECIAL MEMBERS
• Since the returned reference is const we cannot modify it. For instance, if a
new operation setAge(int) were defined in the class Person with the
obvious meaning, the following sentence would generate a compilation error:
(p=p2).setAge(90); //(1)
However, if the header of the assignment operator had been:
Person& operator=(const Person& p)
then, the call (1) would have been correct.
Copy constructor and operator =
It is important to state clearly the difference between the copy constructor (e.g.,
Person(const Person&) and the assignment operator (e.g. Person&
operator=(const Person&)).
• Copy constructor. It creates a new object with the same state as its only
argument. The copy constructor is used mainly in:
1. Creation of new objects in declarations
Person p(p2); or Person p=p2;
2. Pass-by-value of arguments
3. Function return
4. Initializers in class aggregation
5. Exception handling
Uses 1, 2 and 3 are explained in 2.1.5. The issue of initializers is presented in
Chapter 4 and Chapter 6. Exception handling is shown in Chapter 7.
• Assignment operator. It copies the value of the argument object to an
already existing one (i.e., it does not create a new object). Since it does not
create a new object but modifies an existing one, this operator may generate
garbage. For this reason, it should delete any dynamic data which is not used
anymore referred to from the modified object.
This operator is used whenever an object is assigned to another using =
(except for the declarations: Person p=p2;, which use the copy
constructor).
2.4.2. Operator ==
Example
This is the proposal of equality operator for the class Person:
- 88 -
CHAPTER 2: SPECIAL MEMBERS
Notice that, since this operator is not supposed to modify the state of the object to
which it is applied, it has been labelled const.
♦♦♦
Since the application of this operator is symmetric (i.e., both compared objects play
a similar role in the comparison) it may not be natural to apply the operator to one
of them. We can think of overloading the == operator by means of a friend
function:
class Person{
//....
public:
//.....
friend bool operator==(const Person& p, const Person& p2);
};
Notice that, since the friend function is not a class member, it needs two arguments.
2.4.3. Operator []
This operator is often used to access the elements of a container class by index (e.g.,
to access the first, the second, ... the nth element of the container). In the following
example we define a class IntVector to encapsulate and improve arrays of
integers.
Example
class IntVector{
int* v;
int max;
public:
//.......
int main()
{
const int MAX=100;
int n;
- 89 -
CHAPTER 2: SPECIAL MEMBERS
IntVector ivec(MAX);
class Dictionary{
//......
public:
//....
char* operator[](char* d){
int main()
{
Dictionary d;
2.4.4. Operator ()
This operator is used to encapsulate algorithms within classes. Consider the
following example:
Example
class SortAlgorithm
{
public:
void operator()(int v[], int n){
//Algorithm to sort the array of integers v[0..n]
}
};
int main()
{
SortAlgorithm sort;
const int N=100;
int w[N];
fillWithInts(w);
- 90 -
CHAPTER 2: SPECIAL MEMBERS
sort(w,99);
which corresponds to a call to the operator () on the object sort and with arguments
w and 99:
♦♦♦
class Person{
//....
public:
//.....
friend ostream& operator<<(ostream& c, const Person& p2);
};
return c;
}
♦♦♦
The fact that an ostream object is returned helps to chain this operator:
Person p1(...);
Person p2(...);
Person p3(...);
cout<<p1<<p2<<p3;
- 91 -
CHAPTER 2: SPECIAL MEMBERS
If c were not returned, then the previous program should have been implemented
like this:
cout<<p1;
cout<<p2;
cout<<p3;
- 92 -
CHAPTER 2: SPECIAL MEMBERS
#include <iostream>
class MyString{
char* st;
public:
MyString();
MyString(char st[]);
MyString(const MyString&);
#endif
MyString::MyString()
{
st=new char[1];
st[0]='\0';
}
MyString::MyString(char pst[])
{
int i=0;
st=new char[strlen(pst)+1];
strcpy(st,pst);
}
- 93 -
CHAPTER 2: SPECIAL MEMBERS
MyString::MyString(const MyString& c)
{
int i=0;
st=new char[c.getLength()+1];
for(i=0;i<=c.getLength();i++){
st[i]=c.st[i];
}
}
MyString::~MyString()
{
delete [] st;
}
///EXERCICE: IMPLEMENT IT
return 0;
}
- 94 -
CHAPTER 2: SPECIAL MEMBERS
int i=0;
delete[] st;
st=new char[c.getLength()+1];
strcpy(st,c.st);
return *this;
}
i++;
}
return (st[i]<=c.st[i]);
}
MyString* aux;
aux=new MyString;
l1=getLength();
l2=c.getLength();
aux->st=new char[l1+l2+1];
for (i=0;i<l1;i++){
aux->st[i]=st[i];
}
for (j=0;j<l2;j++){
aux->st[i]=c.st[j];
i++;
}
aux->st[i]='\0';
return *aux;
}
void MyString::concat(char* c)
{
int i,j,l1,l2;
char* aux;
aux=new char[getLength()+strlen(c)+1];
i=0;j=0;
- 95 -
CHAPTER 2: SPECIAL MEMBERS
while (st[i]!='\0'){
aux[i]=st[i];
i++;
}
while(c[j]!='\0'){
aux[i]=c[j];
j++; i++;
}
aux[i]='\0';
delete [] st;
st=aux;
}
delete [] st;
st=new char[1]; st[0]='\0';
while (!end){
c.get(aux[0]);
i=0;
while (aux[i]!=test && i<NCAR-1){
i++;
c.get(aux[i]);
}
if (aux[i]==test){
aux[i]='\0';
end=true;
}
else aux[i+1]='\0';
this->concat(aux);
}
}
Remarks:
• In the operator =, we avoid garbage by deallocating the previous value of the
object (delete [] st;).
• The header MyString& operator=(const MyString& c); is the
standard C++ way to overload this operator. The const reference parameter
allows calls of the sort:
- 96 -
CHAPTER 2: SPECIAL MEMBERS
MyString s=MyString("hello");
The fact that the operator returns a MyString object allows:
s1=s2=s3;
int main()
{
MyString ss1, ss3;
MyString ss2("kjkj");
ss3.get(cin,'\n');
MyString ss4(ss2);
ss1=ss2+ss3;
cout<<ss1<<endl;
cout<<"ss4="<<ss4<<endl;
if (ss1<=ss2) cout<<"error"<<endl;
else cout<<"ok"<<endl;
if (ss1<=ss1) cout<<"ok"<<endl;
else cout<<"error"<<endl;
if (ss2<=ss1) cout<<"ok"<<endl;
else cout<<"error"<<endl;
if (ss1==ss1) cout<<"ok"<<endl;
else cout<<"error"<<endl;
if (ss1==ss3) cout<<"error"<<endl;
else cout<<"ok"<<endl;
if (ss3==ss1) cout<<"error"<<endl;
else cout<<"ok"<<endl;
bool err;
cout<<ss3<<endl;
cout<<ss3.getIthChar(4,err);
return 0;
}
- 97 -
- 98 -
Chapter 3
Generic classes and functions
3.1. Generic classes
Consider the classes ListOfIntegers and ListOfCharacters 4 :
class ListOfIntegers{
int v[N];
int top;
public:
ListOfIntegers(){top=0;}
void insertLast(int x){v[top]=x; top++;}
void removeLast(){top--;}
int getLast() const {return v[top];}
bool emptyList() const { return (top==0);}
};
class ListOfCharacters{
char v[N];
int top;
public:
ListOfCharacters(){top=0;}
void insertLast(const char x){v[top]=x; top++;}
void removeLast(){top--;}
int getLast() const {return v[top];}
bool emptyList() const { return (top==0);}
};
Their specification and implementation are exactly the same, except for the fact that
in one case, the elements stored in the list are integers and, in the other case, they
are characters. That is, we have found out that the behaviour of a list (insert an
element, remove an element, retrieve an element, etc.) does not depend on the type
of elements that constitute the list.
The type of elements that may be contained in a list are virtually infinite. Each
application may need a list which stores different kinds of components. Therefore,
we could end up with dozens of different classes List: ListOfIntegers,
ListOfCharacters, ListOfStudents, ListOfCinemas,
ListOfWhatever... which would be essentially equivalent.
4
This is a very basic implementation. Errors coming from list overflow or
underflow have not been taken into account
- 99 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
Something similar occurs with any class that models a collection of elements.
A clear improvement to this situation may be to define a generic class List which
describe the behaviour of a list independently of the type of the elements that
constitute it. The particular type of these elements will be a parameter of the generic
class List and will be given when an object list is created.
This can be done in C++ by means of the so called template classes, as is shown in
the following example:
This is a template class definition. It defines a template class List which depends
on the type parameter T (called template parameter). This parameter will be
instantiated when a specific list object is created (see below).
Notice that the private part of the class definition declares an array of MAX elements
of generic type T (instead of int or char, as we did in the last couple of examples).
The same happens in the header of the operations insertLast(const T& x) and
T getLast();.
A template class definition, as the previous one (List<T>) can be instantiated in
the following way:
class Student{
//...
public:
Student(char* pname, char* pid, char* paddress){...}
//...
};
int main()
{
List<int> lintegers;
List<char> lchars;
List<Student> lstud;
lintegers.insertLast(1);
lchars.insertLast('d');
lstud.insertLast(s);
.....
}
- 100 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
Three list objects have been created. One of them (lintegers) will hold integer
objects, another (lchars) character objects and the last one (lstud), objects of the
class Student. Each one of them has been created by a different instantiation of
the template parameter (T) of the template class List.
An important point is the following one:
The class List (or List<T>) is not an actual class but just a template class
(that is, an instruction manual on how to create a real class). The real class
will be created when such template class is instantiated (i.e., when its
template parameter T is substituted by an actual type). Each specific
instantiation will yield a different class. In the last example three different
classes were created:
List<int>, List<char> and List<Student>
Notice that the declaration:
List<char> lchars;
not only does it create an object (lchars) but also a class (List<char>).
C++ offers a tool called templates to define generic classes (and functions: see
section 3.2).
- 101 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
• Definition:
template <class T>
class A{
//The definition of A may depend on T
};
• Instantiation:
A<int> a; // The template parameter T has been
// instantiated with int
In the previous example we have implemented the class operations within the class
definition. However, they could have been implemented outside, using the
following notation:
Notice that this form of operation definition considers those operations themselves
as template operations. We will show more things about this idea in section 3.2.
- 102 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
A template class may have more than one template parameter and also, it may have
parameters which are not types. Let us consider an example:
Example
We want to define a class called MyVector to store as many as N elements of a type
T.
We decide to use a template and, furthermore, to make the vector capacity a
template parameter.
T v[N];
public:
MyVector(){}
T& operator[](unsigned int i){
if (i<N) return v[i];
else {//out of range error. Deal with it}
}
//.....
};
int main()
{
MyVector<int,100> vint;
MyVector<Student,300> vstud;
vint[0]=3;
//....
}
In this example, the template class MyVector has two template parameters: the
type of the vector components (T) and the capacity of the vector (N). Therefore, it is
possible to define a vector vint which can store as many as 100 integers and
another one vstud with a capacity of 300 students.
♦♦♦
- 103 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
int main()
{
int x[11];
char z[15];
sort(x,10); //(1)
sort(z,11); //(2)
//.....
}
• In the case (1), the template parameter T is instantiated with int since the
argument of the function sort is an array of integers.
At the point (1) , the compiler creates a function called sort<int> in which
the occurrences of T have been substituted for int.
Notice that the function is created only when it is called; not before.
• On the other hand, in the case (2), the template parameter T is instantiated
with char.
In the same way as before, it is at point (2) when the compiler creates the
function sort<char> following the instructions contained in the template
function sort
Notice that the code of the sort(..) function uses the operator < to compare two
elements of the array. This forces the fact that any type that instantiates the template
parameter T should have the operator < overloaded (and this information should
come up in the specification of the sort function). Therefore, the following code
would be incorrect if the class Student had not the operator < defined:
- 104 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
class Student{
char* name;
char* id;
//....
};
int main()
{
Student s[11];
fill(s);
sort(s,10);
//.....
}
It would be correct if an operator like the following was defined for the class
Student:
Observation:
The specification of a template function or class should contain any special
requirements that are to be applied to the instantiations of the template
parameters.
Example
- 105 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
class CompaNames{
public:
bool operator()(Student& s1, Student& s2){
return strcmp(s1.getName(),s2.getName())<0;
}
};
class CompaIds{
public:
bool operator()(Student& s1, Student& s2){
return strcmp(s1.getId(),s2.getId())<0;
}
};
int main()
{
bool b1, b2;
CompaNames lower1;
CompaIds lower2;
i=0;
while (i<n){
j=i+1; min=i;
while(j<=n){
if (lower(v[j],v[min])) min=j;
j++;
}
aux=v[min]; v[min]=v[i]; v[i]=aux;
i++;
}
}
int main()
{
CompaNames lower1;
CompaIds lower2;
Student st[11];
fill(st);
sort(st,10,lower2); //(2):Sorts st by id
}
- 107 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
int main()
{
f<int>();
}
Example
We may use this idea to simplify the sort template function. The third parameter of
the sort template function (i.e., the object to call the comparing function) is a bit
artificial. We can remove it, include a local variable Comp lower; within the
function and use the call:
sort<Student,CompaNames>(st,10); //(1):Sorts st by name
Notice that the long call is now necessary since the compiler cannot infer the class
that will be used for the object comparison.
- 108 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
int main()
{
CompaNames lower1;
CompaIds lower2;
Student st[11];
fill(st);
sort<Student,CompaIds>(st,10); //(2):Sorts st by id
}
• a.cpp:
#include "a.h"
• user.cpp:
#include "a.h"
int main()
{
A<int> x;
int i=0;
x.f(i);
//....
}
- 109 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
When the file user.cpp calls a template function on the parameter x: x.f(i), the
compiler should create an actual function (as it has been stated in section 3.2):
A<int>::f(int i){....}
However, the instructions manual to create such function (i.e., the template
definition of f) is not available to the compiler. Indeed, such template definition is
located in another compilation unit: a.cpp, which will become available only at link
time, when it is too late to create the function (since it is the compiler the
responsible for doing so).
As a result, this will not work.
We suggest, as the easiest way to compile code with template definitions, including
such definitions in the same compilation unit as the program in which they are
called.
In the previous example, this may be achieved by putting the implementation of the
template function f(...) into the file a.h, which will be included by user.cpp:
• a.h:
- 110 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
class Vehicle{
char* brand;
char* model;
unsigned long price;
public:
Vehicle(){}
Vehicle(char* pmodel, char* pbrand,
unsigned long pprice)
{
//....
}
~Vehicle(){//....}
friend ostream& operator<< <>(ostream& c, Vehicle& v);
};
4. Create a stack of vehicles and check that the destructor of the class Stack
calls the destructor of the class Vehicle
template<class T>
class Stack{
T* t;
int tops;
int capacitys;
public:
Stack();
~Stack();
void push(const T& x);
void pop();
T* top1();
- 111 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
T& top2();
bool emptyStack();
int getSize();
int getCapacity();
- 112 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
return c;
}
class Vehicle{
char* brand;
char* model;
unsigned long price;
- 113 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
public:
Vehicle(){}
Vehicle(char* pmodel, char* pbrand,
unsigned long pprice)
{
brand=new char[strlen(pbrand)+1];
model=new char[strlen(pmodel)+1];
strcpy(brand, pbrand);
strcpy(model, pmodel);
price=pprice;
}
~Vehicle(){cout<<"vehicle destructor"<<endl;}
friend ostream& operator<<(ostream& c, Vehicle& v);
};
int main()
{
Vehicle v1("golf","volkswagen",40000);
Vehicle v2("testarrosa","ferrari",90000);
Vehicle v3("a4","audi",60000);
Stack<Vehicle> p;
p.push(v1);
p.push(v2);
p.push(v3);
cout<<p<<endl;
return 0;
}
Remarks:
• The specification of the class Stack<T> should include some constraints
concerning the type with which T will be instantiated:
- The type that will instantiate T must have the operator << overloaded
- It is convenient that such type has the operator = overloaded too to avoid
any sharing of identity between the copied object and the object that
receives the copy (see Chapter 4).
- 114 -
CHAPTER 3: GENERIC CLASSES AND FUNCTIONS
The notation <> is used in order to enforce that this operator is a template
and, therefore, there should be a different instance of it for each different
class obtained from an instantiation of the template parameter T.
• An operation cannot return a reference to a local variable (why? ). For this
reason, one of the two different top() operations that we have defined (T&
top2() ) returns a reference to a new object of class T created dynamically.
The following would not be possible:
if (tops>0){
x=t[tops-1];
return x; //INCORRECT!!! Returns a reference
//to a local variable
}
}
- 115 -
- 116 -
Chapter 4
Associations. Composite objects.
References
So far we have presented classes and objects in isolation. However, the real systems
we want to model are far richer than that: they are constituted by many different
objects which are related one to the other. Therefore, the O.O. paradigm, which is
intended to model real systems, should provide means to relate classes.
There are two main relationships that can be established between classes within the
paradigm of object orientation:
1. Association: A class instance is linked in some semantic way with one or
more instances of another class (e.g., An instance of the class Employee is
linked with an instance of the class Company; therefore, we can establish an
association between both classes).
2. Generalization: A class is more general than another one (e.g., the class
Vehicle is more general than the class Car).
4.1.1. Associations
An association between a pair of classes is a broad relationship useful to establish
semantic links between the instances of both classes. The meaning of association
can be stated in the following way:
- 117 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
Definition (Association)
An association between the classes A and B establishes a semantic
relationship between these classes, according to which, a specific instance of
one class (e.g., A), is linked with 0, 1 or various instances of the other one
(B).
The extension of an association is constituted by the pairs (a,b) of linked
instances (a is an instance of A and b is an instance of B).
This definition will be made more comprehensible with the following list of
examples:
• The association works-for can be stated between the classes Employee and
Company. It means that a specific instance of Employee (e.g., John) is
linked (i.e., works for) a specific company (e.g., ACME). It may happen that
John works for more than one company. It could also happen that John was
unemployed for some time.
• The association is-child-of can be established between the class Person and
itself. A specific instance of this class, say Ann, may be the child of two
specific instances of the same class (e.g., Peter and Joan); it could also
happen that one (or both) of the parents was unknown, in which case, Ann
would be linked to just one (or none) of the instances of Person.
• The staff of a company is constituted of head of departments, commercial
managers and administrative staff. Therefore, we may establish associations
between the classes CompanyStaff and each one of the others: HeadDept,
CommercialMgr and AdminStaff. These associations would link the staff
of the company my-company with the head of department Ann and with the
commercial manager Peter.
• A car is made out of many components. for instance, five wheels, one
steering wheel, some seats, one engine (which, in its turn is composed of
many parts)... For instance, we can establish an association between the
classes Car and Wheel, which would link the car with plate LL2312B with
the five wheels with identifiers: 1166P, 1299P, 8912P, 1112Q, and 2323S.
Fig. 4.1 shows how these associations can be represented using UML (see
Appendix A). The numbers at both ends of the association indicates the cardinality
of each end (e.g., an employee can work at 0, 1 or more companies); a child may
have 0, 1 or 2 known parents; a car has exactly 5 wheels.
We have presented binary associations (i.e., associations with two constituent
classes). However, it is possible to link in one association more than two classes.
For example, a Person has a Contract to work for a Company. The contract has
some attributes as starting date, end date, salary, etc. Since a person may have
several contracts with the same company (i.e., at different dates), we can model this
situation with a ternary association which involves the three entities. However, in
this book, we will restrict to binary associations.
- 118 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
Implementing associations
Associations are usually implemented by means of pointers to the referred class.
For example:
class Employee{
private:
char* name;
char* id;
Company* comp; //in the case that an employee can work at one
//company at maximum. Otherwise:
// List<Company*> lcomp;
public:
...
};
- 119 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
4.1.2. Aggregations
A special case of association is constituted by the ownership (has-a or whole-part)
relationship between classes.
Definition (Aggregation)
An aggregation is a special kind of association between exactly two classes
which represents a whole-part relationship between a (whole) class and
another (part) class. The instances of the former are constituted by instances
of the latter.
We say that an instance of the whole class has one or several instances of the
part class.
Aggregations can be organized in a hierarchical way (i.e., forming trees of
concepts: A is an aggregation of Bs, which, in their turn, are aggregations of
Cs). However, aggregations cannot generate loops (e.g., C cannot be, at the
same time, an aggregation of As).
Fig. 4.3 shows the way in which aggregations can be modelled in UML (see
Appendix A for more information about UML). The black diamond is used for
compositions, which are a particular case of aggregations, as it is presented next.
- 120 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
4.1.3. Compositions
Definition (Composition)
A composition is a strong form of aggregation. It has two additional
properties:
• Any instance of the part class can participate in, at most, one composition
at a given instant.
(However, it can be deleted from one composition and added into
another).
• When the composite object is deleted, all its parts are (usually) deleted
with it.
If we reconsider our previous examples of aggregations we will discover that not all
of them can be considered compositions:
• In the company staff example, we have to take into account that a particular
instance of AdminStaff (e.g., John) may work for two different companies
at the same time. On the other hand, if one (or both) of them closes down,
John should go on existing: he may also be a citizen, with a wife and some
children and he may play some sport during the weekend (provided that
these aspects are also modelled in our network of classes).
That is, the company staff is not an example of class composition.
- 121 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
• In the car example, a specific component (e.g., a wheel) can only be a part of
one car at a time. Furthermore, if that car is removed, all its parts will be (in
general) removed with it.
The car example is an example of class composition.
Implementing composition
Compositions are usually implemented in some of the following ways:
• Using pointers to the component instances (i.e., in the same way as general
associations/aggregations).
class Car{
private:
char* model;
Wheel* wc[5];
Carburetor* cb;
...
public:
...
};
- 122 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
class Car{
private:
char* model;
Wheel wc[5];
Carburetor cb;
...
public:
...
};
- 123 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
4.2.1. Construction
Consider the class Person that was presented in Chapter 2 with a new attribute
birth of class Date. Notice that now, Person may be considered an aggregate
with component class Date.
class Person{
char* name
int age;
char sex;
Date birth;
public:
//......
};
class Date{
private:
unsigned int day;
unsigned int month;
unsigned int year;
- 124 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
public:
//.....More operations
};
pbirth: name:
age: 18
sex: ’f’
birth: 1
1
1900
Initializers
You will have guessed by now that the use of the default constructor of the
subobject sometimes is not the best idea. We may want to customize the
construction of the subobject (i.e., indicate which is the actual birth date of Ann,
instead of the improbable 1-01-1900). We may do this by means of initializers:
- 125 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
Definition (Initializers)
Let C be an aggregate class which contains as attribute, a subobject attrib
of class D (D attrib;). That is:
class C{
//.....
D attrib;
//.....
};
- 126 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
C(....)
:attrib(arg1,...,argk)
{
//Implementation of the C constructor
}
C(....)
:attrib(arg1,...,argk), attrib2(arg21,...,arg2k)
{
//Implementation of the C constructor
}
class C{
private:
D1 at1;
D2 at2;
...
Dn atn;
};
The construction of an object of class C will involve (in the order given):
1. A call to some constructor of the class C (the constructor body is not
executed yet).
2. A call to some constructor of the classes D1, D2, ..., Dn to initialize
the subobjects.
The constructors of the classes D1, D2, .. Dn to be called will be
indicated by means of initializers within the C constructor (right before its
body). If no initializers are given, the default constructor of the classes
D1, D2, .. or Dn will be used.
3. The execution of the body of the C constructor.
The constructors of the C subobjects are called in the order in which the
attributes have been declared (i.e., D1, D2, ..., Dn). However, it is not
advisable to make an implementation rely on the order in which such
constructors are called.
- 127 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
4.2.2. Destruction
The process of destruction of an object of an aggregate class involves the
(automatic) call to the destructors of its subobjects (if any).
That is, the destruction of an object of the class Person involves a call to the
destructor of the class Date. This call is necessary to clean up all the dynamic
storage allocated in the construction of a Date. The definition of class Date does
not involve any dynamic memory allocation. However, consider a slightly different
definition of Date:
class Date{
private:
int day;
char* month;
int year;
public:
Date(int pday, char* pmonth, int pyear){
day=pday;
year=pyear;
month=new char[strlen(pmonth)+1];
strcpy(month,pmonth);
}
~Date()
{
delete [] month;
}
};
int main()
{
Date annBirth(20,"march",1984);
Person p("Ann", 20,'f', annBirth);
//Do things....
return 0;
}
The following (automatic) destruction process will take place at the end of the main
program:
1. Call to the destructor of the class Person
2. Call to the destructor of the class Date
Notice that if the Date destructor were not called, the string month would become
garbage.
Notice also that while the process of construction is performed bottom-up, the
destruction process is carried out top-down.
- 128 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
4.3.1. Equality
The equality operation determines whether two program entities are equal regarding
the semantics of the concepts that they are modelling. We will use the operator ==
in order to compare two entities. In particular, c1==c2; evaluates true if both
entities (c1 and c2) are considered equal.
As shown in Chapter 2, we may consider two different headers for this operator:
The notion of equality in OOP may have various meanings in different contexts.
For that reason it is important to present those meanings accurately. First of all, we
should distinguish between pointer equality and object equality
Pointer equality
Notice that according to this definition, two pointers will not be considered equal if
they are referring two different objects which hold the same values in their
attributes. In fig. 4.6, p and q are equal pointers. However, p and p2 are not (they
point to different objects, regardless their values).
We will use the predefined operator == to compare pointers (it is not necessary to
overload it).
- 129 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
Object equality
Two different notions of object equality may be established:
• Shallow equality (also referred to as one-level equality)
The idea behind shallow equality is that two objects are equal in a shallow
way if they store the same values for non-dynamic attributes and their
corresponding pointer attributes refer exactly to the same object.
Fig. 4.7 shows a typical situation in which two objects (of the class Person)
would be considered shallowly equal. Notice that they have the same value
for their attributes of primitive types, their attributes birth (of class Date)
are shallowly equals and the pointers to their names point to the same
character array.
- 130 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
Notice that two objects shallowly equal share a part of their identity (in this
case, their name). A change in the name attribute of one of them will lead to a
change in the name of the other. The designer should be aware of this
situation and decide whether this design is suitable for each particular
situation.
A shallow equality operation may be defined, either by overloading the
equality operator (==) or by defining a new operation shallowEqual(...).
• Deep equality (also referred to as structural equality)
The meaning of deep equality is that two objects will be considered equal if
they have the same run-time structure with identical values in its non-
dynamic attributes (at any point of that run-time structure).
- 131 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
Example
Consider the following C++ program:
class Player
{
char name[20];
int age;
Player* playfriend;
Team* team;
public:
//.....
}
class Team
{
char name[60];
int foundationYear;
public:
//.....
}
At run-time we may have defined some players and teams with the relationships
shown in figure 4.8. In this case, the following pairs are equal in a deep way: (p1,
p5), (p2, p6) and (t1, t7)
Notice that only the pair (t1, t7) is considered equal in a shallow way.
The run-time structure of p5 (with pointers to p6 and t7) constitutes a deep copy of
p1 (see section 4.3.2).
- 132 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
Example
We describe a car by means of a model and two components: a wheel and an
engine.
Figure 4.9 models this composition by means of references to independent objects;
in this case, two car instances (namely, c1 and c2) of model “Opal Samantha”,
which have as components a wheel with reference 12345 and an engine with
reference 45612.
• c1 and c2 are shallowly distinct, in spite of having the same structure and the
same values. The reason for this is that they refer to different instances of
Wheel and Engine.
They would be considered shallowly equal if both cars would refer to the
same instance of Wheel and Engine.
• c1 and c2 are deeply equal since their structure is identical (i.e., they are
constituted by the same kinds of components with identical values, regardless
of the fact that the specific instances of those components are different).
Figure 4.10 presents an implementation of this composition by means of subobjects
(which is probably the best alternative in this case). In this case, c1 and c2 are
shallowly and deeply equal.
The designer will decide the appropriate notion of equality according to the
semantics and implementation of each specific class.
- 133 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
class Car{
char model[20];
Wheel* wheel;
Engine* engine;
public:
//.....
bool operator==(const Car& c) const;
}
//Shallow version:
- 134 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
The example shows two versions of the operator==: the shallow version and the
deep one. The deep version relies on deep versions of the operator== for the
classes Engine and Wheel.
Notice, finally, that only one version of the operator== can be implemented for a
specific class. The other equality can be implemented another operation (e.g.,
deepEquals(...)).
♦♦♦
The == overloading should be done carefully if the class for which the operator ==
is overloaded can generate some cyclic structure (as in the example shown in figure
4.11).
In section 4.4 (Guided problem) you will find more about this.
- 135 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
is clear that the two trees (a1 and a2) presented in fig. 4.12 should be
considered equal, and this will only be possible if they are compared deeply.
4.3.2. Copy
The copy operation replicates the state of a program entity onto another existing
one. We will use the assignment operator (=) to denote this operation.
c1=c2; denotes the copy of the state of the entity c2 onto the entity c1.
The header of the operator= is the following:
- 136 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
Many issues that have been shown regarding equality make sense also in the case of
copy. In particular, we may distinguish between pointer copy and object copy. On
the other hand, within object copy, we may refer to shallow copy and deep copy.
Pointer copy
Any C++ class have a predefined assignment operator (=) which performs a shallow
copy. This operator may be overloaded, if necessary.
An acceptable example of shallow copy is the assignment of players, presented
above. Let us consider two players (p1 and p2) and let us assign p2 to p1:
p2=p1;
The result of this operation is shown in figure 4.13. Notice that, now p2 and p1 are
equal in a shallow way. Notice also that part of the identity of both objects (the
team and the playfriend attributes) are shared. Last, recall that this is the default
behaviour offered by the assignment operator (=), if it is not overloaded. However,
keep in mind that the default assignment operator may generate garbage.
- 137 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
Example
An example in which a deep copy may be necessary is the copy of a data structure
which has a dynamic part (i.e., a part created at execution time by means of the new
operator), for instance, the copy of a binary tree.
The class IntBinaryTree, shown next, defines trees of integer nodes like the one
presented in figure 4.14.
class Node{
public:
int val;
Node* leftchild;
Node* rightchild;
};
class IntBinaryTree{
Node* root;
- 138 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
if (nod!=NULL){
nodeaux=new Node;
nodeaux->val=nod->val;
nodeaux->leftchild=cloneStructure(nod->leftchild);
nodeaux->rightchild=cloneStructure(nod->rightchild);
}
return nodeaux;
}
The call a1.setRootValue(9); will result in a change in both a1 and a2, which,
in general, is not wanted (see fig. 4.16). This is another example of the anomaly that
we call identity sharing and it is one of the problems of shallow copy.
On the other hand, notice that shallow copy provides a more efficient solution since
it saves the traversal and replication of all the tree structure (however, notice also
that if we keep the requirement of avoiding garbage, we may have to traverse the
- 139 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
tree structure of the old a1 value in order to deallocate it, both in the shallow and
deep cases).
Usually, instead of defining a shallowCopy(...) or deepCopy(...) operation, we
will overload appropriately the assignment operator (=).
Figure 4.15: The shallow and deep copies between binary trees
- 140 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
- 141 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
4.3.3. Clone
Definition (Clonation)
The clone operation creates a new object with a state which is exactly the
same as that of an existing one (which is called the cloned object) and returns
a pointer to the newly created object.
The clone operation for the class C has the following signature:
C* clone();
It returns a pointer to an object which is a copy of the object on which the
operation is applied.
Notice the difference between a copy and a clonation: in the first case, the object
which receives the copy already exists, while in the second case, the clone is a
brand new object.
As in the copy case, we may perform a deep or a shallow clonation.
• Shallow clonation
Example
We perform a shallow clonation in the player example.
Player* Player::clone()
{
Player* aux;
aux=new Player;
strcpy(aux->name,name);
aux->age=age;
aux->playfriend=playfriend;
aux->team=team;
return aux;
}
♦♦♦
• Deep clonation
Example
We propose a deep clonation in the tree example:
IntBinaryTree* IntBinaryTree::clone()
{
IntBinaryTree* aux;
aux=new IntBinaryTree;
aux->root=cloneStructure(root);
return aux;
}
- 142 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
class Player{
private:
char name[20];
int age;
Player* playfriend;
public:
Player(){ strcpy(name,""); age=0; playfriend=NULL;}
Player(char* pname,int page, Player* pfriend)
{
strcpy(name,pname);
age=page;
playfriend=pfriend;
}
void setFriend(Player& p)
{
playfriend=&p;
}
void deepCopy(const Player& p2)
{
//??????????
}
//More operations...........
};
- 143 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
Solution:
class Player{
private:
//.....
void cloneStruct(Player* dst, Player* orig)
{
if (orig!=NULL){
strcpy(dst->name,orig->name);
dst->age=orig->age;
dst->playfriend=new Player;
cloneStruct(dst->playfriend,orig->playfriend);
}
else dst=NULL;
}
public:
//....
void deepCopy(Player& p2)
{
cloneStruct(this,&p2);
}
//....More operations
};
- 144 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
- 145 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
(pointerToTheOriginalPlayer,
pointerToTheCopyOfThatPlayer)
In this way, given a pointer to player, we will be able to determine if that
player has come up before and, in this case, which was its copy.
Design the class PointerTable appropriately. Which basic operations
should it offer?
Solution:
The two main operations that the class PointerTable should offer are the
following:
• void store(Player* p, Player* copiedp)
This operation stores at the table the pair (p,copiedp) (p is a pointer to
Player and copiedp is the pointer to the copy of the player pointed at
by p.
• void getStoredCopy(Player* p, Player*& copiedp)
If the player pointed at by p is stored in the table, this operation obtains a
pointer copiedp to its copy. Otherwise, copiedp=NULL.
We propose a very basic representation of PointerTable by means of an
array of Pair. Each Pair has two pointers (to the original player and its
copy). For the sake of simplicity, we have not used more complex structures
nor have we managed the situation in which more than N players had to be
replicated.
Since the class Pair has a very simple structure and it has been designed
only to be used by PointerTable, we make its attributes public and we
do not define operations to access them. It would have also been correct
defining them as private and providing some operations to access them.
class Pair{
public:
Player* originalp;
Player* copyp;
};
class PointerTable{
private:
Pair v[N];
int nitems;
public:
PointerTable(){nitems=0;}
- 146 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
Player* getStoredCopy(Player* p)
{
int i=0;
while (i<nitems && v[i].originalp!=p){
i++;
}
if (i<nitems && v[i].originalp==p) return v[i].copyp;
else return NULL;
}
};
dst->playfriend=pt.getStoredCopy(orig->playfriend);
- 147 -
CHAPTER 4: ASSOCIATIONS. COMPOSITE OBJECTS. REFERENCES
- 148 -
Chapter 5
Inheritance
- 149 -
CHAPTER 5: INHERITANCE
The latter issue, the substitution principle, is the criterion that is used to determine
whether a class is more general than another one.
For example, we may say that the class Car is more particular than the class
Vehicle, which can also be stated in the following way: the type implemented by
the class Car is a subtype of the type implemented by the class Vehicle 5 .
A clue to identify the fact that a class is more general than another one is the so
called is-a test. If the sentence “A X is a Y” makes sense, it probably means that Y
is more general than X. For instance, “a car is a vehicle”, “a man is a person”, “a
director is an employee”... Recall that these examples should be read more precisely
5
Recall from Chapter 1 that classes can be seen as implementations of abstract types
- 150 -
CHAPTER 5: INHERITANCE
as follows: “any instance of the class Car can be seen also as an instance of the
class Vehicle”
In conclusion, generalization is a convenient relationship between classes that
allows us to define a new class as a specialization (i.e., a particular case) of another
already existing one. Using generalization relationships to relate appropriate classes
leads to a better modelling of the domain. Generalization between classes can be
characterized by means of the so called substitution principle which, in combination
with polymorphism (see Chapter 6), provides a very powerful tool for software
engineering.
Definition (Subclass)
A subclass B is a class that keeps the same structure (attributes) and
behaviour (operations) that another class A (which is called B’s superclass).
We say that B inherits the structure and operations of A.
In addition, the subclass B may add new attributes or operations to those
inherited from A.
- 151 -
CHAPTER 5: INHERITANCE
From the definition of subclasses, ancestors and descendants we may infer the
following feature:
- 152 -
CHAPTER 5: INHERITANCE
class Vehicle{
private:
char* brand;
char* model;
int year;
public:
Vehicle(char* pbrand, char* pmodel, int pyear);
void getModel(char* pmodel) const;
void setModel(char* pmodel);
void getBrand(char* pbrand) const;
void setBrand(char* pbrand);
int getYear();
void setYear(int pyear) const;
void showFeatures() const;
};
public:
ProfessionalVehicle(char* pbrand, char* pmodel, int pyear,
char* powner, int pmaxLoad);
void getCompOwner(char* powner) const;
void setCompOwner(char* powner);
int getMaxLoad();
void setMaxLoad(int load) const;
void showFeatures() const;
};
public:
PersonalVehicle(char* pbrand, char* pmodel, int pyear,
char* powner);
void getOwner(char* powner) const;
void setOwner(char* powner);
void showFeatures() const;
};
int maxSpeed;
char* colour;
int doorNbr;
- 153 -
CHAPTER 5: INHERITANCE
public:
Car(char* pbrand, char* pmodel, int pyear, char* powner,
char* pcolour, int pdoornbr);
//......
void showFeatures() const;
};
public:
Van(char* pbrand, char* pmodel, int pyear,
char* powner, int pmaxLoad);
void showFeatures() const;
//......
};
pv.getModel(mod);
pv.setModel("Pocus");
pv.getCompOwner(cown);
pv.setCompOwner("Locus INC");
- 154 -
CHAPTER 5: INHERITANCE
void Vehicle::showFeatures()
{
cout<<"brand: "<<brand<<" model: "
<<"year: "<<year<<endl;
}
void ProfessionalVehicle::showFeatures()
{
Vehicle::showFeatures();
cout<<"owner company: "<<companyOwner<<
<<"maximum load allowed: "<<maxLoad<<endl;
}
- 155 -
CHAPTER 5: INHERITANCE
5.2.1. Constructors
The first important issue to be mentioned in this section is the following:
Therefore, as it may have been expected, all classes should define their own
constructors. However, there is a relationship between the subclass constructor and
its superclass one.
Let us consider the hierarchy of vehicles presented in section 5.1.3. The sentence
ProfessionalVehicle pv("forrd","forrdvan", 2003, "Loads Ltd", 5000);
generates a call to the constructor of the class ProfessionalVehicle. This
constructor should call the constructor of the class Vehicle
(ProfessionalVehicle superclass) in order to initialize the vehicle attributes
and perform all the operations which are necessary in order to construct a vehicle
properly.
In the same way, when we create an instance of the class Car (hence, we call the
constructor of this class), the constructors of its ancestors (the classes
PersonalVehicle and Vehicle) should be called too. The constructor of
PersonalVehicle will be responsible for setting the attribute owner and the
constructor of Vehicle will set brand, model and year. In addition, each
constructor may also carry out whatever is necessary to create a class instance.
These reflections can be generalized as follows:
- 156 -
CHAPTER 5: INHERITANCE
ProfessionalVehicle::ProfessionalVehicle(char* pbrand,
char* pmodel, int pyear,
char* powner, int pmaxLoad)
:Vehicle(pbrand, pmodel, pyear)
{
maxLoad=pmaxLoad;
companyOwner=new char[strlen(powner)+1];
strcpy(companyOwner,powner);
}
- 157 -
CHAPTER 5: INHERITANCE
• Implicitly
If no initializer is used at the beginning of the subclass constructor, then the
compiler adds a call to the default superclass constructor (i.e., the constructor
without arguments). In this case, the default constructor must be defined for
the superclass.
Recall that if no constructor has been defined for a specific class, the
compiler provides the default one for that class. However, if a specific
constructor (with parameters) has been defined by the programmer for the
superclass, then the compiler will not add the default one. In this case, the
programmer should either add a default constructor to the superclass or make
sure that he/she has included in the subclass constructor an initializer that
calls the existing superclass constructor.
Example
The following code is incorrect since the class Vehicle has no default
constructor and the constructor of the class ProfessionalVehicle does
not include any call to the Vehicle constructor.
ProfessionalVehicle::ProfessionalVehicle(char* pbrand,
char* pmodel, int pyear,
char* powner, int pmaxLoad)
{
maxLoad=pmaxLoad;
companyOwner=new char[strlen(powner)+1];
strcpy(companyOwner,powner);
}
- 158 -
CHAPTER 5: INHERITANCE
Vehicle::Vehicle()
{
year=0;
strcpy(brand, "defaultBrand");
strcpy(model, "defaultModel");
}
♦♦♦
Copy constructors and derived classes
If the subclass has a copy constructor, it can be designed by means of an initializer
that calls the copy constructor of its superclass.
Example
We add a copy constructor to the class Vehicle:
- 159 -
CHAPTER 5: INHERITANCE
5.2.2. Destructors
An object of a derived class may have some features of the base class (which may
have been allocated by the constructor of the base class). The deallocation of those
features is a responsibility of the destructor of the base class. For this reason:
Example
Let us consider the implementation of the destructors for the classes Vehicle and
ProfessionalVehicle.
Vehicle::~Vehicle(){
delete [] brand;
delete [] model;
}
ProfessionalVehicle::~ProfessionalVehicle(){
delete [] companyOwner;
}
void f()
{
The function f() creates an object (v) of class ProfessionalVehicle with the
parameters established by the constructor of this class, which will initialize the
attributes. At the end of f(), an implicit call to the destructor of the class
ProfessionalVehicle (derived class) is done. This destructor is responsible for:
• Deallocate the space reserved for the attribute companyOwner (attribute of
the derived class).
• Call the destructor of the base class (i.e., Vehicle()) which will be in
charge of deallocating the space reserved for the attributes brand and
model.
This process is presented in figure 5.4. This figure also presents what would happen
if the destructor of the base class Vehicle were not defined.
- 160 -
CHAPTER 5: INHERITANCE
(2) This part has been deallocated by the destructor of the class
ProfessionalVehicle. This destructor has been called implicitly at the
end of the f() function.
(1) This part has been deallocated by the destructor of the class Vehicle. This
destructor has been called implicitly by the destructor of
ProfessionalVehicle.
If the destructor of the class Vehicle had not exist, part (1) would have
become garbage.
♦♦♦
- 161 -
CHAPTER 5: INHERITANCE
Example
Consider the following classes (see fig. 5.5).
class A {
private:
int x;
void f1();
protected:
int y;
void f2();
public:
int z;
void f3();
};
class B :public A {
private:
int t;
public:
void g();
};
class C {
public:
void h();
};
• From the code of the functions f1(), f2() and f3() it will be possible to
use all members of class A, g() and h().
• From the code of the function g() it will be possible to use y, z, t,
f2() and f3(). However, f1() and x will not be accessible.
• Finally, from the code of h() it will be possible to use z, g() and f3().
However, y, f2(), x, f1() and t will not be accessible.
♦♦♦
- 162 -
CHAPTER 5: INHERITANCE
class A. More specifically, it has not been stated in which way an object b of class
B, within a function extern to A and B will be able to access the members that B
inherits from A.
Public members are preceded by ’+’, protected members by ’#’ and private members by ’-’.
Figure 5.5: Types of inheritance
For instance, let us extend the previous example with a class D which is a subclass
of the base class B and which is defined in the following way (see fig. 5.5):
class D :public B{
public:
void fd()
{
z=10; //Is z public in B?
y=10; //Is y protected in B?
}
};
- 163 -
CHAPTER 5: INHERITANCE
Consider again the example with the class D, derived from B, that has been added
above:
class D :public B{
public:
void fd()
{
z=10; //Is z public in B?
y=10; //Is y protected in B?
}
};
The correctness of the code of the function fd() will depend on the type of
inheritance of B with respect to A:
• class B :public A {....};
- 164 -
CHAPTER 5: INHERITANCE
5.4.1. Specialization
This is the common use of inheritance. The example of a vehicle hierarchy that we
have presented throughout his chapter illustrates it.
Its most relevant features are the following:
• The subclass is a particular case of the superclass (usually, the is-a rule works
for this use of inheritance: “a car is a vehicle”)
• The subclass satisfies the parent specification (concerning class operations
and invariants).
• In addition the subclass may refine the superclass by adding new features to
those inherited from the superclass and new invariants to those defined for
the superclass. That is, the subclass may add restrictions to the superclass.
• The substitution principle is applicable (wherever an object of the superclass
is expected, an object of the subclass may come up).
- 165 -
CHAPTER 5: INHERITANCE
5.4.2. Interface
A particular case of the specialization use of inheritance arises when the superclass
is a pure interface: it just defines a set of (specified) operations but no
implementation for these operations (recall from Chapter 1 that a class like this is
called a deferred class). The motivation of such a superclass is to guarantee that its
subclasses will provide (and override) at least the operations specified in the
superclass. Usually, this superclass is called interface. In certain languages (such as
Java) interfaces are defined as language constructs. In C++, abstract classes can be
used to deal with interfaces. Abstract classes are shown in Chapter 6 (in which
polymorphism is presented).
Example
Let us imagine the superclass Polygon with the subclasses Square, Rectangle,
Triangle, Pentagon, etc. All these subclasses should have some operations like
getArea(), and getPerimeter(). However, the class Polygon does not have
enough information to implement them. As a result, the class Polygon will just
specify these operations. They will be implemented in its subclasses.
Polygon will be designed in C++ as an abstract class with the so-called pure virtual
operations:
virtual double getArea()=0;
which do not have implementation associated. Chapter 6 will present virtual
operation and abstract classes.
class Polygon
{
public:
virtual double getArea()=0;
//Post: Obtains the area of the polygon on which it is called
//...
};
- 166 -
CHAPTER 5: INHERITANCE
public:
double getArea(){return edgeLength*edgeLength;}
double getPerimeter(){ return edgeLength*4;}
//More operations....
};
♦♦♦
The use of interfaces is very common when different ways to implement a type are
provided (which is usual, for instance, in libraries of data structures). For this
reason, sometimes the subclasses that inherit from an interface are called
realizations or implementations of that interface. Consider the following example:
Example
The type SequenceOfInteger (a sequence whose elements are integers) may be
implemented in at least two different ways: as an array and as a linked list. Fig.
5.6(a) shows the array implementation for the sequence {1,5,6} and fig. 5.6(b)
shows a linked list implementation for the same sequence.
- 167 -
CHAPTER 5: INHERITANCE
class SequenceOfIntegers{
public:
virtual void insertFirst(int x)=0;
virtual void insertLast(int x)=0;
virtual int getElementPosition(int p)=0;
...
};
int seq[N];
int nelems;
public:
void insertFirst(int x){...}
void insertLast(int x){....}
int getElementPosition(int p){return seq[p];}
};
Node* first;
int nelems;
public:
void insertFirst(int x){...}
void insertLast(int x){....}
int getElementPosition(int p){...}
};
♦♦♦
Recall that inheritance from an interface is a particular case of specialization.
More about interfaces and realizations in Chapter 6. Virtual operations and abstract
classes are also presented in Chapter 6.
5.4.3. Similarity
The subclass bears some similarities with the superclass and, for this reason, the
subclass is modelled “as if it were” the superclass (new features may be added to
- 168 -
CHAPTER 5: INHERITANCE
- 169 -
CHAPTER 5: INHERITANCE
• The subclass inherits and offers to its clients all the public operations of the
superclass (even those operations which have no sense for the subclass, since
the subclass is not a subtype).
For example, the operation getIth(int i, T& x) which gets (in x) the
ith element of the list would be inherited by Set. However, it does not make
any sense in the latter class.
Alternatives to inheritance for similarity
We present here some methods to avoid or diminish the problems of this use of
inheritance. The most natural solution should be chosen in each specific situation.
1. Composition
Use a composition (aggregation) relationship (see Chapter 4) instead of a
class-subclass relationship.
Example
Instead of making Set a subclass of List, we may use List as a
representation for Set (i.e., Set will be a client of List). The Set
operations will be implemented in terms of the List operations.
template<class T>
class List{
T l[N];
int nelems;
public:
List(){nelems=0;}
int getNElems(){
return nelems;
}
};
template<class T>
class Set{
List<T> s;
public:
Set(){...}
- 170 -
CHAPTER 5: INHERITANCE
int getNElems(){
return s.getNElems();
}
♦♦♦
2. Factoring
Identify a common superclass S (which is a supertype of the two similar
classes) and define them as subclasses of S.
For example, in the Employee-Student example, we may define a class
Person which is a superclass (and a supertype) of both Employee and
Student. The operations for getting/setting name, identification, address,
etc. may be defined for Person, and inherited by Student and Employee
while other features which are specific, either of students or employees will
be defined in the respective class.
Using factoring we have turned a non-subtype inheritance into a subtype one.
However, this technique cannot be used if the given class hierarchy cannot be
modified.
3. Hiding the parent operations
We may keep one of the similar classes subclass of the other but using a
private inheritance. In this way, the operations of the superclass will not be
offered by the subclass.
Example
class Employee{
char* name;
char* id;
char* address;
int age;
char* companyName
....
public:
char* getName(){...}
char* getCompanyName(){...}
...
};
- 171 -
CHAPTER 5: INHERITANCE
public:
char* getName(){ return Employee::getName();}
};
♦♦♦
Note that with this solution:
- Both classes share the common structure (name, id, address,
age...) and
- The operations of Employee (some of which are not of interest for
Student, like getCompanyName()) are not offered by Student to its
clients
- Student can implement some of its own operations in terms of those of
Employee (e.g., getName()).
Notice that this solution does not preclude the use of an object of the subclass
as a parameter of a function that expects an object of the superclass:
promote(Employee& e) may still be called with a student.
This is probably the worst solution to the similarity situation.
5.4.4. Generalization
This form of inheritance is used when the subclass is more general (instead of more
specialized) than the superclass.
For example, an application to manage the client accounts of a savings bank defines
the class Account which models a bank account which operates in euros. In
particular, it keeps track of the operations performed on an account. This class
defines, among other, the operation getBalance() which returns the balance of
the account in euros. To make it possible to have accounts in different currencies, a
new class MultiCurrencyAccount is defined as a subclass of Account. Some
new operations get/setCurrency(...) are defined and getBalance() is
overridden so that it can provide the balance information in the correct currency.
Clearly, MultiCurrencyAccount is more general than Account (i.e., Account
can be seen as the particular case of MultiCurrencyAccount which can only
manage currency in euros).
Making MultiCurrencyAccount a subclass of Account is a very artificial and
inelegant solution. It would be far better to do the other way round. That is: to
define a MultiCurrencyAccount as a base class and, (in the case that accounts in
euros had specific features), a EuroAccount as a subclass.
- 172 -
CHAPTER 5: INHERITANCE
5.4.5. Instantiation
A common source of errors in OO design and programming is the confusion
between inheritance and instantiation. This may happen when we model an instance
of a class A as if it were an A subclass or the other way round.
It is important to make it clear the difference between both concepts.
• A subclass defines a set of instances which is a subset of the instances
defined by the base class. As we already know, this set of instances is defined
in terms of the list of features (attributes and operations) of the base class
and, possibly, some new attributes and/or operations.
A subclass defines features. In general, it does not give value to features.
For example, Employee is a subclass of Person because it defines the set of
instances of the class Employee in terms of the features that characterize
persons (e.g., name, id, birthdate, etc.) and, in addition, the company for
which they work. Employee does not give value to the features of Person.
• An instance of a class gives a value to the features defined by that class. It
does not define new features. It does not create a new concept (possibly
derived from an existing one) which can be instantiated
For example, John is an instance of Employee; i.e., it gives a value to the
features that define an Employee (e.g., name=”John”, id=”34981j”,
company=”ACME”...). It does not define new features. On the other hand, it
has no sense to create instances of “John”. John itself is an instance.
- 173 -
CHAPTER 5: INHERITANCE
5.4.7. Association
A confusion that may arise in object-oriented programming and design is to use
subclasses and inheritance to express a conceptual association between classes.
For instance, if we want to express that a company has employees, it is incorrect to
model this situation by means of a class-subclass relationship as follows (see fig.
5.8(a) )
class Company{
.....
};
class Employee :public Company{
....
};
This would mean that an employee is a special kind of company, which is not the
case.
What we want to establish, instead, is that an employee is associated to a company
by means of an association that we could name “works-for” (fig. 5.8 (b)). This may
be implemented as follows:
class Employee{
Company* workingComp;
.....
};
- 174 -
CHAPTER 5: INHERITANCE
- 175 -
CHAPTER 5: INHERITANCE
class Person{
char passportId[9];
int age;
Date birthdate;
char* name;
public:
~Person()
{
//.....
}
void printFeatures(ostream& c)
{
//Print the features of a person to the stream c
//(usually c=cout)
}
};
Date marriageDate;
char* partnername;
public:
MarriedPerson(){}
MarriedPerson(char* ppassid, int page, Date& pbirth, char* pname,
Date& pmarr, char* ppart)
- 176 -
CHAPTER 5: INHERITANCE
~MarriedPerson()
{
//....
}
void printFeatures(ostream& c)
{
//....It should call printFeatures from the superclass
}
};
#include <cstring>
#include <iostream>
class Date{
int day;
char* month;
int year;
public:
Date(){cout<<"Default constructor of Date"<<endl;}
Date(int pday, char* pmonth, int pyear)
{
cout<<"Constructor of Date with 3 params"<<endl;
day=pday;
year=pyear;
month=new char[strlen(pmonth)+1];
strcpy(month,pmonth);
}
- 177 -
CHAPTER 5: INHERITANCE
day=pd.day;
year=pd.year;
month=new char[strlen(pd.month)+1];
strcpy(month,pd.month);
}
~Date()
{
cout<<"Destructor of Date"<<endl;
delete [] month;
}
//....more operations.....
public:
Person(){
cout<<"Default constructor of Person"<<endl;
}
~Person()
{
cout<<"Destructor of Person"<<endl;
delete [] name;
}
- 178 -
CHAPTER 5: INHERITANCE
void printFeatures(ostream& c)
{
c<<"passport="<<passportId<< "\n name="<<name
<<"\n age="<< age
<<"\n birth date="<<birthdate<<endl;
}
};
class MarriedPerson :public Person{
Date marriageDate;
char* partnername;
public:
MarriedPerson()
{
cout<<"Default constructor of MarriedPerson"<<endl;
}
MarriedPerson(char* ppassid, int page, Date& pbirth, char* pname,
Date& pmarr, char* ppart)
:Person(ppassid,page,pbirth,pname),
marriageDate(pmarr)
{
cout<<"Constructor of MarriedPerson with 6 params"<<endl;
partnername=new char[strlen(ppart)+1];
strcpy(partnername,pname);
}
~MarriedPerson()
{
cout<<"Destructor of MarriedPerson"<<endl;
delete[] partnername;
}
void printFeatures(ostream& c)
{
Person::printFeatures(c);
c<<"marriage date="<<marriageDate
<< "\n partner name="<<partnername<<endl;
}
};
int main()
{
Date birth(10,"APR",1990);
Date marrd(20,"JUN",2010);
MarriedPerson p2 ("11111111",27,birth,"Ann",marrd,"Mark");
cout<<"\n\n Features of p2"<<endl;
p2.printFeatures(cout);
}
- 179 -
CHAPTER 5: INHERITANCE
(7)Features of p2
passport=11111111
name=Ann
age=27
birth date=10-APR-1990
marriage date=20-JUN-2010
partner name=Ann
Date birth(10,"APR",1990);
Date marrd(20,"JUN",2010);
- 180 -
CHAPTER 5: INHERITANCE
- (4) After the initialization of its attribute birth, in (3), now the
constructor of Person is executed. This completes the initialization of the
attributes that p2 inherits from its superclass. (5) and (6) are responsible
for initializing the p2 attributes corresponding to the class
MarriedPerson.
- (5) First of all the constructor of the component class (Date) is called in
order to initialize the marriage date.
- (6) Finally, the last attribute of MarriedPerson (partnername) is
initialized by means of the execution of the MarriedPerson constructor.
This completes the construction of p2
• (7) This corresponds to the execution of p2.printFeatures(cout);
• (8)-(11) They correspond to the process of destruction of the object p2. All
the involved destructors have been called implicitly at the end of main().
Notice that the destruction is made in the reverse order to the construction.
Let us present the process:
- (8) Destructor of MarriedPerson. The process of destruction of p2
starts with the call to the destructor of MarriedPerson.
- (9) The destructor of MarriedPerson invokes implicitly the destructor
of Date to deallocate marriagedate
- (10) Once the attributes of the class MarriedPerson of p2 have been
deallocated, now it is the turn of the attributes of p2 inherited from the
class Person: First of all the destructor of Person is called.
- (11) Finally, the destructor of Date is called by the destructor of Person
in order to deallocate the attribute birth. The destruction of p2 is
completed.
• (12) Destruction of the object marrd
• (13) Destruction of the object birth
- 181 -
- 182 -
Chapter 6
Polymorphism
v.showFeatures(); //(1)
Clearly, this function may be called to know the information about any vehicle of
any type which is on sale in the store. Sometimes, it will be called using an instance
of the class Car as parameter. In other occasions, an instance of the class Van or
Lorry will be used. In any case, the function should work properly. That is, the
following code:
informCustomer(c);
informCustomer(v);
- 183 -
CHAPTER 6: POLYMORPHISM
Definition (Polymorphism)
Polymorphism is the feature by which:
• A reference to an object may refer to instances of different types at
different moments during the execution of an application and
• A call to a class operation may behave in a different way according to the
class, at run time, of the object to which that operation has been applied.
From this definition, we can establish clearly the two features that should be
provided by programming languages in order to offer polymorphism:
1. A notion of class conformance, so that a reference (e.g., v of class C) may
refer to different classes (which conform with C) at different moments during
the execution of a program. The substitution principle provides this notion of
class conformance. According to this principle, a reference v of class C may
refer to any object of a class descendant of C.
2. The ability to bind a call to a class operation with an implementation of that
operation at run time, instead of at compilation time, as traditional languages
do (notice that, due to the previous feature, an element may refer to different
classes and we are not sure about the specific class to which it is referring
until run time). This ability is called dynamic binding.
- 184 -
CHAPTER 6: POLYMORPHISM
Vehicle
PersonalVehicle ProfessionalVehicle
void Car::showFeatures(){....}
v.showFeatures();
} v is a Van
void Van::showFeatures(){....}
These two features are shown in figure 6.1 and will be explained in more detail in
the next two sections.
Programming languages which supported polymorphism were rare before the
irruption of the object-oriented paradigm. In a hypothetical traditional language
which allowed the definition of classes but which did not support the two features
that yields polymorphism, this problem could have been solved by:
(a) Making v be a generic pointer (which could point to objects of any type).
(b) Adding a type attribute to the class Vehicle, which would state the actual
class of the object at run time (e.g., 1: Vehicle, 2: Van, 3: Car, etc.). When
we created an instance of some class of the vehicle hierarchy, we would
initialize appropriately that type attribute. And when we needed to know
that type (e.g., in performAction() to call the appropriate
showFeatures() function) we would just consult that type attribute.
Using (a) and (b), the new program would look something similar to:
class Vehicle{
//...previous attributes
int type;
public:
//...previous operations
void setType(int t){type=t;}
int getType() {return type;}
};
- 185 -
CHAPTER 6: POLYMORPHISM
This solution could work but it would suffer from some important drawbacks:
• Lack of elegance
The code of informCustomer() needs a conditional instruction (a switch
has been chosen), codification for the different classes, several casts (i.e.,
explicit type conversions, which you may forget about) and a strange pointer
parameter which can point to something different to a Vehicle and lead to
execution problems. Therefore, this solution is dirty, inefficient, error prone
and less comprehensible. These issues are easily understood if this code is
compared with the previous version of the function.
• Reuse difficulties
The code of informCustomer() must be modified and recompiled
whenever new kinds of vehicles are added to the hierarchy. This makes it
difficult the reuse of the hierarchy of vehicles (whenever a new kind of
vehicle is added, all applications that use that hierarchy should be updated
appropriately).
In contrast, the first polymorphic solution provides some clear benefits:
• The code of void informCustomer(Vehicle& v) is independent of the
actual type of the parameter (as long as v refers to some object whose class
belongs to the vehicle hierarchy). This function will work properly regardless
of the actual class of v.
• The code of informCustomer(Vehicle& v) is elegant and simple.
• Both the function informCustomer(Vehicle& v) and the vehicle
hierarchy are more reusable. If, in the future, more vehicle classes are defined
(e.g., SportCar), the function informCustomer(...) will show the
actual features of those new vehicle classes, with no need of either
recompilation or foreseeing those features before the definition of the new
classes.
- 186 -
CHAPTER 6: POLYMORPHISM
//...Some things
v.showFeatures();
//...More things
}
- 187 -
CHAPTER 6: POLYMORPHISM
The foo(...) function should accept as parameter any object whose class
conforms with (is a descendant of) Vehicle. C++ behaves in this way only
if the parameter is passed as a reference or as a pointer:
- void foo(Vehicle& v){...}
Parameter v passed as a reference: descendants of Vehicle (e.g.,
ProfessionalVehicle, Car) may be passed to foo. See figure 6.2(c).
- void foo(Vehicle* v){...}
Parameter v passed as a pointer: pointers to descendants of Vehicle may
be passed to foo. See figure 6.2(b).
- void foo(Vehicle v){...}
Parameter v passed as an object of the class Vehicle: if descendants of
Vehicle are passed to foo, an information loss may occur. Consider, for
instance, the call:
ProfessionalVehicle pv(....);
//....
foo(pv);
Notice that the parameter passing by value carried out by C++ for the
parameter pv induces its copy to the formal parameter v. This copy may
lead to an information loss (see figure 6.2(a)).
(a) (b)
ppv: Forrd
ProfessionalVehicle pv(...); pv: Forrd ProfessionalVehicle* ppv;
Pocus
foo(pv); Pocus ppv=new ProfessionalVehicle(...);
2003
2003 foo(pv); Loads Ltd
Loads Ltd
5000
5000
v points to the same
pv is copied into v ==> object that ppv: OK!!
void foo(Vehicle v) loss of information!!!! void foo(Vehicle* v) v:
{....} {....}
v: Forrd
Pocus
2003
(c)
Figure 6.2: Class conformance in C++
- 188 -
CHAPTER 6: POLYMORPHISM
• Assignment of references
Let us consider the function:
int main()
{
Vehicle v(...);
Car c(...);
f(v,c); //(3)
}
rv v:
brand, model, year
have been copied
rc c:
maxspeed, colour, owner, doornumber
have been lost
After rv=rc;
Figure 6.3: Assignment of references
- 189 -
CHAPTER 6: POLYMORPHISM
(2): rv is a reference to Vehicle and it does not conform with Car (i.e., rv
cannot be seen, in general, as a Car). Therefore this assignment is not
correct.
Notice that (2) is a call to the following operation of the class Car
(provided by default by the compiler):
Car& Car::operator=(const Car&);
It is clear that the class of rv (Vehicle) does not conform with Car,
hence it is an incorrect call.
The class designer could have provided the operation:
Car& operator=(const Vehicle&);
with which, the call (2) would have been correct.
• Assignment of pointers
Let us consider the function:
int main()
{
Vehicle v(....);
Car c(...);
f(&v,&c);
}
c: pc
After pv=pc;
Figure 6.4: Assignment of pointers
- 190 -
CHAPTER 6: POLYMORPHISM
void foo(Vehicle& v)
{
int dn,ml;
if (``v is a Car'')
Notice that the calls v.getDoorNumber() and v.getMaxLoad() will not work
since neither the class Vehicle nor any class to which Vehicle conforms (i.e., a
Vehicle ancestor) do not define these operations. These calls will generate a
compilation error.
In these cases, the rules of class conformance that we have presented in this section
may be too restrictive for the needs of a specific program. In those occasions, C++
provides some tools in order to overcome the limitations imposed by the class
conformance rules. However, we should use carefully these rules, for they may lead
to errors.
They are presented in the following sections.
- 191 -
CHAPTER 6: POLYMORPHISM
void foo(Vehicle* v)
{
int dn;
Car* pc;
pc=dynamic_cast<Car*>(v);
if (pc!=0)
{
dn=pc->getDoorNumber();
}
else
{
//this time v does not point to a Car
}
}
- 192 -
CHAPTER 6: POLYMORPHISM
void foo(Vehicle& v)
{
int dn;
try{
dn=(dynamic_cast<Car&>(v)).getDoorNumber();
}
catch (bad_cast){
//This time v was not of type Car,
//may be in the next execution ....
}
}
The way to notice that a specific dynamic cast has been unsuccessful cannot be by
comparing the result of the dynamic cast to zero:
if (dynamic_cast<Car&>(v))==0) //INCORRECT
{...}
void foo(Vehicle& v)
{
int dn,ml;
if (typeid(v)==typeid(Car)){
dn=(static_cast<Car&>(v)).getDoorNumber();
}
else if (typeid(v)==typeid(Lorry)){
ml=(static_cast<Lorry&>(v)).getMaxLoad();
}
else
//......
}
- 193 -
CHAPTER 6: POLYMORPHISM
Notice that the use of the typeid() operator makes it unnecessary the use of the
dynamic cast (which is, however, possible). The static cast is sufficient, since it is
guarded by a typeid() operator which guarantees that the static cast provides a
conversion to the correct type of the object.
Notice also that the typeid() operator will obtain the run time type of the
parameter provided that this parameter has been declared of a polymorphic class
(i.e., a class with virtual methods 6 ).
The operator typeid() returns a reference to an object of a class type_info
which provides information concerning object types at run time. Specifically, two
operations that can be applied to objects of this class are the operator == (which has
been used in the example above) and the operation char* name() to get the name
of the type of an object:
cout << typeid(v).name();
Notice that the operator typeid() may be applied to:
• References to objects, as in the above example: ...typeid(v)...
• Objects pointed to by pointers:
void foo(Vehicle* v){.... typeid(*v)==typeid(Car).....}
• Type identifiers, as in the above example: ... typeid(Car)...
The use of the class type_info requires the inclusion of <typeinfo>.
The dynamic cast and the typeid() operator constitute the so called Run Time
Type Information (RTTI). They are quite powerful and some times necessary tool
for OOP. However, keep in mind that their use (specially of typeid()) often leads
to the sort of programs that we presented at the beginning of the chapter and which
are far from elegant and reusable at all:
6
A virtual method is a method for which the binding of the method call to a function
that implements it is done at run time (dynamic binding), allowing a polymorphic
behaviour of it. Virtual methods are presented in section 6.3. In this section we will
see that the class Vehicle should define the method showFeatures() to be
virtual.
7
See section 6.3
- 194 -
CHAPTER 6: POLYMORPHISM
and let the run time control determine to which specific version of f() (according
to the run time type of v) should the call be applied.
Therefore, consider the operator typeid() and also dynamic cast as a last resort
(see guided problem 6.11 for an example).
Example 1:
class Complex{
float re;
float im;
public:
Complex(float a){re=a; im=0.0;}
//...
};
int main()
{
Complex c(7.3); //c=7.3 + 0*i
return 0;
}
Example 2:
Consider the simplified Vehicle hierarchy:
class Vehicle
{
char* brand;
char* model;
int year;
public:
- 195 -
CHAPTER 6: POLYMORPHISM
Vehicle(Vehicle& v){
brand=new char[strlen(v.brand)+1];
strcpy(brand,v.brand);
model=new char[strlen(v.model)+1];
strcpy(model,v.model);
year=v.year;
}
};
int main()
{
Vehicle v("forrd", "orrion", 2004);
Car c(v);
return 0;
}
The Car constructor generates a Car object out of a Vehicle. It simply sets
default values to the specific attributes of Car and calls the copy constructor of
Vehicle to set the common ones.
Conversion operator
Constructors cannot achieve two issues regarding type conversion:
• Convert a class object into a predefined type (e.g. int)
This operations would be performed by the constructor of int. Since int is
not a class, it has no constructor to do this.
• Convert an object of a new class (B) into an object of an already defined
class (A).
In order to do this, it would be necessary to modify the specification and the
implementation of A, since the constructor operator that performs the
conversion should be in A. Of course, this is not an elegant solution (usually,
it is simply not acceptable).
Both limitations can be overcome by conversion operators.
- 196 -
CHAPTER 6: POLYMORPHISM
Notice that:
• Operator T is defined in class A
• It does not have any return type
Example:
class Complex{
float re;
float im;
public:
//....
Complex(float r, float i){
re=r; im=i;
}
operator float() const
{return re;}
};
int main()
{
Complex c(1.2, 2.0);
float i=c;
}
- 197 -
CHAPTER 6: POLYMORPHISM
v.showFeatures();
It was expected that this function shown the correct features regardless of the class
of the object with which it was called (brand, model, year, door number and max
speed; if v was a car and brand, model, year, max load and company owner; if v
was a van).
This behaviour is not possible in traditional languages, which create the binding
between a function call (v.showFeatures()) and the code to which this call is
associated at compilation time, that is, before knowing the actual type that the
object v will have at run time (this is called early binding). Since, at compilation
time, the only thing known by the compiler is that v will refer to a Vehicle; it
associates the call v.showFeatures() to the function
Vehicle::showFeatures(). Therefore, at execution time the car/van specific
features will not be shown. That is, if we execute something of the sort:
informCustomer(c);
informCustomer(v);
- 198 -
CHAPTER 6: POLYMORPHISM
class A{
public:
virtual void f();
...
};
Example
Consider again the vehicle hierarchy presented in Chapter 5. If we prefix the
operation showFeatures() by the label virtual we get the intended
polymorphic behaviour.
class Vehicle{
//......
public:
//Some operations....
virtual void showFeatures() const;
};
- 199 -
CHAPTER 6: POLYMORPHISM
//Some operations
v.showFeatures();
int main()
{
Car c("renol", "marrane", 2002, 180, "black", 3);
Van v("renol", "hard", 2000, "ACME LTD", 5000);
informCustomer(c);
informCustomer(v);
return 0;
}
♦♦♦
First case: Call to a virtual function with automatic objects of different types:
int main()
{
Car c("renol", "marrane", 2002, 180, "black", 3);
Van v("renol", "hard", 2000, "ACME LTD", 5000);
c.showFeatures(); //(1)
v.showFeatures(); //(2)
return 0;
}
The calls (1) and (2) are resolved at compilation time ( (1) is bound with
Car::showFeatures() and (2) with Van::showFeatures() ). They are not
polymorphic calls. The dynamic binding mechanism is not invoked here.
v.showFeatures(); //(1)
int main()
{
Car ca("renol", "marrane", 2002, 180, "black", 3);
Van va("renol", "hard", 2000, "ACME LTD", 5000);
informCustomer(ca); //(2)
informCustomer(va); //(3)
return 0;
}
As in the first case, the call (1) is resolved at compilation time without using the
dynamic binding mechanism. The call (1) will be bound with
Vehicle::showFeatures(). Only the attributes brand, model and year will
be shown in both calls to informCustomer(..) ( (2) and (3) ).
Notice that v is a Vehicle that receives a copy of a car (ca) and a van (va).
However, an information loss occurs in this copy and the attributes which are
specific of the class Car or Van are not copied.
- 201 -
CHAPTER 6: POLYMORPHISM
v.showFeatures(); //(1)
int main()
{
Car c("renol", "marrane", 2002, 180, "black", 3);
Van v("renol", "hard", 2000, "ACME LTD", 5000);
informCustomer(c); //(2)
informCustomer(v); //(3)
return 0;
}
In this case, the call (1) is resolved at execution time using the dynamic binding
mechanism, since it is called with a parameter passed by reference. The call (1) will
be bound with Car::showFeatures() in the case (2) and with
Van::showFeatures(), in the case (3). In both cases, the appropriate attributes
will be shown.
v->showFeatures(); //(1)
int main()
{
Car c("renol", "marrane", 2002, 180, "black", 3);
Van v("renol", "hard", 2000, "ACME LTD", 5000);
informCustomer(&c); //(2)
informCustomer(&v); //(3)
return 0;
}
- 202 -
CHAPTER 6: POLYMORPHISM
Since the call (1) is performed on a pointer, the dynamic binding mechanism is
invoked. The call (1) will be bound with Car::showFeatures() in the case (2)
and with Van::showFeatures(), in the case (3). In both cases, the appropriate
attributes will be shown.
- 203 -
CHAPTER 6: POLYMORPHISM
- value=m.retrieveElement(key)
- m.removeElement(key)
- ...
However, there exist many ways to implement a map (e.g., with an array of
<key,value> pairs; using a hash technique; using some tree structure –binary
search trees, B trees–; etc.). Depending on the implementation strategy that
we take we will implement the previous operations in one way or another. At
the level of the Map class, we do not have enough information to implement
them. However, it is a good idea to define the Map class as the root of the
hierarchy of all map implementations since, in this way, we can:
- Establish the set of operations that any map implementation should offer.
Thanks to this, we will be able to work with maps in a polymorphically.
For example, we may define a function that has a Map as parameter and
apply to that parameter any operation defined in the abstract class Map.
We can do this even if we do not know which specific implementation of
Map will be given at run time:
void workWithMaps(Map& m)
{
....
m.insertElement(...);
x=m.retrieveElement(...);
m.removeElement(...);
...
}
This will be explored in problem 6.11.
- Implement those elements which are common to all implementations
(e.g., an operation to get the number of pairs in the map can be
implemented here).
Each different implementation strategy of the Map class can be defined as a
Map subclass which will implement the Map operations according to the
specific implementation strategy.
• Finally, the class Vehicle itself could be defined as an abstract class if in
our application we were only interested in creating instances of more
concrete classes (such as Car or Lorry). This means that the fact that a class
is defined as abstract or not may depend on the context in which it is defined.
- 204 -
CHAPTER 6: POLYMORPHISM
Example
class RegularPolygon{
int edgeNb;
float edgeLength;
public:
RegularPolygon(int edgen, float edgel)
{
edgeNb=edgen; edgeLength=edgel;
}
int getEdgeNumber(){return edgeNb;}
float getEdgeLength(){return edgeLength;}
virtual float getArea()=0;
};
Square(float edgel)
:RegularPolygon(4,edgel)
{}
virtual float getArea()
{
return getEdgeLength()*getEdgeLength();
}
};
float height;
public:
Triangle(float edgel)
:RegularPolygon(3,edgel)
{
height=sqrt(edgel*edgel-edgel*edgel/4);
}
virtual float getArea(){return getEdgeLength()*height/2;}
};
- 205 -
CHAPTER 6: POLYMORPHISM
Remarks:
• The class RegularPolygon has one pure virtual function (getArea(),
hence it is an abstract class. It is not possible to define instances of
RegularPolygon: RegularPolygon rp; is incorrect.
• It is not necessary that all operations defined for an abstract class are pure
virtual. If at this level of definition the information to implement some
operation is already available, it can be implemented (this is what happens
with getEdgeLength() and getEdgeNumber()). However, these
operations will not be called directly on objects of class RegularPolygon
(which cannot exist) but on its subclasses.
• The constructor of a class cannot be virtual (more on this in section 6.5).
Notice that the constructor of RegularPolygon is called from the
constructor of its subclasses.
• The pure virtual function getArea() is overridden in each one of the two
subclasses (in them it is possible to know how to calculate this area). Notice
that it is possible to define instances of Triangle and Square, since all its
operations and all the operations that they inherit have some implementation.
♦♦♦
Example
Consider the following code.
public:
Map(){elemNb=0;}
int getNbElements() {return elemNb;}
virtual void insertElement(char* key, const T& value)=0;
virtual T retrieveElement(char* key)=0;
virtual void removeElement(char* key)=0;
};
Pair<T> v[N];
public:
- 206 -
CHAPTER 6: POLYMORPHISM
ArrayMap()
:Map<T>()
{}
virtual void insertElement(char* key, const T& value){...}
virtual T retrieveElement(char* key){...}
virtual void removeElement(char* key){...}
};
public:
HashMap(....){...}
virtual void insertElement(char* key, const T& value){...}
virtual T retrieveElement(char* key){...}
virtual void removeElement(char* key){...}
};
It contains an abstract base class called Map (which, in addition, is a generic class)
that declares several pure virtual functions which are overridden in its subclasses.
Each subclass (i.e., HashMap and ArrayMap) provides a specific implementation of
a map. Most implementation details have been skipped.
♦♦♦
- 207 -
CHAPTER 6: POLYMORPHISM
void foo(Vehicle& v)
{
Vehicle* vcopy;
//......
vcopy=new Vehicle(v);
//....
}
This will create a dynamic object of type Vehicle and will make the pointer
vcopy point to that object. But what will happen if v, at run time is of a descendant
of Vehicle (e.g. Car)?
The solution is to use the clone() function presented in Chapter 4, which makes a
copy of the object on which it is called. All classes in the Vehicle hierarchy
should define a clone() function. These functions should be virtual in order to
achieve the required polymorphic behaviour.
class Vehicle{
//.....
public:
//.....
virtual Vehicle* clone()
{
Vehicle* aux;
aux=new Vehicle(brand,model,year);
return aux;
}
//.....
};
public:
//.....
virtual Vehicle* clone()
{
Vehicle* aux;
getBrand(br);
getModel(mod);
aux=new ProfessionalVehicle(br, mod,getYear(),
companyOwner, maxLoad);
return aux;
}
//.....
};
Notice that from the class ProfessionalVehicle it is not possible to access the
private attributes of the class Vehicle. For this reason, the accessors to those
- 208 -
CHAPTER 6: POLYMORPHISM
public:
//.....
virtual Vehicle* clone()
{
return new ProfessionalVehicle(*this);
}
//.....
};
void foo(Vehicle* v)
{
//......
delete v;
//....
}
This will work properly if the destructors of the classes in the Vehicle hierarchy
have been defined virtual:
class Vehicle{
//.....
public:
//.....
virtual ~Vehicle()
{
delete [] brand;
delete [] model;
}
//.....
};
- 209 -
CHAPTER 6: POLYMORPHISM
public:
//.....
virtual ~ProfessionalVehicle()
{
delete [] companyOwner;
}
//.....
};
Recall that destructors of derived classes call implicitly the destructors of their base
classes.
Again, section 6.9 shows a practical use of virtual destructors.
It is clear that an instance of RentCar should inherit the members that may be
applied to it as a Car (getMaxSpeed(), getDoorNumber(), getModel()...)
and as a RentableObject (getRentId(), getCurrentRenter()...). Hence,
the following program should be correct:
- 210 -
CHAPTER 6: POLYMORPHISM
int main()
{
int dn, id;
RentCar c(....);
dn=c.getDoorNumber();
id=c.getRentId();
return 0;
}
The notation that C++ uses in order to define a class that inherits from more than
one other classes is the following:
class Programmer{
public:
void program(){....}
class Tester{
public:
void test(){....}
}
- 211 -
CHAPTER 6: POLYMORPHISM
int main()
{
ProgrammerTester pt (...);
pt.program();
pt.test();
}
This idea of using multiple inheritance to model the different roles or groups of
functionalities that an object may exhibit is sometimes expressed by means of
interfaces (see Chapter 1 and section 6.4).
It is fundamental to distinguish between the is-a and has-a relationships.
For instance, a Programmer is a Person, hence, it may be correct to model the class
Programmer as a subclass of Person. On the other hand, a programmer works for
a Company, but we will not model the class Programmer as a subclass of
Company (as in figure 6.6(a) ). A correct modelling is the one presented in fig.
6.6(b).
- 212 -
CHAPTER 6: POLYMORPHISM
classes. This will lead to an ambiguity when those operations are called on the
object of the subclass.
This idea is illustrated in the following example (see fig. 6.7): we add to the class
Car the operation int getPrice() which obtains the price of that car. In
addition, we add to the class RentableObject the operation void getPrice()
which gets the price of one week rental of that object. As a consequence, the class
RentCar will inherit both operations and will consider ambiguous a call to
getPrice() on a RentCar object.
class RentableObject{
int weekRentPrice;
//..MORE ATTRIBUTES
public:
RentableObject(int weekpr){
weekRentPrice=weekpr;
}
//MORE OPERATIONS....
};
- 213 -
CHAPTER 6: POLYMORPHISM
class Car{
int purchasePrice;
//MORE ATTRIBUTES....
public:
Car(int ppr){
purchasePrice=ppr;
}
};
public:
int main()
{
int p;
RentCar c(8000,200);
p=c.getPrice(); ///!!!!!AMBIGUOUS
return 0;
}
int main()
{
int p,p2;
RentCar c(8000,200);
- 214 -
CHAPTER 6: POLYMORPHISM
void foo2(Car& c)
{
c.getPrice();
}
void foo3(CompanyObject& o)
{
o.getPrice(); //!!!!!!AMBIGUITY!!!!
}
- 215 -
CHAPTER 6: POLYMORPHISM
public:
int main()
{
int p;
RentCar c(8000,200);
However, it does not allow the use of polymorphism in order to get the
purchase price of a rent car:
void foo1(Car& c)
{
c.getPrice(); //We want to get the purchase price
}
int main(){
RentCar rc(8000,200);
foo1(rc);
return 0;
}
The call to c.getPrice() within foo1(rc) will get the week rental price,
not the purchase price as it could be expected.
- 216 -
CHAPTER 6: POLYMORPHISM
class A{
public:
int x;
};
- 217 -
CHAPTER 6: POLYMORPHISM
int main()
{
D d;
d.x=10;
}
class CompanyObject{
int identifier;
//More attributes and operations
};
class Car :public virtual CompanyObject{...};
class RentableObject :public virtual CompanyObject{...};
class RentCar :public RentableObject, public Car{...};
int main(){
RentCar rc(...);
rc.identifier=1000; //O.K.
}
int main()
{
D d;
d.x=10; ///!!!!ERROR AMBIGUITY!!!
}
This ambiguity can be solved by qualifying the member x with the class from
which it is inherited:
- 218 -
CHAPTER 6: POLYMORPHISM
• The copy of the member x which the object d inherits from the class B:
d.B::x=10;
• The copy of the member x which the object d inherits from the class C:
d.C::x=100;
In the car rental company example, this behaviour could be interesting if a member
responsible is defined for the class CompanyObject. The class Car would
inherit this member with the meaning “person who is responsible for the car
maintenance”. However, the class RentableObject would inherit the member
responsible with the meaning “person who takes care of the administrative
procedures concerning the rental of this object”. A rent car should have both
responsible persons, hence it could be acceptable the use of a duplicated
inheritance. However, it is not as clear as the non-duplication case.
class Vehicle{
//....
public:
//...
virtual void showFeatures(ostream& c){
c<<"Brand: "<<brand<<" Model: "<<model
<<" Year: "<<year<<endl;
}
Notice that the task of sending the description of v to the stream c has been
delegated to the virtual function showFeatures(c). The call
- 219 -
CHAPTER 6: POLYMORPHISM
#include <cstring>
#include <iostream>
#include "Date.h"
class Person{
protected:
char passportId[9];
int age;
Date birthdate;
char* name;
public:
Person(){}
- 220 -
CHAPTER 6: POLYMORPHISM
~Person()
{
delete [] name;
}
virtual void printFeatures(ostream& c)
{
c<<"passport="<<passportId<< "\n name="<<name
<<"\n age="<< age
<<"\n birth date="<<birthdate<<endl;
}
char* getPersonName()
{
char* aux;
aux=new char[strlen(name)+1];
strcpy(aux,name);
return aux;
}
};
protected:
Date marriageDate;
char* partnername;
public:
MarriedPerson(){}
MarriedPerson(char* ppassid, int page, Date& pbirth, char* pname,
Date& pmarr, char* ppart)
:Person(ppassid,page,pbirth,pname),
marriageDate(pmarr)
{
partnername=new char[strlen(ppart)+1];
strcpy(partnername,ppart);
}
~MarriedPerson()
{
delete[] partnername;
}
- 221 -
CHAPTER 6: POLYMORPHISM
}
};
protected:
char* bankname;
public:
MiddleAgedPerson(){}
MiddleAgedPerson(char* ppassid, int page, Date& pbirth,
char* pname, char* pbank)
:Person(ppassid,page,pbirth,pname)
{
bankname=new char[strlen(pbank)+1];
strcpy(bankname,pbank);
}
~MiddleAgedPerson()
{
delete[] bankname;
}
};
- 222 -
CHAPTER 6: POLYMORPHISM
public:
MiddleAgedMarried(char* ppassid, int page, Date& pbirth,
char* pname, Date& pmarr, char* ppart,
char* pbank)
:MiddleAgedPerson(ppassid,page,pbirth,pname,pbank),
MarriedPerson(ppassid,page,pbirth,pname,pmarr,ppart)
{
}
};
int main()
{
bool err;
Date birth2(10,"MAI",1980);
Date marrd(10,"APR",2001);
MarriedPerson p2 ("11111111",27,birth2,"Ann",marrd,"Mark");
MiddleAgedPerson p3("11111111",27,birth2,
"Peter","International Bank");
MiddleAgedMarried p4("11111111",27,birth2,"Ann",marrd,
"Mark","intercontinental bank");
Remarks:
• Since p2 is a MarriedPerson, (1) writes the name of the partner of p2 (i.e.,
“Mark”)
• Since p3 is a MiddleAgedPerson, (2) writes the name of the bank of p3
(i.e., “International Bank”)
• Since p4 is a MiddleAgedMarriedPerson, the call p4.getName() would
be ambiguous. For that reason, we prefix the call with the name of the class
- 223 -
CHAPTER 6: POLYMORPHISM
in which the getName() operation that we want to call is defined: hence, (3)
gets the bank name and (4), the partner name.
Another possibility would have been to define two new operations in
MiddleAgedMarriedPerson:
char* MiddleAgedMarriedPerson::getName()
{
return MarriedPerson::getName();
}
char* MiddleAgedMarriedPerson::getBankName()
{
return MiddleAgedPerson::getName();
}
If this had been done, we could have accessed the partner name and the bank
name without the prefix of the class to which the operation belonged:
- 224 -
CHAPTER 6: POLYMORPHISM
p4.MarriedPerson::getPersonName();
which refers to the getPersonName() function inherited through
MarriedPerson or
p4.MiddleAgedPerson::getPersonName();
which refers to the getPersonName() function inherited through
MiddleAgedPerson.
int main()
{
MiddleAgedMarried p4(....);
char* nam;
nam=p4.getPersonName(); //ERROR: AMBIGUOUS CALL
nam=p4.MiddleAgedPerson::getPersonName(); //Correct
nam=p4.MarriedPerson::getPersonName(); //Correct
}
The problem is even more apparent with attributes: the attributes defined in the
class Person are duplicated in an instance of the class MiddleAgedMarried
because they are inherited along two different paths. Therefore, p4 has two names,
two ages... If they were defined as public in the class Person it would be possible
to do:
int main()
{
MiddleAgedMarried p4(....);
It is clear that, according to the usual semantics of these attributes in the class
Person, this is not acceptable.
It can be solve by making the inheritance virtual, as follows:
class Person{
//as before
};
- 225 -
CHAPTER 6: POLYMORPHISM
public:
c<<"Bank name="<<bankname<<endl;
}
};
int main()
{
MiddleAgedMarried p4(....);
char* nam;
A consequence of the use of virtual inheritance is that when an object of the class
MiddleAgedMarried is called, the constructors of the classes
MiddleAgedPerson and MarriedPerson (which are called by the constructor of
MiddleAgedMarried) cannot call the constructor of Person (otherwise, that
constructor would be called twice and would duplicate the inherited attributes and
operations). As a result, the constructor of class Person is explicitly called from
the constructor of MiddleAgedMarried:
If it is not done like this, the constructor of MiddleAgedMarried will call the
default constructor of Person.
- 226 -
CHAPTER 6: POLYMORPHISM
void test(Person& p)
{
p.printFeatures(cout);
cout<<"---------------\n\n"<<endl;
}
Solution
Two things are necessary:
• The operation printFeatures(c) defined in the class Person should be
declared virtual so that it may behave polymorphically (with a late binding
between the call and the implementation).
• The function test(p) should get as parameter either a reference or a pointer
to Person.
Since both issues hold, the following main() function will show the expected
behaviour:
int main()
{
bool err;
Date birth(10,"APR",1990,err);
Date birth2(10,"MAI",1980,err);
Date marrd(10,"APR",2001,err);
Date marrd2(1,"MAI",2000,err);
Person p1 ("12345678",21,birth,"Joe");
MarriedPerson p2 ("11111111",27,birth2,"Ann",marrd,"Mark");
MiddleAgedPerson p3("11111111",27,birth2,"Peter",
"International Bank");
MiddleAgedMarried p4("12345678",21,birth,"Joe",marrd2,"Paula",
"Commerce bank");
cout<<"Features of a Person"<<endl;
test(p1);
- 227 -
CHAPTER 6: POLYMORPHISM
test(p2);
cout<<"\nFeatures of a Middle-aged Person"<<endl;
test(p3);
cout<<"\nFeatures of a Middle-aged and Married Person"<<endl;
test(p4);
}
Features of a Person
passport=12345678
name=Joe
age=21
birth date=10-APR-1990
--------------------
Features of a Married Person
passport=11111111
name=Ann
age=27
birth date=10-MAI-1980
marriage date=10-APR-2001
partner name=Mark
--------------------
Features of a Middle-aged Person
passport=11111111
name=Peter
age=27
birth date=10-MAI-1980
bank name=International Bank
--------------------
Features of a Middle-aged and Married Person
passport=12345678
name=Joe
age=21
birth date=10-APR-1990
marriage date=1-MAI-2000
partner name=Paula
Bank name=Commerce bank
--------------------
- 228 -
CHAPTER 6: POLYMORPHISM
void partnerName(MarriedPerson& p)
{
cout<<"Person name="<<p.getPersonName()<<endl;
cout<<"Partner name="<<p.getName()<<endl;
void bankName(MiddleAgedPerson& p)
{
cout<<"Person name="<<p.getPersonName()<<endl;
cout<<"Bank name="<<p.getName()<<endl;
int main()
{
bool err;
Date birth(10,"APR",1990,err);
Date birth2(10,"MAI",1980,err);
Date marrd(10,"APR",2001,err);
Date marrd2(1,"MAI",2000,err);
MarriedPerson p2 ("11111111",27,birth2,"Ann",marrd,"Mark");
MiddleAgedPerson p3("11111111",27,birth2,"Peter",
"International Bank");
MiddleAgedMarried p4("12345678",21,birth,"Joe",marrd2,"Paula",
"Commerce bank");
partnerName(p2); //(1)
bankName(p3); //(2)
partnerName(p4); //(3)
bankName(p4); //(4)
}
- 229 -
CHAPTER 6: POLYMORPHISM
MarriedPerson::getName()
and
MiddleAgedPerson::getName()
Are the calls to getName() done within (3) and (4) ambiguous? Why?
• Which will be the result of the execution of the main function above?
Solution
There is no ambiguity in those calls:
partnerName(p)takes as parameter a reference to a MarriedPerson. Therefore
p.getName() is a call to MarriedPerson::getName(), and shows the partner
name (even in the case (3) in which p is linked to an object of the class
MiddleAgedMarried. Put it another way, the function partnerName(p)
considers p strictly as a MarriedPerson and does not pay attention to other
features that p may have inherited from other lines that do not include
MarriedPerson.
The same argument may be used for bankName(p) which will print the bank name
of p4, regardless of the fact that p4 also inherits getName() from
MarriedPerson.
Notice that:
• there would have been an ambiguity in the call partnerName(p4;) if this
function had been defined as follows:
void partnerName(Person& p)
{
cout<<"Person name="<<p.getPersonName()<<endl;
cout<<"Partner name="<<p.getName()<<endl; //AMBIGUITY IF
//CALLED ON A
//MiddleAgedMarried!!
}
Person name=Ann
Partner name=Mark
Person name=Peter
Bank name=International Bank
- 230 -
CHAPTER 6: POLYMORPHISM
Person name=Joe
Partner name=Paula
Person name=Joe
Bank name=Commerce bank
6.9.2. Solution
• Genericity:
Since it must be generic, we will use a template class Stack:
• Operations:
According to the requirements, the following set of operations seem
adequate 8 :
8
The goal of this problem is not to offer an exhaustive design of the class Stack.
Therefore, we will not implement all its operations nor do we manage error
situations and so on.
- 231 -
CHAPTER 6: POLYMORPHISM
Stack();
~Stack();
void insertTop(const T& x);
void removeTop();
T* getTop() const;
bool isEmpty() const;
};
Remarks:
- Notice the use of const in some operations:
* isEmpty, getTop do not modify the state of the stack.
* The parameter of insertTop does not have to be updated by the
operation. Furthermore, it will be possible to do things of the sort:
int main()
{
Stack<Student> s;
const Student st("Joan", 20, 'm');
s.insertTop(Student("Anna",20,'f'));
//Call with a temporal object
....
}
- 232 -
CHAPTER 6: POLYMORPHISM
- 233 -
CHAPTER 6: POLYMORPHISM
T f() (1)
T p;
{
T x; p=f();
...get the appropriate element on x Returns a copy of x on a
return x; temporal object (tmp) p=tmp;
} (2) (3)
created with the copy constructor:
T tmp(x);
1111
0000
0000
1111 1111
0000
0000
1111 1111
0000
0000
1111
Problems if S x; instead of T x;
0000
1111
x:
0000
1111
tmp:
0000
1111
p: (Copy of x obtained
(where S is a subclass of T) 0000
1111 0000
1111 0000
1111
0000
1111 0000
1111 0000
1111 with T::operator=(...) )
0000
1111 0000
1111 0000
1111
T* f() T* p;
(1)
{ p=f();
T* x=new T;
...get the appropriate element on *x Returns a pointer to *x on a
return x; temporal pointer: p=tmp;
(2) (3)
} T* tmp=x;
1111
0000
x:
0000
1111
0000
1111 p:
No problems!! 0000
1111
tmp: 1111
0000
1111
0000
(p points to the same object as x)
• Representation (1):
Since the capacity of the stack is restricted to N=100 elements and
insertions/deletions are carried out at the end of the list, it seems reasonable
to implement it as an array. Furthermore, since the insertions/deletions take
place at the end, we may record the number of elements of the stack (and the
point of the array in which the insertions/deletions should occur with an
index top):
- 234 -
CHAPTER 6: POLYMORPHISM
• Representation (2):
Each index of the array v should be prepared to store an object of class T or
any subclass of T. However, an array declared T v[N] reserves for each
index the amount of memory necessary to allocate an object of class T. This
makes it impossible to allocate there an object of a subclass of T, which may
have more attributes defined. For example, T may be instantiated with
Vehicle, which has the attributes brand, model and year. Car, a
descendant of Vehicle adds to the attributes inherited from Vehicle,
owner, colour, doornumber and maxspeed.
The class Stack could be defined in the following way:
Stack();
~Stack();
void insertTop(const T& x);
void removeTop();
T* getTop() const;
bool isEmpty() const;
};
Each index of the array v contains a pointer to an object of the class T, which
(by the application of the substitution principle) may point to any instance
from a class descendant of T (see fig. 6.11).
- 235 -
CHAPTER 6: POLYMORPHISM
- 236 -
CHAPTER 6: POLYMORPHISM
Notice that any class that instantiate T at run time will have to implement a
clone() operation. This remark must be made explicit in the specification
of the class List<T>.
• Rest of operations:
Stack<T>::Stack(){top=0;}
Stack<T>::~Stack()
{
int i;
for(i=0;i<top;i++){
delete v[i];
}
}
void Stack<T>::removeTop()
{
top--;
delete v[top];
}
T* Stack<T>::getTop() const
{
T* x;
x=v[top-1]->clone();
return x;
}
• User program:
int main()
{
Stack<Vehicle> lv;
Car c(...);
Lorry lo(...);
lv.insertTop(c);
lv.insertTop(lo);
- 237 -
CHAPTER 6: POLYMORPHISM
while (!lv.isEmpty())
{
cout<<*(lv.getTop())<<endl;
lv.removeTop();
}
• Class Vehicle:
The class Vehicle and all its subclasses should implement a virtual
operation clone():
Solution:
• Main idea:
The main idea of the solution is that the array should be reserved
dynamically.
• Operations:
The constructor header will change as follows:
Stack(int cap );
• Representation:
In order to be able to do something like: v=new T*[cap]; we should have
the following representation:
- 238 -
CHAPTER 6: POLYMORPHISM
Stack(int capac);
~Stack();
void insertTop(const T& x);
void removeTop();
T* getTop() const;
bool isEmpty() const;
};
Stack<T>::Stack(int capac){
top=0;
capacity=capac;
v=new T*[capac];
}
and then:
- 239 -
CHAPTER 6: POLYMORPHISM
Output) way.
Different kinds of containers may structure elements in different ways.
The objective of this problem is to design and implement the hierarchy of
containers of figure 6.12.
- 240 -
CHAPTER 6: POLYMORPHISM
- 241 -
CHAPTER 6: POLYMORPHISM
pc->setFirst();
while(! pc->isEnd())
x=pc->getCurrent(); pc->setNext();
Implementation of Container
class NoObjectException{};
public:
Container();
virtual ~Container(){}
virtual unsigned int getSize() const;
virtual bool isEmpty() const;
virtual bool operator==(const Container& c) const =0;
virtual void setFirst() =0;
virtual T* getCurrent() throw (NoObjectException) =0;
virtual void setNext() throw (NoObjectException) =0;
virtual bool isEnd() =0;
};
Remarks
• An attribute has been defined (nElems) to count the elements of a container
object. It makes sense that this attribute is common to all the container types
that may be created now or in the future. For this reason, the operations that
deal with this attribute (isEmpty() and getSize()) are implemented
here.
- 242 -
CHAPTER 6: POLYMORPHISM
Implementation of Stack
class EmptyStackException{};
class FullStackException{};
public:
Stack();
~Stack(){}
virtual void insertTop(const T&) throw (FullStackException) =0;
virtual void removeTop() throw (EmptyStackException) =0;
virtual T* getTop() const throw (EmptyStackException) =0;
};
template<class T>
Stack<T>::Stack(){}
- 243 -
CHAPTER 6: POLYMORPHISM
T* v[N];
int top;
int current;
public:
ArrayStack();
ArrayStack(ArrayStack<T>&);
~ArrayStack();
bool operator==(const Container<T>&) const;
virtual void insertTop(const T&) throw (FullStackException);
virtual void removeTop() throw (EmptyStackException);
virtual T* getTop() const throw (EmptyStackException);
void setFirst();
T* getCurrent() throw (NoObjectException);
void setNext() throw (NoObjectException);
bool isEnd();
};
template<class T>
ArrayStack<T>::ArrayStack()
{
top=0;
current=-1;
}
template<class T>
ArrayStack<T>::~ArrayStack()
{}
template<class T>
void ArrayStack<T>::insertTop(const T& t)
throw (FullStackException)
{
if (top==N) throw FullStackException();
v[top]=t.clone();
top++;
nElems++;
}
- 244 -
CHAPTER 6: POLYMORPHISM
template<class T>
void ArrayStack<T>::removeTop()
throw (EmptyStackException)
{
delete v[top-1];
top--;
nElems--;
}
template<class T>
void ArrayStack<T>::setFirst()
{
current=top-1;
}
template<class T>
bool ArrayStack<T>::isEnd()
{
return current==-1;
template<class T>
T* ArrayStack<T>::getCurrent()
throw (NoObjectException)
{
if (isEnd()) throw NoObjectException();
else return v[current]->clone();
}
template<class T>
void ArrayStack<T>::setNext()
throw (NoObjectException)
{
if (isEnd()) throw NoObjectException();
else current--;
}
template<class T>
bool ArrayStack<T>::operator==(const Container<T>& c) const
{
//EXERCICE FOR THE READER......
}
Remarks
• Notice that this implementation implements all the operations left as pure
virtual in its ancestors (==, setFirst, getCurrent, setNext,
isEnd, insertTop, removeTop and getTop.
• A significant addition of this class (with respect to that of problem 6.10) is
- 245 -
CHAPTER 6: POLYMORPHISM
Empty stack:
s: nelems: 0
first: NULL
s: nelems: 3
NULL
first:
Implementation of LinkedStack
Node<T>* first;
Node<T>* current;
- 246 -
CHAPTER 6: POLYMORPHISM
public:
LinkedStack();
LinkedStack(LinkedStack<T>&);
~LinkedStack();
bool operator==(const Container<T>&) const;
virtual void insertTop(const T&) throw (FullStackException);
virtual void removeTop() throw (EmptyStackException);
virtual T* getTop() const throw (EmptyStackException);
void setFirst();
T* getCurrent() throw (NoObjectException);
void setNext() throw (NoObjectException);
bool isEnd();
};
template<class T>
LinkedStack<T>::LinkedStack()
{
first=NULL;
current=NULL;
}
template<class T>
LinkedStack<T>::~LinkedStack()
{
Node<T>* aux;
while (first!=NULL) {
aux=first;
first=first->next;
delete aux;
}
}
template<class T>
void LinkedStack<T>::insertTop(const T& t)
throw (FullStackException)
{
Node<T>* aux;
aux=new Node<T>;
aux->val=t.clone();
aux->next=first;
first=aux;
nElems++;
}
- 247 -
CHAPTER 6: POLYMORPHISM
template<class T>
void LinkedStack<T>::removeTop()
throw (EmptyStackException)
{
Node<T> *aux;
template<class T>
void LinkedStack<T>::setFirst()
{
current=first;
}
template<class T>
bool LinkedStack<T>::isEnd()
{
return current==NULL;
}
template<class T>
T* LinkedStack<T>::getCurrent()
throw (NoObjectException)
{
if (isEnd()) throw NoObjectException();
return (current->val)->clone();
}
template<class T>
void LinkedStack<T>::setNext()
throw (NoObjectException)
{
if (isEnd()) throw NoObjectException();
else current=current->next;
}
template<class T>
bool LinkedStack<T>::operator==(const Container<T>& c) const
{
//LEFT AS EXERCICE TO THE READER
}
Remarks
Notice that the node keeps a pointer to the element, not the element itself. The
reason for this is the same as in the ArrayStack case: the stack will be able to
store objects of type T or descendants of T (see problem 6.10).
- 248 -
CHAPTER 6: POLYMORPHISM
template<class T>
Stack<T>* insertElemsIntoStack(T* x, int n)
Again, the specification of this function, deliberately, does not say a single word
about the specific type of the returned object (we know that this specific type
cannot be Stack, since this is an abstract class: it should be LinkedStack or
ArrayStack). However, this is not a problem for the user, who can work with the
returned object without having to know that. The following code converts an array
of students (the class Student is defined by means of a name, an id and an age)
into a stack and then, iterates on that stack and shows its components:
- 249 -
CHAPTER 6: POLYMORPHISM
int main()
{
int i;
Stack<Student>* pss;
Student vs[4];
Student s0("john","1111",18);
Student s1("ann","2222",19);
Student s2("eve","3333",18);
Student s3("joan","4444",20);
Student* ps;
vs[0]=s0;
vs[1]=s1;
vs[2]=s2;
vs[3]=s3;
pss= insertElemsIntoStack(vs,4);
pss->setFirst();
while (!pss->isEnd()){
ps=pss->getCurrent();
cout<<*ps<<endl;
pss->setNext();
}
return 0;
}
template<class T>
Stack<T>* insertElemsIntoStack(T* x, int n)
{
ArrayStack<T>* s;
int i;
s=new ArrayStack<T>;
for(i=0;i<n;i++){
s->insertTop(x[i]);
}
return s;
}
but it could use LinkedStack too. The choice made by the implementers does
not concern the user. Moreover, this choice may change in a later version of the
library.
- 250 -
CHAPTER 6: POLYMORPHISM
This function returns the element in the bottom of the stack s (i.e., the first element
that was inserted in the stack s). This function has been developed by the ACME
team to work together with the other library functions (and, in particular, with
insertElemsIntoStack). Therefore, the developers of this function know that
the parameter s is actually, of type ArrayStack. Thus, they can provide the
following implementation:
- 251 -
CHAPTER 6: POLYMORPHISM
The idea now is to use this class Object as the superclass of any other user defined
class (i.e., everything will be an object).
This class can be used in order to create a stack of objects in such a way that an
element of that stack of objects can be itself a stack. That is, we should be able to
have stacks of stacks of objects, stacks of stacks of stacks of objects (as many times
as we want).
#include <iostream>
class Object{
public:
virtual bool operator==(Object& ob)=0;
virtual void operator=(Object& ob)=0;
virtual Object* clone()=0;
};
/***********************************************
CLASS Stack
**********************************************/
Object* v[N];
int top;
public:
Stack();
void insertTop(Object& ob);
void removeTop(Object& ob);
bool isEmpty();
Object* clone();
void operator=(Object& cobj);
bool operator==(Object& cobj);
};
Stack::Stack()
{
top=0;
}
- 252 -
CHAPTER 6: POLYMORPHISM
equals=true;
i=0;
while(equals && i<top){
equals=(*v[i])== *(co->v[i]);
i++;
}
return equals;
}
Object* Stack::clone()
{
Stack* aux;
int i;
aux=new Stack;
/***************************************************
class A
********************************************************/
public:
A(){}
A(const A& a){ attr=a.attr;}
- 253 -
CHAPTER 6: POLYMORPHISM
Object* clone(){
A* o;
o=new A(*this);
return o;
}
void setAttribute(int pattr)
{
attr=pattr;
}
bool operator==(Object& o)
{
return attr==((A&)o).attr;
}
void operator=(Object& cobj)
{
//.....
}
};
int main()
{
int i;
A x;
Stack c, c2;
for (i=0;i<10;i++){
x.setAttribute(i);
c.insertTop(x);
}
x.setAttribute(12);
c2.insertTop(x);
c2.insertTop(c);
return 0;
}
Notice in this solution that the Object abstract class acts as an interface in the
sense that any object class that has to be an object element should conform to the
Object abstract class.
- 254 -
Chapter 7
Exceptions
Definition (Exception)
Exception is unexpected or prohibited situation that prevents successful
completion of a function or a method.
Unexpected or prohibited situation is not something that should almost never
happen or some disastrous situation. It is an exceptional situation (hence the
name) which prevents some part of system of doing what it was instructed to
do.
Let us start explaining about the exception handling in OOP by first showing the
traditional way of dealing with exceptions. One simple class "Calculator" is shown
in the next example.
#include <iostream>
class Calculator {
double result;
int error; //error situation indicator
public:
Calculator() { result = 0; error = 0; }
- 255 -
CHAPTER 7: EXCEPTIONS
return result;
}
int main() {
Calculator calc;
calc.add(9);
calc.divide(3);
if (calc.getError() != 0) {
cout << "Error: You can not divide by zero!" << endl;
return;
}
calc.add(6);
calc.divide(0);
if (calc.getError() != 0) {
cout << "Error: You can not divide by zero!!" << endl;
return;
}
calc.add(2);
Our "Calculator" only supports two operations: addition and division. In the
"Divide" method one prohibited situation may happen when we try to divide current
result by zero. We handled this situation by setting one field defined in "Calculator"
class to 1 (field "error").
Of course, by indicating this situation, we did not finish with handling the
exception. We have to check "error" field after each call to "Divide()" method, and
if it is set to 1 we need to notify the user about the situation.
There are several problems with this exception handling approach. First, we use one
value (of type "int") for indication of exception. The example shown is trivial and
this one integer value is sufficient. But in programs that are more complex, different
kinds of exceptions are possible and those exceptions can not be described by only
one field.
- 256 -
CHAPTER 7: EXCEPTIONS
The second problem is in "main()" function (in the part of program where we want
to handle the exception). We unnecessary burdened the code by checking for
exception after each method call. Also, this is very annoying for programmers - that
is why they tend to simply ignore the fact that exceptions may happen and do not
check for them at all.
One might ask: "Why should we go through all this trouble? We could simply write
the message about the error to the console and be done with it!" Simply writing the
error message to the console would work only for some trivial examples. But if we
are developing a class or a function that will be used in larger programs than we can
not just write to the console for several reasons:
• We do not know the type of the program that will be calling our function or
using our class. It could be a program with graphic user interface or a web
application. In that case writing to the console does not mean much to the
user.
• By only writing to the console the calling program is not informed about the
error so it may continue execution normally as if the error had never occurred
(and generally we do not want that, because we would like to inform the user
about the error and/or possibly try something else).
- 257 -
CHAPTER 7: EXCEPTIONS
result /= value;
return result;
}
class DivByZeroException {
char message[50];
double dividend;
public:
DivByZeroException(char* message, double dividend) {
strcpy(this->message, message);
this->dividend = dividend;
}
const char* getMessage() { return message; }
double getDividend() { return dividend; }
}
In the line where exception is thrown, the method ends and the program control is
returned to the calling context (in our case into "main" function). Automatic objects
in "Divide" method are destroyed as if the method ended normally.
Throwing an exception is just one part of exception handling. After the exception is
thrown it needs to be "caught" and processed somewhere in higher context. Let us
modify the "main" function in order to catch the exception.
int main()
{
/* try block contains the statements that we want to look out
for exceptions */
try
{
Calculator calc;
calc.add(9);
calc.divide(3);
calc.add(6);
calc.divide(0);
calc.add(2);
cout << "The result is " << calc.getResult() << endl;
}
- 258 -
CHAPTER 7: EXCEPTIONS
Exception catching is done by using try and catch block. Try block holds the
statements that could throw an exception. Note that this block does not contain any
conditional statements for checking if exceptions are thrown or not. That was one of
our goals: to make the code more readable by eliminating unnecessary code for
exception checking. We write the code as if exceptions cannot happen. Exception
handling code is isolated in one place (catch blocks).
There can be one or more catch blocks following the try block. A catch block is
like one small function that has only one argument. Based on the type of the
argument exception handling mechanism decides which catch block to execute.
The first catch block with an argument that matches the type of exception is
executed.
If no match is made, the exception is transferred into even higher context, until the
outermost context is reached. If the exception is not caught in the highest calling
context the program terminates in an abnormal way.
Let us improve our "Calculator" class by adding subtraction and square root
extraction operations. Before we write any code, we must consider what kind of
exceptional situations can arise while doing those operations. The most obvious one
is the following: we can not extract square root from a negative number. We shall
define new class that will hold the information about that exceptional situation
(IllegalOpException).
#include <iostream>
#include <string.h>
#include <math.h>
class IllegalOpException
{
char message[50];
public:
IllegalOpException(char* message)
{ strcpy(this->message, message); }
- 259 -
CHAPTER 7: EXCEPTIONS
class Calculator {
double result;
public:
Calculator() { result = 0; }
double getResult() { return result; }
double add(double value) { result += value; return result; }
double subtract(double value) { result -= value; return result; }
double divide(double value);
double squareRoot();
};
double Calculator::squareRoot() {
if (result < 0)
throw IllegalOpException("Illegal Square root extraction!");
result = sqrt(result);
return result;
}
int main()
{
try
{
Calculator calc;
calc.add(9);
calc.divide(3); //exception may be thrown here
calc.add(6);
calc.subtract(11);
calc.squareRoot(); //exception can also be thrown here
- 260 -
CHAPTER 7: EXCEPTIONS
Inside the "try" block there are two statements that could throw an exception.
Depending on which exception is thrown, the corresponding "catch" block will be
executed. If no exception is thrown all "catch" statements are skipped.
There is one important thing to note in previous example. If the code inside "try"
block throws DivByZeroException both catch blocks could handle this type of
exception (DivByZeroException "is a special kind of" IllegalOpException).
In that case exception handling mechanism executes the first matching catch block.
If IllegalOpException is thrown only second catch block can handle it.
Basically if a "catch" block can handle exceptions of some type "T" it can also
handle the exceptions of any type derived from "T" (notice that this is an
application of the substitution principle presented in Chapter 5).. If the type of the
formal argument of a "catch" block is a reference or a pointer then virtual
mechanism and dynamic binding may be activated. As you can see, the exception
handling in C++ fully supports the principles of object oriented programming (data
abstraction and polymorphism).
Since DivByZeroException is derived from IllegalOpException our main
function can also look like this:
int main()
{
try
{
Calculator calc;
calc.add(9);
calc.divide(3); //exception may be thrown here
calc.add(6);
- 261 -
CHAPTER 7: EXCEPTIONS
calc.subtract(11);
calc.squareRoot(); //exception may also be thrown here
Although two types of exception can be thrown inside try block, only one catch
is enough to handle both exception types.
If we want to handle DivByZeroException differently we must put the catch
block for that exception type before existing catch block. Consider what would
happen if our "main()" function looked like this:
int main() {
try
{
Calculator calc;
calc.add(9);
calc.divide(3);
calc.add(6);
calc.subtract(11);
calc.squareRoot();
- 262 -
CHAPTER 7: EXCEPTIONS
The second catch block will never execute because the first block will catch both
exception types. Most compilers will issue a warning in such situation.
It is possible to write a catch block that can catch any type of exception.
try
{
//... part of code ommited
}
catch (DivByZeroException &dbz)
{
//... part of code ommited
}
catch (IllegalOpException &iop)
{
//... part of code ommited
}
catch (...) //this block can catch any type of exception
{
//... part of code ommited
}
The final catch block (with the ellipses instead of argument) catches any type of
exception. Obviously, it only makes sense to put this block as a last catch block.
Programmers often form a hierarchy of exception classes with common base class.
Standard C++ library for example has one base class ("exception") for all
exceptions that can be thrown by methods in the library.
There is one more advantage in using C++ exception handling mechanism over
traditional way of working with exceptions. In C++, if we do not handle exceptions
in some function (for example we do not have enough information to handle them),
and an exception is thrown inside that function it will still be propagated to the
calling function where it can be handled. This is the default behaviour and we do
not have to write any extra code to accomplish it (that is not the case with
traditional exception handling).
Look at the following example:
void calculateIt() {
//there is no exception handling in this function
Calculator calc;
calc.add(9);
calc.divide(3); //exception may be thrown here
calc.add(6);
calc.subtract(11);
calc.squareRoot(); //exception may also be thrown here
- 263 -
CHAPTER 7: EXCEPTIONS
int main()
{
try
{
calculateIt(); //exceptions will be propagated here
}
catch (DivByZeroException &dbz)
{
cout << "Exception is thrown!" << endl;
cout << dbz.getMessage() << endl;
cout << "You tried to divide " << dbz.getDividend()
<< " by zero!" << endl;
}
catch (IllegalOpException &iop)
{
cout << "Exception is thrown!" << endl;
cout << iop.getMessage() << endl;
}
void write_to_db() {
DbConnection cn = DbConnection("somedb");
try
{
//.. some sql statements executed
}
catch (DbException &e)
{
cn.Close();
throw; //re-throw the exception
}
}
- 264 -
CHAPTER 7: EXCEPTIONS
class Calculator {
The function declared this way should only throw the specified exceptions.
The function declared with empty brackets after "throw" keyword should not throw
any exceptions.
If a function does not contain exception specification then it can throw any
exception:
void some_function();
void unexpected();
- 265 -
CHAPTER 7: EXCEPTIONS
void terminate();
#include <iostream>
#include <string.h>
#include <math.h>
class IllegalOpException
{
char message[50];
public:
IllegalOpException(char* message)
{ strcpy(this->message, message); }
const char* getMessage()
{ return message; }
};
double dividend;
- 266 -
CHAPTER 7: EXCEPTIONS
public:
class Calculator {
double result;
public:
Calculator() { result = 0; }
double getResult() { return result; }
result /= value;
return result;
}
result = sqrt(result);
return result;
}
void calculateIt() {
Calculator calc;
calc.add(9);
calc.divide(3);
calc.add(6);
calc.subtract(11);
calc.squareRoot();
- 267 -
CHAPTER 7: EXCEPTIONS
void my_unexpected() {
cout << "Error: Program threw an unexpected exception!"
<< endl;
abort();
}
void my_terminate() {
cout << "Error: Program did not catch the exception thrown!"
<< endl;
abort();
}
int main()
{
set_unexpected(my_unexpected); //installing our version
//of "unexpected()"
calculateIt();
When you run this program you should get the following output:
- 268 -
CHAPTER 7: EXCEPTIONS
//constructor
public ExampleException(int theData)
{
someData = theData;
}
The code above will not compile in Java. Function "functionTwo()" must either
catch the exception or specify that itself can throw "ExampleException". The
following code shows the two valid versions of "functionTwo()".
Or
public void functionTwo()
{
try
{
functionOne();
}
catch (ExampleException e)
{
..//some code that handles the exception
}
}
- 269 -
CHAPTER 7: EXCEPTIONS
Actually, compiler will not check that the exception is caught or propagated if the
exception thrown inherits from "RuntimeException" (which is Java built-in class)
so what we said above is true only for custom exceptions (which usually inherit
from "Exception" class).
Java also has one more keyword for exception handling – the word "finally".
Finally block may be used instead of catch block. This block contains statements
that should execute even if exception is thrown in try block. For example:
Connection cn = ConnectionFactory.getConnection();
try
{
Statement st = cn.createStatement();
st.execute("UPDATE students SET graduated = 1");
}
finally
{
cn.close();
}
If statements in try block throw an exception, finally block will execute (and
connection will be closed) before the function exits. Finally block will also execute
if no exception is thrown inside try block.
As you can see finally keyword allows a programmer to specify which statements
should execute regardless if the exception is thrown or not. The finally block is
commonly used for releasing some allocated resources, similar to the example
shown.
basestack.h
#ifndef _BASESTACK_H
#define _BASESTACK_H
- 270 -
CHAPTER 7: EXCEPTIONS
class BaseStack
{
public:
virtual void push(const ItemType item) = 0;
virtual ItemType pop(void) = 0;
virtual bool isEmpty(void) = 0;
virtual bool isFull(void) = 0;
};
#endif
#include "basestack.h"
#endif
ArrayStack::ArrayStack(int maxSize)
{
this->maxSize = maxSize;
this->top = -1;
this->items = new ItemType[maxSize];
}
ArrayStack::~ArrayStack(void)
- 271 -
CHAPTER 7: EXCEPTIONS
{
delete [] items;
}
void ArrayStack::push(const ItemType item)
{
items[++top] = item; //first increase top and then put the item
}
ItemType ArrayStack::pop(void)
{
return items[top--]; //first get the item then decrease top
}
bool ArrayStack::isEmpty(void)
{
return (top < 0); //stack is empty is top is negative
}
bool ArrayStack::isFull(void)
{
//stack is full is top+1 >= maxSize
//for example if maxSize is 10 then
//valid values for top are 0 – 9
//meaning if top = 9 then stack is full
return (top + 1 >= maxSize);
}
Figure 7.1 explains the details of how private member top is used.
Figure 7-1
Finally here is the example program that shows how ArrayStack class could be
- 272 -
CHAPTER 7: EXCEPTIONS
used.
int main()
{
ArrayStack st1(5);
st1.push(2.2);
st1.push(2.4);
st1.push(4.2);
st1.push(4.4);
st1.push(4.6);
while (!st1.isEmpty())
cout << st1.pop() << endl;
return 0;
}
Remarks:
Obviously, there are some difficulties with the solution that has just been presented:
• Function ArrayStack::push does not check if the stack is full before
pushing new item on it.
• Function ArrayStack::pop does not check if the stack is empty before
removing the item from top of the stack.
The class relies on the user of the class to ensure that push() is not called on a full
stack, and that pop() is not called for stack that is empty. User of the class could
check for that using isEmpty() and isFull() member functions. Consequently,
the class contract for the Push and Pop operations should include these assumptions
as preconditions.
If the operations were responsible for checking those conditions, those operations
would be more robust since they would have void preconditions. Detailed
discussion about class contracts is given in section 1.4.
- 273 -
CHAPTER 7: EXCEPTIONS
#include "basestack.h"
//two trivial classes are added that will only be used for
//reporting the type of exception that occurred.
#endif
ArrayStack::ArrayStack(int maxSize)
{
this->maxSize = maxSize;
this->top = -1;
this->items = new ItemType[maxSize];
}
ArrayStack::~ArrayStack(void)
{
delete [] items;
}
items[++top] = item;
}
- 274 -
CHAPTER 7: EXCEPTIONS
ItemType ArrayStack::pop(void)
{
if (isEmpty())
throw StackEmptyException();
return items[top--];
}
bool ArrayStack::isEmpty(void)
{
return (top < 0);
}
bool ArrayStack::isFull(void)
{
return (top + 1 >= maxSize);
}
int main()
{
ArrayStack st1(5);
try
{
st1.push(2.2);
st1.push(2.4);
st1.push(4.2);
st1.push(4.4);
st1.push(4.6);
while (!st1.isEmpty())
cout << st1.pop() << endl;
return 0;
}
- 275 -
CHAPTER 7: EXCEPTIONS
Remarks
• As it can be seen in file main.cpp, the object that describes the exception
does not have to be used inside the catch block. The classes
StackFullException and StackEmptyException might seem useless
because they do not have any attributes or operations. They still serve the
purpose. Sometimes, in order to properly handle the exceptional situation, it
is enough to know the type of exception. In another situation that might not
be the case.
- 276 -
CHAPTER 7: EXCEPTIONS
int growBy;
int length;
T* elements;
public:
MyVector(int initCap, int growBy = 10);
~MyVector();
#endif
- 277 -
CHAPTER 7: EXCEPTIONS
int main()
{
MyVector<int> v1(2, 2);
MyVector<double> v2(2, 10);
v1.append(1);
v1.append(2);
v1.append(3);
v2.append(0.1);
v2.append(0.2);
v2.append(0.3);
return 0;
}
Remarks
• Similar to the generic Stack class presented in Chapter 3, there are some
constraints concerning the type T (with which the class will be instantiated).
If T is not a pointer type then it should overload operator = in order to avoid
sharing of identity (since the objects of type T are copied into the elements
array in append operation – see Chapter 4)
- 278 -
CHAPTER 7: EXCEPTIONS
• One consequence of the fact that objects are simply being copied into the
elements array is the lack of polymorphic behaviour of MyVector class. If
polymorphic behaviour is required then T (the type that MyVector is
instantiated with) should be pointer type. For example:
int main()
{
MyVector<BaseClass*> v(50); //use default growBy value of 10
return 0;
}
Since vector has no knowledge about the fact that T is actually a pointer type
then we must explicitly call delete on dynamic objects added to the vector.
• Overloaded operator [] does not check if index given as parameter is within
valid range (0 <= i < length). It is more natural that this check is done by
the operator [].
• Also, when adding elements using append operation or when creating a
vector we should handle bad_alloc exception. If there is not enough
memory to allocate on the free store bad_alloc exception is thrown (this is
the default behaviour which can be changed by calling set_new_handler()
function).
/*
*****************
Exception classes
*****************
*/
- 279 -
CHAPTER 7: EXCEPTIONS
class VectorException
{
protected:
char* message;
public:
VectorException(const char* message);
virtual ~VectorException();
virtual const char* getMessage() const;
};
/*
**********************
Generic class MyVector
**********************
*/
int length;
T* elements;
public:
MyVector(int initCap, int growBy = 10);
~MyVector();
this->capacity = initCap;
this->growBy = growBy;
this->length = 0;
- 280 -
CHAPTER 7: EXCEPTIONS
try
{
this->elements = new T[this->capacity];
}
catch (bad_alloc)
{
throw VectorOutOfMemoryException(); // throw exception
}
}
delete [] elements;
elements = newElements;
}
catch (bad_alloc)
{
throw VectorOutOfMemoryException(); // throw exception
}
}
elements[length] = item;
length++;
}
return elements[index];
}
#endif
- 281 -
CHAPTER 7: EXCEPTIONS
/**********************************************
WARNING: If you run this program, you might get
unexpected results
***********************************************/
int main()
{
try
{
MyVector<double> v2(2)
v2.append(0.1);
v2.append(0.3);
v2.append(0.3);
cout << v2[10] << endl;
}
catch (VectorException& ex)
{
cout << "Exception: " << ex.getMessage() << endl;
}
return 0;
}
- 282 -
CHAPTER 7: EXCEPTIONS
Remarks:
The code shown in the solution has a problem. When we try to access element with
index 10 in vector v2 exception will be thrown. Object of class
VectorOutOfBoundsException which is used to describe the exception has one
member that is a pointer (message).
When an exception is thrown, the expression after throw keyword (in our case a
local object of class VectorOutOfBoundsException) is used for initialization of
a temporary object in static memory. This has to be done because the object that we
created locally will get out of scope when the function terminates. Since a copy of
our local object is being created, copy constructor is called.
We did not provide a copy constructor, so a default version is generated by the
compiler. This will result in sharing of identity between our local object and the
temporary object in static memory (message attributes of both objects will point to
the same character array). When local object gets out of scope (function is
terminated) the destructor will be called for that object, and it will deallocate the
memory at the address stored in the message attribute. At that point, message
member of the temporary object in static memory will point to invalid location.
When getMessage() operation is called in catch block in main function our
program might crash (this is because ex is a reference to the temporary object in
static memory, and message attribute of that object points to invalid location).
The code given above might actually work on some compilers. That depends
on the optimization done by them. Consider the following code:
template <class T>
T& MyVector<T>::operator[](int index)
{
if (index < 0 || index > length - 1)
throw VectorOutOfBoundsException();
return elements[index];
}
Some compilers will try to optimize this code by creating the object directly
in static memory using the constructor given after throw keyword. We should
not rely on this behaviour since it is highly dependent on the specific
compiler.
Even if optimization is done, the semantics of exception throwing must be
honoured. For example, if we declare copy constructor as private, the code
will not compile (even if compiler never calls the copy constructor due to
optimizations).
The solution is to provide copy constructor for the base exception class
(VectorException).
- 283 -
CHAPTER 7: EXCEPTIONS
/**********************************
Exceptions declarations
**********************************/
class VectorException
{
protected:
char* message;
public:
VectorException(const char* message);
VectorException(const VectorException& e); //copy constructor
virtual ~VectorException();
virtual const char* getMessage() const;
};
#endif
VectorException::VectorException(const VectorException& e)
{
message = new char[strlen(e.message)+1];
strcpy(message, e.message);
}
- 284 -
Chapter 8
Standard C++ Library
#include <iostream.h>
#include <iostream>
Using-directive makes all names from a namespace available as if they had been
declared outside their namespace.
- 285 -
CHAPTER 8: STANDARD C++ LIBRARY
A standard header with a name starting with letter "c" is equivalent to a header in C
standard library. For example "cstdlib" has the same functions like "stdlib.h" file in
C standard library with one difference – the functions in "cstdlib" are defined in
"std" namespace.
Fully describing every class and function in the Standard C++ library is not the goal
of this chapter since it would require a whole book or two. Introduction to most
commonly used parts of the library will be made instead. String class and parts of
the Standard Template Library (STL) are presented in this book. STL is a part of
the Standard C++ library and contains commonly used data structures and
algorithms realized as template classes and template functions. The core of the STL
are the three elements: containers, iterators and algorithms.
8.2. Strings
One of the commonly used things in C++ is manipulation with strings of characters.
Traditional ("C" style) way of using strings is error prone. Creating "string" class in
C++ which would simplify the use of strings of characters is often used as a good
exercise for learning classes.
Standard C++ library now contains powerful but simple to use "string" class. This
class takes care of allocating, copying, merging and many other operations that can
be performed on strings.
Standard does not define the way this "string" class is realized, but defines the
interface for using that class. As a consequence, strings do not necessary end with
'\0' character like in "C" style libraries.
The following example demonstrates the way objects of "string" class can be
created and some of the member operations of the "string" class.
#include <iostream>
#include <string>
int main()
{
string s1 = "This is one way of string initialization!";
string s2("This is another way!");
- 286 -
CHAPTER 8: STANDARD C++ LIBRARY
/*
size() operation gives us the current size of a string,
and "capacity()" gives as the maximum size that a string
can grow whithout allocating
additional memory.
*/
cout << "The size of 's1' is: " << s1.size() << endl;
cout << "The capacity of 's1' is: " << s1.capacity() << endl;
return 0;
}
There are many other operations in "string" class. For example, we can search for a
character or a string inside another string using "find()" function. Here is the
example:
#include <iostream>
#include <string>
int main()
{
string s1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return 0;
}
- 287 -
CHAPTER 8: STANDARD C++ LIBRARY
class Book
{
private:
char* title;
char* author;
public:
Book(char* theTitle, char* theAuthor) {
title = new char[strlen(theTitle)+1];
strcpy(title, theTitle);
author = new char[strlen(theAuthor)+1];
strcpy(author, theAuthor);
}
class Library {
private:
int numberOfBooks;
Book* books[100]; //must be an array of pointers since
//there is no default constructor for
//the class "Book"
public:
Library() { numberOfBooks = 0; }
void AddBook(Book* aBook) {
books[numberOfBooks] = aBook;
numberOfBooks++;
}
};
Consider what would happen if we try to add more then a hundred books to the
library. The program would probably crash. Of course, we could modify the
"AddBook()" method to check if the maximum number of books is reached and
throw an exception if it is. But, what if storage of more then a hundred, or more
then a thousand books in the library is needed (or unknown number of books)? We
could make an array in dynamic memory (using operator "new"), and resize it when
it reaches the limit. Now, another question arises: "How can we resize the array?".
- 288 -
CHAPTER 8: STANDARD C++ LIBRARY
We can't! What we can do is allocate a new (bigger) array, copy the elements from
the old one, and destroy the original. Tricky, isn't it?
To make our life easier, Standard C++ Library includes special generic classes
called "containers". Containers are objects that can contain other objects (which is
similar to our "Library" class shown in previous example). These container classes
enable us to create and use collections of objects without worrying about the
resizing the collection, allocating enough space, etc. We simply add objects to the
collection, and then later retrieve these objects from the collection.
There are different container classes in the C++ library, and choosing the right one
depends on the type of problem at hand. Different container classes have different
underlying representation of the data. Some use sequential representation (like
"vector"), others use linked representation (like "list"). However, objects inside the
container can be accessed in similar way using "iterators". When accessing objects
in a container using an iterator, the container is viewed as a simple sequence of
objects. There are also other ways to access the objects in a container, depending on
the container type, but iterators are a common way for all container types.
Now that we know the basics, let us introduce some simple containers (or
collections). The first one is the "vector" container. Here is the example of "vector"
usage.
#include <iostream>
#include <vector>
int main()
{
vector<Book*> library; //the container
vector<Book*>::iterator libIter; //iterator for the container
- 289 -
CHAPTER 8: STANDARD C++ LIBRARY
Does it look complicated? You will see in a moment that it really isn't.
The "Book" class should be clear. Let us look at the main function, line by line.
1) vector<Book*> library;
If you remember generic classes from Chapter 3. this also should be clear enough.
We create object "library" from the "vector" template. Parameter passed to the
generic class <Book*> means that the vector of pointers to books (objects of class
Book) should be created. We can create a vector that can hold any type ("int" for
example) in a similar way.
2) vector<Book*>::iterator libIter;
Another object is created (libIter). This is the iterator that will be used for accessing
the elements in a vector. Why is it created this way? Because the "iterator" class is
nested inside the "vector" class. Again, by passing <Book*> to the generic class we
instruct the compiler that the iterator will iterate over pointers to books.
Three books are added to the library (three pointers to books, actually). Method
"push_back" of the vector class adds elements to the end of the collection.
- 290 -
CHAPTER 8: STANDARD C++ LIBRARY
7) {
8) Book* currentBook = *libIter;
9) cout << currentBook->GetTitle() << " by "
<< currentBook->GetAuthor() << endl;
10) }
By dereferencing the iterator (*libIter) we access the element the iterator currently
points to (the element is of type "Book*"). Finally in line 9, we print the data about
the book. This block is repeated for all elements in the "library" container.
You might ask: "What is the size of this vector container?". We don't have to worry
(much) about it. All the container classes will resize to accommodate new elements
when needed.
If you wish to know the number of elements currently stored in a vector you can use
size() member function.
As you can see vector class is not that complicated. Now, let us examine another
container type: "list". We'll do it using the same example with Book class:
#include <iostream>
#include <list>
class Book
{
private:
char* title;
char* author;
public:
Book(char* theTitle, char* theAuthor)
{
title = new char[strlen(theTitle)+1];
strcpy(title, theTitle);
author = new char[strlen(theAuthor)+1];
strcpy(author, theAuthor);
}
- 291 -
CHAPTER 8: STANDARD C++ LIBRARY
int main()
{
list<Book*> library; //the container
list<Book*>::iterator libIter; //iterator for the container
It is almost the same. That was the whole idea: no matter what container we choose,
we use it the same way.
So what is the difference between using vector and using list container? The
difference is in the way those containers internally store elements. Based on that,
certain operations with one container type can be less efficient then with another
container type. Inserting an element in the middle of the collection with vector is
not efficient (or is expensive) because vector internally stores elements using
arrays. The same operation with "list" is efficient, because the elements are stored
in a doubly linked list. On the other hand, randomly accessing elements in a vector
(by index) is much more efficient then in a list. That is why vector has additional
operations for accessing elements by index (which do not exist in list). The example
shown above presents a common way (using iterators) for accessing the elements in
vector or list.
- 292 -
CHAPTER 8: STANDARD C++ LIBRARY
8.3.1.1 Vectors
Vectors are similar to arrays and they can even be used in a similar way using []
operator. Vector size is automatically increased when new elements are added to it.
Vector has "size()" member function which returns the number of elements
currently stored in the container.
Here is a simple example.
#include <iostream>
#include <vector>
int main()
{
vector<int> numbers;
numbers.push_back(10);
numbers.push_back(20);
numbers.push_back(30);
numbers.push_back(40);
numbers.push_back(50);
return 0;
}
There is another member function shown in the example: max_size(). This function
returns the maximum size to which a vector can grow. The result of the function is
- 293 -
CHAPTER 8: STANDARD C++ LIBRARY
dependent on the data being stored, the type of the container and the operating
system.. The output produced should look something like this:
Element 1 = 10
Element 2 = 20
Element 3 = 30
Element 4 = 40
Element 5 = 50
Elements are added to the end of the vector and also can be removed from the end.
These two operations are fast with vectors. On the other hand, inserting and
removing element in the middle of vector is slow. This is because when inserting in
the middle, all the elements from the point of insertion to the end must be moved to
make room for the new element.
Next example shows push_back(), back(), pop_back(), insert() and remove()
operations.
#include <iostream>
#include <vector>
int main()
{
vector<int> numbers;
numbers.push_back(10);
numbers.push_back(20);
numbers.push_back(30);
numbers.push_back(40);
numbers.push_back(50);
print("Elements", numbers);
cout << "The last element is: " << numbers.back() << endl;
- 294 -
CHAPTER 8: STANDARD C++ LIBRARY
return 0;
}
Elements: 10 20 30 40 50
The last element is: 50
After pop_back(): 10 20 30 40
After insert(): 10 20 25 30 40
After erase(): 10 20 30 40
Insert and erase operation expect iterator to be passed as first argument. The iterator
should point to the element before which the new element will be inserted (for
insert) or to the element which will be deleted (for erase).
8.3.1.2 Lists
Since lists are slow in accessing elements in arbitrary location, operator [] is not
defined for lists. Elements can be added to both ends using "push_front()" and
"push_back()" functions. There are also corresponding "pop" functions for
removing elements from front and back end of the list.
The following example shows the usage of those functions. Note that in the "print"
function, elements are accessed using iterator and not [] operator.
#include <iostream>
#include <list>
int main()
{
list<char*> numbers;
numbers.push_back("three");
- 295 -
CHAPTER 8: STANDARD C++ LIBRARY
numbers.push_back("four");
numbers.push_back("five");
numbers.push_front("two");
numbers.push_front("one");
print("Elements", numbers);
cout << "The last element is: " << numbers.back() << endl;
numbers.pop_back();
print("After pop_back()", numbers);
cout << "The first element is: " << numbers.front() << endl;
numbers.pop_front();
print("After pop_front()", numbers);
return 0;
}
List container uses doubly linked list to store elements. That means that "insert()"
and "erase()" operation on a list are fast since only the pointers to next and previous
elements must be modified.
8.3.1.3 Deque
Elements can be efficiently added to back and front ends of deque container, and
random access (using []) is fast. Inserting and removing the elements in the middle
are still slow. Deque has the same benefits as a vector plus the possibility to add
elements to the front end of the deque.
#include <iostream>
#include <deque>
int main()
{
deque<char*> numbers;
numbers.push_back("three");
numbers.push_back("four");
numbers.push_back("five");
- 296 -
CHAPTER 8: STANDARD C++ LIBRARY
numbers.push_front("two");
numbers.push_front("one");
print("Elements", numbers);
cout << "The last element is: " << numbers.back() << endl;
numbers.pop_back();
print("After pop_back()", numbers);
cout << "The first element is: " << numbers.front() << endl;
numbers.pop_front();
print("After pop_front()", numbers);
return 0;
}
There are different associative containers in STL but they can all be thought of as
variations of the "map" container. For example "set" is a special version of map
where all objects are null (the key provided is the object that needs to be stored).
"Multimap" container allows for several objects to be stored using the same key.
There is also "multiset" container which is a "set" that can store multiple instances
of the same key.
8.3.2.1 Map
A map stores pairs of objects. One object represent the key, and the other object is
the value. Both key and value can be strings, numbers or objects of any other class.
The next example program shows a map where keys are integers and value objects
are strings. Integer numbers represent student id numbers and strings are student
names. Two students can not have the same id, so a map can be used.
#include <iostream>
- 297 -
CHAPTER 8: STANDARD C++ LIBRARY
#include <map>
#include <string>
void main()
{
map<int, string> students;
map<int, string>::iterator it;
int key;
cout << "Enter student id ->";
cin >> key;
it = students.find(key);
if (it != students.end())
cout << "Found: " << it->second << endl;
else
cout << "Not found!" << endl;
When creating objects from map template at least two template parameters must be
supplied. The first parameter is the type of key objects, and the second is the type of
value objects that will be used.
Elements can be added to the map using index operator []. Inside the angle brackets
key object must be given. Key objects must be of the type specified in template
parameters when map container is created.
Finding the element in the map container is usually done using the find() function
(actually all associative containers have a find() member function). This function
returns an iterator which points to the element associated with the given key. If such
element does not exist, function returns an iterator that points behind last element
which can be tested using "end()" function.
Note that iterators in a map container point to objects of type "map:.value_type".
The attribute "first" of a "value_type" object returns the key, and the attribute
"second" returns the value. For example if key "200" is entered in previous example
"it->second" will return the value ("Hadzic, Adel"), and "it->first" would return the
key (200).
- 298 -
CHAPTER 8: STANDARD C++ LIBRARY
Elements in a map are ordered by key objects, which can be seen when the example
above is run. The list of students ordered by student id numbers will be displayed.
Another useful function that is available for all associative containers is the
"count()" function. Count accepts a key and returns the number of objects
associated with that key. For maps and sets count() can return 0 or 1, but for
multimaps and multisets it can return higher integer values. This is because in
multimap and multiset containers more then one value object can be associated with
a single key.
8.3.2.2 Set
As already said, the set container is a specialized version of the map container. It
contains only keys and no values. The following example demonstrates the use of
"set" containers.
#include <iostream>
#include <set>
#include <string>
int main()
{
set<string> words;
set<string>::iterator it;
words.insert("Back");
words.insert("Agent");
words.insert("Agent");
words.insert("Duck");
words.insert("Cold");
return 0;
}
After running the example you should get something similar to this:
The word list:
Agent
Back
Cold
Duck
Two things can be noticed. First, the word "Agent" is displayed only once since set
does not allow duplicate keys. Second, because objects in associative containers are
stored using binary tree for easy searching, the words are sorted.
If multiple occurrences of a single key are required multiset should be used instead.
- 299 -
CHAPTER 8: STANDARD C++ LIBRARY
8.4.1. find()
The following is an example of find() algorithm that looks for the first element with
specific value in a container or in an array.
#include <iostream>
#include <algorithm> //needed for alghoritms
#include <vector>
#include <string>
int main()
{
vector<string> cities;
vector<string>::iterator it;
cities.push_back("Moscow");
cities.push_back("Berlin");
cities.push_back("London");
cities.push_back("Paris");
cities.push_back("Rome");
cities.push_back("Madrid");
return 0;
}
Function find() accepts three parameters. The first two parameters specify the range
of elements that will be searched. Since we passed "cities.begin()" and "cities.end()"
- 300 -
CHAPTER 8: STANDARD C++ LIBRARY
all elements in container "cities" will be examined. The third parameter is the value
that we are searching for ("Rome").
Note that since we are searching inside vector of strings the search is case sensitive.
8.4.2. search()
Search algorithm tries to find a specific sequence of values inside a container. The
sequence that is being searched for is also specified by a container (we can say that
search() operates on two containers). The sample code below uses ordinary arrays
in order to demonstrate the use of STL algorithms with arrays.
#include <iostream>
#include <algorithm>
#include <cstring> //for strlen
int main()
{
//two arrays of characters (source and pattern)
char sentence[] = "The is an array of characters!"; //the source
char word[] = "array"; //the pattern
Function search() accepts four parameters. The first two parameters specify the
range of elements (using iterators or pointers) of the source container inside which
the sequence of elements (the pattern) will be searched. The last two parameters
identify the range of elements of the container which represent the sequence that is
being searched for.
The result is iterator (or pointer when arrays are used) which points to the location
where the found sequence inside source container begins.
If the sequence is not found "container.end()" is returned. How does this work for
ordinary arrays? Almost the same as for STL containers. Member function end() in
- 301 -
CHAPTER 8: STANDARD C++ LIBRARY
every container returns the iterator which points one location after the last
element. It is exactly the same with arrays. If sequence is not found, search() returns
pointer to one element behind last element in the source array ("sentence" in
previous example).
Note that in previous example we ignored the existence of '\0' character at the end
of character array. This works fine because strlen() function returns the size of
character array not including the '\0' character.
8.4.3. sort()
Before describing sort() we have to make a small introduction in function objects.
Function objects are heavily used in STL. Function object is an object of a class that
has only one member and that is overloaded operator () (see Chapter 2 and Chapter
3). Those classes are often generic (templates) so they can work with different
types. As a result that object behaves similar to function pointer.
Function objects are used in STL algorithms to customize the behaviour of some
algorithms. There are several predefined function object classes located in
"functional" header file.
Here is the example of sort() algorithm
#include <iostream>
#include <algorithm>
#include <functional>
#include <vector>
#include <string>
void main()
{
vector<string> cities;
vector<string>::iterator it;
cities.push_back("Moscow");
cities.push_back("Berlin");
cities.push_back("London");
cities.push_back("Paris");
cities.push_back("Rome");
cities.push_back("Madrid");
less<string> asc;
greater<string> desc;
- 302 -
CHAPTER 8: STANDARD C++ LIBRARY
Sort() accepts range of elements (specified by iterators) as the first two parameters.
The third parameter is optional and specifies the function object that will be used
for comparison of elements. If this parameter is not supplied then the function
object of generic class "less" is used.
The standard library provides many useful function objects. Most of the template
classes in "functional" header file are subclasses of "unary_function" or
"binary_function" base template classes. The declaration of the two classes is given
below.
The purpose of this classes is to provide standard names for the argument and return
types. Template classes "less" and "greater" inherit from "binary_function".
For example "less" template class is declared in the following way:
- 303 -
CHAPTER 8: STANDARD C++ LIBRARY
As it can be seen from the code above, overloaded operator() accepts two
parameters of the same type and it returns true or false (as a result of the expression
"x < y"). Template class "greater" is declared in a similar way:
When an object is created from "less" or "greater" template class is created it can be
used similar to a function. The following example shows one way how function
objects may be used.
#include <iostream>
#include <functional>
/*
The following template function uses function object
to compare two elements in a array.
*/
template <class T, class FuncObj>
void bubbleSort(T* array, int size, FuncObj compare)
{
} //end for j
} //end for j
- 304 -
CHAPTER 8: STANDARD C++ LIBRARY
int main()
{
double arr[] = {0, 4.5, 2.1, 3.3, 9.2, 7.8};
greater<double> asc;
less<double> desc;
bubbleSort(arr, 6, asc);
bubbleSort(arr, 6, desc);
return 0;
}
- 305 -
- 306 -
Appendix A
Unified Modelling Language UML
“Modeling is a way of thinking and reasoning about systems. The goal
of modeling is to come up with a representation that is easy to use in
describing systems in a mathematically consistent manner.”
Paul Fishwick,
Simulation Model Design and Execution
- 307 -
APPENDIX A: UML
with his OMT methodology really strong in analysis, but weaker in design
[Rumbaugh1991]. Each methodology had its strengths as well as its weaknesses.
For many years, Rumbaugh’s Object Modeling Technique (OMT), Booch’s O-O
methodology, and Jacobsen’s Objectory methodology were the three primary, but
competing, O-O methodologies.
Unified Modelling Language „was born“ when James Rumbaugh joined Grady
Booch at Rational Software. They both had object oriented syntaxes and needed to
combine them. Semantically they were very similar, it was mainly the symbols that
needed to be unified. The result was UML 1.0. It represents the evolutionary
unification of their experience with other industry engineering best practices.
- 308 -
APPENDIX A: UML
From the 1995 UML belongs to the Object Management Group (OMG). (On the
web site www.omg.org, you can find the PDF version of the UML reference).
- 309 -
APPENDIX A: UML
Although, in the book we used only a few model elements, in this appendix we
presented some more of the most used model elements and diagrams.
- 310 -
APPENDIX A: UML
Within the system being modelled, each concept is represented by a class. Classes
have data structure and behaviour and relationships to other elements.
A graphical representation of class is drawn as a solid-outline rectangle with three
rectangles separated by horizontal lines. The top rectangle holds the class name and
other general properties of the class (including stereotype); the middle rectangle
contains a list of attributes; the bottom rectangle holds a list of operations.
Graphical notation for classes (declaring and using), and textual notation for
referencing classes are shown in next few figures.
Class Name
Class Name
Operation[arg list] [:return type]
Time
- hour : int
- minute : int
- secunde : int
Time
Time
+ Time()
+ setTime(int, int, int) : void setTime()
+ printMilitary() : void
+ printStandard() : void
Point
Point Point
# x : int
# y : int x : int
y : int
ostream &operator<< : friend
+ Point(int=0, int=0)
+ setPoint(int, int) : void
+ getX() const : int
+ getY() const : int
Figure A- 4: UML class notation and example of the class Time and Point
- 311 -
APPENDIX A: UML
+ public visibility
# protected visibility
- private visibility
~ package visibility
0r {abstract}
Class Name Class Name
A nested class is a class that is fully enclosed within another class declaration. If
class is attached to another class by a line with the “anchor” symbol then it is
declared within the Namespace of that class. They are useful for providing objects
that are required by a particular class to function, but have no stand-alone
functionality.
- 312 -
APPENDIX A: UML
DeclaringClass
NestedClass
A.3.1.2 Object
As we know, an object represents a particular instance of a class. The object
notation is derived from the class notation by underlining a name of object (and its
class) in the top rectangle of the graphics representation. The syntax:
objectname : classname
Object is shown with object name, class name (optional) and attribute value
(optional).
The presence of an object in a particular state of a class is shown using the syntax:
objectname : classname ‘[‘ statename-list ‘]’
The list are comma-separated list of names of states that can occur concurrently.
The second part of the graphical representation contains the attributes for the object
and their values as a list.
Each value line has the syntax: attributename : type = value
dinnerTime : Time
hour = 19
minute = 30
second = 0
Figure A-7 UML notation for objects dinnerTime of class Time and startPoint of
class Point
- 313 -
APPENDIX A: UML
A.3.1.3 Interface
An interface is a collection of operations that are used to specify a service of a class
or a component (Booch, 1999). It is a contract of related services and a set of
conditions that must be true for the contract to be faithfully executed
A.3.1.4 Component
A component is a physical and replaceable part of a system that conforms to and
provides the realization of a set of interfaces (Booch, 1999).
Component may be: source code component (shell scripts, data files, *.cpp), run
time component (Java Beans, ActiveC controls, COM objects, CORBA objects,
DLL’s and OCX’s from VB), Executable component (*.exe files) etc.
A.3.1.5 Package
A package is a general purpose mechanism for organizing elements into groups
(Booch,1999). It is a model element which can contain other model elements. That
is a grouping mechanism and do not have any semantics defined. An element can
belong to only one package.
A package could be very suitable to present Modularity.
- 314 -
APPENDIX A: UML
Some of the other numerous model elements (things) and their notation will be
presented through UML diagrams.
A.4. Relationships
The UML defines a number of different kinds of relations. A relationship is a
connection among things. The most important relations in UML are association,
generalization, and dependency.
A.4.1. Association
An association is one of a basic relation defined in UML. UML notation of
association is a solid line connecting two elements (both ends may be connected to
the same element, but the two ends are distinct).
Associations denote a relationship between concepts (classes).
Association link has two ends which are called roles. A role, that identifies one end
of an association, has a name, a multiplicity, a navigability and a type .
* 1..*
Company Person
employer employee
Person
Company Person Manager Name
Works for Name Address
Name ID
Address employer employee Address Supervises
ID Salesperson
- 315 -
APPENDIX A: UML
roles within associations, parts within composites, repetitions, and other purposes.
Its specification is represent with subset of the open set of positive integers. It is
shown as a text string with a comma-separated sequence of integer intervals, where
an interval represents a (possibly infinite) range of integers, in the format: lower-
bound .. upper-bound
Class unspecified
1
Class exactly one
* –any number, also written as 0..*, or 0..n
*
n – an exact number e.g., 1, 3, …
many
Class (zero or more) 0..1 – zero or one
0..1 optional
Class
(zero or one)
m..n numerically
Class
specified
A role can have an arrow indicating its navigability that shows which class has the
responsibility for maintaining the relationship between the classes (the school class
has a responsibility for knowing which students attend).
- 316 -
APPENDIX A: UML
An association class is an element that has both association and class properties.
That is frequent case in many-to-many association because it is difficult to position
the properties at any end of the association.
Authorization
Access right
Priority
startSession()
Company * 1..*
Person
employer employee
Contract
salari : int boss
startData : Data 0..1
worker *
manages
A.4.1.1. Aggregation
An aggregation is a association denoting a „part of“ (or „has-a“) relationship
between the objects of the respective classes.
The term aggregation is frequently used to describe the structure of a class or object
which includes instances (objects) of other user defined classes.
Aggregation can be of two types: shared aggregation and composition.
1 * *
Note: A student can be in more
*
University Faculty PlannedCourses than one department and
* courses can be planned in more
* than one department
Students
- 317 -
APPENDIX A: UML
A.4.1.2 Composition
Composition indicates that one class belongs to the other. Composition is a type of
aggregation which links the lifetimes of the aggregate and its parts. That means that
the parts cannot exist if the aggregate no longer exists and an entity can exist as
part of only one aggregate
(i.e. Destroying a database destroys its tables. But destroying the tables of a
database does not destroy the database)
Composite aggregation is a strong form of aggregation, which requires that a part
instance be included in at most one composite at a time and that the composite
object has sole responsibility for the disposition of its parts.
Composition is shown by a solid filled diamond as an association end adornment.
* 0..n
1
Header Body Attachment
Window
2
scrollbar : Slider
1
title : Header
1
body : Panel
- 318 -
APPENDIX A: UML
A.4.1.3 Generalization
Generalization, as relationship, is another name for inheritance or an "is a"
relationship. It is a relationship between two classes where one class is a specialized
version of another. We can say that it is a relationship between a more general
element (the parent) and a more specific element (the child). This specific element
is fully consistent with the more general element and adds additional necessary
information.
For example, Player is a kind of Person. So the class Player would have a
generalization relationship with the class Person.
- 319 -
APPENDIX A: UML
A.4.1.4 Dependency
Dependency relation shows that a change to one element's (package’s) definition
may cause changes to the other. It is a relationship between two model elements
that relates the model elements themselves and does not require a set of instances
for its meaning. It shows that a change made on the target element, usually, require
a change to the source element in the dependency.
- 320 -
APPENDIX A: UML
UML diagrams are basic and very powerful modelling tool, used in every phase of
system building.
Although, each of nine UML diagrams provides developer or development team
with a different perspective, we can group them into five groups from the system
modelling aspects. Those five groups are: use-case model diagram, static structure
diagrams, interaction diagrams, state diagrams and implementation diagrams.
- 321 -
APPENDIX A: UML
- 322 -
APPENDIX A: UML
Actor: Role that a user plays with respect to the system (student, registrar, teacher).
Actors carry out use cases, look for actors, then their use cases. Actors do not need
to be humans. Actors can get value from the use case or participate in it.
When describing a complex system, its use case model can become quite complex
and can contain redundancy. We reduce the complexity of the model by identifying
commonalities in different use cases. In the process of object-oriented analysis,
each previously defined use case has to be refined to include more and more detail
based on the facts we discovered (i.e. defining user interface requirements). Figure
A-23 shows one use case diagram for the simple catalogue-sale system, and figure
A-24 shows some use case relationship in this system.
- 323 -
APPENDIX A: UML
- 324 -
APPENDIX A: UML
three compartments, name, attributes and operations. Object names are underlined
to indicate that they are instances.
We stated some basic relationships earlier in this chapter.
Generalization is the relationship between a general class and one or more
specialized classes. Generalization enables us to describe all the attributes and
operations that are common to a set of classes. Abstract classes are distinguished
from concrete classes by italicizing the name of abstract classes.
Figure A-25 and A-26 shows two examples of class diagrams.
- 325 -
APPENDIX A: UML
- 326 -
APPENDIX A: UML
- 327 -
APPENDIX A: UML
Shortly, we can say that a sequence diagram represents the interactions that take
place among objects. Sequence diagrams depict services as a connection among the
use case behaviour and objects. Actors are shown as the leftmost column.
- 328 -
APPENDIX A: UML
- 329 -
APPENDIX A: UML
- 330 -
BIBLIOGRAPHY
Bibliography
[Allison1999] Thinking in C: Foundations for Java & C++, by Chuck
Allison (a MindView, 1999).
[Stroustrup1997] The C++ Programming Language, 3rd edition, by Bjarne
Stroustrup (Addison-Wesley, 1997)
[Lippman1998] C++ Primer, 3rd Edition, by Stanley Lippman and Josee
Lajoie (Addison-Wesley 1998)
[Allison1998] C & C++ Code Capsules, by Chuck Allison (Prentice-Hall,
1998).
[ANSI/ISO] The C++ ANSI/ISO Standard.
[Eckel1993] C++ Inside & Out, by Bruce Eckel, (Osborne/McGraw-Hill
1993).
[Eckel2000a] Thinking in C++, Volume 1, Second Edition, by Bruce Eckel,
(MindView Inc 2000)
[Eckel2000b] Thinking in C++, Volume 2, Second Edition, by Bruce Eckel,
(MindView Inc 2000)
[Vandev2002] C++ Templates: The Complete Guide, By
David Vandevoorde, Nicolai M. Josuttis, (Addison Wesley,
2002)
[Lafore1998] Waite Group's Object-Oriented Programming in C++,
Third Edition, by Robert Lafore, (Macmillan Computer
Publishing, 1998)
[Josuttis] The C++ Standard Library, A Tutorial And Reference, by
Nicolai M. Josuttis, (Addison-Wesley)
[Gamma1995] Design Patterns, Elements of Reusable Object-Oriented
Software, by Erich Gamma, Richard Helm, Ralph Johnson,
John Vlissides (Addison-Wesley, 1995)
[Larman2002] Applying UML And Patterns: An Introduction to Object-
Oriented Analysis And Design And Iterative Development
(3rd Edition), by Craig Larman (Prentice-Hall, Inc, 2002)
[Budd2002] An Introduction to Object-Oriented Programming (3rd
Edition), by Timothy A. Budd (Upper Saddle River, N.J.
Pearson Addison Wesley cop. 2002)
[Booch1999] The Unified Modeling Language Users Guide, by Grady
Booch, James Rumbaugh, Ivar Jacobson (Reading, Mass.
Addison-Wesley cop. 1999)
[Booch1991] Object-Oriented Design With Applications, by Grady Booch
(Redwood City [etc.] The Benjamin/Cummings Publishing cop.
1991)
- 331 -
BIBLIOGRAPHY
- 332 -
INDEX
Index
- 333 -