| | Quantum | Quantum
| | Index Number | 5
| | + + |
|Big | 0 | 2 + + +
|Pointer | 1 | 10 | | |
|Array | 2 | 12 | | |
| | 3 | 14 | | |
| | 4 | 11 | | |
| +- + + | |
+ + |
+ + |
| +- Quantum Item |± +
| | Index Number Number | Quantum
|Item | + + | 2
|Reference | 0 | 3 2 + + +
|Array | 1 | 3 4 + + +-+
|(IRA) | 2 | 3 5 + + +-+-+
|segment | 3 | 3 3 + + + | | |
|0 | 4 | 3 1 + + + | | | |
| +- + + | | | | | |
+ + | | | | |
+ + | | | | |
| +- | | | | | |
| | Item # Offset Type Index | | | | | |
| | + + | | | | | |
| | 1 | 5 -+ VARSTRING 4 |±+-+ | | | |
| | 2 | 17 +| VARSTRING 0 |±+ + + | |
|Item | 3 | +-19 || VARSTRING 3 |±+ + | |
|Index | 4 | ++-31 || VARSTRING 1 |±+ + |
| | 5 |+++-38 || VARSTRING 2 |±+ +
| | 6 |||| 38 || UNUSED 0 | | Quantum 3
| | ++++ ++ + + +
| +- ||| |+ + |
| ||| + + | |
| ||+ + | | |
| +- |+ + | | | |
|Quantum | +-+ + +-+ + + |
|Data | | BaldwinP.O.Box 0335NYSteve Heller11510| |
| +- + + |
| 333333333222222222211111111110000000000 |
| 876543210987654321098765432109876543210 |
+ +
We might use this facility to keep track of both customer records and inventory
records, each of which would be logically separate although they may be stored in
the same quantum file. In order to facilitate reconstruction of the file if part of the
IRA is lost, we won't use the same quantum for items from two different arrays; as
a result, the space freed by deleting an item will only be reused when an item is to
be added to the same array.
To find an element in a main object, we index into the main object index by the
object's number, retrieving the quantum number of the big pointer array for that
object. Then we calculate the IRA segment and the index into that segment and
retrieve the quantum number and item number for the element we want. The last
step is to use the item index to find the exact location of the element.
Of course, we need a place to store the main object index. A normal application
might have five or ten main objects; therefore, our allocation of 256 entries in the
main object index is extremely generous in most cases.
11
The Light at the End of the Tunnel
Let's start our examination of the implementation of the quantum file code by
looking at a simple program that uses it to store a number of strings and then
retrieve them (Figure flextest.00).
Initial test program for quantum file implementation (quantum\flextest.cpp)
(Figure flextest.00)
codelist/flextest.00
Possibly the most striking feature of this program is how simple it appears, which
is a direct consequence of the power of C++. Writing a similar program in C would
require much greater knowledge of the implementation of the quantum file
algorithm. However, in C++ all we have to know is which header file to include,
how to open the quantum file, and how to create a FlexArray object, which
behaves like a variable-sized array of variable-sized strings. Once we have dealt
with this small set of prerequisites, we can use this extremely powerful and flexible
data type in virtually the same way we would use a built-in type.
Let's take a detailed look at exactly how we set up and use a quantum file in this
program. First, we have a couple of statements that declare a temporary buffer
variable which we'll use to construct data to store in the quantum file and delete
any previous version of the quantum file itself. Next, we create a QuantumFile
object called QF via the default constructor of the QuantumFile class. Then
we call an Open function on that object, supplying the name of the quantum file to
be created.
The next statement creates a FlexArray object called TestArray. The
arguments to this constructor are a pointer to the QuantumFile object that we'll
use to store the data for TestArray, and a name by which we could reuse this
same FlexArray object in another program or another execution of this
program.
Now we're up to the loop where we will start to use the TestArray object. The
loop steps through the first 100 elements of TestArray, assigning each element
a String consisting of digits corresponding to the position of that element in the
TestArray object. The strstream named Temp is used to format the numeric
values of the loop variable i into String values that can be stored in a
FlexArray object.
Finally, after we've put the values into TestArray, the second loop displays the
values in the TestArray object.
Pay No Attention to the Man Behind the Curtain
At this point, I wouldn't be at all surprised if you had come to the conclusion that I
have something up my sleeve and you would be right. Underneath this apparent
simplicity is a wonderland of complex programming, which I hope I will be able to
explain in an understandable way. Let's begin by examining some of these data
types we've just seen, in enough detail to demystify them.
The first of these data types is the String. As it happens, this is actually a
typedef for a more general type called SVector, so we'll start with the
question of exactly what an SVector might be.
An SVector is very much like an array, with the following exceptions:
1. If you try to store something beyond the end of an SVector, you will get an
error message rather than a mysterious crash.
2. You can find out how many elements an SVector has, by calling its size
member function.
3. You can change the size of an SVector after it has been created, by calling
its resize member function.
4. You can assign one SVector of a particular type to another SVector of the
same type.
5. You can create an SVector by copying from another SVector of the same
type.
While these are very useful characteristics for any type of SVector, they are even
more important in one particular type of SVector: the String, which is an
SVector of chars. As a specialization of the SVector type, the String has
all of the characteristics of any other SVector, but has additional abilities as well.
Primary among these is its ability to be read or written using the standard C++
input/output stream functions. This String type, or one very much like it, is one
of the most obvious improvements in C++ over C, whose corresponding char*
type is notoriously error-prone. In this program, we will use the String data type
in much the same way as we would use the char* data type in a C program.
Unfortunately, we will not be able to go into detail on the implementation of either
the SVector or String classes in this book; we will just use their facilities as
described immediately above.
A Very Useful typedef
Another "type" used in this simple test program is ArrayIndex. This is also a
typedef, but for a much simpler underlying type: unsigned long.
In this case, the reason I didn't just use the underlying type was to enhance
portability. The original version of this program was implemented on a 16-bit
compiler, where the definition of ArrayIndex was unsigned short,
primarily because it was impossible to create an array or SVector having more
than 64K elements. When I ported this program to a 32-bit compiler, I changed the
definition of ArrayIndex and several other similar typedefs and recompiled.
I would like to be able to tell you that the program worked without further changes,
but that would be a lie: it actually took me a couple of days to get it working again.
However, considering the size of the source code for the classes (over 100KB), I
was quite happy to take such a short time to port it.
If I had used the explicit type unsigned short everywhere, it would have
taken me much longer to figure out which occurrences of that type should be
changed to unsigned long and which should remain as they were. As it was, I
found a few places where I did not use a typedef or used the wrong one and ran
into trouble as a result. Overall, though, I did a pretty good job in using appropriate
types originally, which saved me a lot of trouble later.
A Quantum Leap
Now let's start our journey into the interior of the quantum file implementation,
starting with the QuantumFile class itself, whose header file is shown in
Figure blockh.00.
Header file for QuantumFile class (quantum\block.h) (Figure blockh.00)
codelist/blockh.00
I can't say that I'm completely happy with the design of this class. Ideally, the
user of any class should not have to worry about any member data or member
variables that the user cannot or should not use. Unfortunately, this is not easily
accomplished in C++. I believe it is worthwhile to digress slightly here to discuss
the reasons why this is so.
First, let's examine the difficulty of "hiding" private and protected member
variables from the class user. The technical problem here is that the compiler
cannot allocate space for a variable unless its size is known. This implies that you
cannot create a variable of a given class without access to its interface
definition, which must in turn include the definitions of all of its member variables.
This does not allow the class user to use those member variables unless they are
public, so we don't have to worry about non-member functions accidentally
altering member variables. However, it does mean that the user of a class must
ensure that all of the data types needed in the declaration of a class are available
when the header for that class is compiled, and that the names of those data
types do not conflict with any previously defined names. This can be a source of
considerable difficulty when trying to use a fairly large library in a context other
than the one in which it was developed, which of course reduces the payoff that
such reuse would otherwise provide.
The other problem with the design of this class is that it defines a number of
functions that application programmers have no need for. These functions are
intended for use by other classes that contribute to the functionality of the
quantum file implementation. You might think that I could simply make these
functions private to prevent their being called by outside classes. Unfortunately, I
wasn't able to do that with Microsoft Visual C++ 5.0, one of the compilers with
which this code must work, as that compiler apparently cannot understand friend
declarations