|
Dynamic storage allocation
You never know the maximum amount of
storage you might need for a CStash, so the memory pointed to by
storage is allocated from the heap. The
heap is a big block of memory used for allocating smaller
pieces at runtime. You use the heap when you don’t know the size of the
memory you’ll need while you’re writing a program. That is, only at
runtime will you find out that you need space to hold 200 Airplane
variables instead of 20. In Standard C, dynamic-memory allocation functions
include malloc( ),
calloc( ),
realloc( ), and
free( ). Instead of library calls, however,
C++ has a more sophisticated (albeit simpler to use) approach to dynamic memory
that is integrated into the language via the keywords
new and
delete.
The inflate( ) function uses
new to get a bigger chunk of space for the CStash. In this
situation, we will only expand memory and not shrink it, and the
assert( ) will guarantee that a negative
number is not passed to inflate( ) as the increase value. The
new number of elements that can be held (after inflate( ) completes)
is calculated as newQuantity, and this is multiplied by the number of
bytes per element to produce newBytes, which will be the number of bytes
in the allocation. So that we know how many bytes to copy over from the old
location, oldBytes is calculated using the old
quantity.
The actual storage allocation occurs in
the new-expression, which is the expression
involving the new keyword:
new unsigned char[newBytes];
The general form of the new-expression
is:
in which Type describes the type
of variable you want allocated on the heap. In this case, we want an array of
unsigned char that is newBytes long, so that is what appears as
the Type. You can also allocate something as simple as an int by
saying:
new int;
and although this is rarely done, you can
see that the form is consistent.
A new-expression returns a pointer
to an object of the exact type that you asked for. So if you say new
Type, you get back a pointer to a Type. If you say new
int, you get back a pointer to an int. If you want a new
unsigned char array, you get back a pointer to the first element of that
array. The compiler will ensure that you assign the return value of the
new-expression to a pointer of the correct type.
Of course, any time you request memory
it’s possible for the request to fail, if there is no more memory. As you
will learn, C++ has mechanisms that come into play if the memory-allocation
operation is unsuccessful.
Once the new storage is allocated, the
data in the old storage must be copied to the new storage; this is again
accomplished with array indexing, copying one byte at a time in a loop. After
the data is copied, the old storage must be released so that it can be used by
other parts of the program if they need new storage. The delete keyword
is the complement of new, and must be applied to release any storage that
is allocated with new (if you forget to use delete, that storage
remains unavailable, and if this so-called
memory leak happens enough,
you’ll run out of memory). In addition, there’s a special syntax
when you’re deleting an array. It’s as if you must remind the
compiler that this pointer is not just pointing to one object, but to an array
of objects: you put a set of empty square brackets in front of the pointer to be
deleted:
delete []myArray;
Once the old storage has been deleted,
the pointer to the new storage can be assigned to the storage pointer,
the quantity is adjusted, and inflate( ) has completed its
job.
Note that the heap manager is fairly
primitive. It gives you chunks of memory and takes them back when you
delete them. There’s no inherent facility for
heap compaction, which compresses the heap to
provide bigger free chunks. If a program allocates and frees heap storage for a
while, you can end up with a
fragmented heap that has
lots of memory free, but without any pieces that are big enough to allocate the
size you’re looking for at the moment. A heap
compactor complicates a program because it moves memory chunks around, so your
pointers won’t retain their proper values. Some operating environments
have heap compaction built in, but they require you to use special memory
handles (which can be temporarily converted to pointers, after locking
the memory so the heap compactor can’t move it) instead of pointers. You
can also build your own heap-compaction scheme, but this is not a task to be
undertaken lightly.
When you create a
variable on the stack at
compile-time, the storage for that variable is automatically created and freed
by the compiler. The compiler knows exactly how much storage is needed, and it
knows the lifetime of the variables because of scoping. With dynamic memory
allocation, however, the compiler doesn’t know how much storage
you’re going to need, and it doesn’t know the lifetime of
that storage. That is, the storage doesn’t get cleaned up automatically.
Therefore, you’re responsible for releasing the storage using
delete, which tells the heap manager that storage can be used by the next
call to new. The logical place for this to happen in the library is in
the cleanup( ) function because that is where all the closing-up
housekeeping is done.
To test the library, two CStashes
are created. The first holds ints and the second holds arrays of 80
chars:
//: C04:CLibTest.cpp
//{L} CLib
// Test the C-like library
#include "CLib.h"
#include <fstream>
#include <iostream>
#include <string>
#include <cassert>
using namespace std;
int main() {
// Define variables at the beginning
// of the block, as in C:
CStash intStash, stringStash;
int i;
char* cp;
ifstream in;
string line;
const int bufsize = 80;
// Now remember to initialize the variables:
initialize(&intStash, sizeof(int));
for(i = 0; i < 100; i++)
add(&intStash, &i);
for(i = 0; i < count(&intStash); i++)
cout << "fetch(&intStash, " << i << ") = "
<< *(int*)fetch(&intStash, i)
<< endl;
// Holds 80-character strings:
initialize(&stringStash, sizeof(char)*bufsize);
in.open("CLibTest.cpp");
assert(in);
while(getline(in, line))
add(&stringStash, line.c_str());
i = 0;
while((cp = (char*)fetch(&stringStash,i++))!=0)
cout << "fetch(&stringStash, " << i << ") = "
<< cp << endl;
cleanup(&intStash);
cleanup(&stringStash);
} ///:~
Following the form required by C, all the
variables are created at the beginning of the scope of main( ). Of
course, you must remember to initialize the CStash variables later in the
block by calling initialize( ). One of the problems with C libraries
is that you must carefully convey to the user the importance of the
initialization and cleanup
functions. If these functions aren’t called, there
will be a lot of trouble. Unfortunately, the user doesn’t always wonder if
initialization and cleanup are mandatory. They know what they want to
accomplish, and they’re not as concerned about you jumping up and down
saying, “Hey, wait, you have to do this first!” Some users
have even been known to initialize the elements of a structure themselves.
There’s certainly no mechanism in C to prevent it (more
foreshadowing).
The intStash is filled up with
integers, and the stringStash is filled with character arrays. These
character arrays are produced by opening the source code file,
CLibTest.cpp, and reading the lines from it into a
string called line, and then producing a
pointer to the character representation of line using the member function
c_str( ).
After each Stash is loaded, it is
displayed. The intStash is printed using a for loop, which uses
count( ) to establish its limit. The stringStash is printed
with a while, which breaks out when fetch( ) returns zero to
indicate it is out of bounds.
You’ll also notice an additional
cast in
cp = (char*)fetch(&stringStash,i++)
This is due to the
stricter type checking in C++,
which does not allow you to simply assign a void* to any other type (C
allows this).
|
|