Inapoi
Inainte
Cuprins
Running out of storage
What happens when the operator
new cannot find a contiguous
block of storage large enough to hold the desired object? A special function
called the new-handler is called. Or rather, a
pointer to a function is checked, and if the pointer is nonzero, then the
function it points to is called.
The default behavior for the new-handler
is to throw an exception, a subject covered in
Volume 2. However, if you’re using heap allocation in your program,
it’s wise to at least replace the new-handler with a message that says
you’ve run out of memory and then aborts the program. That way, during
debugging, you’ll have a clue about what happened. For the final program
you’ll want to use more robust recovery.
You replace the new-handler by including
new.h and then calling set_new_handler( ) with the address of
the function you want installed:
//: C13:NewHandler.cpp
// Changing the new-handler
#include <iostream>
#include <cstdlib>
#include <new>
using namespace std;
int count = 0;
void out_of_memory() {
cerr << "memory exhausted after " << count
<< " allocations!" << endl;
exit(1);
}
int main() {
set_new_handler(out_of_memory);
while(1) {
count++;
new int[1000]; // Exhausts memory
}
} ///:~
The new-handler function must take no
arguments and have a void return value. The while loop will keep
allocating int objects (and throwing away their return addresses) until
the free store is exhausted. At the very next call to new, no storage can
be allocated, so the new-handler will be called.
The behavior of the new-handler is tied
to operator new, so if you overload operator new (covered in the
next section) the new-handler will not be called by default. If you still want
the new-handler to be called you’ll have to write the code to do so inside
your overloaded operator new.
Of course, you can write more
sophisticated new-handlers, even one to try to reclaim memory (commonly known as
a garbage collector). This is not a job for the
novice
programmer.
Overloading new &
delete
When you create a
new-expression, two things occur. First, storage is
allocated using the operator new, then the constructor is called. In a
delete-expression, the destructor is called, then
storage is deallocated using the operator delete. The constructor and
destructor calls are never under your control (otherwise you might accidentally
subvert them), but you can change the storage allocation functions
operator new and operator delete.
The memory allocation
system used by new and
delete is designed for general-purpose use. In special situations,
however, it doesn’t serve your needs. The most common reason to change the
allocator is efficiency: You might be creating and
destroying so many objects of a particular class that it has become a speed
bottleneck. C++ allows you to overload new and delete to implement
your own storage allocation scheme, so you can handle problems like
this.
Another issue is
heap fragmentation. By
allocating objects of different sizes it’s possible to break up the heap
so that you effectively run out of storage. That is, the storage might be
available, but because of fragmentation no piece is big enough to satisfy your
needs. By creating your own allocator for a particular class, you can ensure
this never happens.
In 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 take the same amount of
time, and there’s 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.
When you overload operator new and
operator delete, it’s important to remember that you’re
changing only the way raw storage is allocated. The compiler will simply
call your new instead of the default version to allocate storage, then
call the constructor for that storage. So, although the compiler allocates
storage and calls the constructor when it sees new, all you can
change when you overload new is the storage allocation portion.
(delete has a similar limitation.)
When you overload operator
new, you also replace the behavior when it runs out of memory, so you
must decide what to do in your operator new: return zero, write a loop to
call the new-handler and retry allocation, or (typically) throw a
bad_alloc exception (discussed in Volume 2, available at
www.BruceEckel.com).
Overloading new and delete
is like overloading any other operator. However, you have a choice of
overloading the global allocator or using a different allocator for a particular
class.
Overloading global new &
delete
This is the drastic approach, when the
global versions of new and delete are unsatisfactory for the whole
system. If you overload the global versions, you make the defaults completely
inaccessible – you can’t even call them from inside your
redefinitions.
The overloaded new must take an
argument of size_t (the Standard C standard type
for sizes). This argument is generated and passed to you by the compiler and is
the size of the object you’re responsible for allocating. You must return
a pointer either to an object of that size (or bigger, if you have some reason
to do so), or to zero if you can’t find the memory (in which case the
constructor is not called!). However, if you can’t find the memory,
you should probably do something more informative than just returning zero, like
calling the new-handler or throwing an exception, to signal that there’s a
problem.
The return value of operator new
is a void*, not a pointer to any particular type. All you’ve
done is produce memory, not a finished object – that doesn’t happen
until the constructor is called, an act the compiler guarantees and which is out
of your control.
The operator delete takes a
void* to memory that was allocated by operator new. It’s a
void* because operator delete only gets the pointer after
the destructor is called, which removes the object-ness from the piece of
storage. The return type is void.
Here’s a simple example showing how
to overload the global new and delete:
//: C13:GlobalOperatorNew.cpp
// Overload global new/delete
#include <cstdio>
#include <cstdlib>
using namespace std;
void* operator new(size_t sz) {
printf("operator new: %d Bytes\n", sz);
void* m = malloc(sz);
if(!m) puts("out of memory");
return m;
}
void operator delete(void* m) {
puts("operator delete");
free(m);
}
class S {
int i[100];
public:
S() { puts("S::S()"); }
~S() { puts("S::~S()"); }
};
int main() {
puts("creating & destroying an int");
int* p = new int(47);
delete p;
puts("creating & destroying an s");
S* s = new S;
delete s;
puts("creating & destroying S[3]");
S* sa = new S[3];
delete []sa;
} ///:~
Here you can see the general form for
overloading new and delete. These use the Standard C library
functions malloc( ) and
free( ) for the allocators (which is
probably what the default new and delete use as well!). However,
they also print messages about what they are doing. Notice that
printf( ) and
puts( ) are used rather than
iostreams. This is because when an iostream
object is created (like the global cin,
cout, and cerr), it calls new to allocate memory. With
printf( ), you don’t get into a deadlock because it
doesn’t call new to initialize itself.
In main( ), objects of
built-in types are created to prove that the overloaded new and
delete are also called in that case. Then a single object of type
S is created, followed by an array of S. For the array,
you’ll see from the number of bytes requested that extra memory is
allocated to store information (inside the array) about the number of objects it
holds. In all cases, the global overloaded versions of new and
delete are
used.
Overloading new & delete for a
class
Although you don’t have to
explicitly say static, when you overload new and delete for
a class, you’re creating static member functions. As before, the
syntax is the same as overloading any other operator. When the compiler sees you
use new to create an object of your class, it chooses the member
operator new over the global version. However, the global versions of
new and delete are used for all other types of objects (unless
they have their own new and delete).
In the following example, a primitive
storage allocation system
is
created for the class Framis. A chunk of memory is set aside in the
static data area at program start-up, and that memory is used to allocate space
for objects of type Framis. To determine which blocks have been
allocated, a simple array of bytes is used, one byte for each
block:
//: C13:Framis.cpp
// Local overloaded new & delete
#include <cstddef> // Size_t
#include <fstream>
#include <iostream>
#include <new>
using namespace std;
ofstream out("Framis.out");
class Framis {
enum { sz = 10 };
char c[sz]; // To take up space, not used
static unsigned char pool[];
static bool alloc_map[];
public:
enum { psize = 100 }; // frami allowed
Framis() { out << "Framis()\n"; }
~Framis() { out << "~Framis() ... "; }
void* operator new(size_t) throw(bad_alloc);
void operator delete(void*);
};
unsigned char Framis::pool[psize * sizeof(Framis)];
bool Framis::alloc_map[psize] = {false};
// Size is ignored -- assume a Framis object
void*
Framis::operator new(size_t) throw(bad_alloc) {
for(int i = 0; i < psize; i++)
if(!alloc_map[i]) {
out << "using block " << i << " ... ";
alloc_map[i] = true; // Mark it used
return pool + (i * sizeof(Framis));
}
out << "out of memory" << endl;
throw bad_alloc();
}
void Framis::operator delete(void* m) {
if(!m) return; // Check for null pointer
// Assume it was created in the pool
// Calculate which block number it is:
unsigned long block = (unsigned long)m
- (unsigned long)pool;
block /= sizeof(Framis);
out << "freeing block " << block << endl;
// Mark it free:
alloc_map[block] = false;
}
int main() {
Framis* f[Framis::psize];
try {
for(int i = 0; i < Framis::psize; i++)
f[i] = new Framis;
new Framis; // Out of memory
} catch(bad_alloc) {
cerr << "Out of memory!" << endl;
}
delete f[10];
f[10] = 0;
// Use released memory:
Framis* x = new Framis;
delete x;
for(int j = 0; j < Framis::psize; j++)
delete f[j]; // Delete f[10] OK
} ///:~
The pool of memory for the Framis
heap is created by allocating an array of bytes large enough to hold
psize Framis objects. The allocation map is psize elements
long, so there’s one bool for every block. All the values in the
allocation map are initialized to false using the aggregate
initialization trick of setting the first element so the compiler automatically
initializes all the rest to their normal default value (which is false,
in the case of bool).
The local operator new has the
same syntax as the global one. All it does is search through the allocation map
looking for a false value, then sets that location to true to
indicate it’s been allocated and returns the address of the corresponding
memory block. If it can’t find any memory, it issues a message to the
trace file and throws a bad_alloc exception.
This is the first example of
exceptions that you’ve seen in this book. Since
detailed discussion of exceptions is delayed until Volume 2, this is a very
simple use of them. In operator new there are two artifacts of exception
handling. First, the function argument list is followed by
throw(bad_alloc), which
tells the compiler and the reader that this function may throw an exception of
type bad_alloc. Second, if there’s no more
memory the function actually does throw the exception in the statement throw
bad_alloc. When an exception is thrown, the function stops executing and
control is passed to an exception handler, which is expressed as a
catch clause.
In main( ), you see the other
part of the picture, which is the try-catch clause. The
try block is surrounded
by braces and contains all the code that may throw exceptions – in this
case, any call to new that involves Framis objects. Immediately
following the try block is one or more
catch clauses, each one
specifying the type of exception that they catch. In this case,
catch(bad_alloc) says that that bad_alloc exceptions will be
caught here. This particular catch clause is only executed when a
bad_alloc exception is thrown, and execution continues after the end of
the last catch clause in the group (there’s only one here, but
there could be more).
In this example, it’s OK to use
iostreams because the global operator new and
delete are untouched.
The operator delete assumes the
Framis address was created in the pool. This is a fair assumption,
because the local operator new will be called whenever you create a
single Framis object on the heap – but not an array of them: global
new is used for arrays. So the user might accidentally have called
operator delete without using the empty bracket syntax to indicate array
destruction. This would cause a problem. Also, the user might be deleting a
pointer to an object created on the stack. If you think these things could
occur, you might want to add a line to make sure the address is within the pool
and on a correct boundary (you may also begin to see the potential of
overloaded new and delete for finding
memory leaks).
operator delete calculates the
block in the pool that this pointer represents, and then sets the allocation
map’s flag for that block to false to indicate the block has been
released.
In main( ), enough
Framis objects are dynamically allocated to run out of memory; this
checks the out-of-memory behavior. Then one of the objects is freed, and another
one is created to show that the released memory is reused.
Because this allocation scheme is
specific to Framis objects, it’s probably much faster than the
general-purpose memory allocation scheme used for the default new and
delete. However, you should note that it doesn’t automatically work
if inheritance is used (inheritance is covered in Chapter
14).
Overloading new & delete for
arrays
If you overload operator new and
delete for a class, those operators are called whenever you create an
object of that class. However, if you 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. You can control the allocation of arrays of
objects by overloading the special array versions of operator new[ ] and
operator delete[ ] for the class. Here’s an example that shows when
the two different versions are called:
//: C13:ArrayOperatorNew.cpp
// Operator new for arrays
#include <new> // Size_t definition
#include <fstream>
using namespace std;
ofstream trace("ArrayOperatorNew.out");
class Widget {
enum { sz = 10 };
int i[sz];
public:
Widget() { trace << "*"; }
~Widget() { trace << "~"; }
void* operator new(size_t sz) {
trace << "Widget::new: "
<< sz << " bytes" << endl;
return ::new char[sz];
}
void operator delete(void* p) {
trace << "Widget::delete" << endl;
::delete []p;
}
void* operator new[](size_t sz) {
trace << "Widget::new[]: "
<< sz << " bytes" << endl;
return ::new char[sz];
}
void operator delete[](void* p) {
trace << "Widget::delete[]" << endl;
::delete []p;
}
};
int main() {
trace << "new Widget" << endl;
Widget* w = new Widget;
trace << "\ndelete Widget" << endl;
delete w;
trace << "\nnew Widget[25]" << endl;
Widget* wa = new Widget[25];
trace << "\ndelete []Widget" << endl;
delete []wa;
} ///:~
Here, the global versions of new
and delete are called so the effect is the same as having no overloaded
versions of new and delete except that trace information is added.
Of course, you can use any memory allocation scheme you want in the overloaded
new and delete.
You can see that the syntax of array
new and delete is the same as for the individual object versions
except for the addition of the brackets. In both cases you’re handed the
size of the memory you must allocate. The size handed to the array version will
be the size of the entire array. It’s worth keeping in mind that the
only thing the overloaded operator new is required to do is hand
back a pointer to a large enough memory block. Although you may perform
initialization on that memory, normally that’s the job of the constructor
that will automatically be called for your memory by the
compiler.
The constructor and destructor simply
print out characters so you can see when they’ve been called. Here’s
what the trace file looks like for one compiler:
new Widget
Widget::new: 40 bytes
*
delete Widget
~Widget::delete
new Widget[25]
Widget::new[]: 1004 bytes
*************************
delete []Widget
~~~~~~~~~~~~~~~~~~~~~~~~~Widget::delete[]
Creating an individual object requires 40
bytes, as you might expect. (This machine uses four bytes for an int.)
The operator new is called, then the constructor (indicated by the
*). In a complementary fashion, calling delete causes the
destructor to be called, then the operator delete.
When an array of Widget objects is
created, the array version of operator new is used, as promised. But
notice that the size requested is four more bytes than expected. This extra four
bytes is where the system keeps information about the array, in particular, the
number of objects in the array. That way, when you say
delete []Widget;
the brackets tell the compiler it’s
an array of objects, so the compiler generates code to look for the number of
objects in the array and to call the destructor that many times. You can see
that, even though the array operator new and operator delete are
only called once for the entire array chunk, the default constructor and
destructor are called for each object in the
array.
 |
|