|
|
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 433
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 434
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 435
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 436
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 437
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 438
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 439
// ------------- 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.
0 0 0 0 0 0 0 0 0 0
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 440
#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 441
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 442
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 443
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 444
}
// ------------ 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 445
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. |
|
|
|