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.
| The C-style FILE mechanism | |||||
The C++ iostream classes
| The VCL streaming classes (TFileStream)
| The VCL database mechanism
| |
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.
Figure A: Here is a portion of the ios hierarchy.
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.
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.
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 aThe 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.
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.
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 #10To 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.
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.
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.
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.
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.