Inapoi
Inainte
Cuprins
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 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;
} ///:~
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 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 delete is actually called for each object pointer
held in the 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;
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
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
} ///:~
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>
#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 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).
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:
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];
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.
 |
|