Lab 8: File I/O


Introduction

Throughout this manual, we have made extensive use of files -- containers on a hard or floppy disk that can be used to store information for long periods of time. For example, each source program that we have written has been stored in a file, and each binary executable program has also been stored in a file. While they may seem to be the same, files differ from programs in that a program is a sequence of instructions, and a file is a container in which a program (among other things) can be stored.

A different use for files is to store data. That is, where each of our exercises have thus far read data from the keyboard and written data to the screen, an alternative approach is to store the data to be read in a file, and read the data from there. Similarly, instead of writing data to the screen, there are situations where it is useful to instead have a program write its data to a file. This approach is particular useful for problems where the amount of data to be processed is so large that entering the data each time the program is executed (interactively) becomes inconvenient. That inconvenience can be eliminated by storing the data in a file, and then having the program read from the file, instead of the keyboard. Today's exercise is to learn how to do so.

When many of us were younger, we enjoyed writing secret messages, in which messages were encoded in such a way as to prevent others from reading them, unless they were in possession of a secret that enabled them to decode the message. Coded messages of this sort have a long history. For example, the Caesar cipher is a simple means of encoding messages that dates from Roman times. To illustrate, the Caesar cipher produces the encoded sentence:

   Rqh li eb odqg, wzr li eb vhd.
when applied to the historic phrase:
   One if by land, two if by sea.
What is the relationship between the letters in the original sentence and those in the encoded sentence?

Today's exercise is to use the Caesar cipher to encode and decode messages stored in files.

Getting Started

Create a new directory for the files of this exercise, and in it, save copies of the files encode.cpp, decode.cpp, message.text, and alice.code. Then personalize the documentation in the file encode.cpp, and take a few moments to study its contents.

An Encoding Program

The first part of today's exercise is to write a program that can be used to encode a message that is stored in a file. To demonstrate both input from and output to a file, we will store the encoded message in a second file.

Design

As usual, we will apply object-centered design to solve this problem.

Behavior. Our program should display a greeting and then prompt for and read the name of the input file. It should then connect an input stream to that file so that we can read from it, and check that the stream opened correctly. It should then prompt for and read the name of the output file. It should then connect an output stream to that file so that we can write to it, and check that the stream opened correctly. For each character in the input file, our program should read the character, encode it using the Caesar cipher, and output the encoded character to the output file. Our program should conclude by disconnecting the streams from the files.

Objects. Using this behavioral description, we can identify the following objects:

Description Type Kind Name
a greeting string constant none
The name of the input file string varying inFile
An input stream ifstream varying inStream
The name of the output file string varying outFile
An output stream ofstream varying outStream
a character from the input file char varying inChar
an encoded character char varying outChar

Using this list of objects, we might specify the behavior of our program as follows:

   Input(inFile), a sequence of unencoded characters.
   Output(outFile), a sequence of encoded characters.

Operations. From our behavioral description, we have these operations:

Description Defined? Name Library?
1 Display a string yes << iostream
2 Read a string yes >> iostream
3 Connect an input stream to a file yes ifstream declaration fstream
4 Connect an output stream to a file yes ofstream declaration fstream
5 Check that a stream opened properly yes assert(), is_open() cassert, fstream
6 Read a char from an input stream yes >> fstream
7 Encode a char using the Caesar cipher yes CaesarEncode() encode.cpp
8 Write a char to an output stream yes << fstream
9 Repeat 7, 8, 9 for each char in the file yes input loop built-in
10 Determine when all chars have been read yes eof() fstream
11 Disconnect a stream from a file yes close() fstream

Algorithm. We can organize these operations into the following algorithm:

   0. Display a greeting.
   1. Prompt for and read inFile, the name of the input file.
   2. Create an ifstream named inStream connecting
       our program to inFile.
   3. Check that inStream opened correctly.
   4. Prompt for and read outFile, the name of the output file.
   5. Create an ofstream named outStream connecting
       our program to outFile.
   6. Check that outStream opened correctly.
   7. Loop through the following steps:
      a. read a character from the input file.
      b. if end-of-file was reached, then terminate repetition.
      c. encode the character.
      d. write the encoded character to the output file.
      End loop.
   8. Close the input and output connections.
   9. Display a "successful completion" message.

Coding

encode.cpp already implements a number of these steps. It should be evident that we can perform

That leaves the file-related operations in steps 2, 3, 5, 6, 7a, 7d and 8 for us to learn how to perform.

Opening a Connection to a File. An executing program is unable to interact directly with a file for a very simple reason: an executing program resides in main memory and a file resides on a secondary memory device, such as a hard disk. However, an executing program can interact indirectly with a file, by opening a connection between the program and that file. In C++, such connections are called fstream objects.

Like any other object, an fstream must be declared before it can be used. If inputFileName is a string object containing the name of an input file, then the declaration

   ifstream inFStreamName(inputFileName.data());
constructs an object named inFStreamName as a connection to that file. (The string function member data() extracts the actual characters from a string. If your compiler is not fully ANSI compliant, you may have to use the c_str() function member instead.) An ifstream thus provides a connection to a file, by which a program can read values from the file.

Using this information, implement step 2 of our algorithm in encode.cpp by declaring an ifstream named inStream that serves as a connection between our program and the file whose name is in inFile.

To perform step 5 of our algorithm, we must open an fstream for output to the output file. Such objects are of type ofstream, and can be declared as follows:

   ofstream outFStreamName(outputFileName.data());
Such a declaration constructs an object named outFStreamName as a connection to the file whose name is stored in outputFileName. If the file does not exist, then a file by that name is created in the working directory. If the file does exist, then its contents are erased (unless ios::append is passed as a second argument, in which case any values written to the file are appended to its end). An ofstream thus provides a connection to a file, by which a program can write values to a file.

Using this information, implement step 5 of our algorithm by declaring an ofstream named outStream that serves as a connection between our program and the file whose name is in outFile.

Checking that a Connection Opened Correctly. Opening files is an operation that is highly susceptible to user errors. For example, suppose the user has accidentally deleted the input file and our program tries to open a connection to it? If an fstream opens as expected, the operation is said to succeed, but if it does not open as expected, the operation is said to fail.

To detect the success of an open operation, fstream objects contain an is_open() function member:

   FStreamName.is_open()
which returns true (1), if FStreamName is open, and returns false (0), otherwise. When combined with an assert(), the is_open() function member provides a readable way to perform steps 3 and 6 of our algorithm. In encode.cpp, use this information to complete step 3 of our algorithm:
    assert ( /* inStream opened successfully */ );
and also step 6 of our algorithm:
    assert ( /* outStream opened successfully */);
by replacing the comments with appropriate calls to the is_open() function member. Then test what you have written by using the compiler to translate encode.cpp. Unless you're ahead of the game, the compiler should generate an error message. What is it?



The reason for this error message is that ifstream and ofstream are themselves identifiers, declared in the header file <fstream>, and so that file must be included, in order for these names to be declared. Add the necessary #include directive to do so, and then recompile your source program and the error should disappear.

When your source program compiles correctly, do not execute your source program yet (or an infinite loop will occur.) Instead, continue to the next part of the exercise.

Input from an fstream. Just as we have used the >> operator is used to read data from the istream named cin, the >> operator can be used to read data from an ifstream opened for input. Since the ifstream connects a file to a program, applying >> to it transfers data from the file to the program. For this reason, this operation is described as reading from the file, even though we are actually operating on the fstream. An expression of the form:

   inputFStreamName >> VariableName
thus serves to read values from an ifstream named inputFStreamName (and thus from the file to which inputFStreamName is a connection) into the variable VariableName. Of course, the type of the value being read must match the type of VariableName, or the operation will fail.

While the input operator is the appropriate operator to solve many problems involving file input, it is not the appropriate operator for our problem. The reason is that like its interactive counter-part, the >> operator skips leading white space characters. That is, if our input were

   One if by land.
   Two if by sea.
and we were to use the >> operator (in a loop) to read each of these characters:
   inStream >> inChar;
then all white space characters (blanks, tabs and newlines) would be skipped, so that only non- white space characters would be processed and output. Our encoded version message would then appear without white space (i.e., missing blanks and newlines) as follows:
   Rqhliebodqg.wzrliebvhd.
To avoid this problem, ifstream objects (like istream objects) contain a get() member function:
   inputFStreamName.get( CharacterVariable );
When execution reaches such a statement, the next character is read from inputFStreamName and stored in CharacterVariable, even if it is a white space character. The get() member function of inStream can thus be used to perform step 7a of our algorithm. In encode.cpp, place a call to get() in the appropriate place to read a character from inStream into inChar. Then compile your program, and continue when what you have written is syntactically correct.

Controlling a File-Input Loop. Files are created by a computer's operating system. When the operating system creates a file, it marks the end of the file with a special end-of-file mark. Input operations are then implemented in such a way as to prevent them from reading beyond the end-of-file mark, since doing so could allow a programmer unauthorized access to the files of another programmer.

This end-of-file mark can be used to control a loop that is reading data from the file. Just as an istream has a function member named eof() that can be used to control an interactive input loop, an ifstream has a function member named eof() that can be used to control a file input loop, with the form:

   inputFStreamName.eof()
When execution reaches this expression, the eof() function returns true (1) if the last read from inputFStreamName tried to read the end-of-file mark, and returns false(0) otherwise.

In a forever loop like the one in the source program, the eof() function can be used to prevent infinite loop behavior. By placing an if-break combination:

   if ( /* end-of-file has been reached */ ) break;
following the input step, repetition will be terminated when all of the data in the file has been processed.

In your source program, place an if-break combination in the appropriate place to perform step 7b of our algorithm, using the eof() member function of inStream as the condition in the if statement. Then compile your source program, to check the syntax of what you have written. When it is syntactically correct, continue to the next part of the exercise.

File Output. Just as we have used the << operator to write data to the ostream named cout, the << operator can be used to write data to an ofstream opened for output. Since the ofstream connects a program to a file, applying << to it transfers data from the program to the file. This operation is thus described as writing to the file, even though it is an ofstream operation. The general form is similar to what we have seen before:

   outputFStreamName << Value ;
where outputFStreamName is an ofstream, and Value is a character or constant we wish to store in the file to which outputFStreamName is a connection.

Use either of these statements to write the encoded character to your output file via outStream, to perform step 7d of our algorithm. Then compile your source program to test the syntax of what you have written, continuing when it is correct.

Closing Files. Once we are done using an fstream to read from or write to a file, we should close it, to break the connection between our program and the file. This is accomplished using the fstream function member close(), whose statement form is

   FStreamName.close();
When execution reaches this statement, the program severs its connection to FStreamName.

In the appropriate place in the source program, place calls to close() to

Then translate your source program, and ensure that it is free of syntax errors.

Testing and Debugging

When your program's syntax is correct, test it using the provided file named message.text. If what you have written is correct, your program should create an output file (e.g., message.code), containing the output:

Rqh Li Eb Odqg
Wzr Li Eb Vhd
If this file is not produced, then your program contains a logical error. Retrace your steps, comparing the statements in your source program to those described in the preceding parts of the exercise, until you find your error. Correct it, retranslate your source program and then re-test your program, until it performs correctly.

Applying What We Have Learned

The last part of today's exercise is for you to apply what you have learned to the problem of decoding a file encoded using the Caesar cipher. Complete the skeleton program decode.cpp, that can be used to decode a message encoded using the Caesar cipher. Do all that is necessary to get this program operational, so that messages encoded with encode.cpp can be decoded with decode.cpp. Put differently, the two programs should complement one another.

To test your program, you can use the output file created by encode.cpp, or alice.code, a selection from Lewis Carroll's Alice In WonderLand.

Phrases you should now understand:

File, ifstream, ofstream, Opening An fstream To A File, File Input, File Output, Closing A File.


Submit:

Hard copies of your final versions of encode.cpp and decode.cpp, plus and an execution record showing their execution.


Back to This Lab's Home Page

Back to the Prelab Questions

Forward to the Homework Projects


Copyright 1998 by Joel C. Adams. All rights reserved.