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
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 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 fiveAnalysis:
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.
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.
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.
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:
class String : public Storable
string(Reader&);
void Write(Writer&);
String::String(Reader& rdr) { rdr>>itsLen; rdr>>itsCString; }
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.
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.
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.
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: 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.
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.
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; };
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