March 1998

File I/O

by Kent Reisdorph

At some point, every programmer needs to do file input and output. When we talk about file I/O, we don't mean database files--the database tools in C++Builder are great for file operations that fit within the database paradigm. However, sooner or later you'll need specialized file I/O. In this article, we'll show you the ins and outs of file I/O. We'll discuss the many different types of file I/O available to you in C++Builder, and we'll demonstrate how to use two types: the C++ iostream classes and the VCL TFileStream class.

This article is long because of the complicated nature of file I/O, but the material presented is vital to understanding file operations. Take it a little at a time and be sure to experiment along the way.

So many choices!

C++Builder gives you several ways to perform file I/O. These choices include:
bullet The C-style FILE mechanism
bullet The C++ iostream classes
bullet The VCL streaming classes (TFileStream)
bullet The VCL database mechanism

Given this selection of file I/O methods, which do you choose? The answer, of course, depends on the type of file I/O you're doing and, to a degree, on your programming experience. For example, if you come from a C background, then you might already be familiar with that method of doing file I/O. If you come from a C++ background, then you might be familiar with iostreams. If you come from Delphi, then you might already have experience with TFileStream. The important thing is that you know what your choices are and then pick the appropriate method for a given programming chore.

So, back to the question of which method to choose. Database programming is...well, database programming, so we can cross the last choice off the list for general file I/O.

We're going to cross the C-style FILE mechanism off the list as well, even though for some folks this is the old standby method. If you currently use this method for file I/O, then there's certainly no harm in continuing to do so. However, this mechanism lacks the object-oriented design that's so prevalent in programming today. The C-style file I/O functions use the FILE structure as a sort of file handle. Functions in this group include fopen(), fread(), fwrite(), fclose(), fseek(), and ftell(). We'll add one thing in defense of the C-style file I/O functions: They're very portable. If portability across multiple platforms is key for you, then you might consider this method. On the other hand, if you're using C++Builder, then portability probably isn't a concern.

That leaves us with two remaining choices: the C++ iostream classes and the VCL streaming class, TFileStream. These two methods of file I/O are quite similar, but there are some major differences. Having a good knowledge of what each method offers allows you to make an informed choice about which to use in a given situation. We'll look at each method in detail; if you'd first like an overview of the streaming concept, see "Today's Buzzword: Streams," which follows this article.

C++ iostream classes

A discussion of the C++ standard library streaming classes could easily occupy an entire book. Obviously we can't go into everything here, so we'll just hit the high points. The main classes you'll be concerned with are the ifstream class (for reading files), the ofstream class (for writing files), and the fstream class (for reading and writing files simultaneously). These classes ultimately derive from the granddaddy of all C++ classes, ios. (Even though all the stream classes are derived from ios, they're often referred to as the "iostream classes." Figure A shows the hierarchy of the ifstream, ofstream, and fstream classes.

Figure A: Here is a portion of the ios hierarchy.
[ Figure A ]

As you can see, the fstream class is derived from iostream, which is derived from both ostream and istream. This structure allows simultaneous reading and writing of a file. (By simultaneous, we mean that you can open a file, write to part of it, change the file position pointer, read from the file at the new location, and so on. The need to write and read a file simultaneously is fairly specialized, so we won't spend much time talking about it.)

Overall, the iostream classes are very powerful and also quite portable (again, if portability is important to you). If you're new to C++ programming, then you may be somewhat dismayed to learn that Borland inadvertently omitted the Help file describing the iostream classes from the C++Builder CD. If you have a previous Borland C++ compiler, you can look in CLASSLIB.HLP for help on the iostream classes; in addition, we'll try to give you enough information so that you can at least begin to use the stream classes. First, let's discuss some information common to all file operations. Then we'll look at reading and writing files individually.

iostream basics

As Figure A shows, the file stream classes derive from ios. As such, most of the basic streaming functionality comes from the ios class. Still, the ios class has only a couple of items in which we're interested when discussing file I/O. First are the open_mode flags. open_mode is an enumeration defined as follows:
enum open_mode { app, ate, in, out, binary, trunc, nocreate, noreplace };
Table A lists the open_mode members and their descriptions.

Table A: open_mode enumeration members
Member Description
app Append data--always write at end of file.
ate Seek to end of file upon original open.
in Open for input (default for ifstreams).
out Open for output (default for ofstreams).
binary Open file in binary mode.
trunc Discard contents if file exists (default if out is specified and neither ate nor app is specified).
nocreate If file does not exist, open fails.
noreplace If file exists, open for output fails unless ate or app is set.

The open_mode enumeration acts as a set of flags that you can or together to specify the file mode when you open a file. For example, let's say you want to open a file in binary mode and append data to the end of the file. The code will look something like this:

ifstream file;
file.open("data.txt", ios::app | ios::binary);
Note that you need to qualify the flag values with the ios class name since open_mode is defined within the ios class. As another example, suppose you want to open a file for writing but ensure that you don't accidentally overwrite an existing file of the same name. You can use the following code:
ofstream file;
file.open("data.txt", ios::out | ios::nocreate);
In this case, the call to open() will fail if a file already exists by the same filename. The other significant aspect of the ios class is the eof() method. This method returns a non-zero value when the stream position indicator reaches the end of the file. You can use eof() to determine when you've reached the end of the file when reading data files. We'll show you examples a little later.

ios insertion and extraction operators

The stream insertion operator (<<) writes data to a stream. Let's say you've opened a file for writing. Once the file is open, you can write a line of text to the file like this:
file << "This is a test";
You can chain multiple insertion operations. For example, the following line of code has the same effect as the previous example:
file << "This is " << "a test";
Note that neither example terminates the string with a carriage return. You can add a CR in one of two ways:
file << "This is a test\n";
// or...
file << "This is a test" << endl;
The first case appends the standard C "\n" escape sequence to the string. The second case uses the endl operator (endl is short for "end of line"). You can also write variables to a file using the insertion operator. For example:
char* buff = "This is a character array.";
int x = 100;
double d = 99.9;
char c = `a';
file << buff << endl << x 
  << endl << d << endl << c << endl;
Executing this code will result in a text file with the following contents:
This is a character array.
100
99.9
a
The important thing to realize here is that the numeric values are converted to strings and stored in the file as text strings, not as binary data as you might expect. The extraction operator (>>) works in the opposite manner... more or less. You'd think that to extract the characters from the file written in the previous example, you could use code like the following:
char buff[40];
int x;
double d;
char c;
file >> buff >> x >> d >> c;
The problem is that the extraction operator stops at the first white-space character it encounters (white space includes things like spaces, tabs, and the new-line character). For instance in the sentence "This is a character array," the word This is read into the variable buff, the space character is ignored, and then the word is is read into the integer variable x--obviously not what you had in mind. From this point on, things go haywire and data corruption is inevitable. So while the extraction operator is great in some cases, it isn't the do-all method of reading files. In just a bit we'll talk about the ifstream class, and then we'll show you how to correctly read the file in the previous example. Note that you can define your own operator << and operator >> for your C++ classes. Once you've defined these operators, you can write code like this:

MyClass mc1;
MyClass mc2;
// save the class data to a file
file << mc1 << mc2;
And then later...
MyClass mc1;
MyClass mc2;
// read the class data from the file
file >> mc1 >> mc2;
Provided that you've written your insertion and extraction operators correctly, the class is saved and restored as easily as that. Unfortunately, a discussion of overloading the << and >> operators is beyond the scope of this article, so we'll leave you with that little tidbit running around in your head. Now, let's move on to a detailed look at file I/O.

ifstream: reading files

Reading files is perhaps more common than writing files, so we'll start there. The ifstream class reads files on disk (the if in ifstream stands for input file). Table B lists the basic ifstream methods. Keep in mind that many of these methods come from ifstream's ancestor class, istream. Most of these functions are overloaded to provide extended functionality. The get() function, for instance, can read a single character or a block of characters, depending on the calling parameters.

Table B: Important ifstream methods
Method Description
close() Closes the file.
get() Reads one or more characters from the file.
getline() Reads a line of text from a text file or reads data from a binary file until the specified delimiter is read.
open() Opens the file for reading
peek() Looks at the next character in the stream but doesn't extract the character.
read() Reads a specified number of characters from the file into memory.
seekg() Moves the file position indicator to a specific location in the file.
tellg() Returns the value of the file position indicator.

As always, an example will go a long way toward furthering your understanding of file I/O operations using ifstream. The code shown in Listing A reads each line of a text file and then adds the line to a Memo component.

Listing A: Using ifstream

#include <fstream.h>
// create an instance of ifstream
ifstream file;
// open the file
file.open("unit1.cpp"); 
// something went wrong
if (!file) return;
// create a buffer for storage
char buff[80]; 
// loop while not at the end of file
while (!file.eof()) { 
  // read a line from the file
  file.getline(buff, sizeof(buff));
  // add it to the Memo
  Memo1->Lines->Add(buff); 
}
// close the file 
file.close();
Notice first that you must include the FSTREAM.H header file, which contains the declarations for all the file-streaming classes. Next, notice that you use the open() method to open a file. You don't have to specify any of the open_mode flags because you want the default ifstream flags of ios::in when reading a text file.

After the call to open(), you check to see whether the file was opened successfully and return if the open operation failed. Next, you read one line at a time from the file with the getline() method, which will retrieve characters until it encounters a CR pair. Now you add the line to the Memo component.

At the top of the loop, you check for the end of file with the eof() method. When the entire file has been read, you close the file with the close() method. (This example ignores the fact that the Lines property of TMemo has a LoadFromFile() method, which is a much easier way of loading a file into a Memo component.)

In this example, we used the open() and close() functions simply to illustrate a point--they aren't strictly needed. The open() function isn't needed because you can use one of the ifstream constructors to create the file object and open the file all at once, as follows:

ifstream file("unit1.cpp");
if (!file) return;
The close() function isn't strictly necessary because the ifstream destructor will close the file for you. (You can certainly call close() explicitly if you wish.) Finally, we wrote the last few steps of the sample code in a way that highlights their purposes--but as written, the code adds one extra blank line of text to the Memo. In order to be technically correct, the code that reads the lines of text should look like this:
while (!file.getline(buff, sizeof(buff)).eof())
  Memo1->Lines->Add(buff);
While this code is sort of ugly and hard to read, it's nevertheless the proper way to check for the end-of-file indicator. Remember our earlier example of the extraction operator that incorrectly read a file? Here's the proper way to read the data:
ifstream file("temp.txt");
char buff[40];
int x;
double d;
char c;
file.getline(buff, sizeof(buff));
file >> x >> d >> c;
First you get the line of text with the getline() method. After that, you can use the extraction operator to read the remaining data from the file. A little later we'll talk more about reading and writing binary data files, but for now let's move on to writing files with the ofstream class.

ofstream: writing files

The ofstream class is the functional opposite of the ifstream class. You'll use ofstream to write data to files on disk. When it comes right down to it, writing files isn't all that complicated. Table C lists the important methods of the ofstream class. For the most part, these functions require little explanation; we'll provide examples in just a bit.

Table C: Important ofstream methods
Method Description
close() Closes the file.
put() Writes a single character to the file.
open() Opens the file for writing.
write() Writes a specified number of bytes from memory to the file.
seekp() Moves the file position indicator to a specific location in the file.
tellp() Returns the value of the file position indicator.

Earlier, we discussed the ios insertion and extraction operators. The insertion operator works very well to write text. The following example writes 10 lines of text to a file:

ofstream outfile("temp.txt");
if (!outfile) return;
for (int i=1;i<11;i++)
  outfile << "This is line #" << i << endl;
outfile.close();
Notice that each line is terminated using the endl manipulator. After this code executes, the file TEMP.TXT looks like this:

This is line #1
This is line #2
This is line #3
This is line #4
This is line #5
This is line #6
This is line #7
This is line #8
This is line #9
This is line #10
To prove this, create a new project in C++Builder and enter the previous code in response to a button click (don't forget to include FSTREAM.H.). After you run the program, open TEMP.TXT in the C++Builder editor, and the file should contain the lines we showed you. You can create this same file using the write() method, but it's more cumbersome. To do so, the for loop in the code would look like this:
for (int i=1;i<11;i++) {
  char buff[20];
  sprintf(buff, "This is line #%d\n", i);
  outfile.write(buff, strlen(buff));
}
The end result is the same, but using the << operator is much cleaner. While the write() method isn't the best for writing text files, it's much more important when writing binary files. Let's take a look at that next.

Dealing with binary data

Dealing with binary data is somewhat different from dealing with text data. For one thing, the data must be written in some logical arrangement and then read in exactly the same way. A data structure like the following allows you to do that fairly easily:
struct Data {
  char Name[20];
  char Phone[20];
  int  Age;
  int  ID;
};
This is a logical, albeit simple, data arrangement. Writing this structure to disk using ofstream is as simple as
Data MyData = {"Billy Bob", "none", 36, 1};
ofstream outfile("names.dat", ios::binary);
outfile.write((char*)&MyData, sizeof(Data));
The write() method expects a char* rather than a void*, so you must take the address of the structure and cast it to a char*. You write the exact number of bytes contained in a data structure (sizeof(Data)), which means that the same number of bytes is written regardless of the data in the structure. The code writes the file in block format with each block occupying the same number of bytes. (Later, you can use this arrangement to read a particular block in the file.) Notice that we used the ios::binary flag when we opened the file for writing. If you write a file in binary mode, then you also need to specify the ios::binary flag when you open the file for reading. Speaking of reading a file, reading the binary data is just as easy:

ifstream infile("names.dat", ios::binary);
if (!infile) return;
Data MyData;
infile.read((char*)&MyData, sizeof(Data));
Here, the structure is filled with the bytes read from the file. You can then do whatever you want with the data in the structure. To read raw binary data from a file one byte at a time, use get(); to write to a file, use put(). For example, a file-copy operation might look like this:

ifstream infile("names.dat", ios::binary);
ofstream outfile("temp.fil", ios::binary);
infile.seekg(0, ifstream::end);
int numBytes = infile.tellg();
infile.seekg(0);
for (int i=0;i<numBytes;i++) {
  char c;
  infile.get(c);
  outfile.put(c);
}
The type of data you're dealing with will dictate the method you use to read and write binary data.

Random file access

Imagine you have a file containing 1,000 records like those we just discussed. Let's further say you want to read record number 999. You could loop through the file, reading records until you finally get to record 999. Obviously this isn't very efficient. A better way would be to set the file-stream pointer to the exact location of record 999 in the file, then read just that record. In the world of file I/O, this is known as seeking. Seeking works effectively only when a file is filled with records of a known size, or if you know the exact layout of a file. In this case, you know the record size, so getting the correct file position is a matter of a simple calculation:
int pos = 998 * sizeof(Data);
Now you can open the file, seek to record number 999, and read the record at that position, as follows:
ifstream infile("names.dat", ios::binary);
infile.seekg(pos);
Data MyData;
infile.read((char*)&MyData, sizeof(Data));
Note that since the first record is at file position 0, the 999th record is at 998 multiplied by the size of a record. Similarly, you can replace or update a record in a file. To do so, you need to open the file in update mode:
ofstream outfile(
  "names.dat", ios::binary | ios::ate);
int pos = 998 * sizeof(Data);
outfile.seekp(pos);
outfile.write((char*)&MyData, sizeof(Data));
Here you use the ios::ate flag in addition to the ios::binary flag. You could use ios::app (append mode) and achieve the same results. If you hadn't specified ios::ate, then the previous file would have been overwritten when the file was opened (oops!). The rest is pretty straightforward--just seek to the appropriate place in the file and write the new information to the file. Finally, you could open the file for simultaneous reading and writing by using the fstream class rather than using ofstream to write a file and ifstream to read the file. Since fstream is derived from both ostream and istream (the ancestor classes of ofstream and ifstream, respectively), all the previously mentioned functions are available for use in fstream. The example in Listing B shows how you could use fstream to swap the first and the tenth records in a file.

Listing B: Swapping records using fstream

Data record1;
Data record2;
// open the file in read and write mode
fstream iofile("temp3.dat",
  ios::binary | ios::in | ios::out);
// seek to the 10th record and read the record
iofile.seekg(9 * sizeof(Data));
iofile.read((char*)&record1, sizeof(Data));
// seek to the first record and read it
iofile.seekg(0);
iofile.read((char*)&record2, sizeof(Data));
// seek to the first record again and write
// the data read from the 10th record
iofile.seekg(0);
iofile.write((char*)&record1, sizeof(Data));
// go back to record #10 and write the data
// read from record #0
iofile.seekg(9 * sizeof(Data));
iofile.write((char*)&record2, sizeof(Data));
iofile.close();
Notice that you set the ios::in and ios::out flags in the constructor. You must do this to tell iostream that you'll be doing both read and write operations on the file. Aside from that, the code snippet in Listing B contains nothing new, other than the fact that you use the read() and write() functions together.

VCL's TFileStream

Now that you know how to read and write files using iostream, let's take a quick look at VCL's answer to file I/O: TFileStream. This section will be fairly short for two reasons: First, most of the concepts discussed earlier also apply to TFileStream. Second, TFileStream is less complicated (and slightly less capable) than iostream. Table D lists the primary properties and methods of TFileStream.

Table D: Important TFileStream properties and methods
Property Description
Position The current value of the file position indicator. Position is a read/write property.
Size The current size of the file's data.
Method constructor Opens a file in a specific mode (create, read, write, or read/write).
CopyFrom() Copies a specified number of bytes from a stream to this stream.
Read() Reads a specified number of bytes from the file to the specified memory location.
Write() Writes a specified number of bytes from a memory location to the file.
Seek() Moves the file position indicator by the specified amount either from the start of the file, the end of the file, or from the current position.

Right away, you should notice that TFileStream has much in common with the iostream classes. In particular, the Read() and Write() methods are functionally identical to the read() and write() methods of the iostream classes. The Position property of TFileStream simplifies seeking in a file and performs the same function as the ifstream methods tellg() and seekg(). You can read Position to determine the current file position, or you can write to Position to move the file position. Doing so is much easier and more intuitive than using tellg() and seekg() as you do in iostream operations.

One rather odd omission in TFileStream is the lack of an equivalent to the ifstream readline() method. As a result, TFileStream is less than ideal for reading text files on a line-by-line basis. This isn't a big problem, however, since many VCL components (TMemo, TListBox, TComboBox, TTreeView, and so on) have LoadFromFile() and SaveToFile() methods that make saving and reloading their contents frightfully easy.

You can use TFileStream as an input file, an output file, or both--you specify the mode and filename when you create a TFileStream object. The following example opens a file for reading:

TFileStream* fs = 
  new TFileStream("names.dat", fmOpenRead);
The next example opens a TFileStream in read/write mode:
TFileStream* fs = 
  new TFileStream("names.dat", fmOpenReadWrite);
In addition to the file mode, you can also specify the share mode. The share mode allows you to specify whether the file is opened for exclusive use or others are allowed to read and write the file while you have it open. For full details on the file open and share modes, see the VCL documentation for TFileStream. Earlier we showed an example of swapping two records in a file using the fstream class. Listing C shows that same example using TFileStream instead of fstream.

Listing C: Swapping files using TFileStream

TFileStream* fs =
  new TFileStream("names.dat", 
  fmOpenReadWrite);
fs->Position = 998 * sizeof(Data);
fs->Read(&record1, sizeof(Data));
fs->Position = 0;
fs->Read(&record2, sizeof(Data));
fs->Position = 0;
fs->Write(&record1, sizeof(Data));
fs->Position = 998 * sizeof(Data);
fs->Write(&record2, sizeof(Data));
delete fs;
Note that the TFileStream destructor will close the file when you delete the TFileStream object. In fact, strange as it may seem, TFileStream doesn't provide methods for opening and closing files--the constructor and destructor take care of those chores. As you can see from the previous example, the basic concept is the same as with iostream, although you may find the TFileStream way of doing things a little cleaner.

One major difference between TFileStream and the iostream classes lies in error handling. TFileStream throws exceptions if something goes wrong (such as "file not found" or trying to seek past the end of a file), whereas the iostream classes leave error control up to the programmer. Here again, the TFileStream way of doing things is slightly superior.

Wrap up

Given these choices, what type of file access do you use? It comes down to personal preference. Both iostream and TFileStream have their strengths and weaknesses. As always, your choice of file access method might depend on the specific task at hand. While TFileStream is a little easier to understand, it lacks some of the power of iostream. We should point out that you could use TFileStream and iostream interchangeably. In other words, a file written with ofstream can be read by TFileStream and vice versa.


Kent Reisdorph is a editor of the C++Builder Developer's Journal as well as director of systems and services at TurboPower Software Company, and a member of TeamB, Borland's volunteer online support group. He's the author of Teach Yourself C++Builder in 21 Days and Teach Yourself C++Builder in 14 Days. You can contact Kent at editor@bridgespublishing.com.