Overview of File Input and Output

Overview of File Input and Output

Overview of File Output

Assumptions: (from CS120)
  1. You know how to open/read/write/close files with C.
  2. You know the difference between text files and binary files.

Basics:

To read and write files, you must include the appropriate header file:
#include <fstream> // No .h extension
The fstream header actually contains definitions for two types: ifstream and ofstream. Output example:

CodeOutput in foo.txt (showing the new lines)
void f1()
{
  std::ofstream outfile;   // instantiate an output stream object
  outfile.open("foo.txt"); // associate a disk file

    // Write some data to the file
  outfile << "This is a line of text" << std::endl;
  outfile << "Another line of text" << std::endl;
  outfile << "An integer: " << 42 << std::endl;
  outfile << "A double: " << 3.1415 << std::endl;

  outfile.close(); // close the file [optional]
}
This is a line of text<NL>
Another line of text<NL>
An integer: 42<NL>
A double: 3.1415<NL>


For automatic objects (stack-based), we don't need to call close since it will be done in the destructor.

We can open the file in the constructor:

void f2()
{
    // instantiate and open output file
  std::ofstream outfile("foo.txt"); 

    // Write some data to the file
  outfile << "This is a line of text" << std::endl;
  outfile << "Another line of text" << std::endl;
  outfile << "An integer: " << 42 << std::endl;
  outfile << "A double: " << 3.1415 << std::endl;

}  // stream is closed in ~ofstream()

Streaming a user-defined type:
CodeOutput (in file1.txt)
void f3()
{
  Student st1("jdoe", 22, 4, 3.76F);
  StopWatch sw1(90);

    // Send to a file
  std::ofstream outfile("file1.txt");
  outfile << st1;
  outfile << sw1;
}
login: jdoe<NL>
  age: 22<NL>
 year: 4<NL>
  GPA: 3.76<NL>
00:01:30<NL>
Of course, this assumes an overloaded operator<< for both classes:

Student class
std::ostream& operator<<(std::ostream& os, const Student& s)
{
  os << "login: " << s.get_login() << std::endl;
  os << "  age: " << s.get_age() << std::endl;
  os << " year: " << s.get_year() << std::endl;
  os << "  GPA: " << s.get_GPA() << std::endl;

  return os;
}
Hopefully, now you understand why it is important to use the ostream parameter passed into the function and NOT simply hard-code cout.

StopWatch class
std::ostream& operator<<(std::ostream& os, const StopWatch& rhs)
{
  int hours, minutes, seconds;

  hours = rhs.seconds_ / 3600;
  minutes = (rhs.seconds_ - (hours * 3600)) / 60;
  seconds = rhs.seconds_ % 60;

  os.fill('0');
  os << std::setw(2) << hours << ':';
  os << std::setw(2) << minutes << ':';
  os << std::setw(2) << seconds << std::endl;

  return os;
}

Of course, it is important to check the status of the stream before using it:

void f4()
{
    // Open and check file status
  std::ofstream outfile("foo.txt"); 
  if (outfile.is_open())
  {
      // Write some data to the file
    outfile << "This is a line of text" << std::endl;
    outfile << "Another line of text" << std::endl;
    outfile << "An integer: " << 42 << std::endl;
    outfile << "A double: " << 3.1415 << std::endl;
  }
  else
    std::cout << "Can't open file for output.\n";
} 

Rebinding files:

CodeOutput (in files)
void f7()
{
  std::ofstream outfile;
  const char* fnames[] = {"file1.txt", 
                          "file2.txt", 
                          "file3.txt"
                         };

  int size = sizeof(fnames) / sizeof(*fnames);
  for (int i = 0; i < size; i++)
  {
    outfile.open(fnames[i]);
    if (!outfile.is_open())
      continue;

    outfile << "Blah, blah, blah" << std::endl;
    outfile << "Blah, blah, blah" << std::endl;
    outfile << "Blah, blah, blah" << std::endl;
    outfile.close();
  }
}
file1.txt:
Blah, blah, blah<NL>
Blah, blah, blah<NL>
Blah, blah, blah<NL>


file2.txt:
Blah, blah, blah<NL>
Blah, blah, blah<NL>
Blah, blah, blah<NL>


file3.txt:
Blah, blah, blah<NL>
Blah, blah, blah<NL>
Blah, blah, blah<NL>
Notice the placement of the constructor for the file object. Why is it not inside the for loop where it is only used.?

Aside:

  1. What is sizeof(fnames) ?
  2. What is sizeof(fnames[1]) ?
  3. What is sizeof(*fnames[1]) ?
Comparing the use of vector and string instead of an array:

vector<const char *>vector<string>
void f8()
{
  std::ofstream outfile;
  std::vector<const char *> fnames;

  fnames.push_back("file1.txt");
  fnames.push_back("file2.txt");
  fnames.push_back("file3.txt");

  for (unsigned i = 0; i < fnames.size(); i++)
  {
    outfile.open(fnames[i]);
    if (!outfile.is_open())
      continue;

    outfile << "Blah, blah, blah" << std::endl;
    outfile << "Blah, blah, blah" << std::endl;
    outfile << "Blah, blah, blah" << std::endl;
    outfile.close();
  }
}
void f8()
{
  std::ofstream outfile;
  std::vector<std::string> fnames;

  fnames.push_back("file1.txt");
  fnames.push_back("file2.txt");
  fnames.push_back("file3.txt");

  for (unsigned i = 0; i < fnames.size(); i++)
  {
    outfile.open(fnames[i].c_str());
    if (!outfile.is_open())
      continue;

    outfile << "Blah, blah, blah" << std::endl;
    outfile << "Blah, blah, blah" << std::endl;
    outfile << "Blah, blah, blah" << std::endl;
    outfile.close();
  }
}
Aside:

Suppose the program used a list instead of a vector. What other changes would need to be made?





This code will work with any container:

std::vector<std::string>::iterator it;
for (it = fnames.begin(); it != fnames.end(); ++it)
{
  outfile.open((*it).c_str());  // could use it->c_str()
  if (!outfile.is_open())
    continue;

    // Other code...
}
This demonstrates again how iterators are very powerful and flexible and why using iterators instead of subscript operators can lead to more useful (reusable) code.

An even more C++-like implementation might look like this:

void f8c()
{
  std::vector<std::string> fnames;
  
  fnames.push_back("file1.txt");
  fnames.push_back("file2.txt");
  fnames.push_back("file3.txt");
  
  for_each(fnames.begin(), fnames.end(), PrintToFile);
}
void PrintToFile(const std::string& fname)
{
  std::ofstream outfile(fname.c_str());
  if (!outfile.is_open())
    return;

  outfile << "Blah, blah, blah" << std::endl;
  outfile << "Blah, blah, blah" << std::endl;
  outfile << "Blah, blah, blah" << std::endl;
}

Or with C++11 uniform initializer syntax:
void f8d()
{
  std::vector<std::string> fnames {"file1.txt", "file2.txt", "file3.txt"};
  for_each(fnames.begin(), fnames.end(), PrintToFile);
}
And while we're at it, using a lambda expression (or anonymous function):
void f8e(void)
{
  std::vector<std::string> fnames {"file1.txt", "file2.txt", "file3.txt"};
  for_each(fnames.begin(), fnames.end(), 
            [](const std::string& fname) 
            { 
              std::ofstream outfile(fname.c_str());
              if (!outfile.is_open())
                return;

              outfile << "Blah, blah, blah" << std::endl;
              outfile << "Blah, blah, blah" << std::endl;
              outfile << "Blah, blah, blah" << std::endl;
            }
          );
}

Overview of File Input

File input is very similar to the use of cin.

Given the text in the file foo.txt from above and showing the spaces and newlines:
This·is·a·line·of·text¶
Another·line·of·text¶
An·integer:·42¶
A·double:·3.1415¶
We can read the words back into the program one at at time:

CodeOutput
void f5()
{
  std::ifstream infile("foo.txt");
  if (!infile.is_open())
    std::cout << "Can't open file.\n";
  else
  {
    std::string str;
    while (!infile.eof())
    {
      infile >> str;
      std::cout << str << std::endl;
    }
  }
}
This
is
a
line
of
text
Another
line
of
text
An
integer:
42
A
double:
3.1415
3.1415

Previous examples

Using FILE pointersUsing C++ streams
void get_numbers2()
{
    // Holds unlimited integers
  std::vector<int> numbers; 

    // Open the file for reading
  FILE *fp = fopen("numbers.txt", "r");
  if (!fp)
    std::cout << "Can't open file.\n";

  // Do something else or exit function?    

    // Process the entire file
  while (!feof(fp))
  {
    int number;

      // Read next integer
    if (fscanf(fp, "%i", &number) == 0)
      break;


      // Add number to the end
    numbers.push_back(number);
  }

    // Close the file
  fclose(fp);

    // Print the vector
  print_vector(numbers);
}
void get_numbers3()
{
    // Holds unlimited integers
  std::vector<int> numbers; 

    // Open the file for reading
  std::ifstream infile("numbers.txt");
  if (!infile.is_open())
    std::cout << "Can't open file.\n";

  // Do something else or exit function?    

    // Process the entire file
  while (!infile.eof())
  {
    int number;

      // Read next integer
    infile >> number;
    if (infile.eof())
       break;

      // Add number to the end
    numbers.push_back(number);
  }
  
  
  
  
    // Print the vector
  print_vector(numbers);
} // File is closed here


Reading in an entire line at a time:

Using FILE pointers w/C-style stringsUsing streams w/std::string
void f10()
{
  FILE *infile = fopen("foo.txt", "rt");
  if (!infile)
    std::cout << "Can't open file.\n";
  else
  {
    while (!feof(infile))
    {
      char str[100];
      fgets(str, 100, infile);
      std::cout << str;
    }
    fclose(infile);
  }
}

Output:
This is a line of text
Another line of text
An integer: 42
A double: 3.1415
A double: 3.1415
void f11()
{
  std::ifstream infile("foo.txt");
  if (!infile.is_open())
    std::cout << "Can't open file.\n";
  else
  {
    std::string str;
    while (!infile.eof())
    {
      std::getline(infile, str);
      std::cout << str << std::endl;
    }
  }
}

Output:
This is a line of text
Another line of text
An integer: 42
A double: 3.1415
[empty line here]
A corrected version:

Using FILE pointers w/C-style stringsUsing streams w/std::string
void f10()
{
  FILE *infile = fopen("foo.txt", "rt");
  if (!infile)
    std::cout << "Can't open file.\n";
  else
  {
    while (!feof(infile))
    {
      char str[100];
      if (!fgets(str, 100, infile))
        break;
      std::cout << str;
    }
    fclose(infile);
  }
}

Output:
This is a line of text
Another line of text
An integer: 42
A double: 3.1415
void f11()
{
  std::ifstream infile("foo.txt");
  if (!infile.is_open())
    std::cout << "Can't open file.\n";
  else
  {
    std::string str;
    while (!infile.eof())
    {
      if (std::getline(infile, str).eof())
        break;
      std::cout << str << std::endl;
    }
  }
}


Output:
This is a line of text
Another line of text
An integer: 42
A double: 3.1415
Refer to Input/output with files for information on state flags (search for Checking state flags).

File Modes

In C, we opened files with the fopen function. It had a signature like this:
FILE *fopen( const char *filename, const char *mode );
The mode parameter specified different attributes of the file:
        | Read (input)   Write (output)  Append (output)
--------+----------------------------------------------        
Text    |      "r"           "w"             "a"
Binary  |      "rb"          "wb"            "ab"
C++ streams use a slightly different approach to modes using flags. These flags are very similar to the ones used by the cin and cout objects and modify their behavior. Intro to I/O.
ModeMeaning
ios_base::inOpen file for input (default for ifstream)
ios_base::outOpen file for output (default for ofstream)
ios_base::appSeek to the end before each write (append)
ios_base::truncTruncate file (delete contents) after opening (default for ofstream)
ios_base::binaryOpen file in binary mode

Sample usage:

ofstream os;
os.open("somefile.bin", ios::app | ios::binary | ios::out); 

More information on C++ streams

In-Depth Example

The assignment is to create a program similar to the Unix (Cygwin) program wc. (Reference)

 Directory of E:\Data\Courses\Notes\CS170\Code\mywc

03/19/2020  03:10p                 819 Circle.cpp
03/19/2020  03:10p               2,191 fibonacci.cpp
03/19/2020  03:10p               4,246 Ocean.cpp
03/19/2020  03:10p              11,959 Polygon.cpp
03/19/2020  03:10p                 164 WarBoats.cpp
               5 File(s)         19,379 bytes
               0 Dir(s)   5,421,133,824 bytes free	
Running the command:
wc Circle.cpp fibonacci.cpp Ocean.cpp Polygon.cpp WarBoats.cpp
produces this output:
   50   110   819 Circle.cpp
   87   265  2191 fibonacci.cpp
  158   450  4246 Ocean.cpp
  402  1353 11959 Polygon.cpp
   14    20   164 WarBoats.cpp
  711  2198 19379 total
High-Level PlanDetailed Plan
  1. Read the filenames from the command line
  2. For each file given on the command line
    1. Process the file
  1. Read the filenames from the command line
  2. For each file given on the command line
    1. Open the file
    2. Initialize counters
    3. For each line in the file
      1. Read a line of text
      2. Increment the line count
      3. Count the characters in the line
        • Increment the character count
      4. Count the words in the line
        • Increment the word count
    4. Print out the counts and filename
    5. Close the file
Helper function to format and print the counts and filename:
// Print the counts and filename formatted
void print_results(size_t char_count, size_t word_count, 
                   size_t line_count, const std::string& filename)
{
  std::cout << std::setw(10) << line_count;
  std::cout << std::setw(10) << word_count;
  std::cout << std::setw(10) << char_count;
  std::cout << " " << filename << std::endl;
}
The function where the "real" work is done: (Process the file)
void CountLWC(const std::string& filename)
{
    // 1. Open the text file for reading
  std::ifstream infile(filename.c_str());
  if (!infile.is_open())
    std::cout << "Can't open file: " << filename << std::endl;
  else
  {
      // 2. Initialize the counters
    size_t char_count = 0; // characters
    size_t word_count = 0; // words
    size_t line_count = 0; // lines

      // 3. For each line in the file
    while (!infile.eof())
    {
        // 1. Read an entire line from the file
      std::string line;
      if (std::getline(infile, line).eof())
        break;

      line_count++;                  // 2. Increment line count
      char_count += line.size() + 1; // 3. Increment char count (Account for newline)
    
        // 4. Count words in the line        
      std::string word;
      std::stringstream words(line);
      while (!words.eof())
      {
        words >> word;     // Try to read next word
        if (!words.fail()) // If there was a next word
          word_count++;    //   count it
      }
    }
      // 4. Print out the counts and filename.
    print_results(char_count, word_count, line_count, filename);
  }
}  // 5. Close the file (The file is closed automatically in the destructor.)
The main function:
int main(int argc, char *argv[])
{
    // Need at least one filename
  if (argc < 2)
  {
    std::cout << "Usage: mywc <textfile1> [textfile2] ...\n";
    return 1;
  }
    
    // files to process   
  std::vector<std::string> filenames;
  
    // Put the filenames into the vector
  for (int i = 1; i < argc; i++)
    filenames.push_back(argv[i]);

    // Count the chars, words, and lines and print them
  for (size_t i = 0; i < filenames.size(); i++)
    CountLWC(filenames[i]);

  return 0;
}


Variation #1 on the main function (std::for_each):
int main(int argc, char *argv[])
{
    // Need at least one filename
  if (argc < 2)
  {
    std::cout << "Usage: mywc <textfile1> [textfile2] ...\n";
    return 1;
  }
    
    // files to process   
  std::vector<std::string> filenames;
  
    // Put the filenames into the vector
  for (int i = 1; i < argc; i++)
    filenames.push_back(argv[i]);

    // Count the chars, words, and lines and print them
  std::for_each(filenames.begin(), filenames.end(), CountLWC);

  return 0;
}


Variation #2 on the main function (no intermediate vector):
int main(int argc, char *argv[])
{
    // Need at least one filename
  if (argc < 2)
  {
    std::cout << "Usage: mywc <textfile1> <textfile2> ...\n";
    return 1;
  }
    // Count the chars, words, and lines and print them
  std::for_each(argv + 1, argv + argc, CountLWC);
  return 0;
}
Accessing command line arguments from CS120.

A closer look at for_each:

template<typename InputIt, typename Op> 
  Op for_each(InputIt first, InputIt last, Op op);
Implementation:
template<typename InputIt, typename Op>
Op for_each(InputIt first, InputIt last, Op op)
{
  while (first != last)
  {
    op(*first);
    ++first;
  }
  return op;
}
These are the files that need to be included:
#include <iostream>   // cout, endl
#include <iomanip>    // setw
#include <vector>     // vector
#include <string>     // string
#include <fstream>    // ifstream
#include <sstream>    // stringstream
#include <algorithm>  // for_each
Running the program:
mywc Circle.cpp fibonacci.cpp Ocean.cpp Polygon.cpp WarBoats.cpp
Output:
        50       110       819  Circle.cpp
        87       265      2191  fibonacci.cpp
       158       450      4246  Ocean.cpp
       402      1353     11959  Polygon.cpp
        14        20       164  WarBoats.cpp
Cygwin/macOS/Linux (wc)Our program (mywc)
   50   110   819 Circle.cpp
   87   265  2191 fibonacci.cpp
  158   450  4246 Ocean.cpp
  402  1353 11959 Polygon.cpp
   14    20   164 WarBoats.cpp
  711  2198 19379 total
    50       110       819  Circle.cpp
    87       265      2191  fibonacci.cpp
   158       450      4246  Ocean.cpp
   402      1353     11959  Polygon.cpp
    14        20       164  WarBoats.cpp

The complete program.

Exercise for students: Add the totals to the output.

Also, there is a caveat with this program: It reads files in one line-at-a-time and assumes that the files are text files. It also assumes that newlines are a single-character (LF) like Linux/Mac OS X, not two characters (CR/LF) like Windows. You would need to modify the file reading logic to handle both systems. Or, read in characters instead of lines and then account for whitespace to delimit words and newlines to delimit lines. This example chose to keep it simple to illustrate the file I/O.