MFC Programmer's SourceBook : Thinking in C++
Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

strstreams

Before there were stringstreams, there were the more primitive strstreams. Although these are not an official part of Standard C++, they have been around a long time so compilers will no doubt leave in the strstream support in perpetuity, to compile legacy code. You should always use stringstreams, but it’s certainly likely that you’ll come across code that uses strstreams and at that point this section should come in handy. In addition, this section should make it fairly clear why stringstreams have replace strstream s.

A strstream works directly with memory instead of a file or standard output. It allows you to use the same reading and formatting functions to manipulate bytes in memory. On old computers the memory was referred to as core so this type of functionality is often called in-core formatting.

The class names for strstreams echo those for file streams. If you want to create a strstream to extract characters from, you create an istrstream. If you want to put characters into a strstream, you create an ostrstream.

String streams work with memory, so you must deal with the issue of where the memory comes from and where it goes. This isn’t terribly complicated, but you must understand it and pay attention (it turned out is was too easy to lose track of this particular issue, thus the birth of stringstreams).

User-allocated storage

The easiest approach to understand is when the user is responsible for allocating the storage. With istrstreams this is the only allowed approach. There are two constructors:

istrstream::istrstream(char* buf);
istrstream::istrstream(char* buf, int size); 

The first constructor takes a pointer to a zero-terminated character array; you can extract bytes until the zero. The second constructor additionally requires the size of the array, which doesn’t have to be zero-terminated. You can extract bytes all the way to buf[size], whether or not you encounter a zero along the way.

When you hand an istrstream constructor the address of an array, that array must already be filled with the characters you want to extract and presumably format into some other data type. Here’s a simple example: [46]

//: C18:Istring.cpp
// Input strstreams
#include <iostream>
#include <strstream>
using namespace std;

int main() {
  istrstream s("47 1.414 This is a test");
  int i;
  float f;
  s >> i >> f; // Whitespace-delimited input
  char buf2[100];
  s >> buf2;
  cout << "i = " << i << ", f = " << f;
  cout << " buf2 = " << buf2 << endl;
  cout << s.rdbuf(); // Get the rest...
} ///:~ 

You can see that this is a more flexible and general approach to transforming character strings to typed values than the Standard C Library functions like atof( ), atoi( ), and so on.

The compiler handles the static storage allocation of the string in

istrstream s("47 1.414 This is a test");

You can also hand it a pointer to a zero-terminated string allocated on the stack or the heap.

In s >> i >> f , the first number is extracted into i and the second into f. This isn’t “the first whitespace-delimited set of characters” because it depends on the data type it’s being extracted into. For example, if the string were instead, “ 1.414 47 This is a test ,” then i would get the value one because the input routine would stop at the decimal point. Then f would get 0.414. This could be useful if you want to break a floating-point number into a whole number and a fraction part. Otherwise it would seem to be an error.

As you may already have guessed, buf2 doesn’t get the rest of the string, just the next whitespace-delimited word. In general, it seems the best place to use the extractor in iostreams is when you know the exact sequence of data in the input stream and you’re converting to some type other than a character string. However, if you want to extract the rest of the string all at once and send it to another iostream, you can use rdbuf( ) as shown.

Output strstreams

Output strstreams also allow you to provide your own storage; in this case it’s the place in memory the bytes are formatted into. The appropriate constructor is

ostrstream::ostrstream(char*, int, int = ios::out);

The first argument is the preallocated buffer where the characters will end up, the second is the size of the buffer, and the third is the mode. If the mode is left as the default, characters are formatted into the starting address of the buffer. If the mode is either ios::ate or ios::app (same effect), the character buffer is assumed to already contain a zero-terminated string, and any new characters are added starting at the zero terminator.

The second constructor argument is the size of the array and is used by the object to ensure it doesn’t overwrite the end of the array. If you fill the array up and try to add more bytes, they won’t go in.

An important thing to remember about ostrstreams is that the zero terminator you normally need at the end of a character array is not inserted for you. When you’re ready to zero-terminate the string, use the special manipulator ends.

Once you’ve created an ostrstream you can insert anything you want, and it will magically end up formatted in the memory buffer. Here’s an example:

//: C18:Ostring.cpp
// Output strstreams
#include <iostream>
#include <strstream>
using namespace std;

int main() {
  const int sz = 100;
  cout << "type an int, a float and a string:";
  int i;
  float f;
  cin >> i >> f;
  cin >> ws; // Throw away white space
  char buf[sz];
  cin.getline(buf, sz); // Get rest of the line
  // (cin.rdbuf() would be awkward)
  ostrstream os(buf, sz, ios::app);
  os << endl;
  os << "integer = " << i << endl;
  os << "float = " << f << endl;
  os << ends;
  cout << buf;
  cout << os.rdbuf(); // Same effect
  cout << os.rdbuf(); // NOT the same effect
} ///:~ 

This is similar to the previous example in fetching the int and float. You might think the logical way to get the rest of the line is to use rdbuf( ); this works, but it’s awkward because all the input including newlines is collected until the user presses control-Z (control-D on Unix) to indicate the end of the input. The approach shown, using getline( ), gets the input until the user presses the carriage return. This input is fetched into buf, which is subsequently used to construct the ostrstream os . If the third argument ios::app weren’t supplied, the constructor would default to writing at the beginning of buf, overwriting the line that was just collected. However, the “append” flag causes it to put the rest of the formatted information at the end of the string.

You can see that, like the other output streams, you can use the ordinary formatting tools for sending bytes to the ostrstream. The only difference is that you’re responsible for inserting the zero at the end with ends. Note that endl inserts a newline in the strstream, but no zero.

Now the information is formatted in buf, and you can send it out directly with cout << buf . However, it’s also possible to send the information out with os.rdbuf( ). When you do this, the get pointer inside the streambuf is moved forward as the characters are output. For this reason, if you say cout << os.rdbuf( ) a second time, nothing happens – the get pointer is already at the end.

Automatic storage allocation

Output strstreams (but not istrstreams) give you a second option for memory allocation: they can do it themselves. All you do is create an ostrstream with no constructor arguments:

ostrstream A;

Now A takes care of all its own storage allocation on the heap. You can put as many bytes into A as you want, and if it runs out of storage, it will allocate more, moving the block of memory, if necessary.

This is a very nice solution if you don’t know how much space you’ll need, because it’s completely flexible. And if you simply format data into the strstream and then hand its streambuf off to another iostream, things work perfectly:

A << "hello, world. i = " << i << endl << ends;
cout << A.rdbuf(); 

This is the best of all possible solutions. But what happens if you want the physical address of the memory that A’s characters have been formatted into? It’s readily available – you simply call the str( ) member function:

char* cp = A.str();

There’s a problem now. What if you want to put more characters into A? It would be OK if you knew A had already allocated enough storage for all the characters you want to give it, but that’s not true. Generally, A will run out of storage when you give it more characters, and ordinarily it would try to allocate more storage on the heap. This would usually require moving the block of memory. But the stream objects has just handed you the address of its memory block, so it can’t very well move that block, because you’re expecting it to be at a particular location.

The way an ostrstream handles this problem is by “freezing” itself. As long as you don’t use str( ) to ask for the internal char*, you can add as many characters as you want to the ostrstream. It will allocate all the necessary storage from the heap, and when the object goes out of scope, that heap storage is automatically released.

However, if you call str( ), the ostrstream becomes “frozen.” You can’t add any more characters to it. Rather, you aren’t supposed to – implementations are not required to detect the error. Adding characters to a frozen ostrstream results in undefined behavior. In addition, the ostrstream is no longer responsible for cleaning up the storage. You took over that responsibility when you asked for the char* with str( ).

To prevent a memory leak, the storage must be cleaned up somehow. There are two approaches. The more common one is to directly release the memory when you’re done. To understand this, you need a sneak preview of two new keywords in C++: new and delete. As you’ll see in Chapter 11, these do quite a bit, but for now you can think of them as replacements for malloc( ) and free( ) in C. The operator new returns a chunk of memory, and delete frees it. It’s important to know about them here because virtually all memory allocation in C++ is performed with new, and this is also true with ostrstream. If it’s allocated with new, it must be released with delete, so if you have an ostrstream A and you get the char* using str( ), the typical way to clean up the storage is

delete []A.str();

This satisfies most needs, but there’s a second, much less common way to release the storage: You can unfreeze the ostrstream. You do this by calling freeze( ), which is a member function of the ostrstream’s streambuf. freeze( ) has a default argument of one, which freezes the stream, but an argument of zero will unfreeze it:

A.rdbuf()->freeze(0);

Now the storage is deallocated when A goes out of scope and its destructor is called. In addition, you can add more bytes to A. However, this may cause the storage to move, so you better not use any pointer you previously got by calling str( ) – it won’t be reliable after adding more characters.

The following example tests the ability to add more characters after a stream has been unfrozen:

//: C18:Walrus.cpp
// Freezing a strstream
#include <iostream>
#include <strstream>
using namespace std;

int main() {
  ostrstream s;
  s << "'The time has come', the walrus said,";
  s << ends;
  cout << s.str() << endl; // String is frozen
  // s is frozen; destructor won't delete
  // the streambuf storage on the heap
  s.seekp(-1, ios::cur); // Back up before NULL
  s.rdbuf()->freeze(0); // Unfreeze it
  // Now destructor releases memory, and
  // you can add more characters (but you
  // better not use the previous str() value)
  s << " 'To speak of many things'" << ends;
  cout << s.rdbuf();
} ///:~ 

After putting the first string into s, an ends is added so the string can be printed using the char* produced by str( ). At that point, s is frozen. We want to add more characters to s, but for it to have any effect, the put pointer must be backed up one so the next character is placed on top of the zero inserted by ends. (Otherwise the string would be printed only up to the original zero.) This is accomplished with seekp( ). Then s is unfrozen by fetching the underlying streambuf pointer using rdbuf( ) and calling freeze(0). At this point s is like it was before calling str( ): We can add more characters, and cleanup will occur automatically, with the destructor.

It is possible to unfreeze an ostrstream and continue adding characters, but it is not common practice. Normally, if you want to add more characters once you’ve gotten the char* of a ostrstream, you create a new one, pour the old stream into the new one using rdbuf( ) and continue adding new characters to the new ostrstream.

Proving movement

If you’re still not convinced you should be responsible for the storage of a ostrstream if you call str( ), here’s an example that demonstrates the storage location is moved, therefore the old pointer returned by str( ) is invalid:

//: C18:Strmove.cpp
// ostrstream memory movement
#include <iostream>
#include <strstream>
using namespace std;

int main() {
  ostrstream s;
  s << "hi";
  char* old = s.str(); // Freezes s
  s.rdbuf()->freeze(0); // Unfreeze
  for(int i = 0; i < 100; i++)
    s << "howdy"; // Should force reallocation
  cout << "old = " << (void*)old << endl;
  cout << "new = " << (void*)s.str(); // Freezes
  delete s.str(); // Release storage
} ///:~ 

After inserting a string to s and capturing the char* with str( ), the string is unfrozen and enough new bytes are inserted to virtually assure the memory is reallocated and most likely moved. After printing out the old and new char* values, the storage is explicitly released with delete because the second call to str( ) froze the string again.

To print out addresses instead of the strings they point to, you must cast the char* to a void*. The operator << for char* prints out the string it is pointing to, while the operator << for void* prints out the hex representation of the pointer.

It’s interesting to note that if you don’t insert a string to s before calling str( ), the result is zero. This means no storage is allocated until the first time you try to insert bytes to the ostrstream.

A better way

Again, remember that this section was only left in to support legacy code. You should always use string and stringstream rather than character arrays and strstream. The former is much safer and easier to use and will help ensure your projects get finished faster.


[46] Note the name has been truncated to handle the DOS limitation on file names. You may need to adjust the header file name if your system supports longer file names (or simply copy the header file).

Contents | Prev | Next


Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru