Day 9: Writing to Disk

So far, you have created a number of complex data structures, but they all have been stored in memory. When your program ends, all the information is lost. Although saving data to disk is fairly straightforward, saving objects to disk can be a bit more complicated. Today you will learn

Writing Objects to Disk

Before you can focus on saving such complex structures as a binary tree to disk, you first must understand how to save individual objects, and how to read them back into memory. Your compiler vendor provides ofstream objects that provide basic file manipulation.

Your program can create an ofstream object, and then use it to open your files and to read data in and out. However, ofstream objects know nothing about your data. They are terrific for reading in a stream of characters, but your objects are more complex than that.

As you can imagine, it is your job to teach your classes how to be streamed to disk. There are a number of ways to do this.

The first question you must answer is whether your files will store mixed types of objects. If it is possible that you will need to read a record without knowing what type of object it is, then you will need to store more information in the file than if every record in that file were of the same type.

The second question is whether you are storing data of a fixed length. If you know that the next object to be read is 20 bytes, for example, you have an easier task than if you don't know how big the object is; in the latter case, the length must be stored with the object.

Listing 9.1 demonstrates a very simple program that opens a file, stores some text to it, closes the file, and then reopens it and reads the text.

Listing 9.1 Demonstrating fin and fout

    1:       // Listing 9.1 - Demonstrating fin and fout
    2:
    3:       #include <fstream.h>
    4:       void main()
    5:       {
    6:          char fileName[80];
    7:          char buffer[255];    // for user input
    8:          cout << "File name: ";
    9:          cin >> fileName;
    10:
    11:         ofstream fout(fileName);  // open for writing
    12:         fout << "This line written directly to the file...\n";
    13:         cout << "Enter text for the file: ";
    14:         cin.ignore(1,'\n');  // eat the new line after the file name
    15:         cin.getline(buffer,255);  // get the user's input
    16:         fout << buffer << "\n";   // and write it to the file
    17:         fout.close();             // close the file, ready for reopen
    18:
    19:         ifstream fin(fileName);    // reopen for reading
    20:         cout << "Here's the contents of the file:\n";
    21:         char ch;
    22:         while (fin.get(ch))
    23:            cout << ch;
    24:
    25:         cout << "\n***End of file contents.***\n";
    26:
    27:         fin.close();            // always pays to be tidy
    28:      }
Output:
    d:\>0901
    File name: mytest
    Enter text for the file: Eternal vigilance is the price of liberty.
    Here's the contents of the file:
    This line written directly to the file...
    Eternal vigilance is the price of liberty.

    ***End of file contents.***

    d:\>dir

     Volume in drive D is unlabeled      Serial number is 1A46:13EA
     Directory of  d:\*.*

    .            <DIR>     10-29-94 12:24p
    ..           <DIR>     10-29-94 12:24p
    0901.cpp          868  10-29-94 12:38p
    0901.exe        78969  10-29-94 12:39p
    mytest             87  10-29-94 12:40p
          79,924 bytes in 3 file(s)
     131,727,360 bytes free

    d:\>type mytest
    This line written directly to the file...
    Eternal vigilance is the price of liberty.
Analysis:

In line 11, an ofstream object, fout, is declared, and the file name is passed in. The default state of this file is to create the file if it doesn't yet exist, and to truncate the file (to delete all its contents) if it does exist.

The user is prompted for text that is written to the file in line 16. Note that ofstream already overloads the insertion operator (<<), so you don't have to.

In line 19, the file is opened for input, and the ofstream member function get() is called repeatedly in line 22.

Writing Data from an Object

Writing the contents of an rWord object would be fairly straightforward if all you wanted to do was to save the data and get it back. Listing 9.2 provides an illustration of this idea.

Listing 9.2 Writing Object Contents to Disk

    1:     #include "word.hpp"
    2:     #include <fstream.h>
    3:
    4:     int main()
    5:     {
    6:          char fileName[80];
    7:          char buffer[255];    // for user input
    8:          cout << "File name: ";
    9:          cin >> fileName;
    10:
    11:         rWord* myArray[5];
    12:         ofstream fout(fileName);  // open for writing
    13:         cin.ignore(1,'\n');  // eat the new line after the file name
    14:
    15:         for (int i = 0; i<5; i++)
    16:         {
    17:             cout << "Please enter a word: " ;
    18:             cin.getline(buffer,255);
    19:             myArray[i] = new rWord(buffer);
    20:             fout << myArray[i]->GetText() << "\n";   // and write it to the
                    file
    21:         }
    22:
    23:         fout.close();             // close the file, ready for reopen
    24:         ifstream fin(fileName);    // reopen for reading
    25:
    26:         cout << "Here's the contents of the file:\n";
    27:         char ch;
    28:         while (fin.get(ch))
    29:            cout << ch;
    30:
    31:         cout << "\n***End of file contents.***\n";
    32:
    33:         fin.close();            // always pays to be tidy
    34:         return 0;
    35:      }
Output:
    d:\>0902
    File name: test2
    Please enter a word: one
    Please enter a word: two
    Please enter a word: three
    Please enter a word: four
    Please enter a word: five
    Here are the contents of the file:
    one
    two
    three
    four
    five

    ***End of file contents.***

    d:\>type test2
    one
    two
    three
    four
    five
Analysis:

In line 11, an array of five pointers to rWord objects is created, and the user is prompted for five words, each of which is added to the array. In line 30, the contents of that word are written to disk, and that is played back to the user in lines 28 and 29.

Object Persistence versus Data Persistence

Listing 9.2 demonstrates data persistence, not object persistence. The text of the word objects is stored, but the object itself is lost. If reserved1 or reserved2 had important information, that would be lost. Also, other information within the contained String object is lost as well.

To save the object itself, all the data members must be stored. Clearly, all the work needed to store the object should be encapsulated in the object itself.

The rWord object consists of two longs and a string. The string, in turn, consists of a C-style, null-terminated string and a long. The right encapsulation is for the program to tell the rWord to save itself to disk. The rWord object should know how to store its longs and how to tell the string to store itself, but it should not know the details of string storage.

Storing Objects to Disk

To achieve this level of encapsulation, you need to provide a method in all your storables that takes an ofstream object and writes its contents into that object.

Instead of creating a method such as Serialize() or WriteToDisk(), I've overloaded the parentheses operator(). The operator() method writes the contents of the object to the disk by way of its iostream parameter. To get the object off the disk, you call a constructor that is overloaded to take an ifstream object.

Stddef.hpp has been modified to accommodate these changes and is shown in listing 9.3. Listing 9.4 is the interface to an improved String object, which knows how to store itself to disk and how to recover String objects from disk. Listing 9.5 shows its implementation.

Listing 9.6 has the interface and implementation for the new rWord object. Finally, listing 9.7 provides a driver program that uses these objects.

Listing 9.3 stddef.hpp

    1:     // **************************************************
    2:     // PROGRAM:  Standard Definitions
    3:     // FILE:     stdef.hpp
    4:     // PURPOSE:  provide standard inclusions and definitions
    5:     // NOTES:
    6:     // AUTHOR:   Jesse Liberty (jl)
    7:     // REVISIONS: 10/23/94 1.0 jl  initial release
    8:     //               10/31/94 1.1 jl persistence
    9:     // **************************************************
    10:
    11:
    12:    #ifndef STDDEF_HPP                 // inclusion guards
    13:    #define STDDEF_HPP
    14:
    15:    const int szLong = sizeof (long int);
    16:    const int szShort = sizeof (short int);
    17:    const int szInt = sizeof (int);
    18:
    19:    enum BOOL { FALSE, TRUE };
    20:
    21:    #include <iostream.h>
    22:
    23:    #endif

Listing 9.4 String.hpp

    1:     // **************************************************
    2:     // PROGRAM:  String declaration
    3:     // FILE:     string.hpp
    4:     // PURPOSE:  provide fundamental string functionality
    5:     // NOTES:
    6:     // AUTHOR:   Jesse Liberty (jl)
    7:     // REVISIONS: 10/23/94 1.0 jl  initial release
    8:     // **************************************************
    9:
    10:    #ifndef STRING_HPP
    11:    #define STRING_HPP
    12:    #include <string.h>
    13:    #include "stdef.hpp"
    14:    #include "storable.hpp"
    15:
    16:    class xOutOfBounds {};
    17:
    18:    class String : public Storable
    19:    {
    20:    public:
    21:
    22:             // constructors
    23:             String();
    24:             String(const char *);
    25:             String (const char *, int length);
    26:             String (const String&);
    27:             String(istream& iff);
    28:             String(Reader&);
    29:             ~String();
    30:
    31:             // helpers and manipulators
    32:             int   GetLength() const { return itsLen; }
    33:             BOOL IsEmpty() const { return (BOOL) (itsLen == 0); }
    34:             void Clear();                // set string to 0 length
    35:
    36:             // accessors
    37:             char operator[](int offset) const;
    38:             char& operator[](int offset);
    39:             const char * GetString()const  { return itsCString; }
    40:
    41:             // casting operators
    42:              operator const char* () const { return itsCString; }
    43:              operator char* () { return itsCString;}
    44:
    45:             // operators
    46:             const String& operator=(const String&);
    47:             const String& operator=(const char *);
    48:
    49:             void operator+=(const String&);
    50:             void operator+=(char);
    51:             void operator+=(const char*);
    52:
    53:             BOOL operator<(const String& rhs)const;
    54:             BOOL operator>(const String& rhs)const;
    55:             BOOL operator<=(const String& rhs)const;
    56:             BOOL operator>=(const String& rhs)const;
    57:             BOOL operator==(const String& rhs)const;
    58:             BOOL operator!=(const String& rhs)const;
    59:
    60:
    61:             // friend functions
    62:             String operator+(const String&);
    63:             String operator+(const char*);
    64:             String operator+(char);
    65:
    66:             void Display()const { cout << itsCString << " "; }
    67:             friend ostream& operator<< (ostream&, const String&);
    68:             ostream& operator() (ostream&);
    69:               void Write(Writer&);
    70:
    71:
    72:    private:
    73:             // returns 0 if same, -1 if this is less than argument,
    74:             // 1 if this is greater than argument
    75:             int StringCompare(const String&) const;  // used by Boolean
                    operators
    76:             char * itsCString;
    77:             int itsLen;
    78:    };
    79:
    80:
    81:    #endif // end inclusion guard

Listing 9.5 String.cpp

    1:     #include "string.hpp"
    2:      // default constructor creates string of 0 bytes
    3:     String::String()
    4:     {
    5:        itsCString = new char[1];
    6:        itsCString[0] = '\0';
    7:        itsLen=0;
    8:     }
    9:
    10:
    11:    String::String(istream& iff)
    12:    {
    13:       iff.read((char*) &itsLen,szLong);
    14:
    15:       itsCString = new char[itsLen+1];
    16:       iff.read(itsCString,itsLen);
    17:       itsCString[itsLen]='\0';
    18:    }
    19:
    20:
    21:    String::String(Reader& rdr)
    22:    {
    23:       rdr>>itsLen;
    24:       rdr>>itsCString;
    25:    }
    26:
    27:    String::String(const char *rhs)
    28:    {
    29:       itsLen = strlen(rhs);
    30:       itsCString = new char[itsLen+1];
    31:       strcpy(itsCString,rhs);
    32:    }
    33:
    34:    String::String (const char *rhs, int length)
    35:    {
    36:       itsLen = strlen(rhs);
    37:       if (length < itsLen)
    38:          itsLen = length;  // max size = length
    39:       itsCString = new char[itsLen+1];
    40:       memcpy(itsCString,rhs,itsLen);
    41:       itsCString[itsLen] = '\0';
    42:    }
    43:
    44:    // copy constructor
    45:    String::String (const String & rhs)
    46:    {
    47:       itsLen=rhs.GetLength();
    48:       itsCString = new char[itsLen+1];
    49:       memcpy(itsCString,rhs.GetString(),itsLen);
    50:       itsCString[rhs.itsLen]='\0';
    51:    }
    52:
    53:    String::~String ()
    54:    {
    55:       Clear();
    56:    }
    57:
    58:    void String::Clear()
    59:    {
    60:       delete [] itsCString;
    61:       itsLen = 0;
    62:    }
    63:
    64:    //non constant offset operator
    65:    char & String::operator[](int offset)
    66:    {
    67:       if (offset > itsLen)
    68:       {
    69:          throw xOutOfBounds();
    70:          return itsCString[itsLen-1];
    71:       }
    72:       else
    73:          return itsCString[offset];
    74:    }
    75:
    76:    // constant offset operator
    77:    char String::operator[](int offset) const
    78:    {
    79:       if (offset > itsLen)
    80:       {
    81:          throw xOutOfBounds();
    82:          return itsCString[itsLen-1];
    83:       }
    84:       else
    85:          return itsCString[offset];
    86:    }
    87:
    88:    // operator equals
    89:    const String& String::operator=(const String & rhs)
    90:    {
    91:       if (this == &rhs)
    92:          return *this;
    93:       delete [] itsCString;
    94:       itsLen=rhs.GetLength();
    95:       itsCString = new char[itsLen+1];
    96:       memcpy(itsCString,rhs.GetString(),itsLen);
    97:       itsCString[rhs.itsLen]='\0';
    98:       return *this;
    99:    }
    100:
    101:   const String& String::operator=(const char * rhs)
    102:   {
    103:      delete [] itsCString;
    104:      itsLen=strlen(rhs);
    105:      itsCString = new char[itsLen+1];
    106:      memcpy(itsCString,rhs,itsLen);
    107:      itsCString[itsLen]='\0';
    108:      return *this;
    109:   }
    110:
    111:
    112:   // changes current string, returns nothing
    113:   void String::operator+=(const String& rhs)
    114:   {
    115:      unsigned short rhsLen = rhs.GetLength();
    116:      unsigned short totalLen = itsLen + rhsLen;
    117:      char *temp = new char[totalLen+1];
    118:      for (int i = 0; i<itsLen; i++)
    119:         temp[i] = itsCString[i];
    120:      for (int j = 0; j<rhsLen; j++, i++)
    121:         temp[i] = rhs[j];
    122:      temp[totalLen]='\0';
    123:      *this = temp;
    124:   }
    125:
    126:   int String::StringCompare(const String& rhs) const
    127:   {
    128:         return strcmp(itsCString, rhs.GetString());
    129:   }
    130:
    131:   String String::operator+(const String& rhs)
    132:   {
    133:
    134:      char * newCString = new char[GetLength() + rhs.GetLength() + 1];
    135:      strcpy(newCString,GetString());
    136:      strcat(newCString,rhs.GetString());
    137:      String newString(newCString);
    138:      return newString;
    139:   }
    140:
    141:
    142:   String String::operator+(const char* rhs)
    143:   {
    144:
    145:      char * newCString = new char[GetLength() + strlen(rhs)+ 1];
    146:      strcpy(newCString,GetString());
    147:      strcat(newCString,rhs);
    148:      String newString(newCString);
    149:      return newString;
    150:   }
    151:
    152:
    153:   String String::operator+(char rhs)
    154:   {
    155:      int oldLen = GetLength();
    156:      char * newCString = new char[oldLen + 2];
    157:      strcpy(newCString,GetString());
    158:      newCString[oldLen] = rhs;
    159:      newCString[oldLen+1] = '\0';
    160:      String newString(newCString);
    161:      return newString;
    162:   }
    163:
    164:
    165:
    166:   BOOL String::operator==(const String& rhs) const
    167:   { return (BOOL) (StringCompare(rhs) == 0); }
    168:   BOOL String::operator!=(const String& rhs)const
    169:      { return (BOOL) (StringCompare(rhs) != 0); }
    170:   BOOL String::operator<(const String& rhs)const
    171:      { return (BOOL) (StringCompare(rhs) < 0); }
    172:   BOOL String::operator>(const String& rhs)const
    173:      { return (BOOL) (StringCompare(rhs) > 0); }
    174:   BOOL String::operator<=(const String& rhs)const
    175:      { return (BOOL) (StringCompare(rhs) <= 0); }
    176:   BOOL String::operator>=(const String& rhs)const
    177:      { return (BOOL) (StringCompare(rhs) >= 0); }
    178:
    179:   ostream& operator<< (ostream& ostr, const String& str)
    180:   {
    181:      ostr << str.itsCString;
    182:      return ostr;
    183:   }
    184:
    185:   ostream& String::operator() (ostream& of)
    186:   {
    187:         of.write( (char*) & itsLen,szLong);
    188:         of.write(itsCString,itsLen);
    189:         return of;
    190:   }
    191:
    192:   void String::Write(Writer& wrtr)
    193:   {
    194:      wrtr<<itsLen;
    195:      wrtr<<itsCString;
    196:   }

Listing 9.6 Implementation for the New rWord Object

    1:        // **************************************************
    2:        // PROGRAM:  Basic word object
    3:        // FILE:     word.hpp
    4:        // PURPOSE:  provide simple word object
    5:        // NOTES:
    6:        // AUTHOR:   Jesse Liberty (jl)
    7:        // REVISIONS: 10/23/94 1.0 jl  initial release
    8:        //               10/31/94 1.1 jl persistence
    9:        // **************************************************
    10:
    11:       #ifndef WORD_HPP
    12:       #define WORD_HPP
    13:
    14:       #include "stdef.hpp"
    15:       #include "string.hpp"
    16:       #include <fstream.h>
    17:
    18:
    19:       class rWord
    20:       {
    21:        public:
    22:           rWord(const String& text):
    23:           itsText(text), reserved1(0L), reserved2(0L)
    24:             {itsTextLength=itsText.GetLength();}
    25:
    26:          //rWord(istream& iff);
    27:           rWord(istream& iff) :itsText(iff)
    28:           {
    29:                iff.read((char*) &reserved1,szLong);
    30:                iff.read((char*) &reserved2,szLong);
    31:           }
    32:
    33:           ~rWord(){}
    34:
    35:           const String& GetText()const { return itsText; }
    36:
    37:           long GetReserved1() const { return  reserved1; }
    38:           long GetReserved2() const { return  reserved2; }
    39:
    40:           void SetReserved1(long val) { reserved1 = val; }
    41:           void SetReserved2(long val) { reserved2 = val; }
    42:
    43:           int operator<(const rWord& rhs)
    44:            { return itsText < rhs.GetText(); }
    45:
    46:           int operator>(const rWord& rhs)
    47:            { return itsText > rhs.GetText(); }
    48:
    49:           BOOL operator<=(const rWord& rhs)
    50:            { return itsText <= rhs.GetText(); }
    51:
    52:           BOOL operator>= (const rWord& rhs)
    53:            { return itsText >= rhs.GetText(); }
    54:
    55:           BOOL operator==(const rWord& rhs)
    56:            { return itsText == rhs.GetText(); }
    57:
    58:           void Display() const
    59:              { cout <<   "  Text: " << itsText << endl; }
    60:
    61:             ostream& rWord::operator() (ostream& of)
    62:             {
    63:                itsText(of);
    64:                of.write((char*)&reserved1,szLong);
    65:                of.write((char*)&reserved2,szLong);
    66:                return of;
    67:             }
    68:
    69:        private:
    70:           long itsTextLength;
    71:           String itsText;
    72:           long reserved1;
    73:           long reserved2;
    74:        };
    75:
    76:       #endif

Listing 9.7 Using the Driver Program for Writing to Disk

    1:     // Listing 9.7 - Using the Driver Program for Writing to Disk
    2:
    3:     #include "word.hpp"
    4:     #include <fstream.h>
    5:
    6:     int main()
    7:     {
    8:          char fileName[80];
    9:          char buffer[255];    // for user input
    10:         cout << "File name: ";
    11:         cin >> fileName;
    12:
    13:         rWord* theWord;
    14:         ofstream fout(fileName,ios::binary);  // open for writing
    15:         cin.ignore(1,'\n');  // eat the new line after the file name
    16:
    17:         for (int i = 0; i<5; i++)
    18:         {
    19:             cout << "Please enter a word: " ;
    20:             cin.getline(buffer,255);
    21:             theWord = new rWord(buffer);
    22:             (*theWord)(fout);   // and write it to the file
    23:         }
    24:
    25:         fout.close();             // close the file, ready for reopen
    26:         ifstream fin(fileName,ios::binary);    // reopen for reading
    27:
    28:         cout << "Here's the contents of the file:\n";
    29:
    30:         for (i = 0; i<5; i++)
    31:         {
    32:             theWord = new rWord(fin);
    33:             cout << theWord->GetText()<< endl;
    34:         }
    35:
    36:          cout << "\n***End of file contents.***\n";
    37:
    38:         fin.close();            // always pays to be tidy
    39:         return 0;
    40:      }
Output:
    d:\>0903
    File name: test3
    Please enter a word: teacher
    Please enter a word: leave
    Please enter a word: them
    Please enter a word: kids
    Please enter a word: alone
    Here are the contents of the file:
    teacher
    leave
    them
    kids
    alone

    ***End of file contents.***
Analysis:

In listing 9.3, stdef.hpp is updated to include szLong, szShort, and szInt; all of which declare constant names for the size of various types.

The entire string.hpp file is shown in listing 9.4 to reduce confusion. The big change is in line 27, with the declaration of a constructor that takes an istream reference, and in line 67, with the overloaded operator().

Listing 9.5 provides the implementation for these methods, along with the rest of the class implementation. Lines 10 through 17 show how a String object is created from a disk stream. In line 12, the length of the string is read; in line 14, a pointer is declared and memory is allocated; and in line 15, the data from the disk is read into that memory location.

Lines 177 through 182 provide the implementation for operator(). This enables the String to write itself to disk. It writes its length and then the contents of itsCString. This is exactly the order in which this data is read in the constructor, and that is not a coincidence. You must write the data out in the order it will be read back in.

Listing 9.6 shows the declaration for the rWord object, including its constructor taking an istream (lines 27 through 31) and its overload of operator() (lines 64 through 68).

The first thing operator() does, in line 64, is to tell itsText, which is a String, to write itself to disk. The remaining members then are written in lines 65 and 66.

In the constructor, itsText is initialized, calling the proper constructor for a String.

The driver program that brings all this together is shown in listing 9.7. The program creates an ostream object and then creates five rWord objects. Each rWord object is told to write itself to disk by calling its operator(), passing in the ofstream object in line 22.

After the words are stored, the file is closed in line 25 and an ifstream object is opened in line 26. This, of course, is the same file opened for reading. Each word is constructed by passing in the ifstream object in line 32.

Creating Classes to Handle Storage

Although the program listed earlier does the job, it has some glaring problems. The program must manage the file operators and each class must independently override operator() and the constructor taking an ifstream object. More problematic, each class must know how to stream every primitive type. That is, both rWord and String, as well as any other class you want to store, must know how to write a long to disk.

The essence of object-oriented programming is to encapsulate this kind of knowledge into a single set of classes that all your other classes can use. The solution is, first, to derive all your storable classes from a common base class; and second, to encapsulate all the disk reading and writing into a pair of reader and writer classes. The next set of listings provides exactly that functionality.

To create the new String class, follow these steps:

  1. Replace line 18 of listing 9.4 with this line:
        class String : public Storable
    

  2. Replace line 28 in listing 9.4 with this line:
                         string(Reader&);
    

  3. Replace line 69 of listing 9.4 with this line:
                         void Write(Writer&);
    

  4. Replace lines 21 through 25 of listing 9.5 with these lines:
           String::String(Reader& rdr)
           {
              rdr>>itsLen;
              rdr>>itsCString;
           }
    

  5. Replace lines 192 through 196 of listing 9.5 with these lines:
         void String::Write(Writer& wrtr)
         {
            wrtr<<itsLen;
            wrtr<<itsCString;
         }
    

These changes cause String to derive from the abstract data type (ADT) Storable, shown in listing 9.8. The implementation for Storable is in listing 9.9. Listing 9.10 provides the new interface to rWord, and listing 9.11 shows the driver program that brings all this together.

Listing 9.8 The Storable Interface

    1:      // **************************************************
    2:      // PROGRAM:  ADT for storage
    3:      // FILE:     storable.hpp
    4:      // PURPOSE:  Object persistence
    5:      // NOTES:
    6:      // AUTHOR:   Jesse Liberty (jl)
    7:      // REVISIONS: 11/1/94 1.0 jl  initial release
    8:      // **************************************************
    9:
    10:     #include <fstream.h>
    11:     #include "stdef.hpp"
    12:
    13:     class Writer
    14:     {
    15:     public:
    16:        Writer(char *fileName):fout(fileName,ios::binary){};
    17:        ~Writer() {fout.close();}
    18:        virtual Writer& operator<<(int&);
    19:        virtual Writer& operator<<(long&);
    20:        virtual Writer& operator<<(short&);
    21:        virtual Writer& operator<<(char*);
    22:
    23:     private:
    24:            ofstream fout;
    25:     };
    26:
    27:     class Reader
    28:     {
    29:     public:
    30:        virtual Reader& operator>>(int&);
    31:        virtual Reader& operator>>(long&);
    32:        virtual Reader& operator>>(short&);
    33:        virtual Reader& operator>>(char*&);
    34:
    35:        Reader(char *fileName):fin(fileName,ios::binary){}
    36:        ~Reader(){fin.close();}
    37:
    38:     private:
    39:        ifstream fin;
    40:     };
    41:
    42:     class Storable
    43:     {
    44:     public:
    45:        Storable() {}
    46:        Storable(Reader&){}
    47:        virtual void Write(Writer&)=0;
    48:
    49:     private:
    50:
    51:     };
    52:
    53:

Listing 9.9 The Storable Implementation

    1:       // **************************************************
    2:       // PROGRAM:  ADT for storage
    3:       // FILE:     storable.cpp
    4:       // PURPOSE:  Object persistence
    5:       // NOTES:
    6:       // AUTHOR:   Jesse Liberty (jl)
    7:       // REVISIONS: 11/1/94 1.0 jl  initial release
    8:       // **************************************************
    9:
    10:      #include "storable.hpp"
    11:      #include <string.h>
    12:
    13:      Writer& Writer::operator<<(int& data)
    14:      {
    15:         fout.write((char*)&data,szInt);
    16:         return *this;
    17:      }
    18:
    19:      Writer& Writer::operator<<(long& data)
    20:      {
    21:         fout.write((char*)&data,szLong);
    22:         return *this;
    23:      }
    24:
    25:      Writer& Writer::operator<<(short& data)
    26:      {
    27:         fout.write((char*)&data,szShort);
    28:         return *this;
    29:      }
    30:
    31:      Writer& Writer::operator<<(char * data)
    32:      {
    33:         int len = strlen(data);
    34:         fout.write((char*)&len,szLong);
    35:         fout.write(data,len);
    36:         return *this;
    37:      }
    38:
    39:      Reader& Reader::operator>>(int& data)
    40:      {
    41:         fin.read((char*)&data,szInt);
    42:         return *this;
    43:      }
    44:      Reader& Reader::operator>>(long& data)
    45:      {
    46:         fin.read((char*)&data,szLong);
    47:         return *this;
    48:      }
    49:      Reader& Reader::operator>>(short& data)
    50:      {
    51:         fin.read((char*)&data,szShort);
    52:         return *this;
    53:      }
    54:      Reader& Reader::operator>>(char *& data)
    55:      {
    56:         int len;
    57:         fin.read((char*) &len,szLong);
    58:         data = new char[len+1];
    59:         fin.read(data,len);
    60:         data[len]='\0';
    61:         return *this;
    62:      }
    63:

Listing 9.10 The New Interface to rWord

    1:     // **************************************************
    2:     // PROGRAM:  Basic word object
    3:     // FILE:     word.hpp
    4:     // PURPOSE:  provide simple word object
    5:     // NOTES:
    6:     // AUTHOR:   Jesse Liberty (jl)
    7:     // REVISIONS: 10/23/94 1.0 jl  initial release
    8:     //              10/31/94 1.1 jl  persistence
    9:     //              11/1/94  1.2 jl  derives from storable
    10:    // **************************************************
    11:
    12:    #ifndef WORD_HPP
    13:    #define WORD_HPP
    14:
    15:    #include "stdef.hpp"
    16:    #include "string.hpp"
    17:    #include <fstream.h>
    18:
    19:
    20:    class rWord : public Storable
    21:    {
    22:     public:
    23:        rWord(const String& text):
    24:        itsText(text), reserved1(0L), reserved2(0L)
    25:          {itsTextLength=itsText.GetLength();}
    26:
    27:        rWord(const char* text):
    28:        itsText(text), reserved1(0L), reserved2(0L)
    29:          {itsTextLength=itsText.GetLength();}
    30:
    31:        rWord::rWord(Reader& rdr) :itsText(rdr)
    32:        {
    33:          rdr >> reserved1;
    34:          rdr >> reserved2;
    35:        }
    36:
    37:
    38:
    39:        ~rWord(){}
    40:
    41:
    42:        const String& GetText()const { return itsText; }
    43:
    44:        long GetReserved1() const { return  reserved1; }
    45:        long GetReserved2() const { return  reserved2; }
    46:
    47:        void SetReserved1(long val) { reserved1 = val; }
    48:        void SetReserved2(long val) { reserved2 = val; }
    49:
    50:
    51:        BOOL operator<(const rWord& rhs)
    52:         { return itsText < rhs.GetText(); }
    53:
    54:        BOOL operator>(const rWord& rhs)
    55:         { return itsText > rhs.GetText(); }
    56:
    57:
    58:        BOOL operator<=(const rWord& rhs)
    59:         { return itsText <= rhs.GetText(); }
    60:
    61:        BOOL operator>= (const rWord& rhs)
    62:         { return itsText >= rhs.GetText(); }
    63:
    64:
    65:        BOOL operator==(const rWord& rhs)
    66:         { return itsText == rhs.GetText(); }
    67:
    68:        void Display() const
    69:           { cout <<   "  Text: " << itsText << endl; }
    70:
    71:       ostream& operator() (ostream&);
    72:
    73:       void Write(Writer& wrtr)
    74:       {
    75:          itsText.Write(wrtr);
    76:          wrtr << reserved1;
    77:          wrtr << reserved2;
    78:       }
    79:
    80:
    81:     private:
    82:        long itsTextLength;
    83:        String itsText;
    84:        long reserved1;
    85:        long reserved2;
    86:     };
    87:
    88:
    89:    #endif

Listing 9.11 Using the Driver Program

    1:     // Listing 9.11 - Using the Driver Program
    2:
    3:     #include "word.hpp"
    4:
    5:     // NOT a member function!
    6:     void operator<<(Writer& wrtr, rWord& rw)
    7:     {
    8:        rw.Write(wrtr);
    9:     }
    10:
    11:    const int howMany = 5;
    12:
    13:    int main()
    14:    {
    15:         char fileName[80];
    16:         char buffer[255];    // for user input
    17:         cout << "File name: ";
    18:         cin >> fileName;
    19:         cin.ignore(1,'\n');  // eat the new line after the file name
    20:         Writer* writer = new Writer(fileName);
    21:         rWord* theWord;
    22:
    23:         for (long i = 0; i<howMany; i++)
    24:         {
    25:             cout << "Please enter a word: " ;
    26:             cin.getline(buffer,255);
    27:             theWord = new rWord(buffer);
    28:             (*writer)<< *theWord;   // and write it to the file
    29:         }
    30:          delete writer;
    31:
    32:          Reader * reader = new Reader(fileName);
    33:
    34:         cout << "Here are the contents of the file:\n";
    35:
    36:         for (i = 0; i<howMany; i++)
    37:         {
    38:             theWord = new rWord(*reader);
    39:             cout << theWord->GetText()<< endl;
    40:         }
    41:
    42:          cout << "\n***End of file contents.***\n";
    43:
    44:          delete reader;
    45:         return 0;
    46:      }
Output:
    d:\>0911
    File name: test3
    Please enter a word: teacher
    Please enter a word: leave
    Please enter a word: them
    Please enter a word: kids
    Please enter a word: alone
    Here are the contents of the file:
    teacher
    leave
    them
    kids
    alone

    ***End of file contents.***
Analysis:

The changes to String described in this section cause the String class to inherit from Storable, which is declared in listing 9.8. Note in line 47 of listing 9.8 that Write() is declared to be pure virtual. This ensures that every derived class must override this method.

Also note that Storable has a constructor that takes a reference to a Reader object. Both Reader and Writer are declared in listing 9.8. In line 16, Writer is declared to take a C-style string; and in the initialization of the Writer object, its member, an ofstream object, is initialized and thus opened.

Writer overloads the insertion operator for many of the built-in types. In a fully developed version of this class, all the built-in types would be represented. It is the job of Writer to provide storage services for all the built-in types.

Classes derived from Storable can store their members by use of these overridden insertion operators. If they have members that are not built-in types, those classes must provide for this.

In listing 9.11, you see that the program wants to write out an rWord object. Because rWord objects are not built-in types, the Writer will not know what to do with this unless you tell it. In lines 6 through 9 of listing 9.11, a global operator<< function is declared that takes both a Writer reference and a reference to an rWord. This method calls the Write() method in rWord, which knows how to write out all the members of an rWord object.

Because Write() is pure virtual in the base class, the compiler will check to make sure that you have overridden this in your derived class. It is the job of the author of the rWord class to override this method to do the right thing.

Listing 9.10 shows the declaration of the rWord class, and in lines 73 through 78, the rWord object writes its contents to the Writer object provided.

Be sure to note that the first thing that rWord does is call the Write() method on its contained String object. In the changes you made to listing 9.8, you saw that String has its own Write method that writes out first the length of itsCString and then itsCString itself.

The constructor for rWord, which takes a Reader reference (shown in lines 31 through 35), initializes itsString by calling the constructor of String, which reads the number of bytes, and then reads that many bytes into itsCString.

This symmetry is essential to the workings of this process. To make this explicit, I'll walk through listing 9.11 in precise detail.

In line 17, the user is prompted for a file name. In a real program, you probably would check your configuration file for this. The file name is stashed in a local variable, fileName. In line 20, a Writer object is created, with the fileName passed in as a parameter.

Listing 9.8, line 16 shows that this constructor initializes an ofstream object fout with the file name passed in as a parameter, and opens the file in binary mode.

In line 21 of listing 9.11, a pointer to an rWord is declared. In the loop beginning in line 23, the user is prompted for words, each of which is used to generate rWord objects. Each time an rWord object is created in line 27, it is written to disk in line 28 by calling operator<< on the Writer object, which was created in line 20.

This call to operator<< causes the program to jump to line 6 of listing 9.11. In line 8, the rWord's Write() method is called. You could have called this directly in line 28, but it is more convenient to be able to use the operator<< overload.

This call to Write() causes the program to jump to line 73 of listing 9.10. In line 75, Write() is called on the member data itsText, which is of type String. You just as easily could have written wrtr << itsText; but then you would need a global function taking a Writer reference as its first parameter and a String object as its second parameter. In a real program, you would want to provide such an operator so that classes, such as rWord that contain a String object can use the overloaded operator<<.

This call to Write() in String causes the program to jump to the implementation of Write() provided before listing 9.8. As you can see, Writer's operator<< is called twice: once for itsLen and then again for itsCstring. The first causes the program to jump to line 18 of listing 9.9, and the second causes the program to jump to line 28 of listing 9.8.

The write of the long simply writes the 4 bytes to the ofstream object. The write of the char* writes the first 4 bytes of length, and then the data itself.

After the string is written, control returns to line 76 of listing 9.10, where reserved1 and reserved2 are written. This again causes the program twice to jump to line 18 of listing 9.9, where the longs are written.

After the entire rWord is written, control returns to line 29 of listing 9.11. After all the rWords are written, the writer object is deleted in line 30. This closes the file. In line 32, a Reader object is created, again passing in the file name.

This calls the constructor shown in line 35 of listing 9.8, where the ifstream object is opened in binary mode using the fileName parameter.

In lines 36 through 40, each of the stored words is returned to memory. The constructor for rWord is called, passing in the Reader object. This causes control to jump to line 31 of listing 9.10. The first thing done is that itsText is initialized, passing in the Reader reference. This then passes control to the constructor of the String object, shown in listing 9.8.

The constructor calls the overloaded operator>>, first passing in the long itsLen and then the character pointer itsCString. The first passes control to line 39 of listing 9.9, and the second passes control to line 47 of listing 9.9.

Reading the long is straightforward: reading in the character string requires reading in the length, allocating the memory (line 51), reading in the data in line 52, and finally null-terminating the string in line 53.

After the string exists, control returns to line 33 of listing 9.10, where the two longs are created again by calling the overloaded operator>> of the reader class.

In line 39 of listing 9.11, each rWord object calls its GetText() method, which returns a String object, which is in turn passed to the overloaded operator<< of the ostream, where it is printed to the screen.

Finally, in line 44, the reader object is deleted.

What Have We Gained?

Although this tour of the inner workings of Storable, Reader, and Writer may not have convinced you that this approach is any simpler, it does nicely encapsulate much of this work in a small number of classes. After Reader and Writer are working properly, you can use them and not worry again about how the primitive types are written to disk.

In fact, because user-defined types are composed of other user-defined types or of primitive types, every class ultimately needs to know only how to call operator<< on each of its members.

From Here to an Object Store

A number of capabilities still are missing in order to generalize the capabilities explored so far into a true object store. For one, each object needs to be identified uniquely in some way that enables objects to refer to one another.

In a true object store, an object should be able to ask the store for "object #12" and get back a valid pointer to a storable object. Also, the object store must be able to handle various types of storable objects, extracting enough information from the stored representation to invoke the correct constructor. You will see more detail on these issues on day 15.

Summary

Today you learned how to write data from an object to a disk file, and more important, how to serialize or stream an object to a file. You learned how to create reader and writer objects to encapsulate disk reads and writes, and you learned about object streaming and instantiating, which together make up the fundamental building blocks of an object store.

Q&A

Q: Why is saving objects to disk so much trouble? Why can't you just take the sizeof() the object and write it out?

A: Because the object itself may contain pointers, which are, of course, just references to things in memory. The next time your program is run, it may not get loaded into the same place in memory, or it may allocate space differently, so those pointers will not point to the correct things.

Q: How about if I am careful not to declare any pointers in my object. Am I safe?

A: This is still not enough, because there may be some pointers that you don't know about. If any contained classes have pointers, the same problem exists. Furthermore, if your class or any of its parent classes, or any contained class, or their parents declared any virtual functions, then there is a hidden pointer buried in the object: the virtual function table pointer.

Q: If I am writing an application for which I want the data to be persistent, do I have to declare all my objects to be subclasses of Storable?

A: Yes, if you want to write it yourself, and you want to use the technique that uses a Reader and a Writer. The right answer, however, is to buy one of the commercially available persistent store libraries. Developing one for yourself, as you did in this chapter, is a great way to understand how these libraries work, but in reality it is incredibly complex to get it right, and you would make better use of your time to buy a good library and use it.

Q: Is this an object-oriented database?

A: No. This is not quite a persistent store for C++, which still is less than an OODB. (You will create a full-featured persistent store before the book is over.) Full-blown OODBs include indexes, class and object versioning, multiuser access, transaction protocols, and other features that are well outside the ambitions of this book.

Workshop

The Workshop provides quiz questions to help you solidify your understanding of the material covered, and exercises to provide you with experience in using what you have learned. Try to answer the quiz and exercise questions before checking the answers in Appendix A, and make sure that you understand the answers before continuing to the next chapter.

Quiz

  1. What is the significance of fixed-size versus variable-size records?

  2. What is the difference between object persistence and data persistence?

  3. What is the purpose of the Writer class?

  4. What is the purpose of the Storable class?

  5. What is required of a class if it is to be storable?

[Click here for Answers]

Exercises

  1. Create a Person class that can be persistent. It should contain three strings for Name, Title, and Address, and a long for DateOfBirth. Be careful with the special constructor taking a Reader and with the Write method.

  2. Add an enum position to your Person class created in exercise 1. The values are posPresident, posVicePresident, posDirector, posManager, posSoftwareEngineer, posAccountant, and so on. The question here is how you make an enum persistent. Try to keep this code portable.

  3. Modify the Writer class to save the data in a buffer rather than writing it directly to the file. The class should have methods for getting the current size of the buffer and the buffer itself. The constructor should be changed to accept the size of the buffer the writer should allocate.

  4. What is wrong with this class (besides the lack of accessors to its private members and the lack of any real functionality)?
        class Foo : public Storable
        {
        public:
             Foo() : c('\0'), i(0), l(0L) { }
             Foo(Reader & rdr) : itsText(rdr) {rdr >> c >> i >> l;}
    
          void Write(Writer & wrtr)
               {wrtr << c << i << l;  itsText.Write(wrtr);}
    
        private:
             char c;
             int I;
             long l;
    
             String itsText;
        };
    

  5. Does this class work as intended? Is the order of reading and writing correctly matched up?
        class Foo : public Storable
        {
        public:
             Foo() : c('\0'), i(0), l(0L) { }
             Foo(Reader & rdr) : itsName(rdr), itsAddress(rdr)
                            {rdr  >> I;}
    
             void Write(Writer & wrtr)
                  {itsName.Write(wrtr); itsAddress.Write(wrtr);
                       wrtr << i;  }
    
        private:
             String itsAddress;
             int I;
             String itsName;
        };
    

Go to: Table of Contents | Next Page