|
Dynamic Object Creation |
|
|
|
Page 3 of 4 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 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 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.
|
|
| |
|
|