C++ Annotations
February 07, 2012 Tuesday

Chapter 9: The IO-stream Library

As an extension to the standard stream (FILE) approach well known from the C programming language, C++ offers an I/O library based on class concepts.


Earlier (in chapter 3) we've already seen examples of the use of the C++ I/O library. In this chapter we'll cover the library to a larger extent.


Apart from defining the insertion (<<) and extraction(>>) operators, the use of the C++ I/O library offers the additional advantage of type safety in all kinds of standard situations. Objects (or plain values) are inserted into the iostreams. Compare this to the situation commonly encountered in C where the fprintf()) function is used to indicate by a format string what kind of value to expect where. Compared to this latter situation C++'s iostream approach uses the objects where their values should appear, as in


cout << "There were " << nMaidens << " virgins present\n";


The compiler notices the type of the nMaidens variable, inserting its proper value at the appropriate place in the sentence inserted into the cout iostream.


Compare this to the situation encountered in C. Although C compilers are getting smarter and smarter over the years, and although a well-designed C compiler may warn you for a mismatch between a format specifier and the type of a variable encountered in the corresponding position of the argument list of a printf() statement, it can't do much more than warn you. The type safety seen in C++ prevents you from making type mismatches, as there are no types to match.


Apart from this, the iostreams offer more or less the same set of possibilities as the standard streams of C: files can be opened, closed, positioned, read, written, etc.. The remainder of this chapter presents an overview.


In general, input is managed by istream objects, having the derived classes ifstream for files, and istrstream for strings (character arrays), whereas output is managed by ostream objects, having the derived classes ofstream for files and ostrstream for strings.


If a file should allow both reading from and writing to, a fstream object should be used.


Finally, in order to use the iostream facilities, the header file iostream.h must be included in source files using these facilities.


9.1: Iostreams: insertion (<<) and extraction (>>)

The insertion and extraction operators are used to write information to or read information from, respectively, ostream and istream objects.



9.1.1: The insertion operator <<

The insertion operator (<<) points to the ostream object wherein the information is inserted. The extraction operator points to the object receiving the information obtained from the istream object.


As an example, the << operator as defined with the class ostream is an overloaded operator having as prototype, e.g.,

ostream &ostream::operator <<(char const *text)


The normal associativity of the <<-operator remains unaltered, so when a statement like

(cout << "hello " << "world")
is encountered, the leftmost two operands are evaluated first (cout << "hello "), and a ostream & object, which is actually the same cout object. From here, the statement is reduced to
(cout << "world")
and the second string is inserted into cout.


Since the << operator has a lot of (overloaded) variants, many types of variables can be inserted into ostream objects. There is an overloaded <<-operator expecting an int, a double, a pointer, etc. etc.. For every part of the information that is inserted into the stream the operator returns the ostream object into which the information so far was inserted, and the next part of the information to be inserted is devoured.


As we have seen in the discussion of friends, even new classes can contain an overloaded << operator to be used with ostream objects (see sections 10.3 and 10.3.1).


Consider the following code example:


    #include <iostream.h>

    int main()
    {
        int
            value = 15,
            *p = &value;
            
        cout << "Value: " << value << "\n"
            << "via p: " << *p << "\n"
            << "value's address: " << &value << "\n"
            << "address via p:   " << p << "\n"
            << "p's address:     " << &p << "\n";
    }

In this form the following output is generated (gnu C++ compiler, version 2.7.2):

    Value: 15
    via p: 15
    value's address: 1
    address via p:   1
    p's address:     1


This is a bit unexpected. How to get the addresses? By using an explicit cast to the generic pointer void * the problem is solved:


    #include <iostream.h>

    int main()
    {
        int
            value = 15,
            *p = &value;
            
        cout << "Value: " << value << "\n"
            << "via p: " << *p << "\n"
            << "value's address: " << (void *)&value << "\n"
            << "address via p:   " << (void *)p << "\n"
            << "p's address:     " << (void *)&p << "\n";
    }


The above code produces, e.g.,


    Value: 15
    via p: 15
    value's address: 0x804a1e4
    address via p:   0x804a1e4
    p's address:     0xbffff9fc    



9.1.2: The extraction operator >>

With the extraction operator, a similar situation holds true as with the insertion operator, the extraction operator operating comparably to the scanf() function. I.e., white space characters are skipped. Also, the operator doesn't expect pointers to variables to be given new values, but references (with the exception of the char *).


Consider the following code:



    int
        i1,
        i2;
    char
        c;
        
    cin >> i1 >> i2;                // see (1)
    
    while (cin >> c && c != '.')    // see (2)
        process(c);
                             
    char                            // see (3)
        buffer[80];
                                    // see (3)
    while (cin >> buffer)
        process(buffer);


This example shows several characteristics of the extraction operator worth noting. Assume the input consists of the following lines:



    125
    22
    h e l l o 
    w o r l d .
    this example shows
    that we're not yet done 
    with C++


  1. In the first part of the example two int values are extracted from the input: these values are assigned, respectively, to i1 and i2. White-space (newlines, spaces, tabs) is skipped, and the values 125 and 22 are assigned to i1 and i2.


    If the assignment fails, e.g., when there are no numbers to be converted, the result of the extraction operator evaluates to a zero result, which can be used for testing purposes, as in:


    if (!(cin >> i1))
  2. In the second part, characters are read. However, white space is skipped, so the characters of the words hello and world are produced by cin, but the blanks that appear in between are not.


    Furthermore, the final '.' is not processed, since that one's used as a sentinel: the delimiter to end the while-loop, when the extraction is still successful.

  3. In the third part, the argument of the extraction operator is yet another type of variable: when a char * is passed, white-space delimited strings are extracted. So, here the words this, example, shows, that, we're, not, yet, done, with and C++ are returned.


    Then, the end of the information is reached. This has two consequences: First, the while-loop terminates. Second, an empty string is copied into the buffer variable.


9.2: Four standard iostreams

In C three standard files are available: stdin, the standard input stream, normally connected to the keyboard, stdout, the (buffered) standard output stream, normally connected to the screen, and stderr, the (unbuffered) standard error stream, normally not redirected, and also connected to the screen.


In C++ comparable iostreams are

  • cin, an istream object from which information can be extracted. This stream is normally connected to the keyboard.
  • cout, an ostream object, into which information can be inserted. This stream is normally connected to the screen.
  • cerr, an ostream object, into which information can be inserted. This stream is normally connected to the screen. Insertions into that stream are unbuffered.
  • clog, an ostream object, comparable to cerr, but using buffered insertions. Again, this stream is normally connected to the screen.



9.3: Files in general

In order to be able to create fstream objects, two header files must be included: iostream.h and fstream.h. Files to read are accessed through ifstream objects, files to write are accessed through ofstream objects. Files may be accessed for reading and writing as well. The general fstream object is used for that purpose.


Apart from the iostream.h header file the headerfile fstream.h must be included when an fstream, ofstream, or ifstream object must be constructed or used.



9.3.1: Writing streams

In order to be able to write to a file an ofstream object must be created, the constructor receiving the name of the file to be opened:


ofstream out("outfile");


By default this will result in the creation of the file, and information inserted into it will be written from the beginning of the file. Actually, this corresponds to the creation of the ofstream object in standard output mode, for which the enumeration value ios::out could have been provided as well:


ofstream out("outfile", ios::out);


Alternatively, instead of (re)writing the file, the ofstream object could be created in the append mode, using the ios::app mode indicator:


ofstream out("outfile", ios::app);


Normally, information will be inserted into the ofstream object using the insertion operator <<, in the way it is used with the standard streams like cout, e.g.:


out << "Information inserted into the 'out' stream\n";


Just like the fopen() function of C may fail, the construction of the ofstream object might not succeed. When an attempt is made to create an ofstream object, it is a good idea to test the successful construction. The ofstream object returns 0 if its construction failed. This value can be used in tests, and the code can throw an exception (see chapter 13) or it can handle the failure itself, as in the following code:



    #include <iostream.h>
    #include <fstream.h>
    
    int main()
    {
        ofstream
            out("/");   // creating 'out' fails
            
        if (!out)
        {
            cerr << "creating ofstream object failed\n";
            exit(1);
        }
    }


9.3.2: Reading streams

In order to be able to read to a file an ifstream object must be created, the constructor receiving the name of the file to be opened:


ifstream in("infile");


By default this will result in the opening of the file for reading. The file must exist for the ifstream object construction to succeed. Instead of the shorthand form to open a file for reading, and explicit ios flag may be used as well:


ifstream in("infile", ios::in);


Normally, information will be extracted from the ifstream object using the extraction operator >>, in the way it is used with the standard stream cin, e.g.:


in >> x >> y;


The extraction operator skips blanks: between words, between characters, between numbers, etc.. Consequently, if the input consists of the following information:


    12 
    13
    a b 
    hello world

then the next code fragment will read 12 and 13 into x and y, will then return the characters a and b, and will finally read hello and world into the character array buffer:

    int
        x,
        y;
    char
        c,
        buffer[10];
        
    cin >> x >> y >> c >> c >> buffer >> buffer;

Notice that no format specifiers are necessary. The type of the variables receiving the extracted information determines the nature of the extraction: integer values for ints, white space delimited strings for char []s, etc..


Just like the fopen() function of C may fail, the construction of the ifstream object might not succeed. When an attempt is made to create an ifstream object, it is a good idea to test the successful construction. The ifstream object returns 0 if its construction failed. This value can be used in tests, and the code can throw an exception (see section 13) or it can handle the failure itself, as in the following code:



    #include <iostream.h>
    #include <fstream.h>
    
    int main()
    {
        ifstream
            in("");         // creating 'in' fails
            
        if (!in)
        {
            cerr << "creating ifstream object failed\n";
            exit(1);
        }
    }


9.3.3: Reading and writing streams

In order to be able to read and write to a file an fstream object must be created. Again, the constructor receives the name of the file to be opened:


fstream inout("infile", ios::in | ios::out);


Note the use of the ios constants ios::in and ios::out, indicating that the file must be opened both for reading and writing. Multiple mode indicators may be used, concatenated by the binary or operator '|'. Alternatively, instead of ios::out, ios::app might have been used, in which case writing will always be done at the end of the file.


With fstream objects, the ios::out will result in the creation of the file, if the file doesn't exist, and if ios::out is the only mode specification of the file. If the mode ios::in is given as well, then the file is created only if it doesn't exist. So, we have the following possibilities:

                       
    -------------------------------------------------------------
                                 Specified Filemode            
                    ---------------------------------------------
     File:                ios::out            ios::in | ios::out
    -------------------------------------------------------------
     exists           File is rewritten     File is used as found
    
    doesn't exist      File is created         File is created
    -------------------------------------------------------------


Once a file has been opened in read and write mode, the << operator may be used to write to the file, while the >> operator may be used to read from the file. These operations may be performed in random order. The following fragment will read a blank-delimited word from the file, will write a string to the file, just beyond the point where the string just read terminated, and will read another string: just beyond the location where the string just written ended:


    ...
    fstream
        f("filename", ios::in | ios::out);
    char
        buffer[80]; // for now assume this 
                    // is long enough
        
    f >> buffer;    // read the first word    

                    // write a well known text 
    f << "hello world";
    
    f >> buffer;    // and read again

Since the operators << and >> can apparently be used with fstream objects, you might wonder whether a series of << and >> operators in one statement might be possible. After all, f >> buffer should produce a fstream &, shouldn't it?


The answer is: it doesn't. The compiler casts the fstream object into an ifstream object in combination with the extraction operator, and into an ofstream object in combination with the insertion operator. Consequently, a statement like

f >> buffer << "grandpa" >> buffer;
results in a compiler error like
no match for `operator <<(class istream, char[8])'
Since the compiler complains about the istream class, the fstream object is apparently considered an ifstream object in combination with the extraction operator.


Of course, random insertions and extractions are hardly used. Generally, insertions and extractions take place at specific locations in the file. In those cases, the position where the insertion or extraction must take place can be controlled and monitored by the seekg() and tellg() memberfunctions. The memberfunction seekg() expects two arguments, the second one having a default value:

seekg(long offset, seek_dir position = ios::beg);
The first argument is a long offset with respect to a seek_dir postion. The seek_dir position may be one of:
  • ios::beg: add offset to the begin of file position. Negative offsets result in an error condition, which must be cleared before any further operations on the file will succeed.
  • ios::end: add offset to the end of file position. Positive offsets result in the insertion of as many padding (char)0 characters as necessary to reach the intended offset.
  • ios::cur: add offset to the current file position. If adding the offset to the current position would result in a position before ios::beg, then, again, an error condition results. If the position would be beyond ios::end, then extra (char)0 characters are supplied.


Error conditions (see also section 9.3.4) occurring due to, e.g., reading beyond end of file, reaching end of file, or positioning before begin of file, can be cleared using the clear() memberfunction. Following clear() processing continues. E.g.,


    ...
    fstream
        f("filename", ios::in | ios::out);
    char
        buffer[80]; // for now assume this 
                    // is long enough

    f.seekg(-10);   // this fails, but...
    f.clear();      // processing f continues

    f >> buffer;    // read the first word    



9.3.4: IOStream Condition States

Operations on streams may succeed and they may fail for several reasons. Whenever an operation fails, further read and write operations on the stream are suspended. Furtunately, it is possible to clear these error condition, so that a program can repair the problem, instead of having to abort.


Several condition member functions of the fstreams exist to manipulate the states of the stream:

  • bad(): this member function returns a non-zero value when an invalid operation has been requested, like seeking before the begin of file position.
  • eof(): this member function returns a non-zero value when the stream has reached .
  • fail(): this member function returns a non-zero value when eof() or bad() returns a non-zero value.
Note that once one of these error conditions are raised, further processing of the stream is suspended. The member function good(), on the other hand, returns a non-zero value when there are no error conditions. Alternatively, the operator '!' could be used for that in combination with fail(). So good() and !fail() return identical logical values.


A subtlety is the following: Assume a stream is constructed, but not attached to an actual file. E.g., the statement ifstream instream creates the stream object, but doesn't assign it to a file. However, if we next check it's status through good() this member will return a non-zero value. The `good' status here indicates that the stream object has been cleanly constructed. It doesn't mean the file is also open. A direct test for that can be performed by inspecting instream.rdbuf()->is_open. If non-zero, the stream is open.


When an error condition has occurred (i.e., fail() returns a non-zero value), and can be repaired, then the member function clear() should be called to clear the error status of the file.



9.3.5: Special functions

Apart from the functions discussed so far, and the extraction and assignment operators, several other functions are available for stream objects which are worthwhile mentioning.
  • gcount(): this function returns the number of characters read by getline() (described below) or read() (described below).
  • flush(): this function flushed the output of the ostream object.
  • get(): returns the next character as an int: End-of-file is returned as , a value which can't be a character.
  • get(char c): this function reads a char from an istream object, and returns the istream object for which the function was called.
    The get() and get(char c) functions read separate characters, and will not skip whitespace.
  • getline(char *buffer, int size, int delimiter = '\n'): this function reads up to size - 1 characters or until delimiter was read into buffer, and appends a final ascii-z. The delimiter is not entered into buffer. The function changes the state of the output-stream to fail if a line was not terminated by the delimiter. Since this situation will prevent the function from reading more information, the function clear must be called in these circumstances to allow the function to produce more information. The frame for reading lines from an istream object is, therefore:
    
        #include <iostream.h>
    
        int main()
        {
            char
                buffer[100];
    
            while (1)
            {
                cin.getline(buffer, 100);
                cout << buffer;
                if (cin.eof())
                    return(0);
    
                if (cin.good())
                    cout << endl;
                else
                    cin.clear();
            }
        }
    
    
  • istream &ignore([int n] [, int delimiter]). This function skips over a certain number of characters, but not beyond the delimiter character. By default, the delimiter character is : the function ignore() will not skip beyond . If the number of characters isn't specified, one character will be skipped.
  • int peek(). This function returns the character that will be read with the next call to the function get().
  • istream &putback(char c). This function attempts to put character c back into the stream. The most recently read character character may always be returned into the stream. If the character can't be returned, is returned. This function is the analogue of C's ungetc() function.
  • int opfx(). This function should be called before any further processing. If the ostream object is in the state `good', flush() is called for that object, and 1 is returned. Otherwise, 0 is returned. The p in opfx() indicates prefix: the function should be called before processing the ostream object.
  • int osfx(): This function is the suffix equivalent for opfx(). called at the conclusion of any processing. All the ostream methods end by calling osfx().
    If the unitbuf flag is set for this stream, osfx() flushes any buffered output for it, while any output buffered for the C output streams stdout and stderr files is flushed if the stdio flag was set for this stream.
  • read(char *buffer, int size): this function reads size bytes from the istream object calling this memberfunction into buffer.
  • write(char const *str, int length): writes length characters in str to the ostream object for which it was called, and it returns the ostream object.



9.3.6: Formatting

While the insertion and extraction operators provide elegant ways to read information from and write information to iostreams, there are situations in which special formatting is required. Formatting may involve the control of the width of an output field or an input buffer or the form (e.g., the radix) in which a value is displayed. The functions (v)form() and (v)scan() can be used for special formatting.


Apart from these memberfunctions, memberfunctions are available for defining the precision and the way numbers are displayed. Apart from using members, manipulators exist for controlling the display form and the width of output and input elements. Different from member functions, manipulators are part of insertion or extraction statements.


9.3.6.1: The (v)form() and (v)scan() members

To format information to be inserted into a stream the member form() is available:

ostream& form(const char *format ...);
Note that this is a member-function, returning a reference to an ostream object. Therefore, it can be used in combination with, e.g., the insertion operator:
cout.form("Hello %s", "world") << endl;
produces a well known sentence.


The memberfunction form() is the analogue of C's fprintf() function. When variadic functions are constructed in which information must be inserted into a stream, the memberfunction vform() can be used, being the analogue of vfprintf().


To scan information from a stream, the memberfunction scan() can be used, which is the analogue of C's fscanf() function. Similarly to vfscanf(), the memberfunction vscan() can be used in variadic functions.


9.3.6.2: Format states: dec, hex, oct manipulators

The iostream objects maintain format states controlling the default formatting of values. The format states can be controlled by memberfunctions and by manipulators. Manipulators are inserted into the stream, the memberfunctions are used by themselves.


The manipulators are dec, hex and oct, enforcing the display of integral numbers in, respectively, decimal, hexadecimal and octal format. The default conversion is decimal. The conversion takes effect on information inserted into the stream after processing the manipulators. So, a statement like:

cout << 16 << ", " << hex << 16 << ", " << oct << 16;
will produce the output
16, 10, 20



9.3.6.3: Setting the precision: the member precision()

The function precision() is used to define the precision of the display of floating point numbers. The function expects the number of digits (not counting the decimal point or the minus sign) that are to be displayed as its argument. For example,


    cout.precision(4);
    cout << sqrt(2) << endl;
    cout.precision(6);
    cout << -sqrt(2) << endl;

results in the following output:

    1.414
    -1.41421


when used without argument, precision() returns the actual precision value:


    cout.precision(4);
    cout << cout.precision() << ", " << sqrt(2) << endl;


Note: precision() is not a manipulator, but a memberfunction. Therefore, cout.precision() rather than precision() is inserted into the stream.



9.3.6.4: Setting the display form: the member setf()

The member-function setf() is used to define the way numbers are displayed. It expects one or two arguments, all flags of the iostream class. In the following examples, cout is used, but other ostream objects might have been used as well:


  • To display the numeric base of integral values, use
    cout.setf(ios::showbase)
    This results in no prefix for decimal values, 0x for hexadecimal values, 0 for octal values. For example:
    
        cout.setf(ios::showbase);
        cout << 16 << ", " << hex << 16 << ", " << oct << 16 << endl;
    
    
    results in:
    16, 0x10, 020
  • To display a trailing decimal point and trailing decimal zeros when real numbers are displayed, use
    cout.setf(ios::showpoint)
    For example:
    
        cout.setf(ios::showpoint);
        cout << 16.0 << ", " << 16.1 << ", " << 16 << endl;
    
    
    results in:
    16.0000, 16.1000, 16
    Note that the last 16 is an integral rather than a real number, and is not given a decimal point.


    If ios::showpoint is not used, then trailing zeros are discarded. If the decimal part is zero, then the decimal point is discarded as well.

  • Comparable to the dec, hex and oct manipulators
    
        cout.setf(ios::dec, ios::basefield);
        cout.setf(ios::hex, ios::basefield);
    
    
    or
    
        cout.setf(ios::oct, ios::basefield);
    
    
    can be used.
  • To control the way real numbers are displayed cout.setf(ios::fixed, ios::floatfield) or cout.setf(ios::scientific, ios::floatfield) can be used. These settings result in, respectively, a fixed value display or a scientific (power of 10) display of numbers. For example,
    
        cout.setf(ios::fixed, ios::floatfield);
        cout << sqrt(200) << endl;
        cout.setf(ios::scientific, ios::floatfield);
        cout << sqrt(200) << endl;
    
    
    results in
    
        14.142136
        1.414214e+01
    
    


As a summary:

  • setf(ios::showbase) is used to display the numeric base of integral values,
  • setf(ios::showpoint) is used to display the trailing decimal point and trailing zeros of real numbers
  • setf(ios::dec, ios::basefield), setf(ios::hex, ios::basefield) and setf(ios::oct, ios::basefield) can be used instead of the dec, hex and oct manipulators.
  • cout.setf(ios::scientific, ios::floatfield) and cout.setf(ios::fixed, ios::floatfield) can be used to obtain a fixed or scientific (power of 10) display of real values.



9.3.6.5: The manipulator setw()

The setw() manipulator expects one argument: the width of the field that's inserted or extracted next. It can be used as manipulator for insertion, where it defines the maximum number of characters that are displayed for the field, and it can be used with extraction, where it defines the maximum number of characters that are inserted into an array.


For example, to insert 20 characters into cout, use:

cout << setw(20) << 8 << endl;


To prevent array-bounds overflow when extracting from cin, setw() can be used as well:

cin >> setw(sizeof(array)) >> array;
The nice feature here is that a long string appearing at cin is split into substrings of at most sizeof(array) - 1 characters, and an ascii-z is appended.


Notes:

  • setw() is valid only for the next field. It does not act like e.g., hex which changes the general state of the output stream for displaying numbers.
  • When setw(sizeof(someArray)) is used, make sure that someArray really is an array, and not a pointer to an array: the size of a pointer, being 2 or 4 bytes, is usually not the size of the array that it points to....
  • In order to use setw() the header-file iomanip.h must be included.


9.3.6.6: String Formatting

Strings can be processed similarly to iostream objects, if objects of the class istrstream or ostrstream are constructed. Objects of these classes read information from memory and write information to memory, respectively. These objects are created by constructors expecting the address of a block of memory (and its size) as its argument. For example to write something into a block of memory using a ostrstream object, the following code could be used:


    char
        buffer[100];
    ostrstream
        os(buffer, 100);    // construct the ostrstream object

                            // fill 'buffer' with a well-known text
    os << "Hello world " << endl << '\0';

    cout << os.str();       // display the string

Note the final '\0' character (the ascii-z) that is appended: ostrstream objects do their own bookkeepping, and also accept non ascii-z terminated information. Therefore, an ascii-z character must be appended to the string when it is to be inserted into an ostream.


Note also the use of the memberfunction str(), returning the string the ostrstream object operates on. Using str() the existence of buffer can be hidden from the users of the ostrstream object.


The following memberfunctions are available for strstream objects:

  • istrstream::istrstream(const char *str [, int size]): This constructor creates an input string class istrstream object, associating it with an existing buffer starting at str, of size size. If size is not specified, the buffer is treated as a null-terminated string.
  • ostrstream::ostrstream(): This constructor creates a new stream for output to a dynamically managed string, which will grow as needed.
  • ostrstream::ostrstream(char *str, int size [, int mode]): This constructor creates a new stream for output to a statically defined string of length size, starting at str. The mode parameter may optionally be specified as one of the iostream modes. By default ios::out is used.
  • int ostrstream::pcount(): returns the current length of the string associated with this ostrstream object.
  • char *ostrstream::str(): The memberfunction returns a pointer to the string managed by this ostrstream object. This function implies freeze(), see below:
  • void ostrstream::freeze ([int n]): If n is nonzero (the default), the string associated with this ostrstream object must not change dynamically anymore. While frozen, it will not be reallocated if it needs more space, and it will not be deallocated when the ostrstream object is destroyed. freeze(1) can be used to refer to the string as a pointer after creating it via ostrstream facilities.
  • int ostrstream::frozen(): This member can be used to test whether freeze(1) is in effect for this string.


In order to use the strstream classes, the header file strstream.h header-file must be included.