Chapter 31

Performing Streaming I/O

In This Chapter

arrow Using stream I/O — an overview

arrow Opening an object for file input and output

arrow Detecting errors when performing file I/O

arrow Formatting output to a file

arrow Using the stream classes on internal buffers for easy string formatting

I gave you a template to follow when generating new programs in Chapter 2. Since you were just starting the journey to C++, I asked you to take a lot of what was in that template on faith; then throughout subsequent chapters, I explained each of the features of the template. There’s just one item remaining: stream input/output (commonly shortened to just stream I/O).

warning.eps I must warn you that stream I/O can’t be covered completely in a single chapter — entire books are devoted to this one topic. Fortunately, however, you don’t need to know too much about stream I/O in order to write the vast majority of programs.

How Stream I/O Works

Stream I/O is based on overloaded versions of operator>>() and operator<<() (known as the right-shift and left-shift operators, respectively).

Note: I don’t cover the << (left-shift) and >> (right-shift) operators in my discussion of arithmetic operators in Chapter 4. That’s because these operators perform bit operations that are beyond the scope of a beginning programming book.

The prototype declarations for the stream operators are found in the include file iostream. The code for these functions is part of the Standard C++ Library that your programs link with by default. That’s why the standard template starts out with #include <iostream> — without it, you can’t perform stream I/O. The following excerpt shows just a few of the prototype declarations that appear in iostream:

  //for input we have:
istream& operator>>(istream& source, int    &dest);
istream& operator>>(istream& source, double &dest);
istream& operator>>(istream& source, char   *pDest);
//...and so forth...

//for output we have:
ostream& operator<<(ostream& dest, const char *pSource);
ostream& operator<<(ostream& dest, int        source);
ostream& operator<<(ostream& dest, double     source);
//...and so it goes...

When overloaded to perform stream input, operator>>() is called the extractor. The class istream is the basic class for performing input from a file. C++ creates an istream object cin and associates it with the keyboard when your program first starts and before main() is executed.

The first prototype in the earlier extract from the iostream include file refers to the function that is invoked when you enter the following C++ code:

  int i;
cin >> i;

As you’ve seen, extracting from cin is the standard way of performing keyboard input.

When overloaded to perform stream output, operator<<() is called the inserter. C++ uses the ostream class for performing formatted output from a file. C++ creates an ostream object cout at program start and associates it with the console display.

The first prototype among the output functions is called when you enter the following:

  cout << "C++ programming is fn()";

Inserting to cout is the standard means for displaying stuff to the operator.

Both cin and cout are declared in the iostream include file. That’s how your program knows what they are.

technicalstuff.eps C++ opens a second ostream object at program startup. This object, cerr, is also associated with the display by default, but it’s used as a standard error output. If you’ve used Linux, Unix, or the Windows console window much, you know that you can redirect standard input and output. For example, the command

  myprogram <file1.txt >file2.txt

says, “Execute myprogram.exe, but read from file1.txt rather than the keyboard, and output to file2.txt rather than the display.” That is, cin is associated with file1.txt and cout with file2.txt. In this case, if you send error messages to cout, the operator will never see them because they’ll be sent to the file. However, messages sent to cerr will continue to go to the display because it isn’t redirected with cout.

tip.eps Always send error messages to cerr rather than to cout just in case cout has been redirected.

Stream Input/Output

C++ provides separate classes for performing input and output to files. These classes, ifstream and ofstream, are defined in the include file fstream.

remember.eps Collectively both ifstream and ofstream are known as fstream classes.

Creating an input object

The class ifstream provides a constructor used to open a file for input:

  ifstream(const char* pszFileName,
                  ios_base::openmode mode);

This constructor opens a file, creates an object of class ifstream, and associates that object with the opened file to be used for input. The first argument to the constructor is a pointer to the name of the file to open. You can provide a full pathname or just the filename.

If you provide the filename without a path, C++ will look in the current directory for the file to read. When executing from your program from within Code::Blocks, the current directory is the directory that contains the project file.

remember.eps Don’t forget that a Windows/DOS backslash is written "\" in C++. Refer to Chapter 5 for details.

The second argument directs some details about how the file is to be opened when the object is created. The type openmode is a user-defined type within the class ios_base. The legal values of mode are defined in Table 31-1. If mode is not provided, the default value is ios_base::in, which means open the file for input. (Pretty logical for a file called ifstream.)

The following example code snippet opens the text file MyData.txt and reads a few integers from it:

  void someFn()
{
    // open the file MyData.txt in the current directory
    ifstream input("MyData.txt");

    int a, b, c;

    input >> a >> b >> c;
    cout <<   "a = " << a
         << ", b = " << b
         << ", c = " << c << endl;
}

To specify the full path, I could write something like the following:

  ifstream input("C:\\MyFolder\MyData.txt");

This command opens the C:\MyFolderMyData.txt file.

The destructor for class ifstream closes the file. In the preceding snippet, the file MyData.txt is closed when control exits someFn() and the input object goes out of scope.

Table 31-1 Constants That Control How Files Are Opened for Input

Flag

Meaning

ios_base::binary

Open file in binary mode (alternative is text mode)

ios_base::in

Open file for input (implied for istream)

Creating an output object

The class ofstream is the output counterpart to ifstream. The constructor for this class opens a file for output using the inserter operator:

  ofstream(const char* pszFileName,
           ios_base::openmode mode);

This constructor opens a file for output. Here again, pszFileName points to the name of the file, whereas mode controls some aspects about how the file is to be opened. Table 31-2 lists the possible values for mode. If you don’t provide a mode, the default value is out + trunc, which means “open the file for output and truncate whatever is already in the file” (the alternative is to append whatever you output to the end of the existing file).

The following example code snippet opens the text file MyData.txt and writes some absolutely true information into it:

  void someFn()
{
    // open the file MyData.txt in the current directory
    ofstream output("MyData.txt");

    output <<   "Stephen is suave and handsome "
           << "and definitely not balding prematurely"
           << endl;
}

The destructor for class ofstream flushes any buffers to disk and closes the file before destructing the object and returning any local memory buffers to the heap when the output object goes out of scope at the end of someFn().

Table 31-2 Constants That Control How Files Are Opened for Output

Flag

Meaning

ios_base::app

Seek to End of File before each write

ios_base::ate

Seek to End of File immediately after opening the file

ios_base::binary

Open file in binary mode (alternative is text mode)

ios_base::out

Open file for output (implied for ostream)

ios_base::trunc

Truncate file, if it exists (default for ostream)

Open modes

Tables 31-1 and 31-2 show the different modes that are possible when opening a file. To set these values properly, you need to answer the following three questions:

  • Do you want to read from the file or write to the file? Use ifstream to read and ofstream to write. If you intend to both read and write to the same file, then use the class fstream and set the mode to in | out, which opens the file for both input and output. Good luck, however, because getting this to work properly is difficult. It’s much better to write to a file with one object and read from the file with another object.
  • If you are writing to the file and it already exists, do you want to add to the existing contents (in which case, open with mode set to out | ate) or delete the contents and start over (in which case, open with mode set to out | trunc)?
  • Are you reading or writing text or binary data? Both ifstream and ofstream default to text mode. Use binary mode if you are reading or writing raw, nontext data. (See the next section in this chapter for a short explanation of binary mode.)

technicalstuff.eps The | is the “binary OR” operator. The result of in | out is an int with the in bit set and the out bit set. You can OR any of the mode flags together.

If the file does not exist when you create the ofstream object, C++ will create an empty output file.

What is binary mode?

You can open a file for input or output in either binary or text mode. The primary difference between binary and text mode lies in the way that newlines are handled. The Unix operating system was written in the days when typewriters were still fashionable (when it was called “typing” instead of “keyboarding”). Unix ends sentences with a carriage return followed by a line feed.

Subsequent operating systems saw no reason to continue using two characters to end a sentence, but they couldn’t agree on which character to use. Some used the carriage return and others the line feed, now renamed newline. The C++ standard is the single newline.

When a file is opened in text mode, the C++ library converts the single newline character into what is appropriate for your operating system on output, whether it’s a carriage-return-plus-line-feed, a single carriage return, or a line feed (or something else entirely). C++ performs the opposite conversion when reading a file. The C++ library does no such conversions for a file opened in binary mode.

remember.eps Always use binary mode when manipulating a file that’s not in human-readable text format. If you don’t, the C++ library will modify any byte in the data stream that happens to be the same as a carriage return or line feed.

Hey, file, what state are you in?

A properly constructed ifstream or ofstream object becomes a stand-in for the file that it’s associated with.

The programmer tends to think of operations on the fstream objects as being the same as operations on the file itself. However, this is only true so long as the object is properly constructed. If an fstream object fails to construct properly, it might not be associated with a file — for example, if an ifstream object is created for a file that doesn’t exist. In this case, C++ rejects stream operations without taking any action at all.

Fortunately, C++ tells you when something is wrong — the member function bad() returns a true if something is wrong with the fstream object and if it cannot be used for input or output. This usually happens when the object cannot be constructed for input because the file doesn’t exist or for output because the program doesn’t have permission to write to the disk or directory. Other system errors can also cause the bad() state to become true.

The term “bad” is descriptive, if a bit excessive (I don’t like to think of computer programs as being bad or good). A lesser state called fail() is set to true if the last read or write operation failed. For example, if you try to read an int and the stream operator can find only characters, then C++ will set the fail() flag. You can call the member function clear() to clear the fail flag and try again — the next call may or may not work. You cannot clear the bad() flag — just like wine, an object gone bad is not recoverable.

warning.eps Attempts to perform input from or output to an object with either the bad() or fail() flag set are ignored.

I mean this literally — no input or output is possible as long as the internal state of the fstream object has an error. The program won’t even try to perform I/O, which isn’t so bad on output — it’s pretty obvious when your program isn’t performing output the way it’s supposed to. This situation can lead to some tricky bugs in programs that perform input, however. It’s very easy to mistake garbage left in the variable, perhaps from a previous read, for valid input from the file.

Consider the following ReadIntegers program, which contains an unsafeFn() that reads values from an input file:

  //
//  ReadIntegers - this program reads integers from
//                 an input file MyFile.txt contained
//                 in the current directory.
//
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
using namespace std;

void unsafeFn()
{
    ifstream myFile("MyFile.txt");
    int nInputValue;

    for(int n = 1; n <= 10; n++)
    {
        // read a value
        myFile >> nInputValue;

        // value successfully read - output it
        cout << n << " - " << nInputValue << endl;
    }
}

int main(int nNumberofArgs, char* pszArgs[])
{
    unsafeFn();

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

The preceding unsafeFn() function reads ten values from MyFile.txt and displays them on the console. That sounds okay, but what if there aren’t ten values in MyFile.txt — what if there are only nine (or five or none!)? This version of the program generated the following output when provided a sample MyFile.txt:

  1 - 1
2 - 2
3 - 3
4 - 4
5 - 5
6 - 6
7 - 7
8 - 7
9 - 7
10 - 7
Press Enter to continue …

The question is, did the file really contain the value 7 four times, or did an error occur after the seventh read? There is no way for the user to tell because once the program gets to the End of File, all subsequent read requests fail. The value of nInputValue is not set to zero or some other “special value.” It retains whatever value it had on the last successful read request, which in this case is 7.

The most flexible means to avoid this problem is to exit the loop as soon as an error occurs using the member function fail(), as demonstrated by the following safeFn() version of the same function (also part of the ReadIntegers program in the online material):

  //
//  ReadIntegers - this program reads integers from
//                 an input file MyFile.txt contained
//                 in the current directory.
//
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
using namespace std;

void safeFn()
{
    ifstream myFile("MyFile.txt");
    int nInputValue;

    for(int n = 0; n < 10; n++)
    {
        // read a value
        myFile >> nInputValue;

        // exit the loop on read error
        if (myFile.fail())
        {
            break;
        }

        // value successfully read - output it
        cout << n << " - " << nInputValue << endl;
    }
}

int main(int nNumberofArgs, char* pszArgs[])
{
    safeFn();

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

This version generated the following output when reading the same MyFile.txt file:

  1 - 1
2 - 2
3 - 3
4 - 4
5 - 5
6 - 6
7 - 7
Press Enter to continue …

Now it’s obvious that there are only seven values in the file rather than the expected ten — and that the number 7 isn’t repeated.

tip.eps Always check the value of fail() after extracting data from an input file to make sure that you’ve actually read a new value.

Notice that the preceding ReadIntegers program adds the line #include <fstream> to the standard template I’ve used for all programs in earlier chapters. This extra include file is necessary to gain access to the ifstream and ofstream classes.

Other Member Functions of the fstream Classes

The fstream classes provide a number of member functions, as shown in Table 31-3 (the list isn’t a complete list of all the functions in these very large classes). The prototype declarations for these member functions reside in the fstream include file. They are described in the remainder of this section.

Table 31-3 Major Methods of the I/O Stream Classes

Method

Meaning

bool bad()

Returns true if a serious error has occurred.

void clear(iostate flags =

  ios_base::goodbit)

Clears (or sets) the I/O state flags.

void close()

Closes the file associated with a stream object.

bool eof()

Returns true if there are no more characters in the read pointer at the End of File.

char fill()

char fill(char newFill)

Returns or sets the fill character.

fmtflags flags()fmtflags flags(fmtflags f)

Returns or sets format flags. (See next section on format flags.)

void flush()

Flushes the output buffer to the disk.

int gcount()

Returns the number of bytes read during the last input.

char get()

Reads individual characters from file.

char getline(

  char* buffer,

  int cou nt,

  char delimiter = ’ ’)

Reads multiple characters up until either End of File, until delimiter encountered, or until count - 1 characters read. Tacks a null onto the end of the line read. Does not store the delimiter read into the buffer. The delimiter defaults to newline, but you can provide a different one if you like.

bool good()

Returns true if no error conditions are set.

void open(

  const char* filename,

  openmode mode)

Same arguments as the constructor. Performs the same file open on an existing object that the constructor performs when creating a new object.

streamsize precision()streamsize precision(streamsize s)

Reads or sets the number of digits displayed for floating point variables.

ostream& put(char ch)

Writes a single character to the stream.

istream& read(

  char* buffer,

  streamsize num)

Reads a block of data. Reads either num bytes or until an End of File is encountered, whichever occurs first.

fmtflags setf(fmtflags)

Sets specific format flags. Returns old value.

fmtflags unsetf(fmtflags)

Clears specific format flags. Returns old value.

int width()

  int width(int w)

Reads or sets the number of characters to be displayed by the next formatted output statement.

ostream& write(

  const char* buffer,

  streamsize num)

Writes a block of data to the output file.

Reading and writing streams directly

The inserter and extractor operators provide a convenient mechanism for reading formatted input. However, sometimes you just want to say, “Give it to me; I don’t care what the format is.” Several member functions are useful in this case.

The simplest function, get(), returns the next character in an input file. Its output equivalent is put(), which writes a single character to an output file. The function getline() returns a string of characters up to some terminator — the default terminator is a newline, but you can specify any other character you like as the third argument to the function. The getline() function strips off the terminator but makes no other attempt to reformat or otherwise interpret the input.

The member function read() is even more basic. This function reads the number of bytes that you specify, or less if the program encounters the End of File. The function gcount() always returns the actual number of bytes read. The output equivalent is write().

The following FileCopy program uses the read() and write() functions to create a backup of any file you give it by making a copy with the string ".backup" appended to the name:

  //
//  CopyFiles - make backup copies of whatever files
//              are passed to the program by creating
//              a file with the same name plus the name
//              ".backup" appended.
//
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <cstring>
using namespace std;

void copyFile(const char* pszSrcFileName)
{
    // create a copy of the specified filename with
    // ",backup" added to the end
    int nTargetNameLength = strlen(pszSrcFileName) + 10;
    char *pszTargetFileName = new char[nTargetNameLength];
    strcpy(pszTargetFileName, pszSrcFileName);
    strcat(pszTargetFileName, ".backup");

    // now open the source file for input and
    // the target file for output
    ifstream input(pszSrcFileName,
                   ios_base::in | ios_base::binary);
    if (input.good())
    {
        ofstream output(pszTargetFileName,
      ios_base::out | ios_base::binary | ios_base::trunc);
        if (output.good())
        {

            while (!input.eof() && input.good())
            {
                char buffer[4096];
                input.read(buffer, 4096);
                output.write(buffer, input.gcount());
            }
        }
    }

    // restore memory to the heap
    delete pszTargetFileName;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    // pass every file name provided to main() to
    // the copyFile() function, one name at a time
    for (int i = 1; i < nNumberofArgs; i++)
    {
        cout << "Copying " << pszArgs[i] << endl;
        copyFile(pszArgs[i]);
    }

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

The program iterates through the arguments passed to it, remembering that pszArgs[0] points to the name of the program itself. The program passes each argument, one at a time, to the function copyFile().

The copyFile() function first creates a copy of the name passed it in the array pszTargetFileName. It then concatenates the string ".backup" to that name. Finally, you get to the good part: copyFile() opens the source file whose name was passed as the argument to the copyFile() function for binary input.

Note: The ios_base:: is necessary when using the in, out, binary, and trunc flags as these flags are const static members of the ios_base class.

remember.eps Use binary mode if you are working with non-text files or you don’t intend to display the contents. In this case, I did not limit the program to work only with text files.

The function only continues executing if input.good() is true, indicating that the input object was created successfully, since it would be impossible to read from the file if the opening operation did not work properly.

tip.eps In a real-world program, I would have displayed some useful error message before returning to the caller.

If the input object is created okay, copyFile() creates an output object using the pszTargetFileName created earlier. This file is opened for binary output. The mode flag is also set to truncate to delete the contents of the target file if it already exists. If output.good() is true, the function executes the next section of the function; otherwise, control jumps to the end.

The function is now ready to copy the contents of one file to the other: It enters a loop in which it reads 4K blocks from the input file and writes them to the output file.

Notice that in the call to write(), copyFile() uses the value returned from input.gcount() rather than a hardcoded 4096. This is because unless the source file just happens to be an integer multiple of 4096 bytes in length (not very likely), the last call to read() will fetch less than the requested number of bytes before encountering the End of File.

The loop terminates when either input reaches the End of File or the input object is no longer good.

remember.eps The ! operator (pronounced “the NOT operator”) inverts the sense of a Boolean expression. In other words, !true is false and !false is true. (You read that last phrase as “NOT true is false and NOT false is true.”)

Immediately before exiting, the function returns the pszTargetFileName array to the heap. Exiting the function causes the destructor for both input and output to be called, which closes both the input and output files.

To execute the program within the Code::Blocks environment, I first selected Project⇒Set Programs’ Arguments to open the Select target dialog box. In the Program arguments field, I entered main.cpp and clicked OK. I could just as well have selected and dropped several files onto the name of the CopyFiles executable file — or, at the command prompt, entered the command name, followed by the names of the files to ".backup".

Chapter 18 discusses the various ways to pass arguments to your program.

When I run the program, I get the following output:

  Copying main.cpp
Press Enter to continue …

Looking into the folder containing the main.cpp source file, I now see a second main.cpp.backup file that has the identical size and contents as the original.

Controlling format

The flags(), setf(), and unsetf() member functions are all used to set or retrieve a set of format flags used to control the format of input extracted from an ifstream or inserted into an ofstream object. The flags get set to some default value that makes sense most of the time when the object is created. However, you can change these flags to control the format of input and/or output. Table 31-4 describes the flags that can be used with the flags(), setf(), and unsetf() member functions.

Table 31-4 I/O Stream Format Flags Used with setf(), unsetf(), and flags()

Flag

If Flag Is True Then …

boolalpha

Displays variables of type bool as either true or false rather than 1 or 0

dec

Reads or writes integers in decimal format (default)

fixed

Displays floating-point number in fixed-point, as opposed to scientific (default), notation

hex

Reads or writes integers in hexadecimal

left

Displays output left-justified (that is, pads on the right)

oct

Reads or writes integers in octal

right

Displays output right-justified (that is, pads on the left)

scientific

Displays floating-point number in scientific format

showbase

Displays a leading 0 for octal output and leading 0x for hexadecimal output

showpoint

Displays a decimal point for floating-point output even if the fractional portion is zero

skipws

Skips over whitespace when using the extractor to read the file

unitbuf

Flushes output after each output operation

uppercase

Replaces lowercase letters with their uppercase equivalents on output

For example, the following code segment displays integer values in hexadecimal (rather than the default, decimal):

  // fetch the previous value so we can restore it
ios_base::fmtflags prevValue = cout.flags();

// clear the decimal flag
cout.unsetf(cout.dec);

// now set the hexadecimal flag
cout.setf(cout.hex);

// ...do stuff...

// restore output to previous state
cout.flags(prevValue);

This example first queries the cout object for the current value of the format flags using the flags() member function. The type of the value returned is ios_base::fmtflags.

I didn’t discuss user-defined types defined within classes — that’s an advanced topic  —  so (for now) just trust me that this type makes sense.

tip.eps It’s always a good idea to record the format flags of an input or output object before changing them so that you can restore them to their previous value once you’re finished.

The program then clears the decimal flag using the unsetf() function (it does this because hexadecimal, octal, and decimal are mutually exclusive format modes) before setting the hex mode using setf(). The setf() sets the hexadecimal flag without changing the value of any other format flags that may be set. Every time an integer is inserted into the cout object for the remainder of the function, C++ will display the value in hexadecimal.

Once the function finishes displaying values in hexadecimal format, it restores the previous value by calling flags(fmtflags). This member function overwrites the current flags without whatever value you pass it.

Further format control is provided by the width(int) member function that sets the minimum width of the next output operation. In the event that the field doesn’t take up the full width specified, the inserter adds the requisite number of fill characters. The default fill character is a space, but you change this by calling fill(char). Whether C++ adds the fill characters on the left or right is determined by whether the left or right format flag is set.

For example, the code snippet

  int i = 123;
cout.setf(cout.right);
cout.unsetf(cout.left);
cout.fill('+'),
cout << "i = [";
cout.width(10);
cout << i;
cout << "]" << endl;

generates the following output:

  i = [+++++++123]

warning.eps Notice that the call to width(int) appears immediately before cout << i. Unlike the other formatting flags, the width(int) call applies only to the very next value that you insert. The call to width() must be repeated after every value that you output.

What’s up with endl?

Most programs in this book terminate an output stream by inserting the object endl. However, some programs include within the text to output a newline. What’s the deal?

The endl object inserts a newline into the output stream, but it takes one more step. Disks are slow devices (compared to computer processors). Writing to disk more often than necessary will slow your program considerably. To avoid this, the ofstream class collects output into an internal buffer. The class writes the contents to disk when the buffer is full.

tip.eps A memory buffer used to speed up output to a slow device like a disk is known as a cache — pronounced “cash.” Writing the contents of the buffer to disk is known as flushing the cache.

The endl object adds a newline to the buffer and then flushes the cache to disk. You can also flush the cache manually by calling the member function flush().

Note that C++ does not cache output to the standard error object, cerr.

Manipulating Manipulators

The span of some formatting member functions is fairly short. The best example of this is the width(n) member function — this function is good only for the next value output. After that it must be set again. You saw this implication in the preceding snippet — the call to cout.width(n) had to appear right in the middle of the inserters:

  cout << "i = [";
cout.width(10);
cout << i;
cout << "]" << endl;

The call to cout.width(10) is good only for the very next output cout << i; it has no effect on the following output cout << "]".

Other functions have a short span, usually because you need to change their values often. For example, switching back and forth between decimal and hexadecimal mode while performing output requires multiple calls to setf(hex) and setf(dec) throughout the program.

This process can be a bit clumsy, so C++ defines a more convenient way to invoke these common member functions: As shown in Table 31-5, a set of so-called manipulators can be inserted directly into the output stream. These manipulators — defined in the include file iomanip — have the same effect as calling the corresponding member function.

Table 31-5 Common Manipulators and Their Equivalent Member Functions

Manipulator

Member Function

Description

dec

setf(dec)

Sets display radix to decimal

hex

setf(hex)

Sets display radix to hexadecimal

oct

setf(oct)

Sets display radix to octal

setfill(c)

fill(c)

Sets the fill character to c

setprecision(n)

precision(n)

Sets the display precision to n

setw(n)

width(n)

Sets the minimum field width for the next output to n

For example, the earlier snippet can be written as follows:

  cout << "i = [" << setw(10) << i << "]" << endl;

I/O manipulators are nothing more than a labor-saving device — they don’t add any new capability.

warning.eps You must include iomanip if you intend to use I/O manipulators.

Using the stringstream Classes

After some practice, you get pretty good at parsing input from a file using the extractors and generating attractive output using the format controls provided with the inserter. It’s a shame that you can’t use that skill to parse character strings that are already in memory.

Well, of course, C++ provides just such a capability (I wouldn’t have mentioned it otherwise). C++ provides a pair of classes that allow you to parse a string in memory by using the same member functions that you’re accustomed to using for file I/O. An object of class istringstream “looks and feels” like an ifstream object. Similarly, an object of class ostringstream responds to the same commands as an ofstream object.

The istringstream class reads input from an object of class string. Similarly, the class ostringstream class creates a string object for output. The istringstream and ostringstream classes are defined in the sstream include file.

I don’t discuss the string class in this book because, in practice, it’s a little beyond the scope of a beginning programmer. However, the string class acts like an ASCIIZ array whose size changes automatically to conform to the size of the string it’s asked to hold. In practice, there’s very little that you have to know about the string class in order to use the stringstream classes.

The following StringStream program parses Student information from an input file by first reading in a line using getline() before parsing it with istringtream.

  
// StringStream - demonstrate the use of string stream
//                classes for parsing input safely
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <sstream>
#include <string>
#include <cstring>
using namespace std;

struct Student
{
  protected:
    char szFirstName[256];
    char szLastName[256];
    int  nStudentID;

  public:
    Student(const char* pszFN, const char* pszLN,int nSID)
    {
        strncpy(szFirstName, pszFN, 256);
        strncpy(szLastName,  pszLN, 256);
        nStudentID = nSID;
    }

    // display - write the student's data into the
    //           array provided; don't exceed the size
    //           of the array set by nLength
    void display(char* pszBuffer, int nLength)
    {
        ostringstream out;

        out << szFirstName << " " << szLastName
            << " [" << nStudentID << "]" << ends;
        string s = out.str();
        strncpy(pszBuffer, s.c_str(), nLength);
    }
};

int main(int nNumberofArgs, char* pszArgs[])
{
    Student *pStudents[128];
    int nCount = 0;

    cout << "Input student <last name, first name ID> "
         << "(Input a blank line to stop input)" << endl;

    for(;;)
    {
        // get another line to parse
        char szLine[256];
        cin.getline(szLine, 256);

        // terminate if line is blank
        if (strlen(szLine) == 0)
        {
            break;
        }

        // associate an istrstream object with the
        // array that we just read
        string s(szLine);
        istringstream input(s);

        // now try to parse the buffer read
        char szLastName[256];
        char szFirstName[256];
        int nSSID;

        // read the last name up to a comma separator
        input.getline(szLastName, 256, ','),

        // read the first name until encountering white
        // space
        input >> szFirstName;

        // now read the student id
        input >> nSSID;

        // skip this line if anything didn't work
        if (input.fail())
        {
            cerr << "Bad input: " << szLine << endl;
            continue;
        }

        // create a Student object with the data
        // input and store it in the array of pointers
        pStudents[nCount++] = new Student(szFirstName,
                                       szLastName, nSSID);
    }

    // display the students input - use the Student's
    // output function to format the student data
    for(int n = 0; n < nCount; n++)
    {
        char szBuffer[256];
        pStudents[n]->display(szBuffer, 256);
        cout << szBuffer << endl;
    }

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

The program starts by creating an array of pointers that it will use to store the Student objects that it creates. It then prompts the user for the format that it expects for the student data to be read.

The program then enters a loop in which it first reads an entire line of input up to and including the terminating newline. If the length of the line read is zero, meaning that nothing was entered but a newline, the program breaks out of the input loop.

If something was entered, the program wraps the line of input in a string object and then associates that object with an istringstream object named input. The next section reads the last name, first name, and Social Security number from this input object just as if it were a file or the keyboard.

These reads are safe — they cannot overflow the szLastName and szFirstName buffers because the extractor cannot possibly return more than 256 characters in any single read — that’s is the maximum number of characters that the original call to getline() could have read.

tip.eps Notice how the program calls getline() passing a ‘,’ as the terminator. This reads characters up to and including the comma that separates the last name and first name.

Once the program has read the three student fields, it checks the input object to see if everything worked by calling input.fail(). If fail() is true, the program throws away whatever it read and spits the line back out to the user with an error message.

The Student constructor is typical of those you’ve seen elsewhere in the book. The program uses the Student::display() function to display the contents of a Student object. It does this in a fairly elegant fashion: simply associating an ostringstream object with a local string and then inserting to the object. The call to s.c_str() returns a pointer to the ASCIIZ string maintained within the string object. All main() has to do is output the result.

This is much more flexible than the alternative of inserting output directly to cout — the program can do anything it wants with the szBuffer array containing the Student data. It can write it to a file, send it to cout, or put it in a table, to name just three possibilities.

Notice that the last object display() inserts is the object ends. This is sort of the stringstream version of endl; however, ends does not insert a newline. Instead, it inserts a null to terminate the ASCIIZ string within the buffer.

warning.eps Always insert an ends last to terminate the ASCIIZ string that you build.

The output from the program appears as follows:

  Input student <last name, first name ID>
(Input a blank line to stop input)
Davis, Stephen 12345678
Ashley 23456789
Bad input: Ashley 23456789
Webb, Jessalyn 34567890

Stephen Davis [12345678]
Jessalyn Webb [34567890]
Press Enter to continue …

Notice how the second line is rejected because it doesn’t follow the specified input format, but the program recovers gracefully to accept input again on the third line. This graceful recovery is very difficult to do any other way.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.166.31