However, if you manipulate an object through a void* as in the case of delete b, the only thing that happens is that the storage for the Object is released – but the destructor is not
Trang 1initialization before the object is used, thus reintroducing a major
source of bugs
It also turns out that many programmers seem to find C’s dynamic
memory functions too confusing and complicated; it’s not
uncommon to find C programmers who use virtual memory
machines allocating huge arrays of variables in the static storage
area to avoid thinking about dynamic memory allocation Because
C++ is attempting to make library use safe and effortless for the
casual programmer, C’s approach to dynamic memory is
unacceptable
operator new
The solution in C++ is to combine all the actions necessary to create
an object into a single operator called new When you create an
object with new (using a new-expression), it allocates enough storage
on the heap to hold the object and calls the constructor for that
storage Thus, if you say
MyType *fp = new MyType(1,2);
at runtime, the equivalent of malloc(sizeof(MyType)) is called
(often, it is literally a call to malloc( )), and the constructor for
MyType is called with the resulting address as the this pointer,
using (1,2) as the argument list By the time the pointer is assigned
to fp, it’s a live, initialized object – you can’t even get your hands
on it before then It’s also automatically the proper MyType type so
no cast is necessary
The default new checks to make sure the memory allocation was
successful before passing the address to the constructor, so you
don’t have to explicitly determine if the call was successful Later in
the chapter you’ll find out what happens if there’s no memory left
You can create a new-expression using any constructor available
for the class If the constructor has no arguments, you write the
new-expression without the constructor argument list:
Trang 2MyType *fp = new MyType;
Notice how simple the process of creating objects on the heap
becomes – a single expression, with all the sizing, conversions, and safety checks built in It’s as easy to create an object on the heap as
it is on the stack
operator delete
The complement to the new-expression is the delete-expression,
which first calls the destructor and then releases the memory (often
with a call to free( )) Just as a new-expression returns a pointer to
the object, a delete-expression requires the address of an object
delete fp;
This destructs and then releases the storage for the dynamically
allocated MyType object created earlier
delete can be called only for an object created by new If you
malloc( ) (or calloc( ) or realloc( )) an object and then delete it, the
behavior is undefined Because most default implementations of
new and delete use malloc( ) and free( ), you’d probably end up
releasing the memory without calling the destructor
If the pointer you’re deleting is zero, nothing will happen For this reason, people often recommend setting a pointer to zero
immediately after you delete it, to prevent deleting it twice
Deleting an object more than once is definitely a bad thing to do, and will cause problems
Trang 3operator<<(std::ostream& os, const Tree* t) {
return os << "Tree height is: "
We can prove that the constructor is called by printing out the
value of the Tree Here, it’s done by overloading the operator<< to
use with an ostream and a Tree* Note, however, that even though
the function is declared as a friend, it is defined as an inline! This is
a mere convenience – defining a friend function as an inline to a
class doesn’t change the friend status or the fact that it’s a global
function and not a class member function Also notice that the
return value is the result of the entire output expression, which is
an ostream& (which it must be, to satisfy the return value type of
the function)
Memory manager overhead
When you create automatic objects on the stack, the size of the
objects and their lifetime is built right into the generated code,
because the compiler knows the exact type, quantity, and scope
Creating objects on the heap involves additional overhead, both in
Trang 4time and in space Here’s a typical scenario (You can replace
malloc( ) with calloc( ) or realloc( ).)
You call malloc( ), which requests a block of memory from the pool (This code may actually be part of malloc( ).)
The pool is searched for a block of memory large enough to satisfy the request This is done by checking a map or directory of some sort that shows which blocks are currently in use and which are available It’s a quick process, but it may take several tries so it might not be deterministic – that is, you can’t necessarily count on
malloc( ) always taking exactly the same amount of time
Before a pointer to that block is returned, the size and location of
the block must be recorded so further calls to malloc( ) won’t use it, and so that when you call free( ), the system knows how much
memory to release
The way all this is implemented can vary widely For example, there’s nothing to prevent primitives for memory allocation being implemented in the processor If you’re curious, you can write test
programs to try to guess the way your malloc( ) is implemented
You can also read the library source code, if you have it (the GNU
C sources are always available)
Early examples redesigned
Using new and delete, the Stash example introduced previously in
this book can be rewritten using all the features discussed in the book so far Examining the new code will also give you a useful review of the topics
At this point in the book, neither the Stash nor Stack classes will
“own” the objects they point to; that is, when the Stash or Stack object goes out of scope, it will not call delete for all the objects it
Trang 5only thing that happens is the memory gets released, because
there’s no type information and no way for the compiler to know
what destructor to call
delete void* is probably a bug
It’s worth making a point that if you call delete for a void*, it’s
almost certainly going to be a bug in your program unless the
destination of that pointer is very simple; in particular, it should
not have a destructor Here’s an example to show you what
void* data; // Some storage
const int size;
const char id;
public:
Object(int sz, char c) : size(sz), id(c) {
data = new char[size];
cout << "Constructing object " << id
<< ", size = " << size << endl;
}
~Object() {
cout << "Destructing object " << id << endl;
delete []data; // OK, just releases storage,
// no destructor calls are necessary
Trang 6The class Object contains a void* that is initialized to “raw” data (it doesn’t point to objects that have destructors) In the Object
destructor, delete is called for this void* with no ill effects, since
the only thing we need to happen is for the storage to be released
However, in main( ) you can see that it’s very necessary that delete
know what type of object it’s working with Here’s the output:
Constructing object a, size = 40
Destructing object a
Constructing object b, size = 40
Because delete a knows that a points to an Object, the destructor is called and thus the storage allocated for data is released However,
if you manipulate an object through a void* as in the case of delete
b, the only thing that happens is that the storage for the Object is
released – but the destructor is not called so there is no release of
the memory that data points to When this program compiles, you
probably won’t see any warning messages; the compiler assumes you know what you’re doing So you get a very quiet memory leak
If you have a memory leak in your program, search through all the
delete statements and check the type of pointer being deleted If it’s
a void* then you’ve probably found one source of your memory
leak (C++ provides ample other opportunities for memory leaks, however)
Cleanup responsibility with pointers
To make the Stash and Stack containers flexible (able to hold any type of object), they will hold void pointers This means that when
a pointer is returned from the Stash or Stack object, you must cast
it to the proper type before using it; as seen above, you must also cast it to the proper type before deleting it or you’ll get a memory leak
The other memory leak issue has to do with making sure that
Trang 7container The container cannot “own” the pointer because it holds
it as a void* and thus cannot perform the proper cleanup The user
must be responsible for cleaning up the objects This produces a
serious problem if you add pointers to objects created on the stack
and objects created on the heap to the same container because a
delete-expression is unsafe for a pointer that hasn’t been allocated
on the heap (And when you fetch a pointer back from the
container, how will you know where its object has been allocated?)
Thus, you must be sure that objects stored in the following versions
of Stash and Stack are made only on the heap, either through
careful programming or by creating classes that can only be built
on the heap
It’s also important to make sure that the client programmer takes
responsibility for cleaning up all the pointers in the container
You’ve seen in previous examples how the Stack class checks in its
destructor that all the Link objects have been popped For a Stash
of pointers, however, another approach is needed
Stash for pointers
This new version of the Stash class, called PStash, holds pointers to
objects that exist by themselves on the heap, whereas the old Stash
in earlier chapters copied the objects by value into the Stash
container Using new and delete, it’s easy and safe to hold pointers
to objects that have been created on the heap
Here’s the header file for the “pointer Stash”:
int quantity; // Number of storage spaces
int next; // Next empty space
// Pointer storage:
void** storage;
Trang 8void inflate(int increase);
public:
PStash() : quantity(0), storage(0), next(0) {}
~PStash();
int add(void* element);
void* operator[](int index) const; // Fetch
// Remove the reference from this PStash:
void* remove(int index);
// Number of elements in Stash:
int count() const { return next; }
};
#endif // PSTASH_H ///:~
The underlying data elements are fairly similar, but now storage is
an array of void pointers, and the allocation of storage for that array is performed with new instead of malloc( ) In the expression
void** st = new void*[quantity + increase];
the type of object allocated is a void*, so the expression allocates an array of void pointers
The destructor deletes the storage where the void pointers are held
rather than attempting to delete what they point at (which, as
previously noted, will release their storage and not call the
destructors because a void pointer has no type information)
The other change is the replacement of the fetch( ) function with operator[ ], which makes more sense syntactically Again, however,
a void* is returned, so the user must remember what types are
stored in the container and cast the pointers when fetching them out (a problem that will be repaired in future chapters)
Here are the member function definitions:
//: C13:PStash.cpp {O}
// Pointer Stash definitions
#include "PStash.h"
#include " /require.h"
Trang 9using namespace std;
int PStash::add(void* element) {
const int inflateSize = 10;
// Operator overloading replacement for fetch
void* PStash::operator[](int index) const {
require(index >= 0,
"PStash::operator[] index negative");
if(index >= next)
return 0; // To indicate the end
// Produce pointer to desired element:
void PStash::inflate(int increase) {
const int psz = sizeof(void*);
void** st = new void*[quantity + increase];
memset(st, 0, (quantity + increase) * psz);
memcpy(st, storage, quantity * psz);
quantity += increase;
delete []storage; // Old storage
storage = st; // Point to new memory
} ///:~
Trang 10The add( ) function is effectively the same as before, except that a
pointer is stored instead of a copy of the whole object
The inflate( ) code is modified to handle the allocation of an array
of void* instead of the previous design, which was only working
with raw bytes Here, instead of using the prior approach of
copying by array indexing, the Standard C library function
memset( ) is first used to set all the new memory to zero (this is not strictly necessary, since the PStash is presumably managing all the
memory correctly – but it usually doesn’t hurt to throw in a bit of
extra care) Then memcpy( ) moves the existing data from the old location to the new Often, functions like memset( ) and memcpy( )
have been optimized over time, so they may be faster than the
loops shown previously But with a function like inflate( ) that will
probably not be used that often you may not see a performance difference However, the fact that the function calls are more
concise than the loops may help prevent coding errors
To put the responsibility of object cleanup squarely on the
shoulders of the client programmer, there are two ways to access
the pointers in the PStash: the operator[], which simply returns the
pointer but leaves it as a member of the container, and a second
member function remove( ), which returns the pointer but also
removes it from the container by assigning that position to zero
When the destructor for PStash is called, it checks to make sure
that all object pointers have been removed; if not, you’re notified so you can prevent a memory leak (more elegant solutions will be forthcoming in later chapters)
Trang 11// 'new' works with built-in types, too Note
// the "pseudo-constructor" syntax:
// Print out the strings:
for(int u = 0; stringStash[u]; u++)
As before, Stashes are created and filled with information, but this
time the information is the pointers resulting from
new-expressions In the first case, note the line:
intStash.add(new int(i));
The expression new int(i) uses the pseudo-constructor form, so
storage for a new int object is created on the heap, and the int is
initialized to the value i
During printing, the value returned by PStash::operator[ ] must be
cast to the proper type; this is repeated for the rest of the PStash
Trang 12objects in the program It’s an undesirable effect of using void
pointers as the underlying representation and will be fixed in later chapters
The second test opens the source code file and reads it one line at a
time into another PStash Each line is read into a string using
getline( ), then a new string is created from line to make an
independent copy of that line If we just passed in the address of
line each time, we’d get a whole bunch of pointers pointing to line,
which would only contain the last line that was read from the file When fetching the pointers, you see the expression:
*(string*)stringStash[v]
The pointer returned from operator[ ] must be cast to a string* to give it the proper type Then the string* is dereferenced so the
expression evaluates to an object, at which point the compiler sees a
string object to send to cout
The objects created on the heap must be destroyed through the use
of the remove( ) statement or else you’ll get a message at runtime
telling you that you haven’t completely cleaned up the objects in
the PStash Notice that in the case of the int pointers, no cast is necessary because there’s no destructor for an int and all we need is
memory release:
delete intStash.remove(k);
However, for the string pointers, if you forget to do the cast you’ll
have another (quiet) memory leak, so the cast is essential:
delete (string*)stringStash.remove(k);
Some of these issues (but not all) can be removed using templates (which you’ll learn about in Chapter 16)
Trang 13new & delete for arrays
In C++, you can create arrays of objects on the stack or on the heap
with equal ease, and (of course) the constructor is called for each
object in the array There’s one constraint, however: There must be
a default constructor, except for aggregate initialization on the
stack (see Chapter 6), because a constructor with no arguments
must be called for every object
When creating arrays of objects on the heap using new, there’s
something else you must do An example of such an array is
MyType* fp = new MyType[100];
This allocates enough storage on the heap for 100 MyType objects
and calls the constructor for each one Now, however, you simply
have a MyType*, which is exactly the same as you’d get if you said
MyType* fp2 = new MyType;
to create a single object Because you wrote the code, you know that
fp is actually the starting address of an array, so it makes sense to
select array elements using an expression like fp[3] But what
happens when you destroy the array? The statements
delete fp2; // OK
delete fp; // Not the desired effect
look exactly the same, and their effect will be the same The
destructor will be called for the MyType object pointed to by the
given address, and then the storage will be released For fp2 this is
fine, but for fp this means that the other 99 destructor calls won’t be
made The proper amount of storage will still be released, however,
because it is allocated in one big chunk, and the size of the whole
chunk is stashed somewhere by the allocation routine
The solution requires you to give the compiler the information that
this is actually the starting address of an array This is
accomplished with the following syntax:
Trang 14delete []fp;
The empty brackets tell the compiler to generate code that fetches the number of objects in the array, stored somewhere when the array is created, and calls the destructor for that many array
objects This is actually an improved syntax from the earlier form, which you may still occasionally see in old code:
Making a pointer more like an array
As an aside, the fp defined above can be changed to point to
anything, which doesn’t make sense for the starting address of an array It makes more sense to define it as a constant, so any attempt
to modify the pointer will be flagged as an error To get this effect, you might try
int const* q = new int[10];
or
const int* q = new int[10];
but in both cases the const will bind to the int, that is, what is being
pointed to, rather than the quality of the pointer itself Instead, you
must say
int* const q = new int[10];
Now the array elements in q can be modified, but any change to q (like q++) is illegal, as it is with an ordinary array identifier
Trang 15Running 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
Trang 16} ///:~
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
Trang 17Another 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
Trang 18Overloading 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:
Trang 19puts("creating & destroying an int");
int* p = new int(47);
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
Trang 20array, 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
char c[sz]; // To take up space, not used
static unsigned char pool[];
static bool alloc_map[];
public:
Trang 21~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
for(int i = 0; i < Framis::psize; i++)
f[i] = new Framis;
new Framis; // Out of memory
// Use released memory:
Framis* x = new Framis;
Trang 22default 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
Trang 23clause 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)
Trang 24Overloading 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
void operator delete(void* p) {
trace << "Widget::delete" << endl;
void operator delete[](void* p) {
trace << "Widget::delete[]" << endl;
Trang 25};
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: