OPENING A FILE FOR RANDOM ACCESS ■ 639 ᮀ Random File Access So far we have only looked at sequential file access. If you need access to specific infor- mation in such a file, you have to walk through the file from top to tail, and new records are always appended at the end of the file. Random file access gives you the option of reading and writing information directly at a pre-defined position. To be able to do this, you need to change the current file position explicitly, that is, you need to point the get/put pointer to the next byte to be manipu- lated. After pointing the pointer, you can revert to using the read and write operations that you are already familiar with. ᮀ Open Modes One prerequisite of random file access is that the position of the records in the file can be precisely identified. This implies opening the file in binary mode to avoid having to transfer additional escape characters to the file. Example: ios::openmode mode = ios::in | ios::out | ios::app | ios::binary; fstream fstr("account.fle", mode); This statement opens the file "Account.fle" in binary mode for reading and append- ing at end-of-file. The file will be created if it did not previously exist. Random read access to the file is possible, but for write operations new records will be appended at the end of the file. To enable random read and write access to a file, the file can be opened as follows: Example: ios::openmode mode = ios::in | ios::out | ios::binary; fstream fstr("account.fle", mode); However, this technique can only be used for existing files. If the file does not exist, you can use the ios::trunc flag to create it. The section “File State” discusses your error handling options if a file, such as "account.fle" cannot be found. 640 ■ CHAPTER 29 MORE ABOUT FILES Access point Positioning flag File Beginning of the file ios::beg Current position ios::cur End of file ios::end • • • • • • • ■ POSITIONING FOR RANDOM ACCESS The three access points in a file POSITIONING FOR RANDOM ACCESS ■ 641 ᮀ Discovering and Changing the Current Position The file stream classes comprise methods to discover and change the current position in a file. The tellp() and tellg() methods return the current position of the put or get pointers as a long value. Example: long rpos = myfile.tellg(); This statement queries the current position of the read pointer in the myfile stream. The current position is always returned as a byte offset relative to the beginning of the file. The current file position can be modified using the seekp() or seekg() method. The position is stated as a byte offset, relative to either the beginning or end of the file, or relative to the current position in the file. ᮀ Positioning Flags Three ios::seekdir type positioning flags are defined in the ios class for this pur- pose; these are ios::beg, ios::cur, and ios::end. Imagine you want to write the object acc to the file "account.fle" at offset pos. You can use the following statements to do so: Example: ofstream fstr("account.fle", ios::out | ios::binary); fstr.seekp(pos, ios::begin); acc.write( fstr ); This calls the write() method in the Account class, which allows an object to write its own data members to a file (see Chapter 18). If you do not specify a positioning flag, the position will be assumed to be relative to the beginning of the file. The statement used to position the write pointer in the last example can therefore be formulated as follows: Example: fstr.seekp(pos ); The byte offset can also be negative for calls to the methods seekp() and seekg(). However, you cannot position the read/write pointer before the beginning of the file. In contrast, it is possible to place the pointer at a position after the end of the file and then perform a write operation, which will create a gap with unknown content in the file. This only makes sense if all the empty slots in the file are of an equal length, as they can be overwritten later. This option is often used when programming hash tables. 642 ■ CHAPTER 29 MORE ABOUT FILES // index.h: Defines the class IndexEntry // #ifndef _INDEX_H #define _INDEX_H #include <fstream> #include <iostream> using namespace std; class IndexEntry { private: long key; // Key long recNr; // Offset public: IndexEntry(long k=0L, long n=0L) { key=k; recNr=n;} // Access methods and: int recordSize() const { return sizeof(key) + sizeof(recNr); } fstream& write( fstream& ind) const; fstream& read( fstream& ind); fstream& write_at(fstream& ind, long pos) const; fstream& read_at( fstream& ind, long pos); }; #endif // index.cpp: Implements the methods // #include "index.h" // . . . fstream& IndexEntry::write_at( fstream& ind, long pos) const { ind.seekp(pos); ind.write((char*)&key, sizeof(key) ); ind.write((char*)&recNr, sizeof(recNr) ); return ind; } fstream& IndexEntry::read_at(fstream& ind, long pos) { ind.seekg(pos); ind.read((char*)&key, sizeof(key) ); ind.read((char*)&recNr, sizeof(recNr)); return ind; } ■ POSITIONING FOR RANDOM ACCESS (CONTINUED) Representing an index entry The read_at() and write_at() methods POSITIONING FOR RANDOM ACCESS (CONTINUED) ■ 643 ᮀ Using Positioning Methods The following statements are commonly used for random positioning seekg( 0L); and seekp( 0L, ios::end ); They set the current position to the beginning or end of a file. You should be aware that the first argument is 0L to indicate that long type is required. If you need to determine the length of a file, you can point the get pointer to the end of the file and then query the position of the pointer: Example: fstr.seekg(0L, ios::end); unsigned long count = fstr.tellg(); The count variable will then contain the number of bytes occupied by the file. These positioning methods are useful for files opened in binary mode. However, it does not make much sense to use them for text files or particularly for devices. In text mode, conversions of LF <=> CR/LF prevent the methods from working correctly. ᮀ Determining Positions in a File The position of records in a files is easy to compute if all the records in the file are the same length. Given that size is the length of a record 0L, size, 2*size, are the positions of the first, second, third records, and so on. If you are working with variable length records, you cannot exactly compute their positions. To enable random access you therefore need to store the positions of the records in a separate structure, a so-called index. The index stores pairs of keys and record positions, so-called index entries in a file. A key, a social security number, or customer id, for example, must uniquely identify a record. If the index is sorted, the position that correlates to the required key can be quickly found using the binary search algorithm. ᮀ The IndexEntry Class The IndexEntry class, used to represent an index entry, is shown opposite. The class comprises methods for reading and writing an index entry at the current file position or at any given position. The appropriate file stream is passed as an argument to the method. 644 ■ CHAPTER 29 MORE ABOUT FILES // index.h: (continued) // Adds the definition of an index // #include <string> class IndexFile { private: fstream index; string name; // Filename of index public: IndexFile(const string& s); ~IndexFile() { index.close(); } void insert( long key, long pos); long search( long key); void retrieve(IndexEntry& entry, long pos ); }; // index.cpp: (continued) // Adds methods of class IndexFile // IndexFile::IndexFile(const string& file) { ios::openmode mode = ios::in | ios::out | ios::binary; index.open(file.c_str(), mode); if(!index) // If the file does not exist { index.clear(); mode |= ios::trunc; index.open(file.c_str(), mode); if(!index) return; } else name = file; } //. . . ■ FILE STATE Representing an index Constructor of class IndexFile FILE STATE ■ 645 ᮀ State Flags A file stream can assume various states, for example, when it reaches the end of a file and cannot continue reading. A file operation can also fail if a file cannot be opened, or if a block is not transferred correctly. The ios class uses state flags to define the various states a file can assume. Each state flag corresponds to a single bit in a status-word, which is represented by the iostate type in the ios class. The following state flags exist: ■ ios::eofbit end of file reached ■ ios::failbit last read or write operation failed ■ ios::badbit an irrecoverable error occurred ■ ios::goodbit the stream is ok, e.g. no other state flag is set. The “flag” ios::goodbit is an exception to the rule since it is not represented by a single bit, but by the value 0 if no other flag has been set. In other words a status-word has the value ios::goodbit if everything is fine! ᮀ Discovering and Changing the State There are multiple methods for discovering and modifying the status of a stream. A method exists for each state flag; these are eof(), fail(), bad(), and good(). They return true when the corresponding flag has been raised. This means you can discover the end of a file with the following statement: Example: if( fstr.eof() ) The status-word of a stream can be read using the rdstate() method. Individual flags can then be queried by a simple comparison: Example: if( myfile.rdstate() == ios::badbit ). . . The clear() method is available for clearing the status-word. If you call clear() without any arguments, all the state flags are cleared. An argument of the iostate type passed to clear() automatically becomes the new status-word for the stream. ᮀ The IndexFile Class The IndexFile class, which uses a file to represent an index, is defined opposite. The constructor for this class uses the clear() method to reset the fail bit after an invalid attempt to open a non-existent file. A new file can then be created. The IndexFile class comprises methods for inserting, seeking, and retrieving index entries, which we will be implementing later in this chapter. 646 ■ CHAPTER 29 MORE ABOUT FILES // exceptio.h : Exception classes for file access // #ifndef _EXCEPTIO_H #define _EXCEPTIO_H #include <string> #include <iostream> using namespace std; class FileError { private: string filename; public: FileError( const string& file) : filename(file){ } string getName() const{ return filename; } }; class OpenError : public FileError { public: OpenError( const string& file):FileError(file){ } }; class ReadError : public FileError { public: ReadError( const string& file):FileError(file){ } }; class WriteError : public FileError { public: WriteError(const string& file):FileError(file){ } }; #endif ■ EXCEPTION HANDLING FOR FILES Defining your own exception classes EXCEPTION HANDLING FOR FILES ■ 647 ᮀ Implementing Your Own Exception Handling You can exploit the error tracking options that state flags give you to implement your own exception handling for files. For example, a method that reads records from a file can throw an exception when the state flag ios::eof is raised, that is, when the end of the file is reached. The opposite page shows typical exception classes organized in a hierarchy that can be used to represent error conditions on opening, reading from, and writing to a file. In each case the file name is saved for evaluation by the exception handler. ᮀ Standard Exception Handling for Streams C++ also provides standard exception handling for streams. You can use the excep- tions() method to specify the flags in the status-word of a stream that will cause exceptions to be thrown. The exceptions() method is defined in the ios stream base class. The method expects one or multiple state flags separated by the | sign. An exception is then thrown for the flags specified. Example: ifstream ifstrm("account.fle"); fstrm.exceptions(ios::failbit | ios::badbit); On accessing the fstrm stream an exception is thrown if either one of the flags ios::failbit or ios::badbit is raised. The operation that caused the error is then terminated and the state flags are cleared by a call to clear(rdstate());. The exception thrown here is of a standard exception class, failure. This type is defined as a public element in the ios base class and comprises the virtual method what() that returns a C string containing the cause of the error. The exception handler will normally send the string to standard error output. You can call exceptions() without any arguments to discover the state flags in a status-word of a stream that can cause an exception to be thrown. If a bit is set in the return value of the exceptions() method, an appropriate exception will be thrown whenever this error occurs. Example: iostate except = fstrm.exceptions(); if( except & ios::eofbit) This statement uses a bitwise AND operator to ascertain whether an exception is thrown when end-of-file is reached. 648 ■ CHAPTER 29 MORE ABOUT FILES // account.h : Defines the classes // Account, DepAcc, and SavAcc // with virtual read and write methods. // // . . . enum TypeId { ACCOUNT, DEP_ACC, SAV_ACC }; class Account { private: // Data members: as previously defined. public: // Constructor, access methods virtual TypeId getTypeId() const { return ACCOUNT;} virtual ostream& write(ostream& fs) const; virtual istream& read(istream& fs); }; class DepAcc : public Account { // Data members, constructor, . . . TypeId getTypeId() const { return DEP_ACC; } ostream& write(ostream& fs) const; istream& read(istream& fs); }; class SavAcc: public Account { // Data members, constructor, . . . TypeId getTypeId() const { return SAV_ACC; } ostream& write(ostream& fs) const; istream& read(istream& fs); }; // account.cpp: Implements the methods. // #include "account.h" ostream& DepAcc::write(ostream& os) const { if(!Account::write(os)) return os; os.write((char*)&limit, sizeof(limit) ); os.write((char*)&deb, sizeof(deb) ); return os; } istream& DepAcc::read(istream& is) { if(!Account::read(is)) return is; is.read((char*)&limit, sizeof(limit) ); is.read((char*)&deb, sizeof(deb)); return is; } // . . . ■ PERSISTENCE OF POLYMORPHIC OBJECTS The methods read() and write() of class DepAcc . index Constructor of class IndexFile FILE STATE ■ 645 ᮀ State Flags A file stream can assume various states, for example, when it reaches the end of a file and cannot continue reading. A file operation can. implies opening the file in binary mode to avoid having to transfer additional escape characters to the file. Example: ios::openmode mode = ios: :in | ios::out | ios::app | ios::binary; fstream fstr("account.fle",. top to tail, and new records are always appended at the end of the file. Random file access gives you the option of reading and writing information directly at a pre-defined position. To be able