Am avut 371696 vizite de la lansarea siteului.




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];

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.

Home   |   Web Faq   |   Radio Online   |   About   |   Products   |   Webmaster Login

The quality software developer.™
© 2003-2004 ruben|labs corp. All Rights Reserved.
Timp de generare a paginii: 17583 secunde
Versiune site: 1.8 SP3 (build 2305-rtm.88542-10.2004)