ZeePedia

Dynamic Object Creation:Early examples redesigned, new & delete for arrays

<< Operator Overloading:Overloadable operators, Overloading assignment
Inheritance & Composition:The constructor initializer list, Name hiding >>
img
13: Dynamic Object
Creation
Sometimes you know the exact quantity, type, and
lifetime of the objects in your program. But not always.
575
img
How many planes will an air-traffic system need to handle? How
many shapes will a CAD system use? How many nodes will there
be in a network?
To solve the general programming problem, it's essential that you
be able to create and destroy objects at runtime. Of course, C has
always provided the dynamic memory allocation functions malloc( )
and free( ) (along with variants of malloc( ) that allocate storage
from the heap (also called the free store) at runtime.
However, this simply won't work in C++. The constructor doesn't
allow you to hand it the address of the memory to initialize, and for
good reason. If you could do that, you might:
1.
Forget. Then guaranteed initialization of objects in C++
wouldn't be guaranteed.
2.
Accidentally do something to the object before you initialize
it, expecting the right thing to happen.
3.
Hand it the wrong-sized object.
And of course, even if you did everything correctly, anyone who
modifies your program is prone to the same errors. Improper
initialization is responsible for a large portion of programming
problems, so it's especially important to guarantee constructor calls
for objects created on the heap.
So how does C++ guarantee proper initialization and cleanup, but
allow you to create objects dynamically on the heap?
The answer is by bringing dynamic object creation into the core of
the language. malloc( )and free( ) are library functions, and thus
outside the control of the compiler. However, if you have an
operator to perform the combined act of dynamic storage allocation
and initialization and another operator to perform the combined act
of cleanup and releasing storage, the compiler can still guarantee
that constructors and destructors will be called for all objects.
576
Thinking in C++
img
In this chapter, you'll learn how C++'s new and delete elegantly
solve this problem by safely creating objects on the heap.
Object creation
When a C++ object is created, two events occur:
1.
Storage is allocated for the object.
2.
The constructor is called to initialize that storage.
By now you should believe that step two always happens. C++
enforces it because uninitialized objects are a major source of
program bugs. It doesn't matter where or how the object is created
­ the constructor is always called.
Step one, however, can occur in several ways, or at alternate times:
1.
Storage can be allocated before the program begins, in the
static storage area. This storage exists for the life of the
program.
2.
Storage can be created on the stack whenever a particular
execution point is reached (an opening brace). That storage is
released automatically at the complementary execution point
(the closing brace). These stack-allocation operations are built
into the instruction set of the processor and are very efficient.
However, you have to know exactly how many variables you
need when you're writing the program so the compiler can
generate the right code.
3.
Storage can be allocated from a pool of memory called the
heap (also known as the free store). This is called dynamic
memory allocation. To allocate this memory, a function is
called at runtime; this means you can decide at any time that
you want some memory and how much you need. You are
also responsible for determining when to release the
13: Dynamic Object Creation
577
img
memory, which means the lifetime of that memory can be as
long as you choose ­ it isn't determined by scope.
Often these three regions are placed in a single contiguous piece of
physical memory: the static area, the stack, and the heap (in an
order determined by the compiler writer). However, there are no
rules. The stack may be in a special place, and the heap may be
implemented by making calls for chunks of memory from the
operating system. As a programmer, these things are normally
shielded from you, so all you need to think about is that the
memory is there when you call for it.
C's approach to the heap
To allocate memory dynamically at runtime, C provides functions
in its standard library: malloc( )and its variants calloc( )and
realloc( )to produce memory from the heap, and free( ) to release
the memory back to the heap. These functions are pragmatic but
primitive and require understanding and care on the part of the
programmer. To create an instance of a class on the heap using C's
dynamic memory functions, you'd have to do something like this:
//: C13:MallocClass.cpp
// Malloc with class objects
// What you'd have to do if not for "new"
#include "../require.h"
#include <cstdlib> // malloc() & free()
#include <cstring> // memset()
#include <iostream>
using namespace std;
class Obj {
int i, j, k;
enum { sz = 100 };
char buf[sz];
public:
void initialize() { // Can't use constructor
cout << "initializing Obj" << endl;
i = j = k = 0;
memset(buf, 0, sz);
}
578
Thinking in C++
img
.
void destroy() const { // Can't use destructor
cout << "destroying Obj" << endl;
}
};
int main() {
Obj* obj = (Obj*)malloc(sizeof(Obj));
require(obj != 0);
obj->initialize();
// ... sometime later:
obj->destroy();
free(obj);
} ///:~
You can see the use of malloc( )to create storage for the object in
the line:
Obj* obj = (Obj*)malloc(sizeof(Obj));
Here, the user must determine the size of the object (one place for
an error). malloc( )returns a void* because it just produces a patch
of memory, not an object. C++ doesn't allow a void* to be assigned
to any other pointer, so it must be cast.
Because malloc( )may fail to find any memory (in which case it
returns zero), you must check the returned pointer to make sure it
was successful.
But the worst problem is this line:
Obj->initialize();
If users make it this far correctly, they must remember to initialize
the object before it is used. Notice that a constructor was not used
because the constructor cannot be called explicitly1 ­ it's called for
you by the compiler when an object is created. The problem here is
that the user now has the option to forget to perform the
1 There is a special syntax called placement new that allows you to call a constructor
for a pre-allocated piece of memory. This is introduced later in the chapter.
13: Dynamic Object Creation
579
img
initialization 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)) called
is
(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:
580
Thinking in C++
img
MyType *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.
A simple example
This example shows that initialization takes place:
//: C13:Tree.h
#ifndef TREE_H
#define TREE_H
#include <iostream>
13: Dynamic Object Creation
581
img
class Tree {
int height;
public:
Tree(int treeHeight) : height(treeHeight) {}
~Tree() { std::cout << "*"; }
friend std::ostream&
operator<<(std::ostream& os, const Tree* t) {
return os << "Tree height is: "
<< t->height << std::endl;
}
};
#endif // TREE_H ///:~
//: C13:NewAndDelete.cpp
// Simple demo of new & delete
#include "Tree.h"
using namespace std;
int main() {
Tree* t = new Tree(40);
cout << t;
delete t;
} ///:~
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
582
Thinking in C++
img
time 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
points to. The reason this is not possible is because, in an attempt to
be generic, they hold void pointers. If you delete a void pointer, the
13: Dynamic Object Creation
583
img
only 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
happens:
//: C13:BadVoidPointerDeletion.cpp
// Deleting void pointers can cause memory leaks
#include <iostream>
using namespace std;
class Object {
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
}
};
int main() {
Object* a = new Object(40, 'a');
delete a;
void* b = new Object(40, 'b');
delete b;
} ///:~
584
Thinking in C++
img
The 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 aknows 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
delete is actually called for each object pointer held in the
13: Dynamic Object Creation
585
img
container. 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":
//: C13:PStash.h
// Holds pointers instead of objects
#ifndef PSTASH_H
#define PSTASH_H
class PStash {
int quantity; // Number of storage spaces
int next; // Next empty space
// Pointer storage:
void** storage;
586
Thinking in C++
img
void 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"
#include <iostream>
#include <cstring> // 'mem' functions
13: Dynamic Object Creation
587
img
using namespace std;
int PStash::add(void* element) {
const int inflateSize = 10;
if(next >= quantity)
inflate(inflateSize);
storage[next++] = element;
return(next - 1); // Index number
}
// No ownership:
PStash::~PStash() {
for(int i = 0; i < next; i++)
require(storage[i] == 0,
"PStash not cleaned up");
delete []storage;
}
// 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:
return storage[index];
}
void* PStash::remove(int index) {
void* v = operator[](index);
// "Remove" the pointer:
if(v != 0) storage[index] = 0;
return v;
}
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
} ///:~
588
Thinking in C++
img
The 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).
A test
Here's the old test program for Stash rewritten for the PStash:
//: C13:PStashTest.cpp
//{L} PStash
// Test of pointer Stash
#include "PStash.h"
#include "../require.h"
#include <iostream>
13: Dynamic Object Creation
589
img
#include <fstream>
#include <string>
using namespace std;
int main() {
PStash intStash;
// 'new' works with built-in types, too. Note
// the "pseudo-constructor" syntax:
for(int i = 0; i < 25; i++)
intStash.add(new int(i));
for(int j = 0; j < intStash.count(); j++)
cout << "intStash[" << j << "] = "
<< *(int*)intStash[j] << endl;
// Clean up:
for(int k = 0; k < intStash.count(); k++)
delete intStash.remove(k);
ifstream in ("PStashTest.cpp");
assure(in, "PStashTest.cpp");
PStash stringStash;
string line;
while(getline(in, line))
stringStash.add(new string(line));
// Print out the strings:
for(int u = 0; stringStash[u]; u++)
cout << "stringStash[" << u << "] = "
<< *(string*)stringStash[u] << endl;
// Clean up:
for(int v = 0; v < stringStash.count(); v++)
delete (string*)stringStash.remove(v);
} ///:~
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
590
Thinking in C++
img
objects 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).
13: Dynamic Object Creation
591
img
new & 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:
592
Thinking in C++
img
delete []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:
delete [100]fp;
which forced the programmer to include the number of objects in
the array and introduced the possibility that the programmer
would get it wrong. The additional overhead of letting the compiler
handle it was very low, and it was considered better to specify the
number of objects in one place instead of two.
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.
13: Dynamic Object Creation
593
img
Running out of storage
What happens when the operator newcannot 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
}
594
Thinking in C++
img
} ///:~
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 newand 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.
13: Dynamic Object Creation
595
img
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 newand 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 operatornew, 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 ).
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.
596
Thinking in C++
img
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 newis 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 deletetakes a void* to memory that was allocated by
operator new It's a void* because operator deleteonly 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);
13: Dynamic Object Creation
597
img
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 iostreamobject 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
598
Thinking in C++
img
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 newover 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"; }
13: Dynamic Object Creation
599
img
~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;
600
Thinking in C++
img
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 newhas 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_allocexception.
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 newthere 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_allocexceptions will be caught here. This particular catch
13: Dynamic Object Creation
601
img
clause is only executed when a bad_allocexception 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 newand delete are untouched.
The operator deleteassumes the Framis address was created in the
pool. This is a fair assumption, because the local operator newwill
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 deletewithout 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 deletecalculates 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).
602
Thinking in C++
img
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 operatornew is
called to allocate enough storage for the array all at once, and the
global operatordelete 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[ ]  the
for
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;
}
13: Dynamic Object Creation
603
img
};
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
604
Thinking in C++
img
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 newis 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 newand operator deleteare only called once for
the entire array chunk, the default constructor and destructor are
called for each object in the array.
Constructor calls
Considering that
MyType* f = new MyType;
calls new to allocate a MyType-sized piece of storage, then invokes
the MyType constructor on that storage, what happens if the
storage allocation in new fails? The constructor is not called in that
case, so although you still have an unsuccessfully created object, at
least you haven't invoked the constructor and handed it a zero this
pointer. Here's an example to prove it:
13: Dynamic Object Creation
605
img
//: C13:NoMemory.cpp
// Constructor isn't called if new fails
#include <iostream>
#include <new> // bad_alloc definition
using namespace std;
class NoMemory {
public:
NoMemory() {
cout << "NoMemory::NoMemory()" << endl;
}
void* operator new(size_t sz) throw(bad_alloc){
cout << "NoMemory::operator new" << endl;
throw bad_alloc(); // "Out of memory"
}
};
int main() {
NoMemory* nm = 0;
try {
nm = new NoMemory;
} catch(bad_alloc) {
cerr << "Out of memory exception" << endl;
}
cout << "nm = " << nm << endl;
} ///:~
When the program runs, it does not print the constructor message,
only the message from operator newand the message in the
exception handler. Because new never returns, the constructor is
never called so its message is not printed.
It's important that nm be initialized to zero because the new
expression never completes, and the pointer should be zero to
make sure you don't misuse it. However, you should actually do
more in the exception handler than just print out a message and
continue on as if the object had been successfully created. Ideally,
you will do something that will cause the program to recover from
the problem, or at the least exit after logging an error.
In earlier versions of C++ it was standard practice to return zero
from new if storage allocation failed. That would prevent
606
Thinking in C++
img
construction from occurring. However, if you try to return zero
from new with a Standard-conforming compiler, it should tell you
that you ought to throw bad_allocinstead.
placement new & delete
There are two other, less common, uses for overloading operator
new.
1.
You may want to place an object in a specific location in
memory. This is especially important with hardware-oriented
embedded systems where an object may be synonymous
with a particular piece of hardware.
2.
You may want to be able to choose from different allocators
when calling new.
Both of these situations are solved with the same mechanism: The
overloaded operator newcan take more than one argument. As
you've seen before, the first argument is always the size of the
object, which is secretly calculated and passed by the compiler. But
the other arguments can be anything you want ­ the address you
want the object placed at, a reference to a memory allocation
function or object, or anything else that is convenient for you.
The way that you pass the extra arguments to operator newduring
a call may seem slightly curious at first. You put the argument list
(without the size_t argument, which is handled by the compiler)
after the keyword new and before the class name of the object
you're creating. For example,
X* xp = new(a) X;
will pass a as the second argument to operator new Of course, this
.
can work only if such an operator newhas been declared.
Here's an example showing how you can place an object at a
particular location:
13: Dynamic Object Creation
607
img
//: C13:PlacementOperatorNew.cpp
// Placement with operator new
#include <cstddef> // Size_t
#include <iostream>
using namespace std;
class X {
int i;
public:
X(int ii = 0) : i(ii) {
cout << "this = " << this << endl;
}
~X() {
cout << "X::~X(): " << this << endl;
}
void* operator new(size_t, void* loc) {
return loc;
}
};
int main() {
int l[10];
cout << "l = " << l << endl;
X* xp = new(l) X(47); // X at location l
xp->X::~X(); // Explicit destructor call
// ONLY use with placement!
} ///:~
Notice that operator newonly returns the pointer that's passed to
it. Thus, the caller decides where the object is going to sit, and the
constructor is called for that memory as part of the new-expression.
Although this example shows only one additional argument,
there's nothing to prevent you from adding more if you need them
for other purposes.
A dilemma occurs when you want to destroy the object. There's
only one version of operator delete so there's no way to say, "Use
,
my special deallocator for this object." You want to call the
destructor, but you don't want the memory to be released by the
dynamic memory mechanism because it wasn't allocated on the
heap.
608
Thinking in C++
img
The answer is a very special syntax. You can explicitly call the
destructor, as in
xp->X::~X(); // Explicit destructor call
A stern warning is in order here. Some people see this as a way to
destroy objects at some time before the end of the scope, rather
than either adjusting the scope or (more correctly) using dynamic
object creation if they want the object's lifetime to be determined at
runtime. You will have serious problems if you call the destructor
this way for an ordinary object created on the stack because the
destructor will be called again at the end of the scope. If you call
the destructor this way for an object that was created on the heap,
the destructor will execute, but the memory won't be released,
which probably isn't what you want. The only reason that the
destructor can be called explicitly this way is to support the
placement syntax for operator new
.
There's also a placement operator deletethat is only called if a
constructor for a placement new expression throws an exception
(so that the memory is automatically cleaned up during the
exception). The placement operator deletehas an argument list that
corresponds to the placement operator newthat is called before the
constructor throws the exception. This topic will be explored in the
exception handling chapter in Volume 2.
Summary
It's convenient and optimally efficient to create automatic objects
on the stack, but to solve the general programming problem you
must be able to create and destroy objects at any time during a
program's execution, particularly to respond to information from
outside the program. Although C's dynamic memory allocation
will get storage from the heap, it doesn't provide the ease of use
and guaranteed construction necessary in C++. By bringing
dynamic object creation into the core of the language with new and
13: Dynamic Object Creation
609
img
delete, you can create objects on the heap as easily as making them
on the stack. In addition, you get a great deal of flexibility. You can
change the behavior of new and delete if they don't suit your
needs, particularly if they aren't efficient enough. Also, you can
modify what happens when the heap runs out of storage.
Exercises
Solutions to selected exercises can be found in the electronic document The Thinking in C++ Annotated
Solution Guide, available for a small fee from .
1.
Create a class Countedthat contains an int id and a
static int countThe default constructor should begin:
.
Counted( ) : id(count++). { should also print its id and
It
that it's being created. The destructor should print that
it's being destroyed and its id. Test your class.
2.
Prove to yourself that new and delete always call the
constructors and destructors by creating an object of
class Counted(from Exercise 1) with new and
destroying it with delete. Also create and destroy an
array of these objects on the heap.
3.
Create a PStash object and fill it with new objects from
Exercise 1. Observe what happens when this PStash
object goes out of scope and its destructor is called.
4.
Create a vector< Counted*>
and fill it with pointers to
new Countedobjects (from Exercise 1). Move through
the vector and print the Counted objects, then move
through again and delete each one.
5.
Repeat Exercise 4, but add a member function f( ) to
Counted that prints a message. Move through the vector
and call f( ) for each object.
6.
Repeat Exercise 5 using a PStash.
7.
Repeat Exercise 5 using Stack4.hfrom Chapter 9.
8.
Dynamically create an array of objects of class Counted
(from Exercise 1). Call delete for the resulting pointer,
without the square brackets. Explain the results.
610
Thinking in C++
img
9.
Create an object of class Counted(from Exercise 1) using
new, cast the resulting pointer to a void*, and delete that.
Explain the results.
10.
Execute NewHandler.cppon your machine to see the
resulting count. Calculate the amount of free store
available for your program.
11.
Create a class with an overloaded operator new and
delete, both the single-object versions and the array
versions. Demonstrate that both versions work.
12.
Devise a test for Framis.cppto show yourself
approximately how much faster the custom new and
delete run than the global new and delete.
13.
Modify NoMemory.cppso that it contains an array of int
and so that it actually allocates memory instead of
throwing bad_alloc In main( ), set up a while loop like
.
the one in NewHandler.cppto run out of memory and
see what happens if your operator newdoes not test to
see if the memory is successfully allocated. Then add the
check to your operator newand throw bad_alloc
.
14.
Create a class with a placement new with a second
argument of type string. The class should contain a static
vector<string>where the second new argument is
stored. The placement new should allocate storage as
normal. In main( ), make calls to your placement new
with string arguments that describe the calls (you may
want to use the preprocessor's __FILE__and __LINE__
macros).
15.
Modify ArrayOperatorNew.cpp  adding a static
by
vector<Widget*>that adds each Widget address that is
allocated in operator newand removes it when it is
released via operator delete (You may need to look up
.
information about vector in your Standard C++ Library
documentation or in the 2nd volume of this book,
available at the Web site.) Create a second class called
MemoryCheckerthat has a destructor that prints out the
number of Widget pointers in your vector. Create a
13: Dynamic Object Creation
611
img
program with a single global instance of
MemoryCheckerand in main( ), dynamically allocate
and destroy several objects and arrays of Widget. Show
that MemoryCheckerreveals memory leaks.
612
Thinking in C++