| |
Main Menu | Next Section |
IV. File Handling
A. Output
We finally get to that part of the chapter that is of most interest
to you, as a programmer on the 'Olympia' project, since it describes
how to finally give our contract program the ability to handle
files. As usual, before we get to the specifics of the Contract
program, let's learn how to work with files in general. Suppose
you wanted to output the simple set of data used in the previous
section to a file called 'person.dat'. Remember, all output is
the in the form of streams. To output the data to the file 'person.dat',
you need to instantiate a stream and connect it to the file 'person.dat'.
(Once a stream is instantiated, it is already connected to the program by the nature of a C++ stream.)
The code to instantiate the stream and connect it to the file is as follows
and a picture of the results might look as follows:
DRAW
If you review the input-output inheritance structure found in Figure 11.2 above, you
will see that 'ofstream' is a class name. Therefore, what we are
doing here is calling the constructor for this class to instantiate
an instance called 'outputData'. We can use any name we want for
this instance - including 'cout', but that would not be adviseable
since you would then lose access to the 'built-in' cout.
The file name inside the parameters,that is "person.dat", is
passed to the constructor which uses it to make the connection
to a file with the same name. If no such file exists, by default one
will be created. Exactly where in the directory structure that
file is created depends on the compiler you are using and the
options you have set. Also, there are additional, optional parameters
one can use in the constructor to control if and how the file
is created, but that is beyond the scope of this text. Check the
"Essentials of C++" for more details.
Now that we have our new stream, we need to use it. In other words,
we need to send output to it. In the above code, we sent the output
to 'cout' so all we need to do is change all the "cout's"
to 'outputData'. Here is the code put in the form of a file complete
with main and the necessary 'include' statements.
// Basic File Handling
// File: fileio1.cpp
#include <iostream.h>
#include <iomanip.h>
#include <fstream.h>
void main()
{ ofstream outputData ("person.dat");
outputData << setiosflags(ios::fixed);
outputData << setprecision(2);
outputData << setw(30) << setiosflags(ios::left) << "Name"
<< setiosflags(ios::right)
<< setw(16) << "Phone Number"
<< setw(6) << "Age" << setw(10) << "Height" << endl;
outputData << setw(30) << setiosflags(ios::left) << "Curtis"
<< setiosflags(ios::right)
<< setw(16) << "505-454-3302"
<< setw(6) << 6 << setw(10) << 5.75 << endl;
outputData << setw(30) << setiosflags(ios::left) << "Fred Smith"
<< setiosflags(ios::right)
<< setw(16) << "505-454-3302"
<< setw(6) << 102 << setw(10) << 5.128<< endl;
}
B. File Input
Now that we have a file with data in it, we can work on reading
the data back into a program. Actually, one can make a data
file in many ways, for example by entering the data by hand using
the editor that came with your compiler or through Notepad. However,
since we already have a data file, why make a new one.
Reading information from a file is much harder than writing to
a file. The reason is that you must write the code to precisely
handle the spacing of the file as well as the order and type of
the each data item. In our case, there are two strings, separated
by a variable number of blanks followed by an integer and a float.
Frankly, it often takes a bit of experimenting to get it right, and often the data in the input file is carefully arranged to make the process easier. In this example, the data was arranged more for human
processing than computer processing but we will deal with it.
Just as with the output file, the first step is to connect the
file to an input stream. The code looks very similar:
This time we work with the class 'ifstream', also declared in
the header file 'fstream.h'. The variable identifier 'inputData'
is the name for the instance of the stream we are creating, and
'person.dat' is again the file name passed as a parameter
to the class constructor. With the output file it made little
difference what file name we provide because the system would
create a new file if one did not already exist. Indeed, the only
concern we might have had was to make sure we did not overwrite
a file we wanted to keep. The input file is just the opposite.
Now we must make sure that the name we provide matches exactly
the name of the file that holds the data we want to read in. Since
we wrote the data to 'person.dat', that is the file we read from.
Clearly, it is possible to make a mistake and request a file that
does not exist. The file may never have been created; it may be
in a different directory; or it may have been accidentally deleted.
If the file does not exist and we do not tell the system what else to do in this case, the program will still continue - with the input requests being ignored because the file has a 'fail' status. Thus, it is
always wise to code in a check to see if the input file was successfully
opened. Here is the code for this:
This means:
To avoid this somewhat ugly syntax and simply make coding easier,
C++ allows one to simply use the stream name. When used where
a Boolean value is expected, a stream name is treated as a request
for the status of the stream. In this case, however, it is equivalent
to a call to inputData.good(). Therefore, true is returned if
everything is OK and false is returned if a failure has occurred.
That is the reason for the '!' symbol.
The code inside the 'if' statement, first, causes an error message
to be output. Then, the call to the function 'exit' causes the
program to terminate. The program simply stops and the rest of
the code is not executed. The parameter value of '1' inside the
function call causes the program to return the value 1, indicating
an error. By convention programs return a 0 if there is a problem
and some value other than 0 if there is an error.
Up to now we have declared 'main' as a function that does not
return anything. Since some operating systems can take advantage
of the value returned by a program, it is really better to have
programs return a value. Thus, in the code below we will see that
'main' has been re-declared as:
Before proceeding any further, let's view a whole program based upon these new ideas. The program below simply reads in the two records output above, skipping the column headings, and outputs the results to another file.
// Basic File Handling
// File: fileio2.cpp
#include <iostream.h>
#include <iomanip.h>
#include <stdlib.h>
#include <fstream.h>
typedef char String[40];
int main()
{
ifstream inputData ("personabc.dat");
if (!inputData)
{ cout << "Cannot open input file\n";
exit(1);
}
String name;
String phoneNo;
String dummy;
int age;
double height;
ofstream outputData ("personout.dat");
if (!outputData)
{ cout << "Cannot open output file\n";
exit(1);
}
outputData << setiosflags(ios::fixed);
outputData << setprecision(2);
outputData << setiosflags(ios::left) << setw(30) << "Name"
<< setiosflags(ios::right)
<< setw(16) << "Phone Number"
<< setw(6) << "Age" << setw(10) << "Height" << endl;
inputData.ignore(133, '\n');
inputData.get(name, 30);
inputData >> phoneNo;
inputData >> age;
inputData >> height;
inputData.getline(dummy, 80);
outputData << setw(30) << setiosflags(ios::left) << name
<< setiosflags(ios::right)
<< setw(16) << phoneNo
<< setw(6) << age << setw(10) << height << endl;
inputData.get(name, 30);
inputData >> phoneNo;
inputData >> age;
inputData >> height;
inputData.getline(dummy, 80);
outputData << setw(30) << setiosflags(ios::left) << name
<< setiosflags(ios::right)
<< setw(16) << phoneNo
<< setw(6) << age << setw(10) << height << endl;
return 0;
}
The first thing to observe is that there is yet another header
file included. The file <stdlib.h> contains the declaration
for the function 'exit'. Note also that we now have a use for
the file 'iostream.h' because we use 'cout' to output the error
message. Next, observe that the function 'main' now returns an
'int'. Because of this, we also have added a new line at the end
of the program to force the program to return a '0' when it has
completed.
Back to the top of the code: After declaring the input stream,
the program declares the variables needed to hold the data to
be read in and also declares the output stream. This time the
output file name is changed, not because it has to be changed
but because it is simply safer. We do not want to ruin our input
file if something happens in the course of running this modified
program. We also add the stream error checking code for the output
file just in case something were to happen - for example, if there
was no room on the disk for a new file.
After the declarations, the program outputs the column headings
and begins to read in the first line of data. Before reading the
data, however, the system must skip past the column headings that
are out on disk. That is the purpose of:
This instruction tells the system to skip up to and including
the first newline character it encounters or to skip 133 characters
- whichever comes first. Now, where does '133' come from? We could
have counted exactly how many characters there are in the heading
but that is a hassle and besides what if we change the heading.
In one sense the number '133' is arbitrary - big enough to make
sure we skip enough characters - but it also reveals that the
coder has been around for awhile - to say the least. Since printers
tend to print either 80 characters plus 1 control character or
132 characers plus one control character, back in the days when
almost all output was sent to a printer, the values 81 and 133
were important.
Anyway, with this line executed, the file pointer is
pointing to the first character on the first line of actual data (the second line of the file), and it is time to read in the name of the first person in the
file. Think of the 'file pointer' as an indicator of where in
the file the next input is to come from. There isn't really a
pointer in the sense that we used pointers earlier in this text.
It is more of an image. One can also think of it as the first unread
data in the input stream.
Notice that we use the member function 'get' instead of writing
"inputData >> name;". Names, of course, are usually
in multiple parts separated by blanks, and, as we know, '>>'
stops at the first blank. If we had used the '>>' operator,
we would have only read in the first name. Not only would the
variable name have had an incomplete value, but, when we went
to read in the telephone number, we would have, instead, read in
the rest of the name because that is where the file pointer would
have been pointing. Because we know that the output produced a
block of 30 characters via the 'setw(30)' manipulator, we use
the same value of 30 in the 'get' function.
Just to show that the '>>' operator can be used (where appropriate),
we use it to read in the phone number - which does not contain any
blanks. Of course, we had better, then, make sure that those who
input telephone numbers do so correctly. The whole issue of data
validation is complex and somewhat tedious, not a good topic for
discussion here where the goal is to simply give you an overview
of file processing. In this case, it might have been better to
stick with the 'get' function to avoid this potential problem.
Next, we use the '>>' operator to read in the age and height.
The 'get' function would not have worked here because it expects
to read in and process characters but we want to process an integer
and a double.
Finally, we need to skip past the newline character at the end
of the line. We, therefore, declare a string variable 'dummy'
to hold this character when it is read in and use 'getline' to
read it and skip to the next data line. (The function 'getline'
expects a string as its first parameter so we declared 'dummy'
to be a string instead of a char even though the newline character
('\0') is really only one character.) All this provides a hint
at the need to adapt ones input code to exactly match the structure
of the file holding the data.
The rest of the code simply outputs the information, using variables instead of constants, and then repeats the same process
for the second line. Had this been a real program, where we did
not know how many data lines there were in the file, we would
have coded this as a while loop with a stopping condition that
tested for the end of the file. We will explore that in the next
section when we work with the Contract program.
C. Entering File Names
Before we get to that, however, there is one other issue. As coded,
if we want to use different files for input or output on different
occasions, we need to modify the actual code for each new file
name and recompile the program. It would be much better if the
program asked the user for the names of the input and output files.
And, now that we know how to read in strings, that is easy to
do.
One way to accomplish this is to declare a function to ask the user for the name of a file. This function will receive a prompt string (so that we can use it both to request the input and output files) and return the file name. Here is the definition for this file:
Topics Covered in the "Essentials of C++"
| |
Main Menu | Next Section |