- •Introduction
- •Saving Time with This Book
- •Conventions Used in This Book
- •Part II: Working with the Pre-Processor
- •Part III: Types
- •Part IV: Classes
- •Part V: Arrays and Templates
- •Part VI: Input and Output
- •Part VII: Using the Built-in Functionality
- •Part VIII: Utilities
- •Part IX: Debugging C++ Applications
- •Part X: The Scary (or Fun!) Stuff
- •Icons Used in This Book
- •Creating and Implementing an Encapsulated Class
- •Creating a Mailing-List Application
- •Testing the Mailing-List Application
- •Customizing a Class with Polymorphism
- •Testing the Virtual Function Code
- •Why Do the Destructors Work?
- •Delayed Construction
- •The cDate Class
- •Testing the cDate Class
- •Creating the Header File
- •Testing the Header File
- •The Assert Problem
- •Fixing the Assert Problem
- •Using the const Construct
- •Identifying the Errors
- •Fixing the Errors
- •Fixing What Went Wrong with the Macro
- •Using Macros Appropriately
- •Using the sizeof Function
- •Evaluating the Results
- •Using sizeof with Pointers
- •Implementing the Range Class
- •Testing the Range Class
- •Creating the Matrix Class
- •Matrix Operations
- •Multiplying a Matrix by a Scalar Value
- •Multiplying a Matrix by Scalar Values, Take 2
- •Testing the Matrix Class
- •Implementing the Enumeration Class
- •Testing the Enumeration Class
- •Implementing Structures
- •Interpreting the Output
- •Defining Constants
- •Testing the Constant Application
- •Using the const Keyword
- •Illustrating Scope
- •Interpreting the Output
- •Using Casts
- •Addressing the Compiler Problems
- •Testing the Changes
- •Implementing Member-Function Pointers
- •Updating Your Code with Member-Function Pointers
- •Testing the Member Pointer Code
- •Customizing Functions We Wrote Ourselves
- •Testing the Default Code
- •Fixing the Problem
- •Testing the Complete Class
- •Implementing Virtual Inheritance
- •Correcting the Code
- •Rules for Creating Overloaded Operators
- •Using Conversion Operators
- •Using Overloaded Operators
- •Testing the MyString Class
- •Rules for Implementing new and delete Handlers
- •Overloading new and delete Handlers
- •Testing the Memory Allocation Tracker
- •Implementing Properties
- •Testing the Property Class
- •Implementing Data Validation with Classes
- •Testing Your SSN Validator Class
- •Creating the Date Class
- •Testing the Date Class
- •Some Final Thoughts on the Date Class
- •Creating a Factory Class
- •Testing the Factory
- •Enhancing the Manager Class
- •Implementing Mix-In Classes
- •Testing the Template Classes
- •Implementing Function Templates
- •Creating Method Templates
- •Using the Vector Class
- •Creating the String Array Class
- •Working with Vector Algorithms
- •Creating an Array of Heterogeneous Objects
- •Creating the Column Class
- •Creating the Row Class
- •Creating the Spreadsheet Class
- •Testing Your Spreadsheet
- •Working with Streams
- •Testing the File-Reading Code
- •Creating the Test File
- •Reading Delimited Files
- •Testing the Code
- •Creating the XML Writer
- •Testing the XML Writer
- •Creating the Configuration-File Class
- •Setting Up Your Test File
- •Building the Language Files
- •Creating an Input Text File
- •Reading the International File
- •Testing the String Reader
- •Creating a Translator Class
- •Testing the Translator Class
- •Creating a Virtual File Class
- •Testing the Virtual File Class
- •Using the auto_ptr Class
- •Creating a Memory Safe Buffer Class
- •Throwing and Logging Exceptions
- •Dealing with Unhandled Exceptions
- •Re-throwing Exceptions
- •Creating the Wildcard Matching Class
- •Testing the Wildcard Matching Class
- •Creating the URL Codec Class
- •Testing the URL Codec Class
- •Testing the Rot13 Algorithm
- •Testing the XOR Algorithm
- •Implementing the transform Function to Convert Strings
- •Testing the String Conversions
- •Implementing the Serialization Interface
- •Creating the Buffer Class
- •Testing the Buffer Class
- •Creating the Multiple-Search-Path Class
- •Testing the Multiple-Search-Path Class
- •Testing the Flow Trace System
- •The assert Macro
- •Logging
- •Testing the Logger Class
- •Design by Contract
- •Adding Logging to the Application
- •Making Functions Inline
- •Avoiding Temporary Objects
- •Passing Objects by Reference
- •Choosing Initialization Instead of Assignment
- •Learning How Code Operates
- •Testing the Properties Class
- •Creating the Locking Mechanism
- •Testing the Locking Mechanism
- •Testing the File-Guardian Class
- •Implementing the Complex Class
- •Creating the Conversion Code
- •Testing the Conversion Code
- •A Sample Program
- •Componentizing
- •Restructuring
- •Specialization
- •Index
40 Using the Standard
Streams to Format
Technique Data
Save Time By
Understanding stream classes
Formatting data with stream classes
Understanding your output
If you’ve been programming in C++ for a long time, you’re probably used to outputting data with the printf, fprintf, and sprintf functions that date back to the C programming days. It is now time to take
the plunge into using the stream components of the standard C++ library, because these components will save you lots of time and heartache. The stream components support input, output, and formatting for data in C++ applications. Like the printf, fprintf, and sprintf functions, streams exist to write to the console, to files, and to format data into strings. Unlike the aforementioned functions, streams are type-safe and extensible, which saves you time by reducing the amount of code you need to write and the amount of debugging you need to do to find problems in output.
The stream components save time by being type-safe, well written, and comprehensive. If you use streams instead of more specific output functions, you will find that your code is smaller, easier to understand, and more portable.
Although most programmers are aware that you can input and output data through the stream classes, most are unaware that the stream classes have a wealth of formatting functionality built into them.
In this technique, I show you how to work with the formatting functionality of the stream classes, how to extract data from a stream, and how to output columns and change floating point precision for data.
Working with Streams
In order to understand just how a stream component can be used in your application to save time and effort, let’s take a look at a simple example of a stream being used in an application. In this case, we will create some data in our program, and then output that data to the user. In addition, we will examine how to extend the stream class by creating our own output control.
226 Technique 40: Using the Standard Streams to Format Data
1. |
In the code editor of your choice, create a new |
|
|
file to hold the code for the implementation of |
|
|
the source file. |
|
|
In this example, the file is named ch40.cpp, |
|
|
although you can use whatever you choose. |
|
2. |
Type the code from Listing 40-1 into your file. |
|
|
Better yet, copy the code from the source file on |
} |
|
this book’s companion Web site. |
|
vector<A>::iterator iter;
for ( iter = dVector.begin(); iter != dVector.end(); ++iter )
{
out.width(8); out << (*iter);
}
out << endl; return out;
LISTING 40-1: USING STREAMS
#include <stdio.h> #include <iostream> #include <sstream> #include <vector>
using namespace std;
void PrintDoubleRow( int numElements, double *dArray, ostream& out )
{
int main(int argc, char **argv)
{
double |
dArray[20]; |
int |
nCount = 0; |
//See whether they gave us any on the command line.
if ( argc > 2 )
{
for ( int i=1; i<argc; ++i )
{
stringstream str; double number;
//First, set up some elements of the
ostream. str.setf(ios::fixed,
//Set the output floating point precision to 2 decimal places.
out.precision(4);
//Only show the decimal point if it is not a whole number.
out << showpoint;
for ( int i=0; i<numElements; ++i )
{
//Set each column to be 8 spaces. out.width(8);
//Output the float.
out << dArray[i];
}
out << endl;
}
template |
< class A > |
|
|
4 |
ostream& |
operator<<( |
ostream& out, |
|
vector< A >& dVector )
{
std::ios_base::floatfield); str.width(0); str.precision(4);
str << argv[i]; str >> number;
dArray[nCount] = number; nCount++;
}
}
else
{
// Prompt the user for input. bool bDone = false;
while ( !bDone )
{
char szBuffer[80];
cout << “Enter a number (or a dash (*) to quit): “;
memset( szBuffer, 0, 80 ); cin >> szBuffer;
if ( szBuffer[0] == ‘*’ ) bDone = true;
else
{
Working with Streams 227
stringstream str; double number;
|
str.setf(ios::fixed, |
|
|
||||
|
|
std::ios_base::float- |
|
|
|||
|
|
field); |
|
|
|
|
|
|
str.width(0); |
|
|
|
|||
|
str.precision(4); |
|
|
||||
|
str << szBuffer; |
|
|
|
|||
|
str >> number; |
|
|
|
|||
|
dArray[nCount] = number; |
|
|
||||
|
nCount++; |
|
|
|
|
||
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
PrintDoubleRow( nCount, dArray, |
1 |
|||||
|
cout ); |
|
|
|
|
||
|
// Now display it as a vector. |
|
2 |
||||
|
vector< double > dVector; |
|
|
||||
|
for ( int i=0; i<nCount; ++i ) |
|
|||||
|
dVector.insert( dVector.end(), |
|
|
||||
|
dArray[i] ); |
|
|
|
|
||
|
cout << “Vector: “ << endl; |
|
3 |
||||
} |
cout << dVector << endl; |
|
|||||
|
Let’s take a look at what is going on here. First, |
|
|||||
|
we create a standard array of double values and |
||||||
|
put data received from the user into the array. |
|
|||||
|
That array is then printed out using the standard |
||||||
|
stream class (see |
1). Next, we are creating an |
|||||
|
“array” using the Standard |
Template Library |
|
|
|||
|
vector class (see |
2). We then print that vec- |
|
||||
|
tor out using a stream |
shown at |
3. But wait, |
|
|||
|
how does this work? Vectors are not among the |
|
|||||
|
standard supported types for streams. If you |
|
|
||||
|
look at the templated function marked with |
4, |
you will see that we have created an overloaded operator that takes a vector object and outputs it to a stream. The compiler will match up our overloaded operator along with the streaming of the vector, and make sure that it all works properly. Note also the use of the width and precision methods of the stream class to set the output width of each column in the vector properly, and only output the right number of decimal points.
3. Save the source-code file in the code editor and close the editor application.
4. Compile the application in your favorite compiler, on your favorite operating system.
If you have done everything right, when you run the application with the following command-line input:
1 2 3 4
you should see the following output from the application on the console window:
$ ./a.exe 1 2 3 4
1.000 2.000 3.000 4.000 Vector:
1.000 2.000 3.000 4.000
As you can see, the output is the same for both the array and vector classes. We can also see that the width of the columns is fixed at eight characters, as we specified in the width method of the stream. Finally, note that the number of decimal points is fixed at three for each entry, once again as specified in the precision method.
Alternatively, you can enter the data at the prompt. To do so, run the program with no input arguments, and then enter the values when prompted from the user. In this case, you should see the following output from the program in the console window:
$ ./a.exe |
|
|
|
Enter a number (or |
a dash (*) to quit): 1 |
||
Enter a number (or |
a dash (*) to quit): 2 |
||
Enter a number (or |
a dash (*) to quit): 3 |
||
Enter a number (or |
a dash (*) to quit): 4 |
||
Enter a number (or |
a dash (*) to quit): * |
||
1.000 |
2.000 |
3.000 |
4.000 |
Vector: |
|
|
|
1.000 |
2.000 |
3.000 |
4.000 |
The output from the program is the same, the only difference is how the data got into the system. Note again that the width of the columns is still fixed and the number of decimal points is still what we specified.
41 Reading In and
Processing Files
Technique
Save Time By
Reading in files with stream classes
Processing files with stream classes
Creating a test file
Interpreting your output
Processing files in C++ is really the same as processing any other sort of input or output. Unlike similar functions in C, however, the file-processing functions in C++ allow you to use the same code for
processing data — from either the keyboard or a file. This generality makes it considerably easier to write code that is easy to test, run, and maintain. And, of course, when code is faster to write and easier to test, it saves you time in the project.
If you use stream classes instead of C-style file functions to access data, you will find the code quicker to write and test — and errors easier to trap. See Technique 40 for more on using stream classes.
This technique shows you how to use the file-stream classes to read in — and process — a simple preferences file. I also tell you how this method compares to the old style of doing things, so that you can easily drop in this code wherever you are using the older C-style functions.
1. In the code editor of your choice, create a new file to hold the code for the implementation of the source file.
In this example, the file is named ch41.cpp, although you can use whatever you choose.
2. Type the code from Listing 41-1 into your file.
Better yet, copy the code from the source file on this book’s companion Web site.
LISTING 41-1: THE FILE-READING CLASS
#include <stdio.h> #include <string.h>
#include <fstream> #include <ios> #include <iostream> #include <string> #include <vector>
Reading In and Processing Files 229
using namespace std;
// The old fashioned way
class Entry
{
private:
char *strName; char *strValue;
void Init()
{
strName = NULL; strValue = NULL;
}
public:
Entry()
{
Init();
}
Entry( const char *name, const char *value )
{
Init();
setName( name ); setValue ( value );
}
Entry( const Entry& aCopy )
{
Init();
setName( aCopy.strName ); setValue( aCopy.strValue );
}
~Entry()
{
if |
( strName ) |
|
|
delete [] |
strName; |
if |
( strValue |
) |
|
delete [] |
strValue; |
}
Entry operator=( const Entry& aCopy )
{
setName( aCopy.strName ); setValue( aCopy.strValue ); return *this;
}
void setName(const char *name)
{
if ( strName )
delete [] strName;
strName = new char[strlen(name)+1 ]; strcpy( strName, name );
}
void setValue( const char *value )
(continued)
230 Technique 41: Reading In and Processing Files
LISTING 41-1 (continued)
{
if ( strValue )
delete [] strValue; strValue = new char[strlen
(value)+1 ];
strcpy( strValue, value );
}
const char *getName( void )
{
return strName;
}
const char *getValue( void )
{
return strValue;
}
};
bool OpenFileAndReadOld( const char *strFileName, Entry* array, int nMaxEntries, int *numFound )
{
FILE *fp |
= fopen ( strFileName, “r” ); |
|
|
if ( fp == NULL ) |
|
|
|
return false; |
|
|
|
int nPos |
= 0; |
|
|
while ( !feof(fp) ) |
|
|
|
{ |
|
|
1 |
char |
szBuffer[ 257 ]; |
|
|
memset( szBuffer, 0, 256 ); |
|
||
if ( |
fgets( szBuffer, 256, fp ) == |
|
|
NULL ) |
|
|
|
|
break; |
|
|
//Look for the position of the ‘=’ sign.
char *str = strstr(szBuffer, “=”); if ( str )
{
//First, get the name. char szName[256]; memset( szName, 0, 256 ); strncpy(szName, szBuffer,
strlen(szBuffer)-strlen(str) );
//Now, get the value.
char szValue[256];
memset( szValue, 0, 256 ); strncpy(szValue, str+1,
strlen(str)-1 );
if ( szValue[strlen(szValue)-1] == ‘\n’ )
szValue[strlen(szValue)-1] = 0;
Entry e( szName, szValue ); if ( nPos < nMaxEntries )
{
array[ nPos ] = e; nPos ++;
}
}
}
*numFound = nPos;
fclose(fp);
return true;
}
The code in Listing 41-1 does things the oldfashioned way (C-style), using file-based functions. Trying it with streams creates reusable operators along the way. That’s next.
3. Now, append the code in Listing 41-2 to the source file using your favorite source-code editor.
LISTING 41-2: USING STREAMS FOR FILE READING
//Various operators used by the application.
ifstream& operator<<( |
string& sIn, ifstream& |
|
{ |
in ) |
3 |
|
while ( !in.eof() |
) |
|
{ |
|
char c; in.get(c);
if ( in.fail() ) return in;
sIn += c;
if ( c == ‘\n’ ) return in;
}
return in;
}
|
|
|
|
Reading In and Processing Files |
231 |
|
string operator-( string& sIn, |
|
4 |
Entry e(name.c_str(), |
|
||
|
char cIn ) |
|
value.c_str()); |
|
||
{ |
|
|
entries.insert( entries.end(), |
|||
|
string sOut = “”; |
|
|
e ); |
|
|
|
for ( int i=0; i<sIn.length(); ++i ) |
|
|
} |
|
|
|
if ( sIn[i] != cIn ) |
|
|
} |
|
|
|
sOut += sIn[i]; |
|
|
|
|
|
} |
return sOut; |
|
|
in.close( ); |
|
|
|
|
|
|
|||
bool OpenFileAndReadNew( const char |
|
|
return true; |
|
||
|
|
} |
|
|
||
|
*szFileName, std::vector< Entry >& |
|
|
|
|
|
{ |
entries ) |
|
|
|
|
|
|
|
|
|
|
||
ifstream in; |
|
|
|
|
|
|
|
|
|
As you can see, the code is much easier to |
|||
|
in.open( szFileName ); |
|
|
|||
|
|
|
|
understand and maintain in the stream version. |
||
|
if ( in.fail() ) |
|
|
Readability is important in coding because it |
||
|
{ |
|
|
takes less time for the maintenance programmer |
||
|
printf(“Unable to open file %s\n”, |
|
to read and understand your objective. The |
|||
|
szFileName ); |
|
|
stream versions of the code form their own |
||
|
return false; |
|
|
|||
|
|
|
description of what we are trying to do, improv- |
|||
|
} |
|
|
|||
|
|
|
ing on the confusing C-style functions. More |
|||
|
|
|
|
|||
|
// Process the file |
|
|
importantly, if we want to test the functions from |
||
|
|
|
the keyboard, it is trivial to pass in the standard |
|||
|
while ( !in.eof() ) |
|
|
|||
|
{ |
|
|
input object instead of a file. The code can cope |
||
|
// Get an input line |
|
|
with both types of input. |
|
|
|
string sLine = “”; |
|
2 |
To understand just how simple the stream ver- |
||
|
sLine << in; |
|
||||
|
sion is compared to the older version, take a |
|||||
|
|
|
||||
|
|
|
|
|||
|
// Skip comments |
|
|
look at two similar segments of the code. The |
||
|
if ( sLine.length() && sLine[0] |
|
|
line marked |
1 in the original listing shows |
|
|
== ‘#’ ) continue; |
|
|
how we read a line in from the input source. The |
||
|
|
|
|
corresponding line in the updated stream ver- |
||
|
|
|
|
sion is marked |
2. Note that the stream version |
|
|
// Remove all carriage returns and |
|
is not only smaller and easier to read, but also it |
|||
|
|
handles problems the original code did not. For |
||||
|
line feeds |
|
|
|||
|
|
|
example, the string class can handle an almost |
|||
|
sLine = sLine - ‘\n’; |
|
|
|||
|
|
|
infinite number of characters, whereas the buffer |
|||
|
sLine = sLine - ‘\r’; |
|
|
|||
|
|
|
used in the original code is fixed in size. |
|
||
|
|
|
|
|
||
|
// Now, extract the pieces |
|
|
Likewise, the stream version automatically |
||
|
int ePos = sLine.find_first_of |
|
|
enters the number of characters into the string |
||
|
(‘=’, 0); |
|
|
class, which makes checking for blank lines sim- |
||
|
if ( ePos != string::npos ) |
|
|
ple. Finally, checking for substrings in stream |
||
|
{ |
|
|
classes is considerably easier than using the |
||
|
// Copy the name |
|
|
clunky old strstr function that required you |
string name = |
to check for NULL |
returns and end of string |
|
sLine.substr(0,ePos); |
|||
|
|
string value = |
comparisons. |
|
|
sLine.substr(ePos+1); |
4. Save the source code as a file in the code |
|
editor.