Previous Table of Contents Next


It is easy to make all the names from the standard library available by saying

   using namespace std;

after including the relevant header files. Our example program could therefore be corrected either by saying

   #include <iostream>
   main()
   {
       std::cout << “Hello, world!” << std::endl;
   }

or

   #include <iostream>
   using namespace std;
   main()
   {
       cout << “Hello, world!” << endl;
   }

In this chapter, we have consistently assumed that the programmer did the latter of these, and we shall continue to do so throughout this section. Moreover, we will not clutter up the exposition by naming the appropriate headers to use in each example, because the presentation is enough of an overview that we expect programmers to have to consult a more detailed description of the library in order to use it anyway. This section aims to introduce enough of the main ideas behind the library to make it possible to use a detailed library description without becoming hopelessly lost.

7.6.1. Input/Output

C++ actually has two I/O libraries: It inherited one of them from C, and acquired a new one along the way. Both libraries are part of the C++ standard, but the native C++ library is generally recommended over the C library.

The main reason to write a new library is extensibility. The most commonly used function in the C library is printf, which relies on a language facility that is used for almost no other purpose: the ability to define functions that take variable numbers of arguments. For example, using the C library, we might say

   printf(“The square root of 2 is %g\n”, sqrt(2.0));

The first argument to printf is a string literal (or, more precisely, a pointer to the initial character of a null-terminated array of characters) that describes what printf is to print. Characters other than % stand for themselves; a % introduces a format specifier that corresponds to one or more of the succeeding arguments to printf and also says how those arguments are to affect the output. So, for example, %g means “take a double value from the argument list and print it in the most general form for floating-point values.”

The form that printf uses is concise and easy to understand. However, it has three significant disadvantages:

  It is difficult to see how to extend the printf strategy to deal with objects of other types. Even if printf were extended with the ability to install additional format characters, it would still be necessary for authors of distinct types to coordinate the format characters they chose to use.
  There is no way for the compiler to verify that the values being printed have types that are compatible with the format string. It is possible, in principle, for the compiler to check the types against the format string if the format string is a literal, but the format string could be computed during execution.
  The decision about what types to print is based on the contents of the format string, which means that type decisions are being made during execution. At least in principle, it must be faster to make such decisions during compilation.

The native C++ I/O library uses overloading to deal with all these problems. The library class ostream has overloaded operator<< members for all the fundamental types, and the istream class has similarly overloaded operator>> members for input. Moreover, additional library classes, such as strings and complex numbers, define their own overloaded operator<< and operator>> functions as appropriate. Operator overloading therefore makes it possible for authors of other library classes to define I/O for those classes without interfering with each other.

The I/O library is so large that few people will ever use more than a small part of it. The most important classes are certainly istream and ostream, which represent input and output files respectively. From each of those classes is then derived a family of classes that represent particular kinds of files. For example, class ifstream is derived from class istream to represent an input file that is identified by its name. If, for example, we have a file named foo, we can prepare to read that file by creating an appropriate ifstream object:

   ifstream ourfile(“foo”);

After that, saying

   ourfile >> x;

will read a value from the file foo into x; the exact way in which it does so will depend on the type of x. The destructor for class ifstream will automatically release any resources that were acquired when we created ourfile, so it is generally not necessary to close files explicitly.

At the beginning of this article, we saw two manipulators called setw and endl. There are many more manipulators, all of which are based on a common idea. A manipulator is nothing more than a function that can act on an I/O stream. So, for example, endl is really a function defined along the following lines:

   ostream& endl(ostream& o)
   {
       // start a new line on output stream o
       // flush the output stream o
       return o;
   }

The reason it is possible to say

   cout << endl

is that among the definitions of overloaded << in class ostream is one that accepts a (pointer to a) function. It “prints” that function by calling the function with the stream itself as its argument. In effect, we have

   class ostream {
   public:
       // ...
       ostream& operator<<(ostream& func(ostream&)) {
           return func(*this);
       }
       // ...
   };

The effect of this is that saying

   cout << endl

is equivalent to saying

   endl(cout)


Previous Table of Contents Next