CSCI 240 Notes - Input and Output

This section of the Notes contains several topics on Input and Output used in various parts of the course.


cin

cin allows the user to enter values into variables from standard input (the keyboard or via I/O redirection).

It can accept one or more values, depending on how many << are used.

Its behavior is slightly different depending on the data type being read.

When you code

int i;
cin >> i;

cin attempts to read an integer from standard input (let's assume the keyboard). It waits until the user presses <Enter> and then attempts to convert the keystrokes entered into an int, and then store it in the variable whose name is supplied has been passed.

So if the user enters

123<Enter>

the characters '1', '2', and '3' are converted by cin to the integer 123 and this value is stored into the memory occupied by i.

You can read several values with one cin. Suppose you want the user to enter integer values:

cin >> int1 >> int2 >> int3;

You would separate the numbers with any whitespace characters.


cin and Input Errors

If the data supplied by the user does not match what is expected by the program, errors can occur.  Sometimes you will get no indication of them, except that the program will not work as expected.

For example, if the user is supposed to enter a number, but enters:

2w3<Enter>

cin stops at the first non-digit, and converts just what has been entered up to there; in this case the '2'. So the value 2 would be stored. No error would be signaled. In this case of 2w3 - the w, the 3, and the \n are still "waiting" to be processed by subsequent input instructions - which are probably expecting something else - maybe a string representing someone's name.  So the "w3" would be read as the name!


The Keyboard Buffer

Keystrokes (chars) entered by user are stored in a special area in memory called the keyboard buffer. cin gets chars from there.

Suppose user types _123<Enter> // the _ represents a space

So in kbd buffer is:  '_' '1' '2' '3' '\n'  // as chars
Or                    32  49  50  51  10    // as ASCII values
 

cin takes chars from the start (the left), removing them as it processes them. Typing adds chars to the end (the right).

cin for numeric values (float, double, int, long int, etc.) works like this for keyboard input:

cin for a character will scan past leading whitespace and then take the next char from the keyboard buffer if there is one. If it is empty, it lets the user type chars until <Enter> is pressed. Then it gets the 1st char in the buffer.


Reading Multi-Word Strings

We have used cin to read strings (both char arrays and the string class) However, it is flawed for most purposes because it stops reading when it hits any whitespace character.  So if the string you want to read consists of words separated by spaces, you'll only get the first word.  You'd have to code additional cins or <<  to get additional words in the string.

For example, to read a first, middle, and last name, you could code:

cin >> fName >> mName >> lName;

But if you don't know how many words are in a line, you are in trouble.

You can use a "method of cin" to read a line of text (up to the \n character) in one operation.  This is object-oriented terminology which will be covered late in the course.  For now, just know how to do this:

cin.getline(strVar, maxlen);

Where

If you want to stop inputting characters when you hit some other character, that character can be a third argument.  For example, to stop on a '.', code:

cin.getline(str, 80, '.');

Incidentally, there is another method that will read the next character in the keyboard buffer whether it is a whitespace character or not:

ch = cin.get();

Where ch is a char variable.  Note that cin.get() returns the value value of the char it reads..


File Input and Output

You may know (see Notes Part 6) that you can run a normal program to write output to a file or read input from a file using I/O redirection. When you do this, the user of the program must type the name(s) of these files on the command line.  Also, you are limited to one input and one output file.

However, you can also have a program itself connect to and read or write from one or more input and/or output files.  The techniques for doing this are based on object oriented principles which are not covered until late in this course, but the techniques are nonetheless simple to use.

Files are objects.  You can think of an object as a special data type.  The data type has a set of special functions associated with it that only work on variables of the particular type.  The functions are called methods (just a different terminology).  We will discuss the methods open(), fail(), getline(), and close() below.

So - how do we do this? 

1. First, you need to

#include <fstream>

2. Then, for each file you want to use, you must declare a variable.  The variable type (or the object type) is either

The word stream is used because files are treated as a stream or sequence of values, one after another.

Example:

ifstream inFile;
ofstream outFile;

3. Then you must "open" or connect the program to the file.

to open a file, you would code something like:

inFile.open("a:test.dat"); 

or

outFile.open("a:test.dat");

Because inFile is of type ifstream, the open method prepares the file to be read.

Because outFile is of type ofstream, the open method prepares the file to be written.  (If there is already a file called test.dat on a: it will be overwritten.)

There are some optional arguments to open() that are covered more fully in the textbook, Chapter 14.  For example, you can arrange to append written data to an existing file.

4. Check that the open() succeeded.  To do this, code:

if (inFile.fail())   // or outFile.fail()
  {
  cout << "open for test.dat failed";
  // exit the program
  exit(1);
  }

Note: on some systems, the exit() method may require <stdlib.h>.  Alternately, if the open is in main() you could just return.

5. Now you can read and write information from/to the file.  For example:

outFile << "Hello";

or

inFile >> num;

5a. Reading strings with embedded spaces presents some problems, since an  ifstream will stop at the first space.  You can use the getline() method (function) to read such strings - see the example below Alternately, you can use multiple input operations if you know how many "words" you want to read:

Example: if your input looks like this:

Chicago IL

inFile >> str1;
inFile >> str2;

then str1 will have "Chicago" and str2 will have "IL".  This will be true even if there are extra spaces or even newline chars between the two words.

6. Close the files.  On some systems (especially for input files) this may not be necessary, but it is considered good programming practice:

inFile.close();
outFile.close();

Example: the program below copies the contents of one file to another.  It reads and writes strings.  When a string is read from a file, the newline character is not put into the string data item in the program, so when we write each string out to the second file, we have to add it back in.  Also, we use the regular console cout to echo the data to the screen (in addition to writing it to the file).

Notice that there are two versions of the read-write loop.  The first uses a sentinel value to signal end-of-input (a line with the single character 'x'); the second tests a special return value from getline:

#include <fstream>
#include <iostream>
using namespace std;

int main()
{
ifstream in; 
ofstream out; 

char s[80];

// Note the two \\ needed to specify one \ 
// in the string.
// Otherwise, the \t would be taken as a tab 
// and the \o would be an undefined 
// escape sequence
in.open("c:\\temp\\original.dat");
out.open("c:\\temp\\newCopy.dat");

if (in.fail()) 
  {
  cout << "open for original.dat failed";
  // exit the program
  exit(1);
  }
// read-write loop, version 1: assumes
// that the last line in the file is "x"
// This code will NOT copy the "x" line
in >> s;
while (strcmp(s, "x") != 0)
  {
  cout << s << "\n";
  out << s << "\n";
  in >> s;
  } 

// read-write loop version 2: uses the function,
// getline().  See note below. This version will 
// copy the last line (the "x").
/*
while (in.getline(s, 80))
  {
  cout << s << "\n";
  out << s << "\n";
  }
*/ 

in.close();
out.close();

return 0;
}

Note:  version 1 above stops reading chars at any whitespace.  So a line consisting of several words separated by spaces would require several reads (in >> s).  getline() reads a whole line up to a newline character (or other delimiter which can be specified as an optional third argument) so you can easily read such a line.

So getline() reads one line from the file in and stores it into s. The function returns

So the return value is used to exit the loop upon end-of-file.


I/O of Structures - Block I/O

To read and write structures to disk, you could read and write the individual data members in order.  However, this is inefficient and awkward.  It is much neater and more efficient to input and output a whole structure at a time, with one operation.

Assume you have a structure defined and created in your program.  Let's say the structure name is TPerson and the structure variable is person:

struct TPerson
  {
  char name[40];
  int id;
  };
TPerson person;

Supply a

#include <fstream>  // Note: just fstream (not if- or of- stream)

Writing a structure

1. Declare the file and open it for output:

ofstream outfile;
// Note the "ios::binary"
outfile.open("a:\\people.dat", ios::binary);  

Don't neglect to check for failure.  Use the fail() method of ofstream:

if (outfile.fail()) ... 

2. Populate person with data:

strcpy(person.name, "Joe");
person.id = 1234;

3. Write the structure to disk:

outfile.write((char *)&person, sizeof(TPerson));

Note the typecast of &person to a char *&person is just an address, but the write method needs a char * argument.  &person doesn't say what kind of data the address is pointing to, but the function insists that it be a pointer-to-char.  So the typecast does that. 

So the first argument tells write where to find the data to write, but notice that it doesn't say "this is a TPerson thing".  It just says where the TPerson begins.

The second argument tells write how many bytes to write out from that beginning address.  In this way, write will output the correct number of bytes for just one TPerson.

4. Do steps 3 and 4 as often as needed to create a file of TPersons.

5. Close the file:

outfile.close();

Reading a structure

Assuming you have written a file of TPerson structures with a program based on the information in the previous section, you can write a program to read these structures back from disk in a similar way.

1. Declare the file and open it for input:

ifstream infile;
// Note the ios: stuff
infile.open("a:\\people.dat", ios::in | ios::binary);

Don't neglect to check for failure.  Use the fail() method of ifstream:

if (infile.fail()) ... 

2. Read the data until end of file:

infile.read((char *)&person, sizeof(TPerson));
while (infile)
  {
  // do whatever with this person: print it or ...
  infile.read((char *)&person, sizeof(TPerson));
  }

Note that the value of infile is tested as the loop exit condition.  infile will be true after a successful read and false when end-of-file is reached.

3. Close the file:

infile.close();

Building an Array of Structures from input

You can declare an array of structs and then fill it with structs read from disk.

1. Declare the array:

TPerson people[80];

2. Open the file as usual, but the read commend will look like this:

infile.read((char *)&people[i], sizeof(TPerson));

Of course, the subscript i will need to be initialized to 0 and will need to be incremented after each read so that the next read puts the next struct into the next array element.