lec13iotree.eps
The mother of all base classes is ios. (In more recent
compilers, ios has been replaced by ios_base
.)
The class ios contains
most of the actual I/O code. It is ios that keeps track of the error
state of the stream. The error flags are an enumerated type within
ios. In addition the ios class converts data for display.
It understands the format of the different types of numbers, how to
output character strings, and how to convert an ASCII string to and from
an integer or a floating-point number.
Standard output, cout, is an object of the class ostream, as is cerr, the standard error output. Standard input, cin, is an object of the class istream. cout, cin, and cerr are automatically constructed as global objects at program start-up. Objects of iostream deal with both input and output. Objects of ifstream deal with input files and objects of the class ofstream deal with output files. Objects fstream deal with files that can one can write to and read from. ofstream, ifstream, and fstream are subclasses that are defined in the include file fstream.h. Notice that fstream.h deals with file stream classes.
The overloaded right shift operator operator>>()
is called
the extractor. It is a member function of the class istream.
The overloaded left shift operator operator<<()
is called
the inserter. It is a member function of the class ostream.
Thus we have
//for input istream& operator>>(istream& source, char *pDest); istream& operator>>(istream& source, int &dest); istream& operator>>(istream& source, char &dest); //...etc... //for output ostream& operator<<(ostream& dest, char *pSource); ostream& operator<<(ostream& dest, int source); ostream& operator<<(ostream& dest, char source);So when we type
#include <iostream.h> void fn() { cout<<"Hello, world\n"; }First, C++ determines that the left-hand argument is of type ostream and the right-hand argument is of type char*. Armed with this knowledge, it finds the prototype
operator<<(ostream&, char*)
in iostream.h. C++
generates a call to this function, the
char* inserter, passing the function the string
"Hello, world\n"
and the object cout as the two arguments.
That is, it calls operator<<(cout, "Hello, world\n")
.
The char* inserter function, which is part of the standard
C++ library, performs the requested output.
#include <fstream.h> int main() { ifstream infile("input.dat"); //input.dat is the name of the file //in your directory ofstream outfile("output.dat"); //output.dat is the name of the file //that will be created in your directory float x; while(infile >> x) //detects end-of-file and exits loop { outfile << "x = " << x << endl; } infile.close(); outfile.close(); return 0; }The statement
ofstream outfile("output.dat");calls one of the constructors of ofstream. It constructs the object outfile using the argument
"output.dat"
.
The constructor that's called is
ofstream::ofstream(const char *pFileName, int mode = ios::out, int prot = filebuff::openprot);The first argument is a pointer to the name of the file to open. The second and third arguments specify how the file will be opened. Since we didn't specify the second and third arguments, the default values take effect. Similarly, the statement
ifstream infile("input.dat");calls the following constructor of the class ifstream:
ifstream::ifstream(const char *pFileName, int mode = ios::in, int prot = filebuff::openprot);The legal values for mode are listed in the following table. The Application column indicates which modes are valid for which file types.
Flag Application Meaning ___________________________________________________________________________ ios::app out Always append output to the end of the file ___________________________________________________________________________ ios::ate out Open and seek to end-of-file ("at end") ___________________________________________________________________________ ios::binary in, out Open file in binary mode (as opposed to text mode) ___________________________________________________________________________ ios::in in Open a file for input ___________________________________________________________________________ ios::nocreate in, out If file doesn't exist, don't create it ___________________________________________________________________________ ios::noreplace out Don't delete the file (open fails if file exists unless you specify app or ate) ___________________________________________________________________________ ios::out out Open file for output ___________________________________________________________________________ ios::trunc out Truncate file to zero length if it already exists (default if file exists and app or ate is not specified) ___________________________________________________________________________These modes are bit fields that are enumerated members of a bit vector in the class ios. That is why they are referred to as ios::out and ios::app and not simply as out and app. Let me give an example of what I mean by ``bit fields''. ios::app might equal 00000001, ios::ate might equal 00000010, ios::out might equal 00000100, etc. So each mode corresponds to one bit which can be 0 or 1. This means that more than one mode's value can be set at the same time using the arithmetic OR. For example, to open an output file with append, you could use the following:
ofstream out("outfile", ios::out | ios::app)The
|
operator takes the union of the two arguments.
Once you specify any part of the mode, you must specify the entire mode.
Thus, when I specified ios::app, I also had to specify
ios::out, because it was no longer specified by default.
The ios::nocreate flag says ``If file doesn't exist, don't create it.''
This is especially useful for input. For example, it is the only way
to test for the existence of a file.
Let me explain the difference between opening a file in text mode versus binary mode. When a file is in text mode, newline characters are converted into a carriage-return/line-feed combination on output. The reverse process occurs on file input: carriage-return/line-feed pairs are converted into a single newline character. This process doesn't occur when the file is opened in binary mode. The default for opening a file is text mode.
The third argument to the fstream constructors specifies the type of file sharing allowed between applications. The possible values are:
Sharing Flag Meaning ___________________________________________________________________ filebuf::openprot Compatibility sharing mode filebuf::sh_compat ___________________________________________________________________ filebuf::sh_none Exclusive mode; no sharing allowed ___________________________________________________________________ filebuf::sh_read Other read opens allowed ___________________________________________________________________ filebuf::sh_write Other write opens allowed ___________________________________________________________________These are also bit fields. So the union of
filebuf::sh_read
and
filebuf::sh_write
allows complete sharing of files between
applications.
As we have seen in our earlier discussion of input and output, it is also possible to open a file for both input and output. This is handled by the fstream class, which inherits from both the ifstream and ofstream classes. The constructor for the fstream class looks the same as those for the ifstream and ofstream classes except the mode argument is not defaulted:
fstream::fstream(const char *pFileName, int mode, int prot = filebuf::openprot);To open such a file, the mode should be set to
ios::in|ios::out
. For example,
#include <fstream.h> int main() { fstream inout("input.dat",ios::in|ios::out); float x; inout >> x; inout<< endl << "x = " << x << endl; inout.close(); return 0; }
Flag Binary Value Meaning ____________________________________________________________________ ios::eofbit 001 End-of-file encountered ____________________________________________________________________ ios::failbit 010 Last I/O operation failed ____________________________________________________________________ ios::badbit 100 Invalid operation attempted ____________________________________________________________________When a read operation encounters the end-of-file, it sets the ios::eofbit in the error flag for the ifstream object. The ios::failbit is set when an I/O operation fails. This could happen if the format of the data is improper. For example, attempting to extract a string of ASCII text into a number sets the failbit. In other words, if the program expects a number to be input and you give it a string of letters, ios::failbit is set. Attempting to read beyond the end-of-file also sets the failbit. Similar to the ios::failbit, the ios::badbit is set when an operation fails. The two flags differ in that the badbit indicates an unrecoverable error, whereas you might be able to recover from the failbit (then again, maybe not, but at least you have a chance). For example, attempting to open a file that doesn't exist sets the badbit. Although it's interesting to see which bits make up the error flag, you don't actually ever check or set these bits directly. Instead, you use the following access functions:
Function Purpose _______________________________________________________________ ios::bad() Returns TRUE if the badbit is set _______________________________________________________________ ios::clear(int=0) Sets the error flag _______________________________________________________________ ios::eof() Returns TRUE if the eofbit is set _______________________________________________________________ ios::fail() Returns TRUE if either the failbit or the badbit is set _______________________________________________________________ ios::good() Returns TRUE if no error bits are set _______________________________________________________________ ios::rdstate() Returns the error flag _______________________________________________________________ ios::operator!() Same as ios::fail() _______________________________________________________________ ios::operator void*() Same as ios::good(); cast operator _______________________________________________________________The check functions are primarily ios::eof(), ios::fail(), and ios::bad(). Each of these return TRUE if their respective bit is set (except ios::fail(), which returns a TRUE if either the failbit or the badbit is set). The ios::good() function returns the inverse of ios::fail(). Just about the worst named function in the entire C++ library is ios::clear(). This function gets its name from the fact that you use it to clear the error flag. However, you also use it to set an error flag. In fact, clear() allows you to set or clear any of the error bits you want. The two overloaded operators are just cute ways of calling ios::good() and ios::fail(). For example,
void fn(istream& in) { //stuff if(!in) //invokes operator!(), which calls in.fail() { //operation failed return; } }Alternatively, we could write the following:
void fn(istream& in) { //stuff if(in) //invokes operator void*(), which calls in.good() { //operation succeeded //stuff } }(Explanation of
operator void*()
. If we had a pointer
istream *pIn to an istream object, then *pIn
would refer to the object pointed to by pIn. In our case
in is an istream object, so we don't need the indirection
operator *.)
Some programmers prefer this form of calling ios::good()
and ios::fail(). A simple example of error checking is
#include <fstream.h> int main() { ifstream infile("input.dat"); //input.dat is the name of the file //in your directory ofstream outfile("output.dat"); //output.dat is the name of the file //that will be created in your directory float x; while(!infile.eof() && infile.good()) //makes sure that end-of-file //hasn't been reached and that //infile is in good shape { infile >> x; outfile << "x = " << x << endl; } infile.close(); outfile.close(); return 0; }(This program outputs the last x twice because the end-of-file is not reached until it goes beyond the last item in input.dat.)
The get() function comes in two flavors. The simplest version inputs a single character. For example, in the following program snippet, get() is used to read input from the input file input.txt:
#include <fstream.h> int main() { ifstream in("input.txt"); char c; while(!in.eof()) { c = in.get(); cout << c; } cout << endl; return 0; }The program starts by opening the file input.txt. The program then loops until the file is empty. On each loop, the program fetches another character and outputs it to the standard output. Notice that get() is not quite the same as
operator>>(istream&,char&)
,
which also fetches a single character from the input stream. The
difference lies in the fact that operator>>()
, by default,
skips any white-space characters (e.g., space, tab, newline, etc.)
found in the file, whereas get() does not. In fact this program
also spits out a strange end-of-file character at the end.
The second version of get() carries the following prototype:
istream& istream::get(char* pszTarget, int nCount, char delim='\n');This version inputs a series of characters terminated either by the appearance of a terminator character in the input stream or by a character count. Notice that you can have any character you want terminate the input. The count character solves the potential bug of inputting more characters than the buffer can hold. For example, the following code is inherently unsafe because it is entirely possible that the string extracted by
operator>>()
is
longer than the 80 characters the buffer can hold:
istream in("input.txt"); char buffer[80]; in >> buffer;A safer alternative would be
istream in("input.txt"); char buffer[80]; in.get(buffer, 80);Because get() now knows the length of the receiving buffer, it will make sure not to extract more characters than the buffer can handle. get() gets not more than 80 characters and puts them into the array buffer. You can have sizeof calculate the buffer for you:
istream in("input.txt"); char buffer[80]; in.get(buffer, sizeof buffer);
The prototype declaration for the getline() function is identical to that of the get() function:
istream& istream::getline(char* pszTarget, int nCount, char delim='\n');In execution, getline() is identical to the second form of get(). The sole exception is that get() extracts characters from the input stream up to, but not including, the delimiter, whereas getline() extracts the delimiter as well. Neither function stores the delimiter into the pszTarget buffer. This makes getline() ideal for reading an entire line of input at a time. It reads this line of input from the input file and puts it in the array pointed to by pszTarget.
#include <fstream.h> int main() { ifstream in("input.txt"); char szTarget[256]; in.getline(szTarget, sizeof szTarget); cout << szTarget << "\n"; return 0; }This program reads an entire line at a time. Notice that when the line that was just read is output to cout, the program must replace the delimiter that was stripped out by getline().
The prototype declaration for the read function is
istream& read(char* pszTarget, int nCount);This function reads a fixed number of characters from the input stream without regard to any type of delimiter. In addition, read() doesn't tack a NULL character to the end of the pszTarget buffer, nor does it attempt to interpret
'\n'
characters.
The put() function carries the following simple prototype:
ostream& ostream::put(char ch);This function does nothing more than output the specified character to the output stream. This is functionally identical to the
operator<<(ostream&, char&)
inserter.
The putback(Ch c) function allows a program to put an unwanted character back to be read some other time, as shown in the class of complex numbers in lecture 6. Ch is a template type, i.e., it's any type you specify.
The write() function outputs a fixed number of characters from the source character string to the output stream. This function carries the following prototype
ostream& ostream::write(const char* pszSource, int nCount);This is also a block-oriented transfer.
ofstream out("ofile.txt"); int nAnInt = 10; out << nAnInt;The constructor for ofstream first creates an ios object. It then constructs a filebuf object for output to the file ofile.txt. During output, the ios object converts the number 10 into the character 1 followed by the character 0. The ios object passes the string ``10'' to the filebuf object for output to the file. This is a nice division of labor. When you create a different type of output object-for example an ostream object that outputs to the display-you get the same ios base class object (all formatting is the same, after all) but a different subclass of streambuf (outputting to a display is not at all the same as outputting to a file).
It's worth taking a moment to understand what disk buffering is.
This is one of the functions performed by streambuf. If
streambuf went to the disk every time ios wanted to
write a few characters to disk, performance would be really slow.
In fact, when you read or write to the disk, you must read an entire
block of data at a time. (It's like Lay's Potato Chips-you can't eat
just one ``byte''.) The size of a block depends on the disk, but it's
usually 512 bytes or more. Therefore, on output, the streambuf
class collects output requests in the buffer until it has several
blocks worth. It then writes the entire buffer to the disk at once.
Writing the output buffer to disk is called ``flushing the buffer.''
For example, endl ends the line ("\n"
)
and then flushes the buffer.
On input, the situation is reversed. When the ios class asks for the first character from the input stream, the input buffer is empty. Rather than read a single character (even if that were possible), the streambuf reads several blocks of data into the input buffer. Then streambuf returns only the first character to ios and keeps the rest. When the next input request comes in, streambuf returns the next character from the input buffer without bothering to read from the disk. The streambuf class doesn't read from the disk again until the input buffer has been emptied by input requests.
A few conditions cause the output buffer to be flushed to disk early. For example, closing the file causes any remaining data that might be hanging around in the buffer to be flushed to disk. The application program can also force the output buffer to be flushed by calling ostream::flush(). For example,
out << student; out.flush()This assures your data is safely on the disk in case the program or the system crashes later on. Finally an output stream can be tied to an input stream so that a request for I/O from the input stream immediately flushes the output stream. For example,
char szname[80]' cout << "Enter your name"; cin >> szName;Things wouldn't work so well if ``Enter your name'' didn't make it to the display because it was cooling its heels in the output buffer. Tying cout to cin flushes the output buffer so that ``Enter your name'' can appear on the screen. Other iostream objects are not automatically tied. However, you can tie an ostream object to an iostream object in such a way that the ostream is automatically flushed when an I/O operation is performed on the iostream. For example,
#include <fstream.h> int main() { ifstream in("input.dat"); ofstream out("output.dat"); //By tying out to in, out.flush() will be called whenever an I/O //operation is performed on in in.tie(&out); // Do some stuff ... // Now untie out from in in.tie(0); return 0; }Notice that you can untie an object by passing a NULL to tie().
Function Purpose _______________________________________________________________ fill(char) Set the fill character for padding during output. _______________________________________________________________ flags(long) Set the formatting flags. _______________________________________________________________ precision(int) Set the precision when outputting a floating-point value _______________________________________________________________ width(int) Set the minimum width. Restricts the number of characters that are input. If fewer characters are required on output, the remaining space is filled with the fill character.Each of these functions also has a void argument list version, which simply queries the current setting. The fill() function sets the fill character. The default for the fill character is the blank space. The precision() function sets the precision. During the display of floating-point numbers, this setting determines the number of digits displayed to the right of the decimal point.
On output, the width() function specifies the minimum field width
to be used in displaying the next field inserted. If the value being output
requires more characters than are specified by width(), the
width is ignored. If the output field is smaller than the specified width,
the difference is made up by repeated application of the fill character.
If the width is zero, the minimum number of characters necessary to
contain the field are used for output. Zero is the default for
the width. The width() function is strange in one respect. When
you set a data member within a structure to a particular value,
you usually expect it to stay set. But this is not so for the width.
Each time you set the width, that setting applies only to the next
operation. After that, the width is reset to zero.
The width() function also has an effect on the input. Setting the
width restricts the number of characters extracted by the char*
extractor. This is important because the operator>>(istream&, char*)
,
unlike the getline() function,
has no place to indicate the size of the buffer receiving the
character string. Settting the width to the size of the buffer ensures
that the buffer boundaries are not exceeded.
Here is an example of how to use a few of these format control functions:
#include <fstream.h> int main() { ifstream infile("input.dat"); if(infile.fail()) { cout << "Couldn't open input.dat" << endl; return -1; } char buffer[5]; //short array of characters infile.width(sizeof buffer); infile >> buffer; cout << buffer << endl; infile.width(sizeof buffer); //have to repeat width() infile >> buffer; cout.width(15); cout.fill('*'); cout << buffer << endl; return 0; }The program starts by opening the input file in the normal fashion. Before extracting from the input file object, however, the program sets the input width to match the size of the buffer. The program then extracts a few characters into buffer and displays them to cout so you can see what you have. Suppose the input file consists of 30 characters, all in a row.
//input.dat file 123456789012345678901234567890If we just had an in >> buffer statement, the program would try to put all 30 characters into the buffer which has length 5. This would crash the program and give a ``bus error''. The output from running the program is
1234 ***********5678Notice that width the input stream width set to 5 characters, the extractor reads only four characters, cleverly leaving a space for the NULL in the final position of the buffer.
The remaining formatting features of ios are hidden in a protected
data member called x_flags
. This data member consists of a series
of 1-bit fields, some of which work together. Because these are
single-bit fields, however, they can be (and are) set in different
combinations to produce the desired effect. They are listed on pages
89-90 of More C++ for Dummies. The only ones that you would
probably use with any frequency are those dealing with floating-point
format. By setting either the ios::fixed or ios::scientific
flag, you specify whether floating-point numbers are displayed in
fixed or scientific format. If neither bit is set, the stream is
in automatic mode, meaning use whatever is most applicable. In automatic
mode, the precision referred to previously specifies the number of digits
to be displayed in the number. In either fixed or scientific mode,
precision specifies the number of digits after the decimal point.
The default is automatic. Several functions access the flag
bits. The function long ios::flags() reads the
current flag word. The function long ios::flags(long lNewFlag)
sets the flag word to the value contained in lNewFlag, and
returns the previous value. An example of how to use this is:
long lPreviousFlags; lPreviousFlags = cout.flags(); //record current flag word cout.flags(cout.flags() | ios::fixed); //fixed floating point notation cout << "7 = " << 7.0 << endl; cout.flags(lPreviousFlags); //reset flags to previous value cout.flags(cout.flags() | ios::scientific); //scientific notation cout << "7 = " << 7.0 << endl;We write
cout.flags(cout.flags() | ios::fixed);so that we have the union of fixed floating point notation with the flags that have already been set. If we had written
cout.flags(ios::fixed);we would have wiped out the all the other format flags that had been set, even the default ones. There are several other functions that you might find more convenient than flags(). For example, the ios::setf(long) function sets a particular flag, and the ios::unsetf(long) function clears that flag. So to set the scientific notation flag, I could write:
cout.setf(ios::scientific);To clear it, I could write:
cout.unsetf(ios::scientific);
Using ios member functions breaks up the flow of the output line. It's not very elegant to write
cout << "I = "; cout.width(10); cout << i << endl;It's prettier to write
cout << "I = " << i << endl;To overcome this problem, one can use a manipulator. Manipulators are objects defined in the include file iomanip.h to have the same effect as the member functions calls. In fact, they call the functions. The only advantage to manipulators is that the program can insert them directly into the stream rather than resort to a separate function call. Some manipulators take arguments and some do not. Technically speaking, you only have to include the header file iomanip.h in order to use the manipulators with arguments. Some of the manipulators are listed here. A more complete list can be found on page 100 of More C++ for Dummies.
Manipulator Member Functions Purpose __________________________________________________________________ endl ostream::flush() Insert \n and flush the buffer __________________________________________________________________ flush ostream::flush() Flush the stream __________________________________________________________________ setfill() ios::fill() Set the fill character __________________________________________________________________ setiosflags() ios::setf() Set the flags __________________________________________________________________ resetiosflags() ios::unsetf() Undo the flags __________________________________________________________________ setprecision() ios::precision() Set the floating-point precision __________________________________________________________________ setw() ios::width() Set the width of the next fieldJust like the width() function, the setw() manipulator must be included in the stream for each object whose width is not the default, zero. The only advantage that function calls have over manipulators is that the functions return the previous settings while the manipulators don't return anything. So if you want to store the previous setting, do something, then restore the previous setting, you can easily do it with function calls. With manipulators, you can only undo the flags with resetiosflags(). Here is an example using manipulators:
#include <iostream.h> #include <iomanip.h> void fn() { cout << setw(8) << 10 << setw(8) << 20 << endl; //keep setting width cout << setiosflags(ios::scientific) << "7.0 = " << 7.0 << endl; //scientific notation cout << resetiosflags(ios::scientific); //turn off scientific notation }
The fact that C++ overloads the left shift operator to perform output means that you can overload the same operator to perform output on classes you define. We have seen examples of this. Recall that the class of complex numbers in lecture 6 had a friend function that overloaded the inserter:
ostream & operator << (ostream &, const complex &);Let's give another example with the class USDollar:
#include <iostream.h> #include <iomanip.h> class USDollar { public: USDollar(double v = 0.0) { dollars = v; cents = int((v - dollars) * 100.0 + 0.5); } operator double() { return dollars + cents / 100.0; } void display(ostream& out) { out << '$' << dollars << '.' //set fill to 0's for cents << setfill('0') << setw(2) << cents //now put it back to spaces << setfill(' '); } protected: unsigned int dollars; unsigned int cents; }; //operator<< - overload the inserter for our class ostream& operator<< (ostream& o, const USDollar& d) { d.display(o); return o; } int main() { USDollar usd(1.50); cout << "Initially usd = " << usd << "\n"; usd = 2.0 * usd; cout << "then usd = " << usd << "\n"; return 0; }The display() function starts by displaying
$
, the dollar
amount, and the obligatory decimal point. Notice that output is to
whatever ostream object it is passed and not necessarily just to
cout. This allows the same function to be used on objects of
ostream and its subclasses such as fstream. When it comes
time to display the cents amount, display() sets the
width to 2 positions and the leading character to 0. This ensures
that numbers smaller than 10 display properly. Notice how the class
USDollar, instead of accessing the display() function
directly, also utilizes an operator<<(ostream&, USDollar&)
. The
programmer can now output USDollar objects as easily as intrinsic
types, as the example in main() demonstrates. The output from the
program is:
Initially usd = $1.50 then usd = $3.00Notice that the
operator<<()
returns the ostream passed to it.
This allows the operator to be chained with other inserters in a
single expression, i.e, this allows us to string output operators together:
complex c, d, x; ostream s; s << c << d << x;Because the
operator<<()
binds left to right, the expression
s << c << d << x;can be interpreted as
(((s << c) << d) << x);The first insertion outputs the complex number c to s. The result of this expression is the object s, which is then passed to
operator<<(ostream &, const complex &)
.
It is important that this operator return its ostream object
so that the object can be passed to the next inserter in turn.
Many times, you would like to make the inserter smart. That is, you would
like to say cout << baseClassObject
and let C++ choose the proper
subclass inserter in the same way that it choose the proper virtual
member function. Because the inserter is not a member function, you
cannot declare it virtual directly. There is a clever way to get
around this:
#include <iostream.h> #include <iomanip.h> class Currency { public: Currency(double v = 0.0) { unit = v; cent = int((v - unit) * 100.0 + 0.5); } virtual void display(ostream& out) = 0; protected: unsigned int unit; unsigned int cent; }; class USDollar : public Currency { public: USDollar(double v = 0.0) : Currency(v) { } //display $123.00 virtual void display(ostream& out) { out << '$' << unit << '.' << setfill('0') << setw(2) << cent << setfill(' '); } }; class DMark : public Currency { public: DMark(double v = 0.0) : Currency(v) { } //display 123.00DM virtual void display(ostream& out) { out << unit << '.' //set fill to 0's for cents << setfill('0') << setw(2) << cent //now put it back to spaces << setfill(' ') << " DM"; } }; ostream& operator<< (ostream& o, Currency& c) { c.display(o); return o; } void fn(Currency& c) { // the following output is polymorphic because the // operator<<(ostream&, Currency&) is defined through a virtual // member function cout << "Deposit was " << c << "\n"; } int main() { //create a dollar and output it using the //proper format for a dollar USDollar usd(1.50); fn(usd); //now create a DMark and output it using its own format DMark d(3.00); fn(d); return 0; }The class Currency has two subclasses, USDollar and DMark. In Currency, the display() function is declared pure virtual. In each of the two subclasses, this function is overloaded with a display() function to output the object in the proper format for that type. The call to display() in
operator<<()
is
now a virtual call. Thus, when operator<<()
is passed
USDollar, it outputs the object as a dollar. When passed
DMark, it outputs the object as a deutsche mark. Thus, although
operator<<()
is not virtual, because it invokes a virtual function,
it acts like a virtual function and the result is:
Deposit was $1.50 Deposit was 3.00 DMThis is another reason why it is better to perform the work of output in a member function, and let the non-member function refer to that function.