- •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
232 Technique 41: Reading In and Processing Files
Notice the operators that are defined in this block of code (shown at the lines marked 3 and 4). These provide a standard way to retrieve a single line of a file into a string object — and a fast, efficient way to remove a character from a string. The nicest thing about C++ operators is that they can be defined externally to the class they manipulate, so they don’t require that the original classes be modified. This is a good way to extend functionality without derivation or modification.
Testing the File-Reading Code
After you have created the file-reading functionality, you should create a test driver that not only ensures that your code is correct, but also shows people how to use your code.
The following steps show you how to create a test driver that illustrates how the two methods for inputting data (OpenAndReadFileOld and OpenAndReadFileNew) are used — and what the output of each will be.
1. In the code editor of your choice, reopen
the existing file to hold the code for your test program.
In this example, I named the test program ch 41.cpp.
2. Append the code from Listing 41-3 into your file.
Better yet, copy the code from the source file on this book’s companion Web site.
LISTING 41-3: THE FILE-READ TEST DRIVER
int main( int argc, char **argv )
{
if ( argc < 2 )
{
printf(“Usage ch5_3 filename\n”); exit(1);
} |
|
Entry |
entries1[50]; |
int |
num = 0; |
printf(“Old Way:\n”);
if ( OpenFileAndReadOld( argv[1], entries1, 50, &num ) == false )
{
printf(“Error processing file %s\n”, argv[1] );
exit(1);
}
for ( int i=0; i<num; ++i )
{
cout << “Entry “ << i << endl; cout << “Name: “ << entries1[i].getName() << endl; cout << “Value: “ << entries1[i].getValue() << endl;
}
printf(“New Way:\n”); std::vector< Entry > entries2; if ( OpenFileAndReadNew( argv[1],
entries2 ) == false )
{
printf(“Error processing file %s\n”, argv[1] );
exit(1);
}
std::vector< Entry >::iterator iter; int nPos = 0;
for ( iter = entries2.begin(); iter != entries2.end(); ++iter )
{
cout << “Entry “ << nPos << endl; cout << “Name: “ << (*iter).getName() << endl;
cout << “Value: “ << (*iter).getValue() << endl; nPos ++;
}
}
This simple program just allows us to read in a test file in two different ways, using both the oldstyle C functions and the new-style stream functions. We then print out the values to compare them. Note that the old-style function requires that we use a fixed-size array, while the stream version uses the newer vector class to allow for an almost-infinite number of entries.
3. Save the source-code file and close the source editor application.
Creating the Test File 233
Creating the Test File
In order to use the test driver, we need some input data for the driver to process. Let’s create a simple test file that contains data that we can read in to compare the old-and new-style functions of input and output.
1. In the code editor of your choice, create a text file for testing the application.
In this case, I used the name test.txt for the test file.
2. Type the following text into the test file:
Config1=A config string
Config2=100
Config3=Another line
# This is a comment Config4=A.$5
3. Save the test file and close the code-editor application.
4. Compile and run the program with your favorite compiler and operating system.
If you have done everything properly, you should see the following output from the program on your console window:
$ ./a.exe test.txt |
|
5 |
Old Way: |
|
|
Entry 0 |
|
Name: Config1
Value: A config string
Entry 1
Name: Config2
Value: 100 |
|
|
Entry 2 |
|
|
Name: Config3 |
|
|
Value: Another line |
|
|
Entry 3 |
|
|
Name: Config4 |
|
|
Value: A.$5 |
|
6 |
New Way: |
|
|
Entry 0 |
|
Name: Config1
Value: A config string
Entry 1
Name: Config2
Value: 100
Entry 2
Name: Config3
Value: Another line
Entry 3
Name: Config4
Value: A.$5
As you can see by the output of our program, both the old and new ways of processing the data work the same. All of the entries shown under the Old Way output line were read in using the old-style C functions, while the output shown under the New Way output line were read in using the new-style streams. By using this simple program, therefore, we can easily convert old-style applications to the new-style functions quickly and easily, saving time and effort.
No functional difference exists between the old-style C functions and the new-style C++ streams. To see this, compare the lines shown at 5 and at 6. Leaving out the additional operators that can be reused anywhere, however, makes the new-style code that handles streams considerably smaller and easier to read; by comparison, the old-style code is cumbersome and confusing.
42 How to Read Delimited Files
Technique
Save Time By
Understanding standard delimited files
Creating generic classes for reading or loading standard delimited files into your application
Testing your output
There are a lot of ways to store data in file systems. One of the most popular is a standard delimited file, in which the individual fields of the records are separated by known delimiters. There are numer-
ous examples of this, from comma separated values (CSV) to XML files to fixed-size records that include null bytes. The capability to load delimited data into your application is very valuable — and it makes your application considerably easier to use.
Extracting the input and output of data formats into separate classes will not only make the classes more reusable across applications, it will also save you lots of time when trying to load known formats into new applications.
This technique builds some generic classes that aid loading and parsing delimited files. I have to make a few assumptions here, although each of these assumptions is fairly easy to adapt if you need to change the logic. The assumptions are as follows:
The files contain only delimiters that separate fields. That is, no fields in the file contain delimiters.
The records exist one per line. This is really just a convenience; it would be easy enough to check for an end-of-record signature other than the end-of-line character.
The fields can vary in size.
Reading Delimited Files
Because the need to read delimited files is such a common problem in the software development world, it makes sense to build a generic method for reading them. By doing this, we save a lot of time because we are able to move the code to read the files from project to project. For example, you can create a generic class that can be used to read any sort of delimited files and test it with a variety of input.
Reading Delimited Files 235
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 ch42.cpp, although you can use whatever you choose.
2. Type the code from Listing 42-1 into your file.
Better yet, copy the code from the source file on this book’s companion Web site.
LISTING 42-1: READING A DELIMITED FILE
#include <stdio.h> #include <string> #include <vector> #include <iostream> #include <fstream>
//Avoid having to type out std:: for all
//STL classes.
using namespace std;
// This class manages a list of delimiters. |
1 |
|
class Delimiters |
|
|
{ |
|
private:
//Array of possible delimiters. Use a vector,
//since the characters could include NULL byte
//or other characters that string can’t handle.
vector< char > _delimiters;
protected:
virtual void Copy ( const Delimiters& aCopy )
{
vector<char>::const_iterator iter; for ( iter =
aCopy._delimiters.begin(); iter != aCopy._delimiters.end(); ++iter )
_delimiters.insert( _delimiters. end(), (*iter) );
}
public:
Delimiters(void)
{
}
Delimiters( const char *delimiterList )
{
for ( int i=0; i<strlen(delimiterList); ++i)
{
_delimiters.insert( _delimiters. end(), delimiterList[i] );
}
}
Delimiters( const Delimiters& aCopy )
{
Copy ( aCopy );
}
Delimiters operator=( const Delimiters& aCopy )
{
Copy( aCopy);
}
virtual ~Delimiters(void)
{
}
//Clear out the entire list. virtual void Clear()
{
_delimiters.erase( _delimiters. begin(), _delimiters.end() );
}
//Add a delimiter to the list. virtual void Add( char c )
{
_delimiters.insert( _delimiters. end(), c );
}
//See whether a given character is in the list.
virtual bool Contains( char c )
{
vector<char>::const_iterator iter; for ( iter = _delimiters.begin(); iter != _delimiters.end(); ++iter )
if ( c == (*iter) ) return true;
return false;
}
// Remove a given delimiter. virtual bool Remove( char c )
{
vector<char>::iterator iter;
for ( iter = _delimiters.begin(); iter != _delimiters.end(); ++iter )
if ( c == (*iter) )
{
(continued)
236 Technique 42: How to Read Delimited Files
LISTING 42-1 (continued)
_delimiters.erase( iter ); return true;
}
return false;
}
};
//This class manages the data for a given row of a
//delimited file.
class DelimitedRow |
|
2 |
|
{ |
|
|
|
private: |
|
|
|
vector< |
string > _columns; |
|
|
protected: |
|
|
|
virtual |
void Copy( const DelimitedRow& aCopy ) |
||
{ |
|
|
|
vector<string>::const_iterator iter; |
|||
for |
( iter = aCopy._columns.begin(); iter != aCopy._columns.end(); ++iter ) |
||
|
_columns.insert( _columns.end(), (*iter) ); |
}
public:
DelimitedRow(void)
{
}
DelimitedRow( const char *col )
{
Add( col );
}
virtual ~DelimitedRow()
{
}
virtual void Add( const char *col )
{
_columns.insert( _columns.end(), col );
}
int NumColumns( void )
{
return _columns.size();
}
string getColumn( int index )
{
if ( index < 0 || index > NumColumns()-1 ) return string(“”);
return _columns[index];
}
};
Reading Delimited Files 237
// This class will handle a single delimited file.
class DelimitedFileParser |
|
3 |
|
{ |
|
|
|
private: |
|
|
|
string |
|
_fileName; |
|
Delimiters |
|
_delim; |
|
ifstream |
|
_in; |
|
vector<DelimitedRow> |
_rows; |
|
|
protected: |
|
|
|
virtual void Copy( const DelimitedFileParser& aCopy ) |
|||
{ |
|
|
|
_fileName = aCopy._fileName; |
|
||
_delim |
= aCopy._delim; |
|
vector<DelimitedRow>::const_iterator iter;
for ( iter = aCopy._rows.begin(); iter != aCopy._rows.end(); ++iter ) _rows.insert( _rows.end(), (*iter) );
}
virtual void _ParseLine( string sIn )
{
//Given a delimiter list, and an input string, parse through
//the string.
DelimitedRow row; string sCol = “”;
for ( int i=0; i<sIn.length(); ++i)
{
if ( _delim.Contains( sIn[i] ) )
{
row.Add( sCol.c_str() ); sCol = “”;
}
else
sCol += sIn[i];
}
row.Add( sCol.c_str() ); _rows.insert( _rows.end(), row );
}
public:
DelimitedFileParser(void)
{
}
DelimitedFileParser( const char *fileName, const char *delimiters ) : _delim( delimiters )
{
Open( fileName );
}
DelimitedFileParser( const DelimitedFileParser& aCopy )
{
Copy ( aCopy );
}
(continued)