CS105: Introduction to Computer Programming: C++

C++: Stream I/O

First an aside: you can access command-line arguments by using an alternate form of the main function:

int main( int argc, char* argv[] ) {

  cout << "You supplied " << (argc-1) << " command-line options" << endl;

  for( int i = 0; i < argc; i++ ) {
    cout << "argument " << i << " is " << argv[i] << endl;
  }

  return 0;
}

In the above example, argv is an array of C-style strings and argc is the size of that array.

There will always be at least one value in argv: the name of your program. All subsequent command-line arguments will come afterwards. This means that if you supply two command-line arguments to your program, argv will have three strings: your program name and the two arguments.

Reading from and writing to files

You can use the ifstream class to read data from a file:

#include <fstream>
using std::ifstream;
using std::ofstream;
#include <string>
using std::string;
#include <iostream>
using std::cout;
using std::endl;

int main() {

  ifstream fin( "myTextFile.txt" );

  string s;

  while( getline(fin,s) ) {        // the getline function reads
                                   // data from fin into the string
				   // s one line at a time
    cout << s << endl;
  }

  fin.close();

}

Instead of reading the text file line-by-line, you can read it word-by-word:

  ifstream fin( "myTextFile.txt" );

  string s;

  while( fin >> s ) {                // read one whitespace-delimited
                                     // word at a time
    cout << s << endl;
  }

  fin.close();

...or we can read one character at a time:

  ifstream fin( "myTextFile.txt" );

  char c;

  while( fin >> c ) {                // read one char at a time
    cout << c << endl;
  }

  fin.close();

The following code uses the ifstream and ofstream classes to read text from one file and write to another, adding line numbers:

int main( int argc, char* argv[] ) {

  // make sure we have at least two command-line arguments
  if( argc < 3 ) {
    cerr << "Usage: " << argv[0] << " <source> <dest>" << endl;
    return -1;
  }

  int lineNum = 0;
  ifstream fin( argv[1] );   // our input file stream
  ofstream fout( argv[2] );  // output file stream
  string s;

  while( getline(fin,s) ) {  // grab input, line-by-line
    fout << right << setw(5) << setfill('.') << lineNum << ": " << s << endl;
    lineNum++;
  }

  fin.close();
  fout.close();
}

Note that in the code above, you can use fin and fout just like you'd use cin and cout, respectively. That's because the classes of these instances share common parent classes. Check out the diagram on this useful page to get an idea of how the C++ input and output streams are organized.

If you want to open a file with a string filename, you'll have to use the c_str() method of the string class:

  string filename = "myFile.txt";
  ifstream fin( filename );         // this won't compile!
  ifstream fin( filename.c_str() ); // use this instead
  ...
  fin.close();

This is necessary because the constructor in question is expecting a C-style string, not an instance of the C++ string class.

Stream Manipulators

In the above code, you might have noticed that some rather odd things get put in the fout stream. What are right, setw(5), and setfill('.')? They are stream manipulators, which are used to set properties of the associated stream.

Consider the following code, which uses stream manipulators to display numbers in decimal, hexadecimal, and octal:

  #include <iostream>
  using std::dec;
  using std::hex;
  using std::oct;
  #include <iomanip>
  using std::setw;
  using std::setfill;
  ...

  for( int i = 0; i < 30; i++ ) {
    cout << setfill('.') 
 	 << setw(10) << dec << i 
	 << setw(10) << hex << i 
	 << setw(10) << oct << i << endl;
  }
Some manipulators (like setfill) are sticky, which means that the change they make to the stream will remain in effect until changed by some other manipulator. Other manipulators (like hex) only affect the next thing that's dumped into the stream.

Interestingly enough, we've been using one manipulator for a while now without even knowing it. The endl manipulator dumps a newline into the stream and flushes that stream, ensuring that any buffered output will be written to the stream immediately.

String Streams

So far, we've seen streams that interact with the console (cin), screen (cout), and files (ifstream and ofstream). You can also use streams to read and write to strings, using the stringstream classes:

  #include <sstream>
  using std::istringstream;
  ...

  // this string contains the data that we want to parse
  string employeeRecord = "Simpson Homer 7g 36 6333.3 4.3";
  // we'll use an istringstream to read data from that string
  // and into a few variables
  istringstream iss( employeeRecord );

  string firstName, lastName;
  iss >> lastName;
  iss >> firstName;

  string sector;
  iss >> sector;

  int age = -1;
  iss >> age;

  double salary = -1;
  iss >> salary;

  double donutsPerDay = -1;
  iss >> donutsPerDay;

  cout << setfill('.');
  cout << left << setw(20) << "Name: " << firstName << " " << lastName << endl;
  cout << left << setw(20) << "Sector: " << sector << endl;
  cout << left << setw(20) << "Age: " << age << endl;
  cout << left << setw(20) << "Monthly Salary: " << salary << endl;
  cout << left << setw(20) << "Donuts/Day: " << donutsPerDay << endl;

There is also a corresponding ostringstream class that can be used to build up a string:

  #include <sstream>
  using std::ostringstream;
  ...

  // we'll write data to oss, just like a normal stream
  ostringstream oss;
  for( int i = 0; i < 30; i++ ) {
    oss << setfill('.') 
	<< setw(10) << dec << i 
	<< setw(10) << hex << i 
	<< setw(10) << oct << i << endl;
  }

  // extract the string from oss with the str() function --
  // it contains all of the data that we wrote
  string s = oss.str();
  cout << s;
  cout << "s.length(): " << s.length() << endl;