9.9. More about stringstream
Now it's time to get back to our discussion of stringstream. A stringstream is very similar to an ostream, except that once we have written data to a stringstream, we can read it from the stringstream into a variable just as though we were reading from a file or the keyboard. An example is the program shown in Figure 9.20, which uses a stringstream object to combine a year, month, and day number to make one string containing all of those values.
We've discussed the put pointer and the buffer, but what about the get and end pointers? Here's what they're for:
- 1. The get pointer holds the address of the next byte in the input area of the stream, or the next byte we get if we use >> to read data from the stringstream.
- 2. The end pointer indicates the end of the stringstream. Attempting to read anything at or after this position will cause the read to fail because there is nothing else to read.
You probably won't be surprised to learn that Susan wasn't that thrilled with all these new kinds of pointers, or with streams in general for that matter.
- Susan: The only thing that can be worse than pointers is different kinds of pointers. How are get and end different from other kinds of pointers? Ick.
- Steve: They are effectively the addresses of the current places in the buffer where characters will be read and written, respectively. Since they are not directly accessible to the programmer, their actual representation is irrelevant; all that matters is how they work.
- Susan: You just have no idea how hard this stream stuff is.
- Steve: Why is it any harder than cout? It's just the same, except that the actual destination may vary.
- Susan: No, there is no cout when we're using these other streams.
- Steve: Yes, but that's the only difference between writing to a stringstream and writing to cout, which never bothered you before.
- Susan: But I can see the screen; I can't see these other things.
- Steve: Yes, but you can't see the stream in either case. Anyway, don't the diagrams help?
- Susan: Yes, they help but it still isn't the same thing as cout. Anyway, I'm not really sure at any given time exactly where the data really is. Where are these put, get, and end pointers?
- Steve: They're stored in the stringstream object as part of its member data, just as m_Name and the other member variables are stored in a StockItem object.
- Susan: Okay, but what about the buffer?
- Steve: That's in an area of memory allocated for that purpose by the stringstream member functions. The streams classes use the get and put pointers to keep track of the exact position of the data in the buffer, so we don't have to worry about it.
- Susan: I still don't like it. It's not like cout.
- Steve: Yes, but cout is just an ostream, and a stringstream is just like an ostream and an istream combined; you can write to it just like an ostream and then read back from it just like an istream.
- Susan: I still think streams are going to be my downfall. They're just too vague and there seem to be so many of them. I know you say that they are good, and I believe you, but they are still a little mysterious. This is not going to be the last you've heard of this.
- Steve: It seems to me that you've managed to survive lots of other new concepts. I suspect streams won't be any exception.
Now let's get back to our diagrams of streams. After the statement FormatStream << year;, the stringstream might look like Figure 9.22.
The put pointer has moved to the next free byte in the stringstream, but the get pointer hasn't moved because we haven't gotten anything from the stringstream.
The next statement is FormatStream << month;, which leaves the stringstream looking like Figure 9.23.
Now it's time to get back what we put into the stringstream. That's the job of the next statement, FormatStream >> date;. Afterward the variable date has the value "1996728" and the stringstream looks like Figure 9.25.
In other words, we've read to the end of the stringstream, so we can't read from it again until we "reset" it. This shouldn't seem too strange, as it is exactly analogous to what happens when we've read all of the data in a file through an ifstream, which causes the next read to "fail".1
In these diagrams, the end and put pointers always point to the same place, so why do we need both? Because we can reset the put pointer to any place in the stringstream before the current end pointer and write over the data that has already been written. We don't make use of that facility in this book, but it's there when needed.
Controlling Output Formatting
Now let's get back to the problem of converting a date to a string so that we can compare it with another date. You might think that all we have to do is write each data item out to the stringstream and then read the resulting formatted data back in, as in the program in Figure 9.20. However, it's a bit more complicated than that, because in order to compare two of these values correctly we need to control the exact format in which the data will be written. To see why this is necessary, consider the program shown in Figure 9.26.
What's wrong with this picture? Well, the string comparison of the first value with the second shows that the first is less than the second. Clearly this is wrong, since the date the first string represents is later than the date the second string represents. The problem is that we're not formatting the output correctly; what we have to do is make month numbers less than 10 come out with a leading 0 (e.g., July as 07 rather than 7). The same consideration applies to the day number; we want it to be two digits in every case. Of course, if we knew that a particular number was only one digit, we could just add a leading 0 to it explicitly, but that wouldn't work correctly if the month or day number already had two digits.
To make sure the output is correct without worrying about how many digits the value originally had, we can use iostream member functions called manipulators, which are defined not in <iostream> but in another header file called <iomanip>. This header file defines setfill, setw, and a number of other manipulators that we don't need to worry about at the moment. These manipulators operate on fields; a field can be defined as the result of one << operator. In this case, we use the setw manipulator to specify the width of each field to be formatted, and the setfill manipulator to set the character to be used to fill in the otherwise empty places in each field.
- Susan: Why are manipulators needed? Why can't you just add the 0 where it is needed?
- Steve: Well, to determine that, we'd have to test each value to see whether it was large enough to fill its field. It's a lot easier just to use setw and setfill to do the work for us.
Using iostream Manipulators
The new program is shown in Figure 9.29. Let's go over how it works. To start with, setfill takes an argument specifying the char that will be used to fill in any otherwise unused positions in an output field. We want those unused positions to be filled with `0' characters so that our output strings will consist entirely of numeric digits.2
The setfill manipulator, like most iostream manipulators, is "sticky"; that is, it applies to all of the fields that follow it in the same stream.3 However, this is not true of setw, which sets the minimum width (i.e., the minimum number of characters) of the next field. Hence, we need three setw manipulators, one for each field in the output. The year field is four digits, while the month and day fields are two digits each.
Now let's get back to our DatedStockItem::Today function. Once we understand the manipulators we're using, it should be obvious how we can produce a formatted value on our stringstream, but how do we get it back?
That turns out to be easy. Since the get pointer is still pointing to the beginning of the stringstream, the statement FormatStream >> TodaysDate; reads data from the stringstream into a string called TodaysDate just as if we were reading data from cin or a file.
- Susan: How do you know that FormatStream >> TodaysDate will get your data back?
- Steve: Because that's how stringstreams work. You write data to them just like you write to a regular ostream, then read from them just like you read from a regular istream. They're really our friends!
Base class Constructors
Now that we've taken care of the new function, Today, let's take a look at some of the other functions of the DatedStockItem class that differ significantly from their counterparts in the base class StockItem, the constructors and the Reorder function.4
We'll start with the default constructor, which of course is called DatedStockItem::DatedStockItem() (Figure 9.30). It's a very short function, but there's a bit more here than meets the eye.
A very good question here is what happens to the base class part of the object. This is taken care of by the default constructor of the StockItem class, which will be invoked by default to initialize that part of this object.
- Susan: I don't understand your good question. What do you mean by base class part?
- Steve: The base class part is the embedded base class object in the derived class object.
- Susan: So derived classes use the default constructor from the base classes?
- Steve: They always use some base class constructor to construct the base class part of a derived class object. By default, they use the default constructor for the base class object, but you can specify which base class constructor you want to use.
- Susan: If that is so, then why are you writing a constructor for DatedStockItem?
- Steve: Because the base class constructor only constructs the base class part of a derived class object (such as DatedStockItem). The rest of the derived class object has to be constructed too, and that job is handled by the derived class constructor.
The following is a general rule: Any base class part of a derived class object will automatically be initialized when the derived object is created at run time, by a base class constructor. By default, the default base class constructor will be called when we don't specify which base class constructor we want to execute. In other words, the code in Figure 9.30 is translated by the compiler as though it were the code in Figure 9.31.
The line : StockItem(), specifies which base class constructor we want to use to initialize the base class part of the DatedStockItem object. This is a construct called a base class initializer, which is the only permissible type of expression in a member initialization list other than a member initialization expression. In this case, we're calling the default constructor for the base class, StockItem.
- Susan: What's a member initialization list again? I forget.
- Steve: It's a set of expressions that you can specify before the opening { of a constructor. These expressions are used to initialize the member variables of the object being constructed and (in the case of a derived class object) the base class part of the object.
After clearing that up, Susan wanted to make sure that she understood the reason for the base class initializer, which led to the following exchange:
- Susan: Okay, let me see if I can get this straight. The derived class will use the base class default constructor unless you specify otherwise.
- Steve: Correct so far.
- Susan: But to specify it, you have to use a base class initializer?
- Steve: Right. If you don't want the base class part to be initialized with the base class default constructor, you have to use a base class initializer to specify which base class constructor you want to use. Some base class constructor will always be called to initialize the base class part; the only question is which one.
- Susan: It just doesn't "know" to go to the base class as a default unless you tell it to?
- Steve: No, it always goes to the base class whether or not you tell it to; the question is which base class constructor is called.
- Susan: OK, then say that the base class initializer is necessary to let the compiler know which constructor you are using to initialize the base class. This is not clear.
- Steve: Hopefully, we've clarified it by now.
Whether we allow the compiler to call the default base class constructor automatically, as in Figure 9.30, or explicitly specify the default base class constructor, as in Figure 9.31, the path of execution for the default DatedStockItem constructor is as illustrated in Figure 9.32.
At step 1, the DatedStockItem constructor calls the default constructor for StockItem. In step 2, the default constructor for StockItem initializes all the variables in the StockItem class to their default values. Once the default constructor for StockItem is finished, in step 3, it returns to the DatedStockItem constructor. In step 4, that constructor finishes the initialization of the DatedStockItem object by initializing m_Expires to the default string value ("").
This is fine as long as the base class default constructor does the job for us. However, if it doesn't do what we want, we can specify which base constructor we wish to call, as shown in the "normal" constructor for the DatedStockItem class (Figure 9.33).
The member initialization expression "StockItem(Name, InStock, Price, MinimumStock, MinimumReorder, Distributor, UPC)" specifies which base class constructor will be used to initialize the base class part of the DatedStockItem object. This base class initializer specifies a call to the "normal" constructor for the base class, StockItem. Thus, the StockItem part of the DatedStockItem object will be initialized exactly as though it were being created by the corresponding constructor for StockItem. Figure 9.34 illustrates how this works.
At step 1, the DatedStockItem constructor calls the "normal" constructor for StockItem. In step 2, the "normal" constructor for StockItem initializes all the variables in the StockItem class to the values specified in the argument list to the constructor. Once the "normal" constructor for StockItem is finished, in step 3, it returns to the DatedStockItem constructor. In step 4, that constructor finishes the initialization of the DatedStockItem object by initializing m_Expires to the value of the argument Expires.
As you can see by these examples, using a base class initializer calls an appropriate base class constructor to initialize the base class part of an object, which in turn means that our derived class constructor won't have to keep track of the details of the base class.
This is an example of one of the main benefits claimed for object-oriented programming; we can confine the details of a class to the internals of that class. In this case, after specifying the base class initializer the only remaining task for the DatedStockItem constructor is to initialize the member variable m_Expires.
- Susan: How does all the information of step 3 get into step 4? And exactly what part of the code here is the base class initializer? I don't see which part it is.
- Steve: The information from 3 gets into the derived class DatedStockItem object in the upper part of the diagram because the DatedStockItem object contains a base class part consisting of a StockItem object. That's the object being initialized by the call to the base class constructor caused by the base class initializer, which consists of the following two lines:
- : StockItem(Name,InStock,Price,MinimumStock,
MinimumReorder, Distributor,UPC)There's another reason besides simplicity for using a base class initializer rather than setting the values of the member variables of the base class part of our object directly; it's much safer. If we set the values of the base class variables ourselves, and if the base class definition were later changed to include some new variables initialized according to arguments to the normal constructor, we might very well neglect to modify our derived class code to set the values of the new variables. On the other hand, let's assume that we used a base class initializer; then, if its arguments changed later (as they presumably would if new variables needed to be initialized), a derived class constructor that called that base class initializer would no longer compile. That would alert us to the change we would have to make.
The Reorder Function for the DatedStockItem class
Now that we have dealt with the constructors, let's take a look at the Reorder function (Figure 9.35).