|
|||||
CS201
Introduction to Programming
Lecture
Handout
Introduction
to Programming
Lecture
No. 34
Reading
Material
Deitel
& Deitel - C++ How to
Program
Chapter
8
Summary
·
Arrays of
Objects
·
Dynamic
Arrays of Objects
·
Overloading
new and delete
Operators
·
Example
of Overloading new and delete as
Non-members
·
Example
of Overloading new and delete as
Members
·
Overloading []
Operator to Create
Arrays
Arrays
of Objects
A class
is a user-defined data type.
Objects are instances of
classes the way int
variables
are
instances of ints. Previously, we
have worked with arrays of
ints. Now,
we are going
to work
with arrays of
objects.
The
declaration of arrays of user-defined
data types is identical to
the array of primitive
data
types.
Following
is a snapshot of our veteran
Date
class:
/*
Snapshot of Date class discussed in
previous lectures */
class
Date
{
private:
int
day, month, year;
public:
/*
Parameterless constructor, it is created by
the compiler automatically
when we
don't
write it for any of our
class. */
Page
432
CS201
Introduction to Programming
Date(
)
{
cout <<
"\n Parameterless constructor called
...";
month =
day = year = 0;
}
/*
Parameterized constructor; has three
ints as
parameters. */
Date(int
month, int day, int
year)
{
cout <<
"\n Constructor with three
int parameters called
...";
this->month
= month;
this->day
= day;
this->year
= year;
}
~Date (
)
{
cout
<< "\n Destructor called
...";
}
...
...
};
Consider
the example of declaring an array of 10
date
objects of Date
class. In
this case,
the
declaration of arrays will be as
under:
Following
is the declaration:
Date
myDates [10] ;
With
this line (when this
line is executed), we are
creating 10 new objects of
Date
class.
We know
that a constructor is called whenever an
object is created. For every object
like
myDate[0],
myDate[1],.... myDate[9], the
constructor of the Date
class is
called.
Theimportant
thing to know here is that
which constructor of Date
class is being called to
construct
objects of the array myDates. As we
are not doing any
initialization of the
array
objects
explicitly, the default constructor
(parameterless constructor) of the
Date class is
called.
Remember, the default constructor is
defined by the C++ compiler
automatically
for
every class that has no
parameterless constructor defined
already. In our case of Date
class, we
have defined a parameterless constructor,
therefore, the compiler will
not
generate
default constructor automatically.
We can
also initialize the array
elements at the declaration
time. This initialization
is
similar
to that done for native data
types. For int
array, we
used to do initialization in
the
following
manner:
int
array [10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
;
Similarly,
we initialize Date
array
while declaring it:
Page
433
CS201
Introduction to Programming
Date
yourDate [3] = { Date(10, 24, 1980),
Date(06, 14, 1985), Date(07, 09,1986)
};
The
above statement will call
the parameterized constructor
Date
(int month, int day,
int
year)
of
Date
class to
create three objects of myDate
array.
This parameterized
constructor
carries out initialization of
the objects data member (month,
day, year)
with
the
values supplied as arguments to
it.
It will
be interesting to know, how the
following statement
works:
Date myDate
[10] = { Date(09, 03, 1970), Date(08,
23, 1974) } ;
We are
trying to declare an array of 10
Date objects while supplying
only initialization
values
for the first two
elements. At first, we might be
doubtful if the statement
is
compiled
successfully. Not only it compiles
successfully but also does
the initialization
of the
first two objects ( myDate[0],
myDate[1] ). What
will happen to the
remaining
objects
in the array? Actually, all
the 10
objects
are created successfully by the
above
statement.
The parameterized constructor is called
for the first two
objects ( myDate[0],
myDate[1]
)
and parameterless constructor is called
for the remaining
objects
(myDate[2],
myDate[3], ..., myDate[9]).
You
might have noticed that at
the array initialization
stage, we have explicitly
called
parameterized
constructor of Date
for
every object. We may specify only
the argument
when a
constructor with only one
parameter is called.
/* A
snapshot of String class
discussed in previous lectures
*/
class
String
{
private
:
char
*buf ;
public:
//
Constructors
String
();
String(
const char *s )
{
buf =
new char [ 30 ];
strcpy
(buf,s);
}
...
...
};
Page
434
CS201
Introduction to Programming
For
example, in the above-mentioned case of
String
class, we
have a constructor that
is
accepting
one argument of type char
*.
While writing our code, we
can declare and
initialize
an array of Strings as
follows:
String
message [10] = {
"First
line of message\n",
"Second
line of message\n",
String(
"Third line of
message\n"),
String(
)
};
See
the initializing arguments
for first two objects i.e,
(message[0],
message[1]) in
the
array.
Here only one string is being
passed. Therefore, for the
first two objects,
constructor
with one parameter of type
char
* of
String
class is
called automatically. That
constructor
is String
( char *
str
).
For the third object (message[2]), the
same
constructor
with one char
* as
parameter is being called explicitly.
For fourth object
(message[3]),
parameterless constructor i.e., String
( ) is being
called explicitly, though,
this was
optional as parameterless constructor is
called up automatically when no
initialization
is made. As there is no explicit
initialization for the remaining
six objects,
the
parameterless constructor is called up
automatically.
Can we
create arrays of objects
dynamically? As usual, the
answer is yes. Let's discuss
it
in
detail.
Dynamic
Arrays of Objects
Consider
the following
statement:
1.
String
*text ;
2.
text
= new String [5] ;
In line
1, we have declared a pointer text
of
String
type.
In line
2, we are creating an array of
5
objects
of String
type.
This statement
allocates
space
for each object of the
array, calls the parameterless
constructor for each object
and
starting
address of the first object is
assigned to the pointer text.
The
important point to be noted
here is that in line 2, we
can't initialize objects
because
there is
no way to provide initializers for
the elements of an array
allocated with new.
The
default constructor (parameterless
constructor) is called for each
element in the array
allocated
with new.
Remember, the default constructor
for a class is generated by
C++
compiler
automatically if it is not defined
already in the class
definition.
To
deallocate these arrays of
objects, the delete
operator
is used in the same way as
it is
used
for the native data
types.
There
are few cautions that
should be taken care of
while performing these
operations of
allocation
and deallocation with arrays of
objects.
Firstly,
while deallocating an array allocated
with new
operator,
it is important to tell
the
compiler
that an array of objects is
being deleted. The brackets
( []
)
are written in our
delete
statement
after the delete
keyword
to inform the delete
operator
that it is going to
Page
435
CS201
Introduction to Programming
delete an
array. The consequences of
using the wrong syntax are
serious. For example, if
we want
to delete previously created
array of five String
objects
using the following
statement:
delete
text; // Incorrect syntax of
deleting an array
The
delete operator in this case
will not be aware of deleting
(deallocating) an array of
objects.
This statement will call
the destructor only for
the object pointed by the
text
pointer i.e.
String[0]
and
deallocate the space
allocated to this object. The
requirement is
to call
the destructor for all
the objects inside the array
and deallocate the space
allocated
to all of
these objects. But on
account of the wrong syntax,
only the first object is
deleted
and
the remaining four objects (
String[1],
String[2], String[3], String[4]
pointed
by
text[1],
text[2], text[3], text[4]
respectively ) remain
in the memory intact. The
memory
space
occupied by these four
objects results in memory
leak as the
same program or any
other
program on the same computer
cannot use it unless it is
deallocated.
Calling
the destructor while destroying an object
becomes essential when we
have
allocated
some memory in free store
from inside the object (usually
from within the
constructor).
To
destroy an array of objects
allocated on free store
using the new
operator,
an array
equivalent of
delete
operator
is used. The array equivalent of
delete
operator
is to write
empty
square brackets after the
delete
keyword
(delete
[] ).
So the correct statement
is:
delete
[] text ;
This
statement destroys the whole
array properly. It calls destructor for
each object inside
the
array and deallocates the
space allotted to each object. Actually,
by looking at the
brackets
( []
)
after delete, the
compiler generates code to
determine the size of the
array
at
runtime and deallocate the
whole array properly. Here, it will
generate code to
deallocate
an array of 5
objects
of String
type.
If we
create an array of Date
objects
and want to delete them
without specifying
array
operator:
It will look as
under:
// Bad
Technique: deleting an array of objects
without []
//
for a class that is not doing dynamic
memory allocation internally
Date *
ppointments;
appointments
= new Date[10];
...
delete
appointments; // Same as delete []
appointments;
Although,
this is good to deallocate an array of
objects without specifying
array operator
([]) as
there is no dynamic memory
allocation occurring from inside the
Date
class.
But
this is a
bad practice. In future, the
implementation of this class
may change. It may
contain
some dynamic memory
allocation code. So it is always
safer to use array
operator
( []
) to
delete arrays.
Can we
overload new
and
delete
operators?
Yes, it is possible to overload new
and
delete
operators
to customize memory management. These
operators can be overloaded
in
global
(non-member) scope and in class
scope as member operators.
Page
436
CS201
Introduction to Programming
Overloading
of new and delete
Operators
Firstly,
we should know what happens
when we use new
operator
to create objects.
The
memory
space is allocated for the
object and then its
constructor is called. Similarly,
when we
use delete
operator
with our objects, the
destructor is called for the object
before
deallocating
the storage to the
object.
When we
overload new
or
delete
operators,
it can only lead to a change
in the allocation
and
deallocation part. The call to
the constructor after
allocating memory while
using new
operator
and call to the destructor
before deallocating memory while
using delete
operator
will be there. These calls to
constructors and destructors
are controlled by the
language
itself and these cannot be
altered by the programmer.
One of
the reasons of overloading new
and
delete
operators
can be their limited
current
functionality.
For example, we allocate space on
free store using the
new
operator
for
1000
ints. It
will fail and return
0
if
there is no contiguous space
for 1000
ints in
free
store.
Rather the free store
has become fragmented. The
total available memory is
much
more than
1000
ints , but
in fragments. There is no contiguous
segment of at least 1000
ints. The
built-in new
operator
will fail to get the
required space but we can
overload our
own
new
operator
to de-fragment the memory to
get at least 1000
ints space.
Similarly,
we can
overload delete
operator
to deallocate memory.
In the
embedded and real-time systems, a
program may have to run
for a very long
time
with
restricted resources. Such a
system may also require
that memory allocation
always
takes
the same amount of time.
There is no allowance for heap
exhaustion or
fragmentation. A
custom memory allocator is the solution. Otherwise,
programmers will
avoid
using new
and
delete
altogether
in such cases and miss
out on a valuable C++
asset.
There
are also downsides of this overloading.
If we overload new
or
delete
operator
at
global
level, the corresponding
built-in new
or
delete
operator
will not be visible to
whole
of the
program. Instead our
globally written overloaded
operator takes over its
place all
over.
Every call to new
operator
will use our provided new
operator's
implementation.
Even in
the implementation of new
operator,
we cannot use the built-in
new
operator.
Nonetheless,when
we overload new
operator
at a class level then this
implementation of
new
operator
will be visible to only
objects of this class. For
all other types
(excluding
this
class) will still use
the built-in new
operator.
For example, if we overload new
operator
for our class Date
then
whenever we use new
with
Date, our
overloaded
implementation
is called.
Date*
datePtr = new Date;
This
statement will cause to call
our overloaded new
operator.
However, when we use
new
with
any other type anywhere in
our program as under:
int*
intPtr = new int
[10];
The
built-in new
operator
is called. Therefore, it is safer to
overload new
and
delete
operators
for specific types instead
of overloading it globally.
Page
437
CS201
Introduction to Programming
An
important point to consider
while overloading new
operator
is the return value
when
the
new
operator
fails to fulfill the
request. Whether the
operator function will
return 0
or
throw an
exception.
Following
are the prototypes of the
new
and
delete
operators:
void
* operator new ( size_t size )
;
void
operator delete ( void * ptr)
;
The
new
operator
returns a void
* besides
accepting a parameter of whole
numbers size_t.
This
prototype will remain as it is while overloading
new
operator.
In the implementation
of
overloaded new
operator,
we may use calloc(
) or
malloc(
) for
memory allocation and
write
some memory block's
initialization code.
The
delete operator returns nothing
(void) and
accepts a pointer of void
* to
the memory
block. So
the same pointer that is
returned by the new
operator,
is passed as an argument
to the
delete
operator.
Remember, these rules apply to
both if operators (new
and
delete)
are
overloaded as member or non-member operators
(as global operators).
Importantly,
whenever
we use these operators with
classes, we must know their sequence of
events
that is
always there with these
operators. For new
operator,
memory block is
allocated
first
before calling the
constructor. For delete
operator,
destructor for the object is
called
first
and then the memory
block is deallocated. Importantly,
our overloaded operators
of
new
and
delete
only
takes the part of allocation
and deallocation respectively and calls
to
constructors
and destructors remain intact in the
same sequence.
Because
of this sequence of events,
the behavior of these new
and
delete
operators
is
different
from the built-in operators
of new
and
delete. The
overloaded new
operator
returns
void
* when it
is overloaded as non-member (global).
However, it returns an
object pointer
like the built-in new
operator,
when overloaded as a member
function.
It is
important to understand that
these operator functions behave
like static
functions
when
overloaded as member functions despite
not being declared with
static
keyword.
static
functions
can access only the
static
data
members that are available to
the class
even
before an object is created. As we
already know that new
operator
is called to
construct
objects, it has to be available before
the object is constructed. Similarly,
the
delete
operator
is called when the object has
already been destructed by
calling destructor
of the
object.
Example
of Overloading new and
delete as Non-members
Suppose
we want new
to
initialize the contents of a
memory block to zero
before
returning
it. We can achieve this by
writing the operator functions as
follows:
/* The
following program explains the customized
new and delete operators
*/
#include
<iostream.h>
#include
<stdlib.h>
#include
<stddef.h>
Page
438
CS201
Introduction to Programming
//
------------- Overloaded new
operator
void *
operator new ( size_t size )
{
void *
rtn = calloc( 1, size ); // Calling
calloc() to allocate and
initialize memory
return
rtn;
}
//
----------- Overloaded delete
operator
void
operator delete ( void * ptr
)
{
free( ptr
); // Calling free() to deallocate
memory
}
void
main()
{
//
Allocate a zero-filled
array
int
*ip = new int[10];
//
Display the array
for (
int i = 0; i < 10; i ++ )
cout
<< " " << ip[i];
//
Release the memory
delete []
ip;
}
The
output of the program is as
follows.
0000000000
Note
that the new
operator
takes a parameter of type size_t. This
parameter holds the
size
of the
object being allocated, and
the compiler automatically
sets its value whenever
we
use
new. Also
note that the new
operator
returns a void
pointer.
Any new
operator
we
write
must have this parameter and
return type.
In this
particular example, new
calls
the standard C function calloc
to
allocate memory
and
initialize it to zero.
The
delete
operator
takes a void
pointer as a
parameter. This parameter points to
the
block to
be deallocated. Also note
that the delete
operator
has a void
return
type. Any
delete
operator
we write, must have this
parameter and return
type.
In this
example, delete
simply
calls the standard C function
free
to
deallocate the
memory.
Example
of Overloading new and
delete as Members
// Class-specific
new and delete
operators
Page
439
CS201
Introduction to Programming
#include
<iostream.h>
#include
<string.h>
#include
<stddef.h>
const
int MAXNAMES = 100;
class
Name
{
public:
Name(
const char *s ) { strncpy( name, s, 25 );
}
void
display() const { cout <<
'\n' << name; }
void *
operator new ( size_t size
);
void
operator delete( void * ptr
);
~Name()
{}; // do-nothing destructor
private:
char
name[25];
};
//
-------- Simple memory pool
to handle fixed number of
Names
char
pool[MAXNAMES] [sizeof( Name
)];
int
inuse[MAXNAMES];
//
-------- Overloaded new
operator for the Name
class
void *
Name :: operator new( size_t size
)
{
for(
int p = 0; p < MAXNAMES; p++
)
if(
!inuse[p] )
{
inuse[p]
= 1;
return
pool + p;
}
return
0;
}
//
--------- Overloaded delete
operator for the Names
class
void
Name :: operator delete(
void *ptr )
{
inuse[((char
*)ptr - pool[0]) / sizeof(
Name )] = 0;
}
void
main()
{
Name *
directory[MAXNAMES];
char
name[25];
for(
int i = 0; i < MAXNAMES; i++
)
{
Page
440
CS201
Introduction to Programming
cout
<< "Enter name # " << i+1
<< ": ";
cin
>> name;
directory[i]
= new Name( name );
}
for( i =
0; i < MAXNAMES; i++ )
{
directory[i]->display();
delete
directory[i];
}
}
The
output of the above program is given
below.
Enter
name # 1: ahmed
Enter
name # 2: ali
Enter
name # 3: jamil
Enter
name # 4: huzaifa
Enter
name # 5: arshad
Enter
name # 6: umar
Enter
name # 7: saleem
Enter
name # 8: kamran
Enter
name # 9: babar
Enter
name # 10: wasim
ahmed
ali
jamil
huzaifa
arshad
umar
saleem
kamran
babar
wasim
This
program declares a global array called
pool
that
can store all the
Name
objects
expected.
There is also an associated
integer array called inuse, which
contains true/false
flags
that indicate whether the corresponding
entry in the pool
is in
use.
When the
statement directory[i]
= new Name( name ) is
executed, the compiler
calls
the
class's new
operator.
The new
operator
finds an unused entry in pool, marks it as
used,
and
returns its address. Then
the compiler calls Name's
constructor, which uses
that
memory
and initializes it with a
character string. Finally, a pointer to
the resulting object
is
assigned to an entry in directory.
When the
statement delete
directory[i] is
executed, the compiler calls
Name
's
destructor.
In this
example, the destructor does nothing; it
is defined only as a placeholder.
Then the
Page
441
CS201
Introduction to Programming
compiler
calls the class's delete
operator.
The delete
operator
finds the specified
object's
location in
the array and marks it as
unused, so the space is available
for subsequent
allocations.
Note
that new
is
called before the constructor,
and that delete
is
called after the
destructor.
Overloading
[ ] Operator to Create
Arrays
We know
that if we overload operators
new
and
delete
for a
class, those
overloaded
operators
are called whenever we create an object
of that class. However, when
we create
an array
of those class objects, the
global operator new( )
is
called to allocate enough
storage
for the array all at
once, and the global
operator
delete( ) is called to
release that
storage.
We can
control the allocation of
arrays of objects by overloading the
special array
versions
of operator
new[ ] and
operator
delete[ ] for
the class.
Previously,
while employing global new
operator
to create an array of objects, we
used to
tell
the delete
operator
by using the array operator(
[]
) to
deallocate memory for
an
array.
But it is our responsibility to provide
or to overload different type of new
and
different
type of delete.
There is
a common problem when working
with arrays. While
traversing elements
from
the
array, we might run off
the end of the array.
This problem might not be
caught by the
compiler.
However, some latest
compilers might be able to
detect this.
int
iarray [10] ;
for
( int i = 0; i < 100; i++ )
{
//
Some code to manipulate array
elements
}
If a variable is
used in the condition of the
loop instead of the constant
value, the
probability
of that error increases.
Variable might have an
entirely different value
than
that
anticipated by the programmer.
We can
overcome this problem of array
bound by overloading array operator
`[]'.
As
usual
before overloading, we should be clear
about the functionality or
semantics of the
array
operator. We use array
operator to access an element of
array. For example,
when
we write
iarray[5], we are
accessing the 6th element
inside array iarray. As we
want to
check
for validity of index every
time, an array element is
accessed. We can do this
by
declaring
the size of the array using
#define
and
checking the index
against
the size every
time
the array is
accessed.
#define
MAXNUM 1000
int
iarray [MAXNUM];
Page
442
CS201
Introduction to Programming
Below is
the syntax of declaration line of
overloaded array
operator:
int&
operator [] ( int index ) ;
In the
body of this operator, we can
check whether the index
is
greater or equal to
the
MAXNUM
constant.
If this is the case, the
function may throw an exception. At
the
moment,
the function only displays an
error message. If index
is
less than MAXNUM
and
greater
than or equal to zero, a
reference to the value at
the index
location
is returned.
Let's
write a class IntArray
and
see the array
manipulation.
/*
The
following example defines the
IntArray class, where each object
contains
an array
of integers. This class
overloads the [] operator to
perform
range
checking.
*/
#include
<iostream.h>
#include
<string.h>
class
IntArray
{
public:
IntArray(
int len );
int
getLength( ) const;
int &
operator[] ( int index );
~IntArray(
);
private:
int
length;
int
*aray;
};
//
------------ Constructor
IntArray
:: IntArray( int len )
{
if( len
> 0 )
{
length =
len;
aray =
new int[len];
//
initialize contents of array to
zero
memset(
aray, 0, sizeof( int ) * len
);
}
else
{
length =
0;
aray =
0;
}
Page
443
CS201
Introduction to Programming
}
//
------------ Function to return
length
inline
int IntArray :: getLength()
const
{
return
length;
}
//
------------ Overloaded subscript
operator
//
Returns a reference
int &
IntArray :: operator []( int
index )
{
static
int dummy = 0;
if(
(index = 0) &&
(index
< length) )
return
aray[index];
else
{
cout
<< "Error: index out of
range.\n";
return
dummy;
}
}
//
------------ Destructor
IntArray
:: ~IntArray()
{
delete
aray;
}
void
main()
{
IntArray
numbers( 10 );
int
i;
for( i =
0; i < 10; i ++ )
numbers[i]
= i;
// Use
numbers[i] as lvalue
for( i =
0; i < 10; i++ )
cout
<< numbers[i] << '\n';
}
This
program first declares numbers
of
type IntArray
object
that can hold ten
integers.
Later, it
assigns a value to each
element in the array. Note
that the array
expression
appears
on the left side of the
assignment. This is legal as the
operator[]
function
returns
a
reference to an integer. This
means the expression numbers[i]
acts as
an alias for an
element
in the private array and it
can be the recipient of an assignment
statement. In this
situation, returning a
reference is not simply more
efficient but also
necessary.
Page
444
CS201
Introduction to Programming
The
operator[]
function
checks whether the specified index
value
is within range or not.
If it is
within the range, the
function returns a reference to
the corresponding element
in
the
private array. If it is not, the
function prints out an error
message and returns a
reference
to a static
integer.
This prevents out-of-range
array references from
overwriting
other
regions of memory while
causing unexpected program
behavior.
Tips
·
The
default
constructor is
defined by the C++ compiler
automatically for
every
class
that has no default constructor
(parameterless constructor) defined
already.
·
The
default constructor (parameterless
constructor) is called for each
element in
the
array allocated with new.
·
The
new
operator
returns a void
*,
accepts a parameter of type size_t.
·
The
delete
operator
returns nothing (void) and
accepts a pointer of void
* to
the
memory
block.
·
With
new
operator
function, a block of memory is
allocated first and
then
constructor
is called.
·
With
delete
operator,
destructor of the object is called first
and then memory
block is
deallocated.
·
By overloading
new
and
delete
operators,
only allocation and deallocation
part
can be
overridden.
·
The
same pointer that is returned by
the new
operator,
is passed as an argument to
the
delete
operator.
These rules apply to both, if
operators (new
and
delete)
are
overloaded
as member or non-member operators (as
global operators).
·
By overloading
the array operator ( [] ),
one can implement mechanism to
check
for
array bound.
Page
445
Table of Contents:
|
|||||