How to interact with files in C++

Matias Korman, Supriya Sanjay, Ranjit Thomas, and Mark A. Sheldon

Edited by Spencer Ha


Tufts University
Comp 15: Data Structures


Watch the video series associated with this document first in addition to reading the document. It could be found here

Cplusplus has a great library for all File I/O keywords and their purpose.

Table of Contents

Introduction

Many assignments in Comp 15 will require your programs to open, read from, and write to files. Although the idea is conceptually simple, there are a few technicalities that you have to be on the lookout for. This handout contains a brief overview of file input and output (I/O) and some examples you can use to see how things work.

The basic notion is that of a file stream, which is a flow of characters from one place to another. You already know about cin, cout, and cerr, which are built-in global variables that contain input and output streams, which, by default, go between your program and the terminal.

The C++ libraries come with classes you can use for input/output of characters to/from files:

Instances of these classes have useful functions associated with them. An ifstream variable can be declared to read a file, while an ofstream variable can be declared to write into a file.

You declare variables of these types just as you would for integers or a another class:


int      num_berserkers = 0;
ifstream berserker_data;

The above declarations create two variables, one that holds an integer (initialized to 0 here) and one that holds an input file stream object.

Once you have a stream object (for either reading or writing) you need to open the file you want to access. The input stream is connected to your program, but, until you open it, it isn't connected to any external file. Opening the file connects the stream to the file. This is done by calling the open method with the file's name as the argument. Once successfully opened, a stream can be used to transfer data between the file and your program.

When you're done using a file, you need close it. This alerts the operating system that you're not going to communicate with the file any more, and any outstanding operations can be performed and some space associated with the stream can be recycled. Closing the file is done by calling the close method of the stream object.


const string BERSERKER_FILE_NAME = "berserkers.text";
// ...

        ifstream berserker_data;

        berserker_data.open(BERSERKER_FiLE_NAME);
        if (not berserker_data.is_open()) {
                cerr << "Unable to open bersker file:  "
                     << BERSERKER_FILE_NAME
                     << endl;
                exit(EXIT_FAILURE);      // Abort program if cannot read file
        }
        // ...
        // read in berserker info (see below)
        // ...
        berserker_data.close();
        // ...

Note: After you open a file, you always check to see whether you succeeded. The file may not open because it doesn't exist (perhaps you have the name wrong — remember Unix file names are case sensitive) or because you don't have permission to read the file.

Once you have opened a stream, you can use it with « and » just as you're used to, and there are a variety of other useful functions, too.

Reading from a file

One way to read from a file is one line at a time. There is a getline function that does just that. Here is a program that asks for a file name, copies the contents of the file to standard output (cout), then asks for another file name and copies that file's contents to its standard output. Comments have been omitted to make the example shorter; we hope it is clear. Note the inclusion of the fstream header file.

The two loops are different to illustrate different ways of using getline, which reads a line of text (up through the newline character '' or end-of-file) from the given stream and saves the result, without the newline in the specified string variable. However, getline can fail. The likely reason is that the end of file was reached without any data being found. You can tell whether this has happened after you have tried to read something in a couple ways: 1) You can ask the stream whether the last operation encountered an end of file condition (input_stream.eof()) or failed for any reason at all (input_stream.fail()). 2) You can ask the result of calling getline whether it failed (because getline returns a reference to the stream read from). Study carefully the usage pattern: Try to read, check for success, on success process the data, try to read again, check for success, on success process the data, etc., etc.

Subtlety: Note that it is possible for getline to succeed and also encounter the end-of-file in the same operation: if the file does not end in a new line character, the line will be terminated at the end of file. The data are valid (the operation succeeded) but end-of-file was also encountered. If you don't know the file will end in a new line character, this is something to consider.

#include <iostream>
#include <fstream>

using namespace std;

/*
 * Return filename entered on cin in response to prompt
 */
string getFileName(string prompt)
{
        string filename;

        cout << prompt;
	cin  >> filename;

	return filename;
}        

int main()
{
	ifstream input;
        string   line;

        input.open(getFileName("File name:  "));

	getline(input, line);             // <--------- here -------------+
        while (not input.eof()) {         // Must read before loop test --+
                cout << line << endl;     //                              |
                getline(input, line);     // <-------- and here ----------+
        }
        input.close();

        input.open(getFileName("Another file name:  "));

        while (not getline(input, line).fail())  // <-- Read followed by test
                cerr << line << endl;            //     all in one expression
        input.close();

	cout << "Goodbye!" << endl;

        return 0;
}

Reading individual data items

In the above example we used getline (myfile, line). This function reads a full line from myfile and stores it in the variable line. Many assignments in Comp 15 simply use whitespace-sparated data. Perhaps each line of an input file has simple commands for a program (like say Hello, or write Goodbye. In your program, you may want to skip whitespace characters, read in a word as a string, then get the next word, and so on. You do this with the » operator you've been using with cin. For example, to read in a command and a word (like "say" and "hello" in the above example) you could write something like:


        input_stream >> command >> word;

where command and word are both variables of type string.

Similarly, you can read in integers or floating point numbers or any other type of data — it's just like reading from cin.

Of course, your code can make decisions like:


        string command;
        input_stream >> command;

        if (input_stream.fail())
                // do whatever you should do if no more commands
        if (command == "say") {
                string word;
                input_stream >> word;
                cout << word << endl;
        } else if (command == "add") {
                int n1, n2;
                intput_stream >> n1 >> n2;  // not checking for failure
                cout << (n1 + n2) << endl;
        }
        // ...

Writing to a file

Writing to files is very similar. For comp15 assignments, you will probably only need to use the following method: outfile « "writing this string out";

/*
 * Simple example to show how to write data to a file.
 * Prompts for file name, and then copies standard input to output
 * file line by line.
 *
 * Mark A. Sheldon, Tufts University, 2018 Spring
 */

#include <iostream>
#include <fstream>
#include <sstream>

using namespace std;

/*
 * Return filename entered on cin in response to prompt
 */
string getFileName(string prompt)
{
        string filename;
	string input_line;

        cout << prompt;
	getline(cin, input_line);           // capture/discard rest of line
        stringstream input_ss(input_line);  // including  '\n' so subsequent
        input_ss >> filename;               // reads don't don't get extra
                                            // stuff
	return filename;
}        

int main()
{
	ofstream output;
        string   line;

        output.open(getFileName("File name:  "));

        cout << "Start typing your data, hit C-d to end" << endl;

	getline(cin, line);
        while (not cin.eof()) {
		output << line << endl;
                getline(cin, line);
        }
        output.close();

        return 0;
}

The above code also introduces a new type of stream called stringstream. Stringstream allows a string to be treated as a stream. We can use this to be able to either read in strings from a file or ouput strings to a file. This is especially useful in the conversion of strings to integers or other numerical data types. Furthermore, stringstreams are also used to parse in information from a file. A further coding example could be found in the accompany File I/O video series ("Using streams in c++ to read from / write to files") whose link is located at the top of the document.

Note that istringstream is used for reading from files and ostringstream is used for outputing to a file. They are both part of the overarching stringstream class. df

Interactive mode

Often we want our program to be interactive. That is, rather than reading from a file we want to get input from the keyboard (similarly, instead of writing to a file we want to show the results directly on the screen). For that, we use cin and cout as you know. Technically speaking, cin is an object of type istream (the parent class of ifstream, but you need not worry about this). Similarily, cout is an object of type ostream (the parent class of ofstream).

For COMP15, we want your program to be able to read from either cin or from a file. We also want your program to either output to cout or to a file. By using functions and good modularity, it is possible to do all four tasks with just two functions. One for inputting and one for outputing. It's totally ok not ot fully understand how to do this yet. You will learn throughout the semester and the COMP15 staff is more than willing to help you through it during lectures and office hours.

Cool fact: You can write a function that takes an istream and/or ostream, and then you can pass in either cin/cout or a stream that you have opened. In the following example, note how copy_file can be called with any input/output streams, whether cin/cout or streams opened by the program:

/*
 * Usage:  file_copy [input_file output_file]
 *
 * If no arguments given, copies cin to cout
 * otherwise, copies given input file to output file.
 *
 * Exits with error if cannot open files.
 *
 * Note:  Students will likely not know about templates in C++.
 *        Templates allow us to write a function that works on 
 *        different types.
 *
 *        This program defines and uses a template function for
 *        opening files and exiting the program if the file cannot
 *        be opened.  The template allows the function to be used
 *        on both ifstreams and ofstreams.  You might think that
 *        subtyping would work, but it seems not.
 *
 *        If you don't understand the templates, it's ok.  You can
 *        just test each stream after you try to open it.
 *        
 */

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>


using namespace std;

const string USAGE = "Usage:  file_copy [input_file output_file]";

void abort(string error_message);
void copy_file(istream &input, ostream &outupt);

template<typename streamtype>
void open_or_die(streamtype &stream, string file_name);

int main(int argc, char *argv[])
{
        ifstream instream;
        ofstream outstream;

        if (argc == 2 or argc > 3)     // 1 or > 2 args on command line
                abort(USAGE);
        else if (argc == 1)            // No command line args
                copy_file(cin, cout);
        else {                         // Got two file names

                /* Passes in the input stream and the first file in the command */
                open_or_die(instream,  argv[1]);

                /* Passes in the output stream and the second file in the command */
                open_or_die(outstream, argv[2]);
                
                copy_file(instream, outstream);
        }
        return 0;
}

void abort(string error_message)
{
        cerr << error_message << endl;
        exit(EXIT_FAILURE);
}

/*
 * Try to open the given file.
 * Abort on failure.
 * If the function returns, caller can assume stream is open.
 */
template<typename streamtype>
void open_or_die(streamtype &stream, string file_name)
{
        stream.open(file_name);
        if (not stream.is_open())
                abort("Unable to open:  " + file_name);
}

/*
 * Copy input to output character by character
 */
void copy_file(istream &input, ostream &output)
{
        char c;

        while (not input.get(c).fail())
                output << c;
}


That is pretty much all you need for Comp 15. We suggest you practice these operations by creating a small program that either reads from keystrokes and or a file and writes to either screen or to another file.