Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Book Number:
Main Editor:
Udbenici
16
prof. dr Safet Krki
Technical Review:
Language Review:
Authors
Book Covers:
DTP:
Copyright
Mirza Hadikaduni
Authors
Authors, 2005
FIT Mostar, 2005
Edicions de la Universitat de Leida, 2005
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.
Printed by tamparija "FOJNICA" d.o.o. Fojnica
Supported by TEMPUS project CD JEP 16110-2001
CIP Katalogizacija u publikaciji
Nacionalna i univerzitetska biblioteka
Bosne i Hercegovine, Sarajevo
004.42(075.8)
RIBO, Josep Maria
Introduction to OOP with C++ /
Josep Maria Ribo, Ismet Maksumi,
Sinia ehaji Mostar:
Univerzitetska knjiga, 2005.
333. str: graf. prikazi; 25 cm.
(Biblioteka Udbenici; knj. 16)
ISBN 84-8409-199-6
1. Maksumi, Ismet 2. ehaji, Sinia
COBISS.BH-ID 14230534
Published by
Univerzitetska knjiga Mostar, 2005
Edicions de la Universitat de Leida, 2005
Introduction to OOP
with C++
Published by
Univerzitetska knjiga Mostar
Edicions de la Universitat de Leida,
July, 2005
Table of contents
Chapter 1: Principles od OOP. Classes. Objects...................................................1
1.1. Modelling concepts.........................................................................................1
1.1.1. Abstract types...........................................................................................2
1.2. Concept of class .............................................................................................. 4
1.3. Concept of object ..........................................................................................10
1.3.1. State .......................................................................................................11
1.3.2. Behaviour...............................................................................................11
1.3.3. Identity ...................................................................................................12
1.3.4. Classes vs. objects.................................................................................. 13
1.3.5. The object orientation programming paradigm......................................14
1.4. Class contracts .............................................................................................. 15
1.4.1. Defining a class contract ........................................................................ 16
1.4.2. Preconditions..........................................................................................16
1.4.3. Postconditions ........................................................................................20
1.4.4. Class invariants ...................................................................................... 21
1.4.5. Correctness of a class implementation...................................................23
1.4.6. Example: Contract for the class Date...................................................24
1.5. Detailed class and object definition in C++ ..................................................26
1.5.1. Private and public members...................................................................29
1.5.2. Operation declaration and implementation ............................................30
1.5.3. Object creation .......................................................................................30
1.5.4. Access to members ................................................................................37
1.5.5. References vs. pointers ..........................................................................39
1.5.6. Splitting class definition into different files...........................................40
1.6. Some (pleasant) consequences of class definition ........................................42
1.7. Guided problems. The class MyString......................................................44
1.7.1. The problem ........................................................................................... 44
1.7.2. Solution ..................................................................................................45
1.7.3. The file Makefile...............................................................................50
1.8. Guided problems. The word counter............................................................. 50
1.8.1. The class WordCounter .....................................................................50
1.8.2. A client program ....................................................................................56
1.8.3. The class WordCounterInterface................................................57
Chapter 2: Special members .................................................................................61
2.1. Constructors ..................................................................................................61
2.1.1. Notion of constructors............................................................................61
2.1.2. Constructor overloading.........................................................................64
2.1.3. Default constructor.................................................................................65
2.1.4. Default parameters .................................................................................65
2.1.5. Copy constructor....................................................................................66
2.1.6. Construction call in the creation of arrays of objects.............................71
2.1.7. Creation of temporary objects................................................................71
2.1.8. Dynamic object construction .................................................................72
Chapter 1
Principles of OOP. Classes. Objects
1.1. Modelling concepts
One important problem that a software engineer must face when he/she is to
develop a software application in a specific domain is the following:
The application domain is packed with human-oriented, high-level concepts (e.g.,
student, academic subject, professor, bill, etc.), which offer several natural ways to
interact with them (e.g., a student may be enrolled in a subject, a professor may be
responsible for some subjects, a student should pay an enrolment bill, etc.).
However, the constructs offered by traditional programming languages (i.e., those
which are not either object-oriented or object-based languages) to map those
concepts are usually quite low-level and machine-oriented (array, integer,
pointer...).
The immediate consequence of this abstraction gap between what is needed by the
software developer and what is offered by the programming language is that any
piece of software, ranging from medium-size programs to huge applications,
becomes increasingly difficult to construct, test, maintain and reuse.
The object-oriented programming (OOP) paradigm aims at bridging this abstraction
gap, so that, we can construct a software application using directly entities that
represent high-level domain concepts (student, subject, date, etc.).
Probably, the most natural way in which a programming language can declare and
use such high-level entities is by allowing the definition of new kinds of types (in
addition to the predefined types as integer, char, array...).
In the same way as predefined types are defined in terms of a set of operations that
can be applied to the entities of those types (e.g., the predefined type integer has the
operations +, -, *, / and remainder associated to; these operations can be applied to
integers) it makes perfectly sense to define new high-level types giving the list of
services they offer to their prospective users. To keep the same notation as with
predefined types, we will call operations to the services offered by these new highlevel types.
Example
We have to build an application to manage the academic issues of a university (e.g.,
syllabus, students, subjects, professors, enrolment, etc). In this context we can
define a high-level type called Subject to refer to academic subjects so that it
provides the following services or operations:
...
In this context, we may declare an entity sbj of type Subject and then set/get the
title or the professor of sbj using the provided operations.
Example
As a part of the design of a race simulation application we have to model the
concept of car. For that reason we may define a new high-level type called Car
with the following services:
...
Notice that there can be different modelling of the same concept, corresponding to
the various views that different users may have of that concept. For example, if the
notion of car was modelled for a workshop management application, then the list of
services should be different (e.g., date of the last oil change, problems detected in
the car and not solved yet, state of the tyres, etc.).
-2-
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.
Since we define these new high-level types from an abstract point of view, they
may be called abstract types.
Definition (Abstract type)
An abstract type denotes a set of entities characterized by a list of operations
that may be applied to them together with a precise specification of each one
of these operations.
Usually, the list of operations that define a type and their specification are
referred to as the type behaviour, type specification or the type contract.
An abstract type is also called interface.
The set of entities which share the operations defined for a type are called
instances of that type.
As we have already mentioned, an example of abstract type may be the type
Student. This is an abstract type whose instances are each one of the specific
students (Joe, Ann ...) and its operations can be: set the name of a student, enrol a
student in a subject, get the list of subjects in which a specific student has enrolled,
etc.
Usually, we will refer to abstract types just as types.
Notice that the approach that we have taken is coincident to what we do in everyday
life to manage complexity. For instance, cars are very complicated machines.
However, the only thing that a car driver needs to care about is that when he/she
presses the accelerator the car speeds up; he/she does not matter the background
mechanics and electronics that make this action possible. In fact, it would be very
difficult, if not impossible, to drive having in mind, at the same time, the
functionality expected from the car (the what: speed up, change direction, brake,
etc.) and the way in which this functionality is internally achieved (the how: in
-3-
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.
Two instances of the type String (to represent his/her name and id.).
-4-
Notice that, in order to represent the type Student we use already defined
abstract types (i.e., String, Date).
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:
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;
};
void Date::create(int pday, int pmonth,
int pyear, bool& err){
if (correctDate(pday,pmonth,pyear){
day=pday;
month=pmonth;
year=pyear;
err=false;
}
else{
err=true;
....
}
}
void Date::setDay(int pday, bool& err){
if (correctDate(pday,month,year)){
day=pday; err= false;
}
else err=true;
}
....
-6-
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 implementation
In its turn, class implementation is further split in two aspects:
-
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;
What we have learned from the previous example can be stated more precisely with
the following definition:
In this book we will use indistinctly the terms parameter and argument
-7-
-8-
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 -
If d has been defined as an integer variable, d will have the value of 10 at the end of
this operation.
1.3.1. State
Definition (Object state)
The state of an object is constituted by the object attributes together with the
information stored in them at a given run time instant.
Obviously, the state of an object may change along the time.
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
Definition (Object behaviour)
The behaviour of an object describes how other objects (or, in general, a
piece of software) may interact with it.
The interaction with an object obj is always carried out by means of the
operations defined by the class of which obj is an instance. Those operations
provide the object behaviour.
Different kinds of operations may be considered.
Definition (Types of operations)
There may be considered three different kinds of operations according to
what they do to the state of the class instance to which they are applied:
Creators or constructors. They create and initialize the instance to
which they are applied.
Modifiers. They modify the state of the instance to which they are
applied.
Consultors or queries. They return some piece of the state of the instance
to which they are applied but they do not modify this state.
- 11 -
Creators: create
const operations
Notice that while creators and modifiers do change the state of an object, consultors
do not.
In C++, this fact can be stated by declaring the consultor operations as const:
class Date{
public:
int getDay() const;
...
private:
...
};
int Date::getDay() const
{
return day;
}
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
Definition (Object identity)
Identity is the property of an object which makes it different from any other
object [Khoshafian, Copeland Booch]
- 12 -
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 -
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.
Figure 1.3
- 14 -
- 15 -
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.
Preconditions should be written in terms of:
The class interface members (i.e., the public members of the operation).
The object to which the operation is applied.
The operation parameters (particularly, the input or input/output parameters).
They should never contain anything concerning the class representation (i.e.,
the private members of the class)
Preconditions must be guaranteed by clients before calling the operation. Therefore,
the operation implementation should not check whether the precondition holds or
not. This is clients responsibility. In fact, including redundant checkings in
operation implementations leads to a redundant and less reliable and
comprehensible code. [Meyer1997] contains an excellent discussion about this
particular issue.
We can choose between weak preconditions (void or almost void preconditions
which put almost no requirement on the client call) or strong ones (preconditions
which constrain the client call).
- 16 -
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).
Precondition: d-m-a represents a correct date later than 1-01-1900 .
This means that the client, before calling today.create(d,m,y) should
make sure that d-m-y constitutes a correct date.
In this case, the operation implementation will not check d-m-y to ensure
that it is a correct date. Hence, no error will be produced by the operation.
The criterion to decide which kind of precondition should be applied may be the
following:
Assign the responsibility for checking a condition to the part (user or
operation implementation) which can take it in a more natural way.
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 -
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
class
PhoneDirectory
provides
an
operation
long
getPhoneNumber(char namePerson[]) which obtains the telephone
- 18 -
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.
PhoneDirectory
provides
an
operation
void
class
insertPhone(char namePerson[], long phoneNb) which inserts the
pair (namePerson, phoneNb) into the directory. However, the directory
The
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 -
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 -
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.
Call: x.addDays(ndays)
Pre: void
Post: The resulting date x is the date x@pre plus ndays days.
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
Some remarks should be made concerning invariants, which introduce some aspects
that will be presented in later chapters:
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
//"uncontrolled period". No operation should be
//applied to d
//mo=d.getMonth();
//INCORRECT!!!!
//Now, this is OK
Class invariant:
Any object of the class should model a valid date posterior or
equal to 1-01-1900.
void
pmonth,
int
pyear,
*
*
Pre: void
Post: dat contains the date pday-pmonth-pyear and
err=false.
If pday-pmonth-pyear is not a correct date posterior to 101-1900, dat is 1-01-1900 and err=true
Call: d=dat.getDay();
*
*
Pre: void
Post: d is the day of the date dat. dat=dat@pre.
Similar to getDay()
-
*
*
Pre: void
Post: dat has the same month and year as dat@pre. pday is
the new day of the date dat and err=false.
If the date pday-dat.getMonth()-dat.getYear() is not
correct, dat=dat@pre and err=true
Similar to setDay(...)
-
Call: dat.copy(dat2);
*
*
Pre: void
Post: dat gets the value of dat2 (dat=dat2)
* Call: b=dat.equals(dat2);
* Pre: void
* Post: b is true if dat has the same value as dat2. dat has not
been modified.
-
* Call: dat.addDays(ndays);
* Pre: void
* Post: dat is the date dat@pre plus ndays days.
The class contract will be written in a textual file associated to the file(s) containing
the class definition (see below).
Sections 1.8 and subsequent contain more examples of class contract.
- 25 -
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>
using namespace std;
class Date{
private:
unsigned int day;
unsigned int month;
unsigned int year;
bool correctDate(unsigned int pday, unsigned int pmonth,
unsigned int pyear) const;
int leap(unsigned int y) const;
public:
void create(unsigned int pday, unsigned int pmonth,
unsigned int pyear, bool& err);
void setDay(unsigned int pday, bool& err);
void setMonth(unsigned int pmonth, bool& err);
void setYear(unsigned int pyear, bool& err);
unsigned int getDay() const;
unsigned int getMonth() const;
unsigned int getYear() const;
void copy(Date d);
bool equals(Date d) const;
void addDays(unsigned int ndays);
};
#endif
- 26 -
File Date.cpp
#include <iostream>
#include "Date.h"
using namespace std;
int Date::leap(unsigned int pyear) const
{
if (pyear%4==0 && pyear%100!=0) return 1;
else return 0;
}
bool Date::correctDate(unsigned int pday, unsigned int pmonth,
unsigned int pyear) const
{
unsigned int max;
if (pmonth==1 || pmonth==3 || pmonth==5 || pmonth==7 ||
pmonth==8 || pmonth==10 || pmonth==12){ max=31;}
else if (pmonth==2){ max=28+leap(pyear);}
else max=30;
return (pday<=max && pmonth<=12 && pyear>=1900) ;
}
void Date::create(unsigned int pday, unsigned int pmonth,
unsigned int pyear, bool& err)
{
if (correctDate(pday, pmonth, pyear))
{
err=false;
day=pday;
month=pmonth;
year=pyear;
}
else
{
err=true;
day=1;
month=1;
year=1900;
}
}
void Date::setDay(unsigned int pday, bool& err)
{
if (correctDate(pday,month,year))
{
day=pday;
err=false;
}
else err=true;
}
- 27 -
File User.cpp
#include "Date.h"
#include <iostream>
using namespace std;
- 28 -
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.
Public members are those members that are visible from any function
extern to the class. They come up following the label :public.
The members (usually operations) that constitute the class interface are
declared as public.
In the example, create(...); getDay(); getMonth(); etc. are public
members.
Private members are those members which are only visible from the
operations of the class in which they have been defined 3 . They come up
following the label :private (by default, class members are considered
private).
The attributes that constitute the class representation (or class structure) are
usually declared as private. In addition, a class may also have private
operations, which are useful to help in the implementation of public
operations.
In the example, the attributes day, month, year are private attributes.
The operations correctDate(...) and leap(...) are also private (see
file date.h). Private operations are useful in order to implement class
operations in a more structured way and also to avoid redundancy of code.
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 -
- 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:
unsigned int Date::getDay()
{
return day;
}
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 -
Automatic objects
Definition (Automatic object)
An automatic object is an object created by means of a declaration of the
kind: T x; (e.g., Date birthday;).
The declaration associates a name and an identity to the declared object,
which makes it different from any other object.
An automatic object is created whenever its declaration is executed and is
destroyed at the end of the block in which that declaration comes up.
In particular, notice that any local variable (i.e., a variable which is declared
within the scope of a function) is destroyed at the end of that function.
In order to complete the creation of an object (including its initialization) an
implicit call to a constructor is automatically performed. Constructors are presented
in Chapter 2.
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.
A pointer to an object, which is an instance of the class Date, is created in the
following way:
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 -
Date day;
day=*pbirthday;
Dynamic objects
Definition (Dynamic object)
A dynamic object is an object created using the operator new:
T* px; px=new T;
The execution of this sentence creates an object of the class T pointed by the
pointer called px.
(e.g.,Date* panniversary; panniversary=new Date;)
A dynamic object is created by allocating dynamic memory.
A dynamic object is unnamed. It is accessed by means of a pointer.
A dynamic object is created when the new operator is executed and is
destroyed using the operator delete: delete px; (not at the end of the block
in which it has been created).
Dynamic objects are sometimes called objects allocated dynamically.
Dynamic objects may be used to define arrays whose dimension is not known until
run time (i.e., dimension not known at compilation time):
void f(int n)
{
Date* v;
v=new Date[n];
//....
};
- 32 -
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 -
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.
- 34 -
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;
//Insert thousands of elements into list before
//calling f(li)......
f(li);
//Now is better
- 35 -
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;
f(l1);
//Also possible if the parameter is not const
f(l2);
//Only possible if the parameter is const
f(List(l1)); //Only possible if the parameter is const
return 0;
}
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.
This notation implies that we are invoking the operation create with the
parameters 10, 12 and 2003 on the automatic object called birthday.
rbirthday.setMonth(11,err);
birthday.setMonth(11,err); //Both are equivalent
- 38 -
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 -
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);
Date x;
Date* y;
y=&x;
x.create(10,11,2004,err);
y->create(10,11,2004,err);
(*y).create(10,11,2004,err);
Date x;
Date& y;
//INCORRECT
Date* px;
Date& y=x;
//CORRECT
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.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).
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.
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 -
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(...)).
3. Link both object files
Using the GNU C compiler, this could be done by issuing the instruction:
g++ User.o Date.o -o exec
- 44 -
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:
void create(char* st);
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:
void putChar(unsigned int pos, char c, bool& error);
Call: s.putChar(pos,c,error);
Pre: void
Post: s is the same MyString as s@pre except for the character at
- 45 -
File MyString.h
This file contains the representation of the class MyString and the operation
headers.
#ifndef MYSTRING_H
#define MYSTRING_H
#include <iostream>
using namespace std;
class MyString{
char* st;
public:
void create();
void create(char st[]);
char getIthChar(unsigned int pos, bool& err) const;
unsigned int getLength() const;
unsigned int substring(const MyString& c) const;
bool equals(const MyString& c) const;
- 46 -
Remarks:
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>
using namespace std;
void MyString::create()
{
st=new char[1];
st[0]='\0';
}
void MyString::create(char pst[])
{
int i=0;
st=new char[strlen(pst)+1];
strcpy(st,pst);
}
char MyString::getIthChar(unsigned int pos, bool& err) const
{
if (pos<getLength()){
err=false;
return st[pos];
}
else{
err=true;
return '\0';
}
}
- 47 -
- 48 -
Remarks:
If we use the operation create(), then, at least we reserve one character for
\0
- 49 -
The operation lowerOrEqual relies on the fact that the arrays of characters
end in \0, which, by definition has a value of 0.
File userString.cpp
This file contains a program that uses the class MyString
#include "MyString.h"
int main()
{
MyString ss1, ss2, ss3;
ss2.create("kjkj");
ss1.create();
ss3.create();
ss3.get(cin,'\n');
if (ss1.lowerOrEqual(s2)) cout<<"ss1 lower or equal"<<endl;
return 0;
}
userString.cpp
MyString.o:
MyString.h MyString.cpp
g++ -c MyString.cpp
- 50 -
void
int
int getNumbWords()
Get the number of different words that have been stored in the word counter.
counter.
The
meaning
of
j-th
word
is
the
same
as
in
getNumbOccurrences(j).
- 51 -
- 52 -
- 53 -
- 54 -
Remarks:
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.
Solution
Clearly, the best solution is the second one. If the first operation was implemented
(option a), a new operation should be added to the class WordCounter for each
different interface with the user.
Always try to decouple the classes which are responsible for the program logic
from those that interact with the user. This decoupling will lead to a higher
independence between both sets of classes and it will make the connection of
different user interfaces with the same logic classes easier.
Therefore, we will have the following interaction between objects:
- 56 -
After
that,
change
WordCounterInterface
WordCounterInterface2 which has the following interface:
for
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)?
- 57 -
- 58 -
w.get(cin,' ');
while (!(w.equals(wfi))){
wc.countWord(w);
w.get(cin,' ');
}
Notice
- 59 -
- 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
2.1.1. Notion of constructors
According to what has been presented in Chapter 1, before using an object which is
an instance of a specific class, it is necessary to create it. In the case of automatic
objects, its creation process consists of two steps:
1. Declare an object identifier (i.e., a variable) as an instance of that specific
class and allocate memory space for it. For example:
Date today;
With this statement, the identifier today is bound to an object of the class
Date, for which some memory is reserved to store the attributes that
compose the object structure (see fig. 2.1(a)).
2. Initialize that object using one of the class operations which are intended to
do so. For example,
- 61 -
- 62 -
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:
Date(unsigned int pday, unsigned int pmonth,
unsigned int pyear, bool& error);
//Rest of the operations, as before
};
Date::Date(unsigned int pday, unsigned int pmonth,
unsigned int pyear, bool& error)
{
if (correctDate(pday, pmonth, pyear)){
error=false;
day=pday;
month=pmonth;
year=pyear;
}
else {error=true;}
}
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 1012-2003 and err=false.
As usual, the constructor code may also come up within the class definition:
class Date
{
private:
//As before......
- 63 -
Date d2;
Date d3();
- 64 -
would be still valid. It would generate a call to the default constructor created by the
compiler.
However, if the class Date has some constructor, the compiler does not create a
default one. For this reason, if the class Date defines only the constructor with four
arguments:
Date(int pday, int pmonth, int pyear, int& error)
Since now the class Date does not have any constructor with no parameters.
The instruction
Date d;
- 65 -
would be ambiguous.
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.
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 -
- 67 -
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 -
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);)
Date f()
{Date x;
.....
return x;
}
int main()
{
Date a;
.....
a=f();
}
- 69 -
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 -
The creation of each object that composes the array. This is achieved by
means of a call to the default constructor of the class of the array
components.
generates ten calls to the default constructor of the class Date. Notice that it is
necessary that the class Date has a default constructor defined.
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:
void f(Date d)
{.....}
int main()
{
....
f(Date(10,9,2001,err));
}
Date f()
{
Date d(10,11,2001,err);
....
return d;
}
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 -
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];
- 73 -
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>
using namespace std;
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=new Person[5]; //this generates 5 calls to the
//default constructor
vp[0].setName("John");
vp[0].setAge(20);
vp[0].setSex('m');
//.....
}
- 74 -
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.
//(1)
p=new Date(1,1,2001,err);
//(2)
p=new Date(4,4,2003,err);
//(3)
p=&d1;
//(4)
- 75 -
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-42003. 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.
The previous example may make us think that garbage can only be generated if we
create dynamic objects by calling explicitly the operator new. However, this
operator may also be called implicitly when an automatic object is created. We
present an example next:
Example
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;
}
- 76 -
//(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 -
//(1)
delete pi;
//(2)
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;
pi=new int[13];
//(1)
delete [] pi;
//(2)
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.
x is a local variable in the function f(..) and the execution control has
reached the end of f(..).
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 -
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.
//(1)
//(2)
- 80 -
It calls the destructor of the class Person. This destructor is responsible for
deallocating the array of characters name.
Notice that if the destructor of Person had not been defined, the array of characters
which the attribute name points to would have remained as garbage, even after the
deleting sentence (2) (see fig. 2.10).
//(1)
p2=new Person("John",20,'m');
//(2)
- 81 -
//(3)
delete p2;
//(4)
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 -
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.
- 83 -
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 -
To create a new operation which has the name of the operator which is
traditionally used to carry out that functionality (e.g., =). This is called
operator overloading.
If the second approach is taken we will have the same set of operators that will be
applied to different classes. Therefore we will have a higher degree of
standardization in class operations. When we use a class, if a certain operator (e.g.,
assignment operator) is meaningful for that class, we can expect to have the
operator = overloaded for it.
Operator overloading is a special case of function overloading. In previous sections
we have overloaded the constructor operations.
Definition (Function overloading)
To overload a function consists in defining a new function which has the
same name as another existing one. The arguments of the new function will
be different (in number or type or both) from the already existing one.
Operators are a specific case of function that can be overloaded.
In general, the compiler is responsible for deciding at compilation time which
function definition should be associated to a call to an overloaded function
(early binding). This decision depends on the type of the object on which the
function is called and/or the type of its arguments.
However, in the case of polymorphic functions (see Chapter 6), the compiler
cannot decide the function definition that should be bound to each call. In this
case, the binding is performed at execution time (late binding).
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 -
//(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:
const Person& operator=(const Person& p2)
{
name=p2.name;
sex=p2.sex;
age=p2.age;
return *this;
}
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 -
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)
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.
2.4.2. Operator ==
Example
This is the proposal of equality operator for the class Person:
bool operator==(const Person& p) const
{
return (strcmp(p.name, name)==0 && p.sex==sex && p.age==age);
}
- 88 -
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);
};
bool operator==(const Person& p, const Person& p2)
{
return (strcmp(p.name, p2.name)==0 && p.sex==p2.sex &&
p.age==p2.age);
}
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 operator[](int i){
if (i>=0 && i<max) return v[i];
else //Out of range. Deal with the error....
}
};
int main()
{
const int MAX=100;
int n;
- 89 -
Other types can be used either as argument or as return types. For instance:
Example
class Dictionary{
//......
public:
//....
char* operator[](char* d){
//access to the word d of the
//dictionary and return its meaning
}
};
int main()
{
Dictionary d;
cout <<"The meaning of onion is"
<<d["onion"]<<endl;
}
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);
sort(w,99);
- 90 -
which corresponds to a call to the operator () on the object sort and with arguments
w and 99:
sort.operator()(w,99)
The fact that an ostream object is returned helps to chain this operator:
Person p1(...);
Person p2(...);
Person p3(...);
cout<<p1<<p2<<p3;
- 91 -
If c were not returned, then the previous program should have been implemented
like this:
cout<<p1;
cout<<p2;
cout<<p3;
=,
create
and
lowerOrEqual
and to add:
A destructor
concat(..)
- 92 -
- 93 -
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;
}
char MyString::getIthChar(unsigned int pos, bool& err) const
{
if (pos<getLength()){
err=false;
return st[pos];
}
else{
err=true;
return '\0';
}
}
unsigned int MyString::getLength() const
{
return strlen(st);
}
unsigned int MyString::substring(const MyString& c) const
{
///EXERCICE: IMPLEMENT IT
return 0;
}
bool MyString::operator==(const MyString& c) const
{
int i=0;
int l;
l=getLength();
if (c.getLength()!=l) return false;
else{
while (c.st[i]==st[i] && i<l){
i=i+1;
}
return (c.st[i]==st[i]);
}
}
- 94 -
- 95 -
Remarks:
MyString s=MyString("hello");
- 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,
ListOfWhatever... which would be essentially equivalent.
ListOfCinemas,
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:
template <class T>
class List{
T v[MAX];
int top;
public:
List(){top=0;}
void insertLast(const T& x) {v[top]=x; top++;}
void removeLast(){top--;}
T getLast() const {return v[top];}
bool emptyList() const {return top==0;}
};
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;
Student s("Joe", "123987E", "12, Barbecue Street");
lintegers.insertLast(1);
lchars.insertLast('d');
lstud.insertLast(s);
.....
}
- 100 -
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>
not only does it create an object (lchars) but also a class (List<char>).
We summarize all these issues in the following definition:
Definition (Generic class)
A class A is generic if its definition depends on one or more types (usually,
classes) that act as parameters of A.
A generic class is instantiated when the type parameter is substituted by an
actual type.
C++ offers a tool called templates to define generic classes (and functions: see
section 3.2).
Definition (Template class definition)
A template class definition is a class definition which depends on one or
more parameter types (called template parameters). This template class
definition is used in order to create an actual class by instantiating the
template parameter with a specific type when an object instance of that class
is created.
An actual class A does not exist until its template parameter has been
instantiated.
The following notation is used:
- 101 -
Definition:
template <class T>
class A{
//The definition of A may depend on T
};
Instantiation:
A<int>
a;
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 -
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.
//(1)
//(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 -
It would be correct if an operator like the following was defined for the class
Student:
bool operator<(Student& s1, Student& s2){
//Return true if s1 is lower than s2 according
//to the defined criteria (e.g., names or id).
}
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
template <class T>
void sort (T v[], int n)
- 105 -
//(1):Sorts st by name
sort(st,10,lower2);
//(2):Sorts st by id
- 107 -
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.
template <class T, class Comp>
void sort (T v[], int n)
{
int i, j, min, aux;
Comp lower;
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++;
}
}
- 108 -
int main()
{
CompaNames lower1;
CompaIds lower2;
Student st[11];
fill(st);
sort<Student,CompaNames>(st,10);
//(1):Sorts st by name
sort<Student,CompaIds>(st,10);
//(2):Sorts st by id
a.h:
a.cpp:
#include "a.h"
template <class T>
void A<T>::f(T p){//....}
user.cpp:
#include "a.h"
int main()
{
A<int> x;
int i=0;
x.f(i);
//....
}
- 109 -
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:
4. Create a stack of vehicles and check that the destructor of the class Stack
calls the destructor of the class Vehicle
- 111 -
- 112 -
- 113 -
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 -
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:
template <class T>
T& Stack<T>::top2()
{
T x;
if (tops>0){
x=t[tops-1];
return x;
}
}
- 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).
In addition to these couple of relationships, we may consider instantiation as
another relationship between classes: a class can be defined as a particular
instantiation of a template class (e.g., a List of integers is an instantiation of a
List of Something; see Chapter 3); therefore, we can establish a relationship
between the template class and the instantiated one. However, from a rigorous point
of view, this is not exact since template classes are not proper classes. They only
become actual classes when they have been instantiated.
This chapter is devoted to the first one of these relationships (association). Chapter
5 explores generalization and, as mentioned, Chapter 3 deals with template classes
and instantiation.
- 117 -
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.
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 -
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 -
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).
Some of the above presented examples of associations can be considered as
aggregations:
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 -
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 -
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 -
class Car{
private:
char* model;
Wheel wc[5];
Carburetor cb;
...
public:
...
};
- 123 -
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:
Person(char* pname, int page, char psex)
{
name=new char[strlen(pname)+1];
strcpy(name,pname);
age=page;
sex=psex;
}
//......
};
class Date{
private:
unsigned int day;
unsigned int month;
unsigned int year;
- 124 -
public:
Date(){day=1; month=1; year=2000;}
Date(unsigned int pday=1, unsigned int pmonth=1,
unsigned int pyear=2000)
{day=pday; month=pmonth; year=pyear;}
Date(const Date& da){day=da.day;month=da.month; year=da.year;}
//.....More operations
};
name:
age:
sex:
birth:
18
f
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 -
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;
//.....
};
C(....)
:attrib(arg1,...,argk)
{
//Implementation of the C constructor
}
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 -
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;
}
};
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 -
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
Definition (Pointer equality)
Two pointers (declared as pointers to type T) are equal if either:
1. They are both null pointers or
2. They both refer to the same object of type T
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 -
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.
Definition (Shallow equality)
Two objects x and y of the same type are equal in a shallow way if their
respective attributes are equal one to one (the i-th attribute of x should be
equal to the i-th attribute of y) according to the following criterion:
- If the attribute is a primitive type (bool, int, float, double,
long, char), the values of that attribute should be the same for both
objects.
- If the attribute is a pointer (e.g., char*, Person*...), both pointers
should be equal, according to the notion of pointer equality given above.
- If the attribute is a subobject (e.g., Person), both subobjects (i.e., both
attributes) should be equal in a shallow way.
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 -
- 131 -
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 -
- 133 -
- 134 -
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).
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.
- 136 -
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
Definition (Pointer copy)
A copy of a pointer to an object pc2 onto another pointer pc1 makes both
pointers refer to the object to which pc2 referred.
The copy of a pointer does not lead to the copy of the object to which it
refers.
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 -
- 138 -
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 -
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 -
- 141 -
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();
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 6 presents polymorphism. In that moment more issues about clonation will
come up.
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 -
(pointerToTheOriginalPlayer,
pointerToTheCopyOfThatPlayer)
- 146 -
- 147 -
- 148 -
Chapter 5
Inheritance
5.1. The meaning of inheritance
5.1.1. Class generalization and substitution principle
A main aspect of object-orientation is the modelling of concepts of the problem
domain by means of abstract types, which, in turn, are implemented using classes.
However, domain concepts do not come up in an isolated way. Instead, each one is
related to others in different manners. One of these manners is association, which
has been discussed in Chapter 4. Another one is generalization. This chapter deals
with the latter.
Let us start with an example: An application may need to define classes to model
different kinds of vehicles (e.g., cars, vans, lorries, etc.). As a first step, we can
define a class for each one of them: Vehicle, Car, Lorry, Van. Then, we may
consider the fact that the class Vehicle is more general than any one of the other
classes (e.g., Car). By saying that Vehicle is more general than Car we mean
the following issues:
A car is a special kind of vehicle. We often say that a car is a vehicle,
which actually means that any instance of the class Car can be seen also as
an instance of the class Vehicle.
The set of instances of the class Car is, actually, a subset of the instances of
the class Vehicle.
Anything which is true for a vehicle is also true for a car (i.e., the invariant of
the class Vehicle should also hold for the class Car; for instance: if any
well-constructed vehicle must have an owner, then any well-constructed car
must have an owner too).
Any attribute held by a vehicle is also held by a car (e.g., brand, model,
owner, etc) and any operation that can be applied to a vehicle, can also be
applied to a car (e.g., setOwner(...)). However, notice that a car could have
other attributes or operations which are not defined for general vehicles (e.g.,
doorNumber, which probably has no sense for lorries).
In any place, within a program, in which a vehicle is expected, a car may
come up (e.g., if a procedure expects a vehicle as a parameter, it should be
happy to get a car as such parameter).
This property is called substitution principle (see fig. 5.1).
- 149 -
CHAPTER 5: INHERITANCE
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.
- 151 -
CHAPTER 5: INHERITANCE
- 152 -
CHAPTER 5: INHERITANCE
- 153 -
CHAPTER 5: INHERITANCE
public:
Car(char* pbrand, char* pmodel, int pyear, char* powner,
char* pcolour, int pdoornbr);
//......
void showFeatures() const;
};
class Van :public ProfessionalVehicle{
public:
Van(char* pbrand, char* pmodel, int pyear,
char* powner, int pmaxLoad);
void showFeatures() const;
//......
};
- 154 -
CHAPTER 5: INHERITANCE
- Redefinition
of
showFeatures()
ProfessionalVehicle:
for
the
class
void ProfessionalVehicle::showFeatures()
{
Vehicle::showFeatures();
cout<<"owner company: "<<companyOwner<<
<<"maximum load allowed: "<<maxLoad<<endl;
}
- 155 -
CHAPTER 5: INHERITANCE
CHAPTER 5: INHERITANCE
the
presented in 4.2.1.
- 157 -
CHAPTER 5: INHERITANCE
- 158 -
CHAPTER 5: INHERITANCE
Notice
the
initializer
:Vehicle(pv). It is responsible
Vehicle::Vehicle(const Vehicle& v) (see figure 5.3).
for
calling
- 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:
The destructor of a subclass calls implicitly the destructor of its superclass.
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;
}
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
On the other hand, public members of class A are visible within any function.
In addition to private and public members, C++ allows the definition of a new
category of members: protected members.
Definition (Protected members)
Protected members of class A are visible only:
Within the implementation of any operation of A
Within the implementation of any operation of any class which is derived
from A.
- 161 -
CHAPTER 5: INHERITANCE
From the code of the functions f1(), f2() and f3() it will be possible to
use all members of class A, g() and h().
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.
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.
CHAPTER 5: INHERITANCE
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;
- 166 -
CHAPTER 5: INHERITANCE
public:
double getArea(){return edgeLength*edgeLength;}
double getPerimeter(){ return edgeLength*4;}
//More operations....
};
class Rectangle:public Polygon
{
double edgeLength1;
double edgeLength2;
public:
double getArea(){return edgeLength1*edgeLength2;}
double getPerimeter(){ return edgeLength1*2 + edgeLength2*2;}
//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.
CHAPTER 5: INHERITANCE
superclass called SequenceOfIntegers which will turn out to be an interface and will
be implemented differently by each one of its subclasses. The subclasses
ArraySeqOfInt and LinkedSeqOfInt are realizations of the interface
SequenceOfIntegers. We present here that implementation.
class SequenceOfIntegers{
public:
virtual void insertFirst(int x)=0;
virtual void insertLast(int x)=0;
virtual int getElementPosition(int p)=0;
...
};
class ArraySeqOfInt :public SequenceOfInteger{
int seq[N];
int nelems;
public:
void insertFirst(int x){...}
void insertLast(int x){....}
int getElementPosition(int p){return seq[p];}
};
class LinkedSeqOfInt :public SequenceOfInteger{
class Node{
public:
int elem;
Node* next;
};
Node* first;
int nelems;
public:
void insertFirst(int x){...}
void insertLast(int x){....}
int getElementPosition(int p){...}
};
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.
- 170 -
CHAPTER 5: INHERITANCE
void insert(const T& x){
if (!s.belongs(x)) s.insert(x,1);
else ; //error management.....
}
bool belongs(const T& x){
return s.belongs(x);
}
int getNElems(){
return s.getNElems();
}
void intersection (const Set& s2){
//....
}
};
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
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
CHAPTER 5: INHERITANCE
- 176 -
CHAPTER 5: INHERITANCE
// It should call the superclass constructor
{
//.....
}
~MarriedPerson()
{
//....
}
void printFeatures(ostream& c)
{
//....It should call printFeatures from the superclass
}
};
- 177 -
CHAPTER 5: INHERITANCE
Date(const Date& pd)
{
cout<<"Copy constructor of Date"<<endl;
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.....
friend
};
- 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;
}
};
- 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
(8) Destructor of MarriedPerson
(9) Destructor of Date
(10) Destructor of Person
(11) Destructor of Date
(12) Destructor of Date
(13) Destructor of Date
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
6.1. Concept of polymorphism
Inheritance and the substitution principle offer a new approach to program design :
a program may use objects whose actual type is not known until run time.
This idea improves a great deal the abstraction, elegance and reusability of the
resulting programs. Let us consider the following example.
A second-hand vehicle dealer wants to provide their prospective customers with online information about the features of the vehicles on sale.
To achieve this goal, the software engineer who is responsible for the application
designs the following function intended to show on the standard output the features
of a specific vehicle (which is passed to the function as parameter).
void informCustomer(const Vehicle& v)
{
cout<<"The features of the vehicle you have selected are:"<<endl;
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:
Car c("renol", "marrane", 2002, 180, "black", 3);
Van v("renol", "hard", 2000, "ACME LTD", 5000);
informCustomer(c);
informCustomer(v);
- 183 -
CHAPTER 6: POLYMORPHISM
The features of the vehicle you have selected are:
brand: renol model: hard year: 2000 owner company: ACME LTD
maximum load allowed: 5000
Please, contact the counter to know more details
- 184 -
CHAPTER 6: POLYMORPHISM
Vehicle
PersonalVehicle
Car
ProfessionalVehicle
Van
Lorry
void Car::showFeatures(){....}
void informCustomer(Vehicle& v
v is a Car
){
v.showFeatures();
v is a Van
void Van::showFeatures(){....}
{
switch (((Vehicle*)v)->getType()){
case 1: ((Vehicle*)v)->Vehicle::showFeatures(); break;
- 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.
Therefore, it becomes clear that polymorphism is a very powerful tool to design
elegant and reusable programs. However, as we have already made clear, a
programming language that wants to support it needs to provide two features (a
notion of class conformance and dynamic binding) which deserve some more
attention. The next two sections are devoted to them.
- 186 -
CHAPTER 6: POLYMORPHISM
- 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
//....
foo(pv);
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)
ProfessionalVehicle pv(...);
foo(pv);
(b)
pv:
Forrd
Pocus
2003
Loads Ltd
5000
void foo(Vehicle v)
{....}
v:
ppv:
ProfessionalVehicle* ppv;
ppv=new ProfessionalVehicle(...);
foo(pv);
void foo(Vehicle* v)
{....}
Forrd
Pocus
2003
ProfessionalVehicle pv(...);
foo(pv);
v,pv:
void foo(Vehicle& v)
{....}
Forrd
Pocus
2003
Moves Ltd
5000
(c)
v:
Forrd
Pocus
2003
Loads Ltd
5000
v points to the same
object that ppv: OK!!
CHAPTER 6: POLYMORPHISM
Assignment of references
Let us consider the function:
void f(Vehicle& rv, Car& rc)
{
rv=rc;
//(1)
CORRECT
rc=rv;
//(2)
INCORRECT!!!!!
}
int main()
{
Vehicle v(...);
Car c(...);
f(v,c);
//(3)
rv v:
rc c:
maxspeed, colour, owner, doornumber
have been lost
After rv=rc;
Recall that this operation is provided by default by the compiler (or has
been overloaded by the class designer). Notice that this operation may
accept a reference to Car as parameter because Car conforms with
Vehicle.
Notice that if the reference rv does not refer to an object of class Car
but to an object of class Vehicle (as in (3) ), then there will be loss of
information, since several attributes of rc will not be copied to rv (i.e.,
doornumber, maxspeed, owner, colour) (see figure 6.3).
- 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&);
pv
c:
pc
After pv=pc;
CHAPTER 6: POLYMORPHISM
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.
It is also possible:
dn=((Car&)v).getDoorNumber();
This static type conversion (from a superclass to a subclass) can only be applied to
pointers and references to objects (unless a specific conversion operator has been
defined, as presented in 6.2.4), not to objects themselves.
Needless to say that the static type conversion must be used very carefully: if the
converted object is not of the expected subclass (in the example Car), the
- 191 -
CHAPTER 6: POLYMORPHISM
- 192 -
CHAPTER 6: POLYMORPHISM
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
Type identification
(This section uses the concept of virtual operation which will be presented in section 6.3. You can
skip it for the moment)
In addition to dynamic cast, C++ provides a specific way to make explicit the type
of a reference (or of the object pointed to by a pointer) at run time: the typeid()
operator.
To illustrate the way it works, we consider again the previous example:
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();
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
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).
//c=7.3 + 0*i
return 0;
}
Example 2:
Consider the simplified Vehicle hierarchy:
class Vehicle
{
char* brand;
char* model;
int year;
public:
Vehicle(char* br, char* mod, int ye){
//...as usual...
}
- 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;
}
};
class Car :public Vehicle{
int maxSpeed;
int doorNbr;
public:
Car (Vehicle v)
:Vehicle(v)
{
maxSpeed=100;
doorNbr=5;
cout<<"constructor"<<endl;
}
};
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
- 197 -
CHAPTER 6: POLYMORPHISM
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
v.showFeatures()
to
the
function
associates
the
call
Vehicle::showFeatures(). Therefore, at execution time the car/van specific
features will not be shown. That is, if we execute something of the sort:
Car c("renol", "marrane", 2002, 180, "black", 3);
Van v("renol", "hard", 2000, "ACME LTD", 5000);
informCustomer(c);
informCustomer(v);
CHAPTER 6: POLYMORPHISM
- 199 -
CHAPTER 6: POLYMORPHISM
//Some operations
virtual
};
CHAPTER 6: POLYMORPHISM
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();
v.showFeatures();
return 0;
}
//(1)
//(2)
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.
Second case: Call to a virtual function on a parameter passed by value:
void informCustomer(const Vehicle v)
{
cout<<"The features of the vehicle you have selected are:"<<endl;
v.showFeatures(); //(1)
cout<<"Please,
details\n\n"<<endl;
}
contact
the
counter
to
know
more
int main()
{
Car ca("renol", "marrane", 2002, 180, "black", 3);
Van va("renol", "hard", 2000, "ACME LTD", 5000);
informCustomer(ca);
informCustomer(va);
return 0;
}
//(2)
//(3)
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
contact
the
counter
to
know
more
int main()
{
Car c("renol", "marrane", 2002, 180, "black", 3);
Van v("renol", "hard", 2000, "ACME LTD", 5000);
informCustomer(c);
informCustomer(v);
return 0;
}
//(2)
//(3)
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.
Fourth case:Call to a virtual function on a parameter, which is a pointer:
void informCustomer(const Vehicle* v)
{
cout<<"The features of the vehicle you have selected are:"<<endl;
v->showFeatures(); //(1)
cout<<"Please,
details\n\n"<<endl;
}
contact
the
counter
int main()
{
Car c("renol", "marrane", 2002, 180, "black", 3);
Van v("renol", "hard", 2000, "ACME LTD", 5000);
informCustomer(&c);
informCustomer(&v);
return 0;
}
//(2)
//(3)
- 202 -
to
know
more
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.
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(...);
...
}
- 204 -
CHAPTER 6: POLYMORPHISM
These operations which are virtual and equalled to zero are called pure
virtual functions.
A pure virtual function cannot have any implementation in the abstract class.
It must be overridden in its descendants.
There cannot be any object which is an instance of an abstract class
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;
};
class Square :public RegularPolygon{
public:
Square(float edgel)
:RegularPolygon(4,edgel)
{}
virtual float getArea()
{
return getEdgeLength()*getEdgeLength();
}
};
class Triangle :public RegularPolygon{
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.
template <class T>
class Map{
int elemNb;
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;
};
template <class T>
class Pair{
public:
char* key;
T val;
};
template <class T>
class ArrayMap :public Map<T>{
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){...}
};
template <class T>
class HashMap :public Map<T>{
//Representation of a Map as a Hash table.....
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.
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;
}
//.....
};
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
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
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.
- 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 RentCar :public Car, public RentableObject{
//.....
public:
RentCar(....)
{
//....
}
//.....
};
- 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.
- 213 -
CHAPTER 6: POLYMORPHISM
class Car{
int purchasePrice;
//MORE ATTRIBUTES....
public:
Car(int ppr){
purchasePrice=ppr;
}
virtual int getPrice(){
return purchasePrice;
}
};
class RentCar :public Car, public RentableObject{
public:
RentCar(int purchasep, int rentp)
:Car(purchasep),RentableObject(rentp)
{}
//MORE OPERATIONS AND ATTRIBUTES.....
};
int main()
{
int p;
RentCar c(8000,200);
p=c.getPrice();
return 0;
///!!!!!AMBIGUOUS
p2=c.Car::getPrice();
- 214 -
CHAPTER 6: POLYMORPHISM
void foo1(RentableObject& ro)
{
ro.getPrice();
}
CHAPTER 6: POLYMORPHISM
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
Virtual inheritance
The most usual case is that in which the subclass D should inherit just one copy of
the members of A. This behaviour will be obtained declaring B as a virtual
subclass of A and C as a virtual subclass of A:
class A{
public:
int x;
};
class B :public
virtual A{};
- 217 -
CHAPTER 6: POLYMORPHISM
int main()
{
D d;
d.x=10;
}
//O.K.
Duplicated inheritance
In limited cases we want to inherit the members of the common ancestors twice. In
such cases, we get the so called duplicated inheritance.
This is exactly what we have if the label virtual is not used in the definition of
the inheritance. The class D inherits two copies of the member x. For this reason,
the call to the member x on an object of class D will be ambiguous:
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.
Notice that the task of sending the description of v to the stream c has been
showFeatures(c).
The
call
delegated
to
the
virtual
function
- 219 -
CHAPTER 6: POLYMORPHISM
Notice also that it is not necessary to define the operator << as a friend function of
each specific subclass of Vehicle. The previous overloaded << operator will be
called for any object v which is a subclass of Vehicle.
inheritance.
which returns the name of the partner of the person to which it is applied.
The class MiddleAgedPerson should include an attribute bankname and an
operation with the following header:
char* MiddleAgedPerson::getName()
which returns the name of the bank where this person has his/her accounts.
Make all the attributes of all classes protected.
Solution
#include <cstring>
#include <iostream>
#include "Date.h"
using namespace std;
class Person{
protected:
char passportId[9];
int age;
Date birthdate;
char* name;
public:
Person(){}
- 220 -
CHAPTER 6: POLYMORPHISM
Person(char* ppassid, int page, Date& pbirth, char* pname)
:birthdate(pbirth)
{
age=page;
strcpy(passportId,ppassid);
name=new char[strlen(pname)+1];
strcpy(name,pname);
}
~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;
}
};
class MarriedPerson :public Person{
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;
}
virtual void printFeatures(ostream& c)
{
Person::printFeatures(c);
c<<"marriage date="<<marriageDate<< "\n partner name="
<<partnername<<endl;
}
- 221 -
CHAPTER 6: POLYMORPHISM
- 222 -
CHAPTER 6: POLYMORPHISM
//(1)
//(2)
MiddleAgedMarried p4("11111111",27,birth2,"Ann",marrd,
"Mark","intercontinental bank");
cout<<"Middle aged and married person: bank name :"
<< p4.MiddleAgedPerson::getName()<<endl;
//(3)
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:
p4.getBankName();
p4.getName();
Solution
Any instance of the class MiddleAgedMarried will inherit two instances of each
of the attributes/operations of Person. This is due to the fact that, by default,
attributes and operations of the root common class are inherited following two
different paths:
Person-MarriedPerson-MiddleAgedMarried
and
Person-MiddleAgedPerson-MiddleAgedMarried.
For this reason, there would be an ambiguity in a call to an operation inherited from
Person (for example, p4.getPersonName() in the main function). In order to
CHAPTER 6: POLYMORPHISM
p4.MarriedPerson::getPersonName();
which
refers
to
the
getPersonName()
function
inherited
through
function
inherited
through
MarriedPerson or
p4.MiddleAgedPerson::getPersonName();
which
refers
to
the
getPersonName()
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(....);
p4.age=80;
p4.MiddleAgedPerson::age=90;
p4.MarriedPerson::age=70;
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
};
class MarriedPerson :virtual public Person{
//as before
};
class MiddleAgedPerson :virtual public Person{
//as before
};
- 225 -
CHAPTER 6: POLYMORPHISM
class MiddleAgedMarried :virtual public MiddleAgedPerson,
virtual public MarriedPerson {
public:
MiddleAgedMarried(char* ppassid, int page, Date& pbirth,
char* pname, Date& pmarr, char* ppart,
char* pbank):
Person(ppassid,page,pbirth,pname),
MiddleAgedPerson(ppassid,page,pbirth,pname,pbank),
MarriedPerson(ppassid,page,pbirth,pname,pmarr,ppart)
{
}
virtual void printFeatures(ostream& c)
{
MarriedPerson::printFeatures(c);
c<<"Bank name="<<bankname<<endl;
}
};
int main()
{
MiddleAgedMarried p4(....);
char* nam;
nam=p4.getPersonName();
p4.age=90;
//Now it is correct!!
//Also correct (if age was declared
//public in Person)
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:
MiddleAgedMarried(char* ppassid, int page, Date& pbirth,
char* pname, Date& pmarr, char* ppart,
char* pbank):
Person(ppassid,page,pbirth,pname),
MiddleAgedPerson(ppassid,page,pbirth,pname,pbank),
MarriedPerson(ppassid,page,pbirth,pname,pmarr,ppart)
{
}
If it is not done like this, the constructor of MiddleAgedMarried will call the
default constructor of Person.
- 226 -
CHAPTER 6: POLYMORPHISM
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<<"***Polymorphism of the action test****\n\n"<<endl;
cout<<"Features of a Person"<<endl;
test(p1);
cout<<"\nFeatures of a Married Person"<<endl;
- 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);
}
- 228 -
CHAPTER 6: POLYMORPHISM
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!!
}
- 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:
template <class T>
class Stack{
//...
};
Operations:
According to the requirements, the following set of operations seem
adequate 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
template <class T>
class Stack{
//...
public:
Stack();
~Stack();
void insertTop(const T& x);
void removeTop();
T* getTop() const;
bool isEmpty() const;
};
Remarks:
- Notice the use of const in some operations:
....
}
* T getTop() const;
The returned object will be copied (using the copy constructor of the
class T) to a temporal object of the class T. For that reason, if the
object that has been actually returned is an instance of a subclass of T,
there will be a loss of information.
This choice should not be used if a polymorphic behaviour is required.
CHAPTER 6: POLYMORPHISM
* T* getTop() const;
This operation does not have any problem, since:
T* x;
x=s.getTop();
does not mean a call to the operator = of the class T but just a copy of
pointers. Therefore, we will get a polymorphic behaviour regardless of
the way in which it is used.
The differences between the three operations are shown in figure 6.10.
- 233 -
CHAPTER 6: POLYMORPHISM
(1): T p;
p=f();
(1)
T f()
{
T x;
...get the appropriate element on x
return x;
(2)
}
T p;
p=f();
Returns a copy of x on a
temporal object (tmp)
created with the copy constructor:
(3)
p=tmp;
T tmp(x);
1111
0000
0000
1111
x:
0000
1111
0000
1111
0000
1111
0000
1111
Problems if S x; instead of T x;
(where S is a subclass of T)
(2): T p;
p=f();
1111
0000
0000
1111
0000
1111
0000
1111
0000
1111
0000
1111
tmp:
(1)
T p;
p=f();
T& f()
{
T* x=new T;
...get the appropriate element on *x
return *x;
(2)
}
Returns a reference to *x on a
temporal reference object:
Problems if S* x; instead of T* x;
(where S is a subclass of T)
if we assign the result of f() to another object
p=f();
p=tmp;
1111
0000
0000
1111
0000
1111
0000
1111
1111
0000
1111
0000
1111
0000
0000
1111
p:
(Copy of *x obtained
0000
1111
0000
1111
with T::operator=(...) )
1111
0000
1111
0000
T* f()
(1)
{
T* x=new T;
...get the appropriate element on *x
return x;
(2)
}
p=f();
Returns a pointer to *x on a
temporal pointer:
(3)
p=tmp;
T* tmp=x;
x:
tmp:
No problems!!
(3)
T& tmp=*x;
tmp, x:
(3): T* p;
1111
0000
0000
1111
p:
(Copy of x obtained
0000
1111
0000
1111
with T::operator=(...) )
0000
1111
0000
1111
1111
0000
0000
1111
0000
1111
0000
1111
1111
0000
1111
0000
p:
(p points to the same object as x)
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:
template <class T>
class Stack{
T* v[N];
int top;
public:
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>::insertTop(const T& x)
{
v[top]=x.clone();
top++;
}
void Stack<T>::removeTop()
{
top--;
delete v[top];
}
T* Stack<T>::getTop() const
{
T* x;
x=v[top-1]->clone();
return x;
}
bool Stack<T>::isEmpty() const
{
return top==0;
}
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():
virtual Vehicle* Vehicle::clone()
{
return new Vehicle(*this);
}
virtual Vehicle* Car::clone()
{
return new Car(*this);
}
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
template <class T>
class Stack{
T** v;
int capacity;
int top;
public:
Stack(int capac);
~Stack();
void insertTop(const T& x);
void removeTop();
T* getTop() const;
bool isEmpty() const;
};
and then:
template <class T>
class Stack{
TPointer<T>* v;
int capacity;
int top;
//....
};
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.
CHAPTER 6: POLYMORPHISM
At this point, the user does not know whether the exact type of pc is an
ArrayStack or a LinkedStack or may be some other type of container
that we have not considered in this example. However, nothing prevents the
user from applying to pc the operations defined for Container. For
instance, the user can perform an iteration over pc even without knowing its
specific type:
T* x;
- 241 -
CHAPTER 6: POLYMORPHISM
pc->setFirst();
while(! pc->isEnd())
x=pc->getCurrent(); pc->setNext();
Implementation of Container
class NoObjectException{};
template <class T>
class Container{
protected:
unsigned int nElems;
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;
};
template <class T>
Container<T>::Container(){
nElems=0;
}
template <class T>
unsigned int Container<T>::getSize() const
{
return nElems;
}
template <class T>
bool Container<T>::isEmpty() const
{
return nElems==0;
}
Remarks
CHAPTER 6: POLYMORPHISM
Implementation of Stack
class EmptyStackException{};
class FullStackException{};
template <class T>
class Stack :public Container<T> {
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(){}
CHAPTER 6: POLYMORPHISM
- 244 -
CHAPTER 6: POLYMORPHISM
template<class T>
void ArrayStack<T>::removeTop()
throw (EmptyStackException)
{
if (isEmpty()) 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
nelems: 0
NULL
first:
nelems: 3
NULL
first:
john
123456
18
ann
2223334
19
joan
1299999
18
Implementation of LinkedStack
template <class T>
class Node{
public:
T* val;
Node<T>* next;
};
template <class T>
class LinkedStack :public Stack<T> {
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++;
}
template<class T> T* LinkedStack<T>::getTop() const
throw (EmptyStackException)
{
if (isEmpty()) throw EmptyStackException();
return first->val->clone();
}
- 247 -
CHAPTER 6: POLYMORPHISM
template<class T>
void LinkedStack<T>::removeTop()
throw (EmptyStackException)
{
Node<T> *aux;
if (isEmpty()) throw EmptyStackException();
aux = first;
first= first->next;
delete aux;
nElems--;
}
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
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;
}
The actual implementation of insertElemsIntoStack(...) (developed by
the ACME team) uses ArrayStack:
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:
template <class T>
T* getBottomElem(Stack<T>& s) {
if (s.isEmpty()) throw EmptyStackException();
try{
return (dynamic_cast<ArrayStack<T>&>(s)).getElemPos(0);
}
catch (bad_cast) {
//Stack of unexpected type!!!!!
return 0;
}
}
- 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).
- 252 -
CHAPTER 6: POLYMORPHISM
/***************************************************
class A
********************************************************/
class A :public Object
{
int attr;
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
7.1. Exceptions in computer programs
What is an exception in a computer program?
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.
These kinds of situations exist in any program, regardless of the programming
language. However, not all the programming languages have mechanisms for
handling these situations (exceptions). C++ programming language has such a
mechanism which simplifies working with exceptions. The code written in C++ is
more readable because it is not required to check for exceptions after each method
or function call. Exception handling mechanism in C++ is fully object oriented.
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>
using namespace std;
class Calculator {
double result;
int error; //error situation indicator
public:
Calculator() { result = 0; error = 0; }
int getError() { return error; }
double getResult() { return result; }
double add(double value) { result += value; return result; };
double divide(double value);
};
- 255 -
CHAPTER 7: EXCEPTIONS
double Calculator::divide(double value) {
if (value != 0)
result /= value;
else
error = 1; //if the divider is zero,
//then indicate an exception
return result;
}
int main() {
Calculator calc;
calc.add(9);
calc.divide(3);
//after each call to "divide" operation
//we have to check for exceptions
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);
cout << "The result is " << calc.getResult() << endl;
return 0;
}
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
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
catch (DivByZeroException &dbz) //this catch block handles the
//DivByZeroException
{
cout << "Exception is thrown!" << endl;
cout << dbz.getMessage() << endl;
cout << "You tried to divide " << dbz.getDividend()
<< " by zero!" << endl;
}
cout << "End of program!" << endl; //this statement
//will always be executed
return 0;
}
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>
using namespace std;
class IllegalOpException
{
char message[50];
public:
IllegalOpException(char* message)
{ strcpy(this->message, message); }
- 259 -
CHAPTER 7: EXCEPTIONS
const char* getMessage()
{ return message; }
};
class DivByZeroException: public IllegalOpException {
double dividend;
public:
DivByZeroException(char* message, double dividend):
IllegalOpException(message)
{
this->dividend = dividend;
}
double getDividend() { return dividend; }
};
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::divide(double value) {
if (value == 0)
throw DivByZeroException("Division by zero not allowed",
result);
result /= value;
return result;
}
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
cout << "The result is " << calc.getResult() << endl;
}
- 260 -
CHAPTER 7: EXCEPTIONS
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;
}
cout << "End of program!" << endl;
return 0;
}
message
now inherited
from
from
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);
calc.add(6);
- 261 -
CHAPTER 7: EXCEPTIONS
calc.subtract(11);
calc.squareRoot();
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();
cout << "The result is " << calc.getResult() << endl;
}
catch (IllegalOpException &iop)
{
cout << "Exception is thrown!" << endl;
cout << iop.getMessage() << endl;
}
catch (DivByZeroException &dbz)
{
/*
This block will never execute because the block above
catches both exception types:
"IllegalOpException" and "DivByZeroException"
*/
}
cout << "End of program!" << endl;
return 0;
}
- 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
cout << "The result is " << calc.getResult() << endl;
}
- 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;
}
cout << "End of program!" << endl;
return 0;
}
- 264 -
CHAPTER 7: EXCEPTIONS
The function declared this way should only throw the specified exceptions.
void some_function() throw ();
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();
- 265 -
CHAPTER 7: EXCEPTIONS
- 266 -
CHAPTER 7: EXCEPTIONS
public:
DivByZeroException(char* message, double dividend):
IllegalOpException(message)
{
this->dividend = dividend;
}
double getDividend() { return dividend; }
};
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) throw (DivByZeroException);
double squareRoot() throw (); //this method should not
//throw exceptions
};
double Calculator::divide(double value) throw (DivByZeroException)
{
if (value == 0)
throw DivByZeroException("Division by zero not allowed",
result);
result /= value;
return result;
}
//the following function violates the exception specification
//by throwing an exception while it is specified that it should
//not throw any exceptions
double Calculator::squareRoot() throw () {
if (result < 0)
throw IllegalOpException("Illegal Square root extraction!");
result = sqrt(result);
return result;
}
void calculateIt() {
Calculator calc;
calc.add(9);
calc.divide(3);
calc.add(6);
calc.subtract(11);
calc.squareRoot();
cout << "The result is " << calc.GetResult() << endl;
}
- 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()"
set_terminate(my_terminate);
calculateIt();
cout << "End of program!" << endl;
return 0;
}
When you run this program you should get the following output:
Error: Program threw an unexpected exception!
Abnormal program termination
CHAPTER 7: EXCEPTIONS
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()".
public void functionTwo() throws ExampleException
{
functionOne();
}
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.
- 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
- 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.
Solution: file main.cpp
#include <iostream>
#include "arraystack.h"
using namespace std;
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:
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
- 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);
}
- 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.
MyVector class should be generic class which enables the use of the class
- 276 -
CHAPTER 7: EXCEPTIONS
int growBy;
int length;
T* elements;
public:
MyVector(int initCap, int growBy = 10);
~MyVector();
void append(const T& item);
int getLength() { return length; };
int getCapacity() { return capacity; };
T& operator[](int index);
};
template <class T>
MyVector<T>::MyVector(int initCap, int growBy)
{
this->capacity = initCap;
this->growBy = growBy;
this->length = 0;
this->elements = new T[this->capacity];
}
template <class T>
MyVector<T>::~MyVector()
{
delete [] elements;
}
template <class T>
void MyVector<T>::append(const T& item)
{
if (length == capacity)
//if vector reached it's capacity
{
capacity += growBy;
//increase capacity by "growBy" value
T* newElements = new T[capacity]; //create new array in dynamic
//memory
for (int i = 0; i < length; i++)
newElements[i] = elements[i];
delete [] elements;
elements = newElements;
}
elements[length] = item;
length++;
}
template <class T>
T& MyVector<T>::operator[](int index)
{
return elements[index];
}
#endif
- 277 -
CHAPTER 7: EXCEPTIONS
v1.append(1);
v1.append(2);
v1.append(3);
v2.append(0.1);
v2.append(0.2);
v2.append(0.3);
cout << "*******************" << endl;
cout << "Vector v1 contents:" << endl;
cout << "*******************" << endl;
for (int i = 0; i < v1.getLength(); i++)
cout << v1[i] << endl;
cout << "*******************" << endl;
cout << "Vector v2 contents:" << endl;
cout << "*******************" << endl;
for (int i = 0; i < v2.getLength(); i++)
cout << v2[i] << endl;
cout <<
<<
cout <<
<<
"Length, capacity
v1.getLength() <<
"Length, capacity
v2.getLength() <<
of
",
of
",
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
BaseClass* b1 = new BaseClass;
v.append(b1);
DerivedClass* b2 = new DerivedClass; //DerivedClass inherits from
//BaseClass
v.append(b2);
//...
//do whatever with vector
//...
for (int i = 0; i < v.getLength(); i++)
delete v[i];
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.
- 279 -
CHAPTER 7: EXCEPTIONS
class VectorException
{
protected:
char* message;
public:
VectorException(const char* message);
virtual ~VectorException();
virtual const char* getMessage() const;
};
class VectorOutOfBoundsException: public VectorException
{
public:
VectorOutOfBoundsException();
};
class VectorOutOfMemoryException: public VectorException
{
public:
VectorOutOfMemoryException();
};
/*
**********************
Generic class MyVector
**********************
*/
template <class T>
class MyVector
{
int capacity;
int growBy;
int length;
T* elements;
public:
MyVector(int initCap, int growBy = 10);
~MyVector();
void append(const T& item);
int getLength() { return length; };
int getCapacity() { return capacity; };
T& operator[](int index);
};
template <class T>
MyVector<T>::MyVector(int initCap, int growBy)
{
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
}
}
template <class T>
MyVector<T>::~MyVector()
{
delete [] elements;
}
template <class T>
void MyVector<T>::append(const T& item)
{
f (length == capacity)
{
capacity += growBy;
try
{
T* newElements = new T[capacity];
for (int i = 0; i < length; i++)
newElements[i] = elements[i];
delete [] elements;
elements = newElements;
}
catch (bad_alloc)
{
throw VectorOutOfMemoryException();
}
// throw exception
}
elements[length] = item;
length++;
}
template <class T>
T& MyVector<T>::operator[](int index)
{
if (index < 0 || index > length - 1)
throw VectorOutOfBoundsException();
return elements[index];
}
#endif
- 281 -
CHAPTER 7: EXCEPTIONS
- 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
- 284 -
Chapter 8
Standard C++ Library
8.1. What is the "Standard C++ Library"
Since one of the principles of object oriented programming paradigm is code reuse,
C++ compilers usually ship with a library of commonly used classes which can be
used in our programs. This library is standardized, which means that it should
contain the same components regardless of the compiler being used.
All the elements of the standard library are defined in "std" namespace. A
namespace is a mechanism for expressing logical grouping. That is, if some
declarations logically belong together according to some criteria, they can be put
in a common namespace to express that fact [Stroustrup1997]. A namespace is a
scope and the usual scope rules hold for namespaces. A name declared in a
namespace can be used when qualified by the name of its namespace. For example
expression std::cout refers to the object cout inside std namespace.
The Standard C++ library is presented as a set of header files. C++ library contains
large number of facilities which a programmer can use for solving almost any
common problem.
Before standardization, different C++ compilers had different libraries which had
similar functions, classes and other elements but those elements were not always
compatible. Standard C++ library contains all those functions that existed before
(and more), but they are standard now and should be compatible with different
compilers. In order to maintain backward compatibility, header files in Standard
C++ library follow different naming conventions then pre-standard libraries. The
following line of code shows the "old" way of using libraries provided by the
compiler:
#include <iostream.h>
Using-directive makes all names from a namespace available as if they had been
declared outside their namespace.
- 285 -
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>
using namespace std;
int main()
{
string s1 = "This is one way of string initialization!";
string s2("This is another way!");
//string "s3" is created and initialized with first
//5 characters of string "s2"
string s3(s2, 0, 5);
//string "s4" is initialized with 2 characters from
//the middle of "s2"
string s4(s2, 5, 2);
//string "s5" is initialized from the 5th character
//to the end of string "s2"
string s5(s2, 5);
- 286 -
<<
<<
<<
<<
<<
<<
s1
s2
s3
s4
s5
s6
<<
<<
<<
<<
<<
<<
endl;
endl;
endl;
endl;
endl;
endl;
/*
size() operation gives us the
and "capacity()" gives as the
can grow whithout allocating
additional memory.
*/
cout << "The size of 's1' is:
cout << "The capacity of 's1'
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>
using namespace std;
int main()
{
string s1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
int position = s1.find("IJ");
if (position != string::npos)
cout << "'IJ' is found in position: " << position << endl;
else
cout << "'IJ' is not found" << endl;
return 0;
}
- 287 -
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 -
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>
using namespace std;
//this is the same class from the previous example
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);
}
const char* GetTitle() { return title; }
const char* GetAuthor() { return author; }
};
int main()
{
vector<Book*> library; //the container
vector<Book*>::iterator libIter; //iterator for the container
//we add elements using "push_back" method
library.push_back(new Book("Hamlet", "William Sheaksperae"));
- 289 -
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.
3)
4)
5)
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.
6)
- 290 -
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>
using namespace std;
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);
}
const char* GetTitle() { return title; }
const char* GetAuthor() { return author; }
};
- 291 -
int main()
{
list<Book*> library; //the container
list<Book*>::iterator libIter; //iterator for the container
//we add elements using "push_back" method
library.push_back(new Book("Hamlet", "William Sheaksperae"));
library.push_back(new Book("Lord of the Rings", "Meho Dzeger"));
library.push_back(new Book("Vlak u snijegu", "Mato Lovrak"));
//accessing the elements using iterator
for (libIter = library.begin();
libIter != library.end();
libIter++)
{
Book* currentBook = *libIter;
cout << currentBook->GetTitle() << " by "
<< currentBook->GetAuthor() << endl;
}
return 0;
}
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 -
vector
list
deque
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>
using namespace std;
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);
for (unsigned int i = 0; i < numbers.size(); i++)
cout << "Element " << (i+1) << " = " << numbers[i] << endl;
cout << endl;
cout << "Maximum number of elements: "
<< numbers.max_size() << endl;
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 -
dependent on the data being stored, the type of the container and the operating
system.. The output produced should look something like this:
Element
Element
Element
Element
Element
1
2
3
4
5
=
=
=
=
=
10
20
30
40
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>
using namespace std;
//function which displays all the elements in a vector
void print(char* desc, vector<int> v)
{
cout << desc << ": ";
for (unsigned int i = 0; i < v.size(); i++)
cout << v[i] << " ";
cout << endl;
}
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;
numbers.pop_back();
- 294 -
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>
using namespace std;
void print(char* desc, list<char*> l)
{
cout << desc << ": ";
for (list<char*>::iterator it = l.begin(); it != l.end(); it++)
cout << (*it) << " ";
cout << endl;
}
int main()
{
list<char*> numbers;
numbers.push_back("three");
- 295 -
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>
using namespace std;
void print(char* desc, deque<char*> d)
{
cout << desc << ": ";
for (unsigned int i = 0; i < d.size(); i++)
cout << d[i] << " ";
cout << endl;
}
int main()
{
deque<char*> numbers;
numbers.push_back("three");
numbers.push_back("four");
numbers.push_back("five");
- 296 -
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 -
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 -
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>
using namespace std;
int main()
{
set<string> words;
set<string>::iterator it;
words.insert("Back");
words.insert("Agent");
words.insert("Agent");
words.insert("Duck");
words.insert("Cold");
cout << "The word list: " << endl;
for (it = words.begin(); it != words.end(); it++)
cout << *it << endl;
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 -
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
#include
#include
#include
<iostream>
<algorithm> //needed for alghoritms
<vector>
<string>
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 -
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
using namespace std;
int main()
{
//two arrays of characters (source and pattern)
char sentence[] = "The is an array of characters!"; //the source
char word[] = "array"; //the pattern
int slen = strlen(sentence);
int wlen = strlen(word);
char* result = search(sentence,
sentence + slen,
word,
word + wlen);
if (result != (sentence + slen))
{
int position = result - sentence;
cout << "Found at position: " << position << endl;
}
else
cout << "Not found!" << endl;
}
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 -
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
#include
#include
#include
#include
<iostream>
<algorithm>
<functional>
<vector>
<string>
- 302 -
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.
template <class Arg, class Res>
struct unary_function
{
typedef Arg argument_type;
typedef Res result_type;
};
template <class Arg, class Arg2, class Res>
struct binary_function
{
typedef Arg first_argument_type;
typedef Arg2 second_argument_type;
typedef Res result_type;
};
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:
template <class T>
struct less: public binary_function<T, T, bool>
{
bool operator() (const T& x, const T& y) const
{
return x<y;
}
};
- 303 -
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:
template <class T>
struct greater: public binary_function<T, T, bool>
{
bool operator() (const T& x, const T& y) const
{
return x>y;
}
};
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>
using namespace std;
/*
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)
{
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size - i - 1; j++)
{
//compare(...) calls overloaded operator()
//of a function object pased as the third argument
if (compare(array[j], array[j+1]))
{
T temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
} //end for j
} //end for j
}
- 304 -
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);
for (int i = 0; i < 6; i++)
cout << arr[i] << " ";
cout<<endl;
bubbleSort(arr, 6, desc);
for (int i = 0; i < 6; i++)
cout << arr[i] << " ";
cout<<endl;
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, Rumbaughs Object Modeling Technique (OMT), Boochs O-O
methodology, and Jacobsens 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
- 309 -
APPENDIX A: UML
- 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
attribute[:Type] [=initial value]
Class Name
Time
- hour : int
- minute : int
- secunde : int
+ Time()
+ setTime(int, int, int) : void
+ printMilitary() : void
+ printStandard() : void
Time
Time
setTime()
Point
Point
# x : int
# y : int
Point
x : int
y : int
Figure A- 4: UML class notation and example of the class Time and Point
An attribute is shown as a text string.. The default syntax is:
visibility name : type-expression [ multiplicity ordering ] = initial-value {
property-string }
where visibility is one of:
- 311 -
APPENDIX A: UML
+ public visibility
# protected visibility
- private visibility
~ package visibility
An operation (function) is shown as a text string.
Behaviour of object depends on its class (remember each object knows its class).
Operations take parameters of certain type, and return result of certain type.
The default syntax is:
visibility name ( parameter-list ) : return-type-expression { property-string }
where visibility is one of:
+ public visibility
# protected visibility
- private visibility
~ package visibility
Notation for abstract class is shown on the fig. A-5.
Class Name
0r
{abstract}
Class Name
- 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
stratPoint : Point
x = 0.18
y = 1.23
endPoint : Point
x = 4.28
y = 7.27
: Point
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,
DLLs and OCXs 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
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 .
Company
1..*
*
employer
Person
employee
Person
Company
Name
Address
Works for
employer
employee
Person
Name
Address
ID
Manager
Name
Address
ID
Supervises
Salesperson
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: lowerbound .. upper-bound
Class
unspecified
Class
exactly one
Class
many
(zero or more)
Class
optional
(zero or one)
Class
numerically
specified
0..1
m..n
- 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.
User
Workstation
authorized on
Authorization
Access right
Priority
startSession()
1..*
employee
*
employer
Company
Person
Contract
boss
salari : int
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.
University
Faculty
PlannedCourses
*
*
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.
under composition
an element can be
part of at most one
aggregate
EmailMessage
Header
Body
0..n
Attachment
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.
APPENDIX A: UML
A.4.1.4 Dependency
Dependency relation shows that a change to one element's (packages) 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.
Name
Description
access
Access
bind
Binding
APPENDIX A: UML
import
Import
use
Usage
- 321 -
APPENDIX A: UML
- 322 -
APPENDIX A: UML
- 323 -
APPENDIX A: UML
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.
APPENDIX A: UML
- 326 -
APPENDIX A: UML
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
APPENDIX A: UML
- 330 -
BIBLIOGRAPHY
Bibliography
[Allison1999]
[Allison1998]
[ANSI/ISO]
[Eckel1993]
[Eckel2000a]
[Eckel2000b]
[Vandev2002]
[Lafore1998]
[Josuttis]
[Gamma1995]
[Larman2002]
Applying UML And Patterns: An Introduction to ObjectOriented Analysis And Design And Iterative Development
(3rd Edition), by Craig Larman (Prentice-Hall, Inc, 2002)
[Budd2002]
[Booch1999]
[Booch1991]
BIBLIOGRAPHY
[Meyer1997]
[Liskov]
- 332 -
INDEX
Index
- 333 -