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.
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
ew 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).
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 newstring 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).