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:
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:
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
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:
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).
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru