EXPLORATION 14

image

Introduction to File I/O

Reading from standard input and writing to standard output works fine for many trivial programs, and it is a standard idiom for UNIX and related operating systems. Nonetheless, real programs must be able to open named files for reading, writing, or both. This Exploration introduces the basics of file I/O. Later Explorations will tackle more sophisticated I/O issues.

Reading Files

The most common file-related task in these early Explorations will involve reading from a file instead of from the standard input stream. One of the greatest benefits of this is it saves a lot of tedious typing. Some IDEs make it difficult to redirect input and output, so it’s easier to read from a file and sometimes write to a file. Listing 14-1 shows a rudimentary program that reads integers from a file named list1401.txt and writes them, one per line, to the standard output stream. If the program cannot open the file, it prints an error message.

Listing 14-1.  Copying Integers from a File to Standard Output

#include <cstdio>
#include <fstream>
#include <iostream>
 
int main()
{
  std::ifstream in{"list1401.txt"};
  if (not in)
    std::perror("list1401.txt");
  else
  {
    int x{};
    while (in >> x)
      std::cout << x << ' ';
    in.close();
 
  }
}

The <fstream> header declares ifstream, which is the type you use to read from a file. To open a file, simply name the file in ifstream’s initializer. If the file cannot be opened, the ifstream object is left in an error state, a condition for which you can test using an if statement. When you are done reading from the file, call the close() member function. After closing the stream, you cannot read from it anymore.

Once the file is open, read from it the same way you read from std::cin. All the input operators that are declared in <istream> work equally well for an ifstream, as they do for std::cin.

The std::perror (declared in <cstdio>) function prints an error message. If the file cannot be opened, the exact reason is saved in a global variable, and perror uses that variable to decide which message to print. It also prints its argument.

Run the program when you know the input file does not exist. What message does the program display?

_____________________________________________________________

If you can, create the input file, then change the protection on the file, so you can no longer read it. Run the program.

What message do you get this time?

_____________________________________________________________

Writing Files

As you have probably guessed, to write to a file, you define an ofstream object. To open the file, simply name the file in the variable’s initializer. If the file does not exist, it will be created. If the file does exist, its old contents are discarded in preparation for writing new contents. If the file cannot be opened, the ofstream object is left in an error state, so remember to test it before you try to use it. Use an ofstream object the same way you use std::cout.

Modify Listing 14-1 to write the numbers to a named file. This time, name the input file list1402.in and name the output file list1402.out. Compare your solution with mine in Listing 14-2.

Listing 14-2.  Copying Integers from a Named File to a Named File

#include <cstdio>
#include <fstream>
#include <iostream>
 
int main()
{
  std::ifstream in{"list1402.in"};
  if (not in)
    std::perror("list1402.in");
  else
  {
    std::ofstream out{"list1402.out"};
    if (not out)
      std::perror("list1402.out");
    else
    {
      int x{};
      while (in >> x)
        out << x << ' ';
      out.close();
      in.close();
    }
  }
}

Like ifstream, the ofstream type is declared in <fstream>.

The program opens the input file first. If that succeeds, it opens the output file. If the order were reversed, the program might create the output file then fail to open the input file, and the result would be a wasted, empty file. Always open input files first.

Also notice that the program does not close the input file, if it cannot open the output file. Don’t worry: it closes the input file just fine. When in is destroyed at the end of main, the file is automatically closed.

I know what you’re thinking: If in is automatically closed, why call close at all? Why not let in close automatically in all cases? For an input file, that’s actually okay. Feel free to delete the in.close(); statement from the program. For an output file, however, doing so is unwise.

Some output errors do not arise until the file is closed, and the operating system flushes all its internal buffers and does all the other cleanup it needs to do when closing a file. Thus, an output stream object might not receive an error from the operating system until you call close(). Detecting and handling these errors is an advanced skill. The first step toward developing that skill is to adopt the habit of calling close() explicitly for output files. When it comes time to add the error-checking, you will have a place where you can add it.

Try running the program in Listing 14-2 in various error scenarios. Create the output file, list1402.out, and then use the operating system to mark the file as read-only. What happens?

_____________________________________________________________

If you noticed that the program does not check whether the output operations succeed, congratulations for having sharp eyes! C++ offers a few different ways to check for output errors, but they all have drawbacks. The easiest is to test whether the output stream is in an error state. You can check the stream after every output operation, but that approach is cumbersome, and few people write code that way. Another way lets the stream check for an error condition after every operation and alerts your program with an exception. You’ll learn about this technique in Exploration 45. A frighteningly common technique is to ignore output errors altogether. As a compromise, I recommend testing for errors after calling close(). Listing 14-3 shows the final version of the program.

Listing 14-3.  Copying Integers, with Minimal Error-Checking

#include <cstdio>
#include <fstream>
#include <iostream>
 
int main()
{
  std::ifstream in{"list1402.in"};
  if (not in)
    std::perror("list1402.in");
  else
  {
    std::ofstream out{"list1402.out"};
    if (not out)
      std::perror("list1402.out");
    else
    {
      int x{};
      while (in >> x)
        out << x << ' ';
      out.close();
      if (not out)
        std::perror("list1402.out");
    }
  }
}

Basic I/O is not difficult, but it can quickly become a morass of gooey, complicated code when you start to throw in sophisticated error-handling, international issues, binary I/O, and so on. Later Explorations will introduce most of these topics, but only when the time is ripe. For now, however, go back to earlier programs and practice modifying them to read and write named files instead of the standard input and output streams. For the sake of brevity (if for no other reason), the examples in the book will continue to use the standard I/O streams. If your IDE interferes with redirecting the standard I/O streams, or if you just prefer named files, you now know how to change the examples to meet your needs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset