Lesson #3

The IOStreams Library

Overview

In this lesson I will cover the following topics:

  1. What are IOStreams and why bother learning about them.

  2. Basic use of inserter and extractor operators.

  3. I/O manipulators.

  4. File I/O using IOStreams.

Body

What are IOStreams?

The IOStreams library is a very powerful library for doing input/output (I/O) operations. It provides all the I/O features of the C standard library, and it can be extended in four significant ways that the C library cannot.

  1. C++ allows you to create your own data types. With IOStreams you can define how to do I/O operations for your types and use those operations in your program in a natural way. This would be like defining your own format specifiers (%d, %f, etc) for printf.

    This is different from what is true for some other languages (Java) that allow you to convert an object into a string representation, and then output the resulting string. C++ allows you to define string representations as well, but it also allows you to define the way I/O is done differently from the way string conversions are done for cases where that is useful.

  2. You can arrange things so that the standard IOStreams functions will do I/O operations on any I/O device you can imagine. The C standard library really only understands files or things that look like files. That may cover a lot of ground, but it isn't as flexible as it could be. The IOStreams library can be taught to use windows, network connections, raw memory, or any other sequential device as an I/O device.

  3. When using IOStreams you can change the meaning of "character" so that the library transfers not just simple characters of ASCII text but any other character-like entity you might want.

  4. You can define your own "manipulators" to control the state of the I/O device in a convenient way without resorting to specialized control functions.

If these features don't make sense to you, don't worry about it. They are, for the most part, advanced topics. In this course, we will do a bit of #1, but we won't talk further about the other possibilities at all. My point is to convince you that IOStreams is worth learning. Later you may want to do some advanced things with it, and you'll need to understand the basics that we will cover here first.

Keep in mind that IOStreams is just a library. It is not built into the language. Thus, the techniques used by IOStreams could be used in libraries of your own creation. Furthermore, since IOStreams is a standard part of C++, using such techniques would actually make your own libraries easier to understand in the long run. All C++ programmers know at least something about IOStreams. Anything that uses IOStreams or resembles IOStreams will make sense to all C++ programmers.

The basics

Unlike the standard C I/O library, IOStreams functions are declared in several header files. In C, you can just #include <stdio.h>, but to use IOStreams you may need to #include several files. Here are the common ones:

#include <iostream>  // Needed for the basic operations and for cout and cin.
#include <iomanip>   // Manipulators with parameters.
#include <fstream>   // Needed for I/O to and from files.
#include <sstream>   // Needed for I/O to and from standard strings.

There are other headers too, but they are mostly only needed for exotic operations. We won't worry about them here.

In C the operators << and >> are used to do bit shift operations on integers. You can use them that way in C++ too, but C++ allows you to redefine the meaning of most operators when they are applied to user-defined types. The IOStreams library uses that facility extensively to change the meaning of << and >> when they are applied to stream objects. In fact, because I/O is much more common than bit shifting, under C++, the original meaning of those operators has become secondary.

The << operator is called the inserter operator because you use it to insert things into a stream. The >> operator is called the extractor operator because you use it to extract things from a stream. Keep in mind that a stream is just a sequence of characters. When you insert a character into an output stream, that character is sent to the corresponding output device. When you extract a character from an input stream, you are getting the next character from the corresponding input device.

When you #include <iostream> you make available several global stream objects:

std::cout    // Standard output
std::cin     // Standard input
std::cerr    // Standard error
std::clog    // Standard logging

These objects are of user-defined type (std::cout, std::cerr, and std::clog are of type std::ostream; std::cin is of type std::istream). The exact nature of these objects does not concern us. All we need to know is that we can insert things into a std::ostream and extract things from a std::istream. How that works exactly is part of the library and hidden from the casual IOStreams user.

Keep in mind that the standard output device is normally the console's display, the standard input device is the console's keyboard, and the standard error device is also the console's display. These associations can be changed using I/O redirection at the command line. However, I/O redirection is handled by the operating system and is not something the program even knows is happening (that is the beauty of it).

The point of std::cerr is to be a place where error messages can be written. Even if the standard output is redirected (e.g., to a file), the standard error stream typically is not. Thus, error messages will still appear on the console where the user can see them. The std::clog stream (the "log" stream) is also connected to the standard error stream, but is buffered so that messages may not appear immediately, but I/O is (potentially) more efficient. It is suitable for writing informational (log) messages that the user will see even if the standard output is redirected.

There are overloadings of << (for ostreams) and >> (for istreams) that apply to all the built-in types:

#include <iostream>

int main( )
{
    int   i = 10;
    long  l = 20;
    char  c = 'X';
    float f = 3.14F;
    const char *p = "Hello, World\n";

    std::cout << i;  // Prints '10' on the standard output device.
    std::cout << l;  // Prints '20'
    std::cout << c;  // Prints 'X'
    std::cout << f;  // Prints '3.14000'
    std::cout << p;  // Prints 'Hello, World'
    return 0;
}

Unlike with printf, you don't need to worry about picking the right format specifier. The compiler selects the appropriate version of << for you based on the type of the object you are trying to insert. This is one advantage IOStreams has over the traditional C I/O library and is often called type-safe I/O.

You can also read values by extracting them from the input object:

#include <iostream>

int main( )
{
    int age;

    std::cout << "How old are you? ";
    std::cin  >> age;
    std::cout << "So you say that you are " << age << " years old?";
    return 0;
}

The program above demonstrates several other features of IOStreams. Because of the way operator << works, you can chain together uses of it as expected. A statement such as:

std::cout << "So you say that you are " << age << " years old?";

is handled as:

((std::cout << "So you say that you are ") << age) << " years old?";

Each application of << returns its left operand (by reference). Thus the value of:

(std::cout << "So you say that you are ")

is just std::cout again. This means that std::cout becomes the left operand in the next application of <<.

The extraction operator works somewhat like scanf and has the same sort of quirks. It will skip leading whitespace, and it needs to look ahead by one character. As a result, like scanf, it can be somewhat difficult to use. However, you can also call methods of the stream classes in order to do things that are not possible with the inserter/extractor mechanism. I will talk about methods extensively in a later lesson. For now, note that the way I perform a named operation on a stream is by using a "." operator (like accessing a member of a structure). Here is how you would read the standard input one character at a time.

#include <iostream>

int main( )
{
    char ch;

    while( std::cin.get( ch ) ) {
        // Process ch
    }
    return 0;
}

The get method returns a character into ch by reference (no need to pass a pointer) and it also returns a false value when the end-of-file is encountered. There is also a version of get that can read an entire line.

#include <iostream>

int main( )
{
    char line_buffer[256 + 1];

    while( std::cin.get( line_buffer, 256 + 1 ) ) {
        // Process the line in line_buffer
    }
    return 0;
}

However, if you really wanted to read a file a line at a time, you would probably use the std::string type. Standard strings can grow to any size necessary, so there is no need to worry about overflowing buffers in that case. I will talk about std::string in a later lesson.

Manipulators

There are also a number of special objects called manipulators that change the status of a stream. By inserting these manipulators into a stream, you can control the way the stream behaves. It is possible to create your own manipulators if necessary, but the standard library provides a number of useful ones already.

std::flush // Flushes the output buffer.
std::endl  // Inserts a '\n' into the output stream and flushes the output buffer.
std::ends  // Inserts a null character into an output stream (useful when writing to strings).
std::ws	   // Extracts white space from an input stream and throws it away.

std::setw(int)         // Sets the width of the next field only.
std::setfill(char)     // Sets the fill character.
std::setprecision(int) // Sets the number of significant digits for floating point numbers.

The manipulators without parameters are all declared in <iostream>. If you need to use a manipulator that takes parameters, you will need to also #include <iomanip>.

Here is an example it illustrates how some of these manipulators work. This statement:

std::cout << '(' << std::setw(4) << std::setfill('#') << 12 << ')';

produces: (##12) on the output stream. The std::setw manipulator sets the field width of the next field to four. The std::setfill manipulator sets the fill character to '#'. Since the number being printed is only two characters long, the other two spaces in the field are filled with '#'. Note that the std::setw manipulator only affects the next field (single character output fields are not affected), but the std::setfill manipulator affects all following fields until it is used again to set the fill character to something else (such as a space).

As you can see, manipulators are used to control the format of the output and play a role similar to that played by the various control characters allowed in printf's format string. Using manipulators tends to be more verbose than an equivalent printf format string, but manipulators are much for flexible and extensible than printf.

File I/O

Doing file I/O with IOStreams is easy. If you wish to write to a file, you need to declare an object of type std::ofstream. If you wish to read from a file, you need to declare an object of type std::ifstream. The act of declaring these objects opens the files as appropriate. The files will be closed automatically by the IOStreams library; you do not have to worry about closing them yourself. Once a file is open, you can treat it like any other stream object.

Here is a program that opens a file and displays its contents on the standard output device. Notice how I have to #include <fstream> if I want to use std::ofstream or std::ifstream.

#include <iostream>
#include <fstream>

int main( )
{
    char          ch;
    std::ifstream input_file( "input.txt" );
        // Declare the object and open the file.

    // Did the open work?
    if( !input_file ) {
        std::cerr << "Can't open input.txt for reading!\n";
        return 1;
    }

    // Read the input file one character at a time.
    while( input_file.get( ch ) ) {
        std::cout.put( ch );
    }
    return 0;
}

Notice how I print error messages to std::cerr. This is traditional. I could have used std::cout instead, but it makes a difference when I/O redirection is being used.

Notice also that I didn't have to use the std:: qualifier all that often. Once I declared input_file to be type std::ifstream I no longer needed to qualify my use of input_file. Although input_file's type is a standard library type, the name input_file itself is not in name space std. This is a common situation. Since C++ programs often make heavy use of new types defined in libraries, once the objects are declared few additional namespace qualifications are necessary.

String I/O

Just as files are places where characters can be stored sequentially, so also are strings. The IOStreams library allows itself to be extended to cover any sequential storage device. Because strings are so important and commonly used, the standard provides the necessary facilities to do I/O to strings. However, in order to discuss that fully, you need to know something about the std::string type. I will wait and talk about how you can do I/O to strings when I talk about strings in a later lesson.

Summary

  1. Although C++ programs can use the C standard I/O library, the C++ IOStreams library is much more powerful and extensible. You should get in the habit of using IOStreams in your C++ programs so that when you want to make use of its advanced features you will have the experience to do so.

  2. You can write to the standard output device by using the << operator on the std::cout object. You can read from the standard input device by using the >> operator on the std::cin object. These operators behave similarly to C's printf and scanf functions. They do formatted I/O. Unformatted, character-at-a-time, I/O is also possible using stream method functions.

  3. I/O manipulators are special objects that can be pushed into a stream to change the stream's state. They are typically used to control the formatting of other output.

  4. Data can be read from a file by declaring a std::ifstream object and then using it like any other input stream. Data can be written to a file by declaring a std::ofstream object and then using it like any other output stream. You must #include <fstream> to use these two library types.

© Copyright 2023 by Peter Chapin.
Last Revised: July 18, 2023