ACCU Home page ACCU Conference Page
Search Contact us ACCU at Flickr ACCU at GitHib ACCU at Facebook ACCU at Linked-in ACCU at Twitter Skip Navigation

pinC++ Streams (Part 2)

Overload Journal #2 - Jun 1993 + Programming Topics   Author: Mike Toms

Welcome to the second part of Streams. In this part I am going to demonstrate some of the various methods of accessing files using streams. Where possible I will try and use an OO approach, but often a C-like approach has to be used.

The basic classes for file access are ifstream and ofstream (Input File STREAM and Output File STREAM). For the purposes of this article, I will be splitting file access into two categories, text and binary.

TEXT FILES

Text files are the simplest way to hold information on disk. A text file (at its simplest) consists of a series of ASCII characters, separated into lines by the end-of-line (EOL) (also called new-line) character.

READING A TEXT FILE LINE-BY-LINE

In this first example, I will demonstrate how to read a simple text file and display its contents on the screen.

//
//  LISTERl.CPP
//
//  Run as LISTER1 <FileToBeListed>
//
#include <iostream.h>
#include <fstream.h>

void main(int argc, char *argv[])
{
char read_buffer[ 132];

if (argc != 2)
{
cerr << "ERROR> One filename must be supplied"
<< endl;
cerr << "E.G. lister myfile.txt"
<< endl; return;
}

ifstream text_file(argv[l]);
if(!tcxt_file)
cout << "Problem opening text file "
<< argv[l] << endl;
else
{
while (text_file)
{
text_file.getline(read_buffer,sizeof(read buffer));
cout << read_buffer << endl;
}
}
}

The ifstream object text_file opens the file entered as the first parameter on the command line during its construction (and closes it automatically during destruction). As the constructor is not permitted to return a value of any kind, the status of the open must be tested by another means.

TESTING A STREAM FOR ERRORS

This can be achieved in two ways. The first is to use one of the functions that have a Boolean return value, these are good(), bad() and eof() used as follows:

if (text_file.good())  // i/o operation was successful 
if (text_file.bad())   // i/o operation failed
if (text_file.eof())   // end-of-file encountered

The other is to use the overloaded ! and void* operators in the following manner:

if (text_file )     // I/O operation was successful (void*) 
if ( !text_file )  // I/O operation failed

OBTAINING MORE DETAIL ABOUT A STREAM ERROR

The cause of the error can be investigated by a call to a member function of ios called rdstate(), which will return an error value. These error flags take the form of bits set in the return value and can be tested by bitwise anding with one of the three ios flags as follows:

if (text_file.rdstate() & ios::eofbit)
// end of file occurred
if (text_file.rdstate() & ios::failbit)
// last operation failed.
if (text_file.rdstate() & ios::badbit)
// invalid operation.

Borland's stream library has one extra flag that is non standard:

if (text_file.rdstate() & ios::hardfail) 
// unrecoverable error

MORE TEXT READING

In the last example, the reading of the file is performed by the getline() member function. This extracts the characters up to and including the EOL delimiter, and places them, (excluding the EOL) into the read buffer. This is not the only way to read in a text file, there are a whole family of 'get' member functions that can be used to extract characters, words, strings or streambufs from a file stream. Also the EOL delimiter can be changed when required to use a different character to mark the end of line.

If a file contains a series of character strings terminated with the $ symbol it could be safely read by means of the line:

text_file.getline(read_buffer, sizeof(buffer), '$');

The dollar symbol will not appear in the read_buffer.

READING TEXT FILES VIA EXTRACTOR OPERATOR

Using the getline() method of reading a file is no better than reading it in C. An ifstream object is an input stream in the same manner as cin, and can be used to read in formatted information, as in the following program.

//
//   NAMELIST.CPP
//
//   Run as NAMELIST names.lis
//
#include <iostream.h>
#include <fstream.h>

void main(int argc, char *argv[])
{
char surname[15];
char first_name[15];
char middle_initial;
int age;
if (argc != 2)
{
cerr << "ERROR> One filename must be supplied"
<< endl;
cerr << "E.G. lister myfile.txt" << endl;
return;
}

ifstream text_file(argv[l]);
if(!text_file)
cout << "Problem opening text file "
<< argv[l] << endl;
else
{
while (text_file)
{
text_file >> surname >> first_name
>> middle_initial >> age;
if(text_file)
{
cout << first_name << " "
<< middle_initial << " "
<< surname
<< " aged " << age << " years old"
<< endl;
}
}
}
}

There are several things to note in this program, the major one being that you must test the result of the stream before using it (something I often forget to do!). Also note that the names, space-separated in the original file, appear as individual null-terminated strings when read into data items. If one of the names does not have a middle initial the program will not work correctly.

This leads to the problem of how best to read in string items, as each space-separated group of characters will be broken into individual strings. One method is to have a class String which will read a quoted string from a stream, only terminating on the second quote character. Another would to use null terminated strings in files.

Another way to tackle this problem (untidy, but it works) is that until the next item after the char* is accepted, the value will get appended to the current char*. This method is not capable of distinguishing two strings from one another and is not a good general-purpose solution.

//
//   NAMELIS2.CPP (EXTRACT OF)
//
//    Run as NAMELIS2 names.lis
//
char name[40];
char temp[40];
int age;

text_file >> name;
if (text_file)
do
{
text_file >> age;
if (text_file.rdstate() & ios::failbit)
{
// clear the ios::failbit not changing other flags
text_file.clear(text_file.rdstate() & ~ios::failbit);
text_file >> temp;
strcat (name, "");
strcat (name, temp);
}
else
break;
} while (1);

The other solution, relying on quoted strings, is achieved as follows:

class read_string
{
protected:
char* ps;
public:
read_string(char* pps) { ps = pps; *ps = '\0'; }
friend istream& operator >> (istream&, read_string&);
};

istream& operator >> (istream& is, read_string& r)
{
char charread;
if ((charread=is.peek()) == '\n')
charread = is.get(); // discard any surplus linefeed
if ((charread=is.peek()) != '\"') // reject if not quote
{
// set failbit not able to read string
is.clear(ios::failbit);
}
else
{
charread = is.get(); // discard first quote character
charread = is.get();
while (charread != '\"' && is)
{
*r.ps++ = charread;
charread = is.get();
}
*r.ps=0;
}
return is;
}

The code is now a lot cleaner as the quoted string can be read directly into name via name_string object:

char name[40]; 
while (text_file)
{
read_string name_string(name);
text_file >> name_string;
if(text_file)
text_file >> age;
if(text_file)
{
cout << name
<<  " aged " << age
<< " years old" << endl;
}
}

The final solution, relying on null terminated strings is achieved as follows:

class read_string
{
protected:
char* ps;
public:
read_string(char* pps) { ps = pps; *ps = '\0'; }
friend istream& operator >> (istream&, read_string&);
};

istream& operator >> (istream& is, read_string& r)
{
char charread;
if ((charread=is.peek()) == '\n')
charread = is.get(); // discard any surplus linefeed
charread = is.get();
while (charread && is)
{
*r.ps++ = charread;
charread = is.get();
}
*r.ps=0;
return is;
}

The usage of this type of string will be the same as for the quoted string.

WRITING TO TEXT FILES

Writing to text files is as simple as reading from them. One diminutive complication is that the output file must be opened in the correct manner. The default opening method of a file for output is such that it truncates any existing file of the same name.

ofstream output_file("writer.dat");

which is the equivalent of:

ofstream output_file("writer.dat", ios::trunc);

In order to append to an existing file, an additional parameter must be passed:

ofstream append_file("exists.dat", ios::app);

It is also possible to prevent automatic creation of a file that does not already exist:

ofstream old_file("old.dat", ios::nocreate);

and to ensure the use of a file only if it already exists:

ofstream new_file("new.dat", ios::noreplace);

The writing of the data is easy, as the following program to copy a series of files from the command line into a target file demonstrates. The syntax of the command is:

insert <fl> [<f2> ...] into <f3>

The full program can be found on the companion disk (called insert.cpp). The essence of the writing operation is the same as for cout where the line:

target_file << buffer << endl;

writes each line in turn to the file indicated by <f3>.

FORMATTED WRITING TO TEXT FILES

As with reading files in, writing out can be performed on a series of data types strung together as in the following example:

int a = 5; 
float b = 7.5F;
char c[] = "Hello World";
ofstream data_file("data.dat");
int i = 5;
while (i--)
datafile << a++ << b++ << c << endl;

will produce the output in data.dat of:

57.5Hello World 68.5Hello World etc.

If we intend to read the strings back in, it would be useful to space-separate them:

char ds = ' '; // data separator
data_file a++ << ds << b++ << ds << c << endl;

WRITING STRUCTS TO TEXT FILES

When dealing with structs, it is possible to define friend overloads of the extractor and insertor operators such that the struct is written and read back in correctly. (Yes this can be done from within a struct and not a class as in program TXTSTRUC.CPP on the companion disk). The strings again pose a problem, but this can be negated to a small extent by writing the string bounded by quotes as in the above example or by using a custom delimiter when the string is written, thus enabling that delimiter to be used to read the string back in. This can cause problems if the overloaded inserter is subsequently used to output the struct to the screen or printer.

A better way of writing and reading structs is to use a binary format. This will be dealt with later.

MOVING THE FILE POINTER IN A STREAM

Before moving off text based I/O, I must mention that it is possible to move around a text stream in a non­sequential manner, though this is more often of use in a binary file. In the same way that a C program would use fseek to reposition a file pointer within a file, there are a pair of member functions available to derivatives of istream both called seekg (for seek get). These can be used in the following manner:

Type 1:
// go 200 bytes into the file
stream text_file.seekg(200);
// goto start of memory string
memstring.seekg(0);

Type 2:
// goto 2nd record
text_file.seekg(sizeof(rec), ios::beg);
// goto next record
text_file.seekg(sizeof(rec), ios::cur);
// goto prev record
text_file.seekg(-sizeof(rec),ios::cur);
// goto last record
text_file.seekg(-sizeof(rec),ios::end);

The type 1 uses the beginning of the stream as its start point, and the long parameter supplied is an offset from the beginning of the stream. The type 2 uses the second parameter to determine the start point (beginning, current or end of file), and the first parameter to determine the offset from that position. Note that this time the first parameter can be negative to indicate that the jump is prior to the position indicated.

The member function tellg(void) can be used to determine the current position in an input stream (in bytes from the start of the stream)".

There are equivalent member functions for streams derived from ostream, and they are called seekp (for seek put) and tellp().

PRINTER OUTPUT

As hinted at previously, output to a printer is the same as for any other stream, by use of the command:

ofstream printer("lpt1:"); if (printer)
{
printer << "Hello printer" << endl;
}

BINARY FILES

As mentioned earlier, binary files are better for storing and retrieving structs. In this example only the essential information is highlighted, as the majority of the processing is the same as previous examples. The complete program BINSTRUC.CPP can be found on the companion disk.

Files opened in binary mode require an additional parameter:

ifstream infile ("fred.bin", ios::binary); 
ofstream outfile ("fred.bin", ios::binary);

Data is written by means of the write member function

outfile.write((char*) &mystruct, sizeof(mystruct));

and read back in by means of the read member function

infile.read((char*) &mystruct, sizeof(mystruct));

Tacky isn't it? This method of reading and writing is no better than using C. Where a file is required for both input and output, the stream object can be of type fstream, which is the equivalent of a hypothetical iofstream.

SIMPLE PERSISTENCE

In order to make it appear more acceptable we really need to make our objects (or structs) persistent. In the case of non-derived objects this is straightforward to accomplish. To save hierarchical objects is not difficult, but reading them back in can present a few problems, so I will save this subject for another day.

Again the means to accomplish the task of adding simple persistence to objects is by means of overloading the inserter and extractor operators.

The following program is a simple editor for a list of aircraft specifications , which utilise the overloading techniques. It is not a bullet-proof implementation, but merely demonstrates how simple persistence can be achieved.

//
//   SPERSIST.CPP
//
//   Run as SPERSIST
//
#include <iostream.h>
#include <fstream.h>
#include <string.h>

class aircraft
{
protected:
char maker[20];
char name[20];
int max_speed;
long max_altitude;
int max_range;
public:
aircraft(void);
aircraft(char* m, char* n, int s, long a, int r);
void display_data(void);
void get_data(void);
size_t size(void)
{
 return sizeof(maker)
+sizeof(name)
+sizeof(max_speed)
 +sizeof(max_altitude)
+sizeof(max_range);
}
friend fstream& operator << (fstream& , aircraft&);
friend fstream& operator >> (fstream& , aircraft&);
};

aircraft::aircraft(void)
{
memset(maker, 0, sizeof(maker));
memset(name, 0, sizeof(name));
max_speed = 0;
max_altitude = 0L;
max_range = 0;
}

aircraft::aircraft(char* m, char* n, int s, long a, int r)
{
memset(maker, 0, sizeof(maker));
memset(name, 0, sizeof(name));
strcpy( maker, m);
strcpy(name, n);
max_speed = s;
max_altitude = a;
max_range = r;
}

void aircraft::display_data(void)
{
cout << endl
<< "NAME: " <<  maker << " "
<< name << endl
<< "SPEED:" << max_speed << endl
<< "ALTITUDE: " << max_altitude << endl
<< "RANGE: " << max_range << endl;
}

void aircraft::get_data(void)
{
cout << endl
<< "CURRENT NAME:" << maker
<< " " << name << endl
<< "Enter new Maker Name : ";
memset(maker, 0, sizeof(maker));
cin >> maker,
cout << "Enter new Model Name : ";
memset(name, 0, sizeof(name));
cin >> name;
cout << "Current maximum SPEED: "
<< max_speed << endl
<< "Enter new maximum SPEED : ";
cin >> max_speed;
cout << "Current maximum ALTITUDE:"
<< max_altitude << endl
<< "Enter new maximum ALTITUDE : ";
cin >> max_altitude;
cout << "Current maximum RANGE: "
<< max_range << endl
<< "Enter new maximum RANGE : ";
cin >> max_range;
}

fstream& operator<<(fstream& ofs, aircraft& a)
{
ofs.write(a.maker, sizeof(a.maker));
ofs.write(a.name, sizeof(a.name));
ofs.write((unsigned char*) &a.max_speed,
sizeof(a.max_speed));
ofs.write((unsigned char*) &a.max_altitude,
sizeof(a.max_altitude));
ofs.write((unsigned char*) &a.max_range,
sizeof(a.max_range));
return ofs;
}

fstream& operator>>(fstream& ifs, aircraft& a)
{
ifs.read(a.maker, sizeof(a.maker));
ifs.read(a.name, sizeof(a.name));
ifs.read((unsigned char*) &a.max_speed,
sizeof(a.max_speed));
ifs.read((unsigned char*) &a.max_altitude,
sizeof(a.max_altitude));
ifs.read((unsigned char*) &a.max_range,
sizeof(a.max_range));
return ifs;
}

void main(void)
{
fstream plane_file("aircraft.bin",
ios::binary | ios::in | ios::out);
aircraft plane;
char choice = '\0';
do
{
cout << endl
<< "A - Add new aircraft"     << endl
<< "M - Modify Current"       << endl
<< "N - Goto Next Record"     << endl
<< "P - Goto Previous Record" << endl
<< "X - To terminate program" << endl;

do
{
cout << endl << "Enter Choice :";
cin >> choice;
choice &= 0xdf; // force to uppercase
} while (! strchr("AMNPX", choice));
// until choice is one of list
long prev_offset = -2L * (long)plane.size();
switch (choice)
{
case 'A':
aircraft newplane; // create a temp aircraft
newplane.get_data(); // and load data from user
plane_file.seekp(0L, ios::end); // goto EOF
plane_file << newplane << flush; // append
break;
case 'M':
plane.get_data(); // reload current aircraft
// set file pointer to start of the record
plane_file.seekp(plane_file.tellg()-(long)plane.size());
plane_file << plane << flush; // and write it
break;
 case'P':
//jump to previous record
plane_file.seekg(prev_offset, ios::cur);
case 'N':
plane_file >> plane; // read record
if(plane_file)
plane.display_data();
else
{
// on bounds error, set read pointer
cout << Unable to read record" << endl;
plane_file.clear();
// to start of file
plane_file.seekg(0L);
}
break;
case 'X':
cout << "Thank you for entering data" << endl;
break;
}
} while (choice != 'X');
}

In this program, the overloaded extractor and inserter operators are used to read from and write to the disk. I have copped out and used get_data() and display_data() member functions rather than confuse things by further overloading of inserter and extractor operators. You can see that positioning the file pointers is messy, as is the error handling (almost non-existent), and with the exception of writing and reading in the correct format, there is little gained over C in this area.

A BETTER WAY?

Provided that memory is not a scarce resource, a simple technique that can be used is to generate a template class which controls the loading and storing of a file in a list container; this template class can be used to hold a variety of different classes. Again, it is only put together as an example and you should spend some effort to improve it before serious use commences. Also, I have chosen to use a sorted list, and I have therefore been forced to add a less than operator as well as an equality operator to the list of member functions. In this case I am using the BIDS templates, but any template container class will have a usable list of some sort.

//
//   TEMPPERS.CPP (EXTRACT FROM)
//
#include <conio.h>
#include <listimp.h>

class aircraft
{
protected:
... as before
public:
... as before plus
aircraft(aircraft&);
int operator == (aircraft &);
int operator < (aircraft &);
};

aircraft::aircraft(aircraft& a)
{
memset(maker, 0, sizeof(maker));
memset(name, 0 , sizeof(name));
strcpy(maker, a.maker);
strcpy(name, a.name);
max_speed = a.max_speed;
max_altitude = a.max_altitude;
max_range = a.max_range;
}

int aircraft::operator == (aircraft& a)
{
if (!strcmp(maker,a.maker) &&
!strcmp(name,a.name) &&
max_speed == a.max_speed &&
max_altitude == a.max_altitude &&
max_range == a.max_range )
return 1;
else
return 0;
}

int aircraft::operator < (aircraft& a)
{
int result;
// test maker for < == >
if ((result = strcmp(maker,a.maker)) < 0 ) return 1;
else if (result) return 0;
// test name for < == >
if ( (result = strcmp(name,a.name)) < 0 ) return 1;
else return 0;
// no sorting takes place on attributes, only on keys
}

template<class T> class persist
{
protected:
char filename[60];
public:
persist(char* fname);
~persist(void);
BI_SListImp<T> list;
};

template<class T> persist<T>::persist(char* fname)
{
strcpy(filename, fname);
fstream thisfile( filename , ios::binary | ios::in);
T readval,
// add each record in file to container
while (thisfile)
{
thisfile >> readval;
if (thisfile) list.add(readval);
}
}

template<class T> persist<T>::~persist(void)
{
fstream thisfile(filename,
ios::trunc | ios::binary | ios::out);
BI_ListIteratorImp<T> next(list);
// write each item in the container back to the tile
while (next && thisfile)
{
thisfile << next++;
}
delete filename;
}

void main(void)
{
// create an aircraft list linked to the file aircraft.bin
// and automatically load them in
persist<aircraft> airfile("aircraft.bin");
// create another aircraft
aircraft sw("Supermarine", "Walrus",
180, 24000L, 2300);
// and add it to the list
airfile.list.add(sw);

BI_ListIteratorImp<aircraft> next(airfile.list);

// Display the contents of the list
cout << "The contents of the aircraft file are : "
<< endl;
while (next)
{
(next++).display_data();
cout << "Press a key " << endl;
getch();
}
// and automatically save the list as airfile object // goes out of scope
}

The main() function demonstrates how easy it is to set up a list loaded with the file specified. Any alterations to the contents of this list are saved automatically when the airfile object goes out of scope.

The problem with such techniques is "What happens when I have a class hierarchy to consider?". The answer in short is "You are in trouble". Although it is straightforward to overload the inserter operators to format the outgoing record in the correct format, reading them back in presents more of a problem. You cannot rely on the compiler sorting out which class type the record belongs to. It is always possible to precede the record with a marker to indicate what kind of object is coming back in, and this I will do in the next instalment, but a serious problem will then occur if any attempt is made to go backwards or non-sequentially through the file. There is no guarantee that all the records will be of the same length, and so positioning at the nth record is impossible with a simple structure.

Overload Journal #2 - Jun 1993 + Programming Topics