This discussion does not instruct you to add any new code to your files. Code is sometimes repeated to illustrate a point, but you do not need to add it.
The DMTEST program has no real user interface. The main function in file DMTEST.CPP simply creates a database, fills it with person objects, and demonstrates its capabilities. In a later chapter of the tutorial, the data model will be integrated with a Microsoft Windows user interface. A sample program which provides a character-based user interface to the database is also provided on your distribution disks.
Previously, in the discussion of the CPersonList class, you saw briefly how to add person objects to a list, how to search a list for a person, and how to delete the list and its contents when you finished with it. The following discussion recaps and adds to the previous discussion.
How the Database Is Created and Destroyed
MakeDataBase is interesting primarily for how it makes and returns a filled database. It makes the database by constructing a CPersonList object dynamically in the heap, using the new operator. After filling the list with CPerson objects, also constructed in the heap with new, MakeDataBase returns a pointer to the list object. This lets the database object and the objects stored in it persist after the function returns.
Because the database and its contents were created as dynamic objects in the heap, you must explicitly destroy them when you finish. Thus the last two lines in the main function call the CPersonList member function DeleteAll to delete the CPerson objects in the list and then use the delete operator to delete the list object itself:
pDataBase2 -> DeleteAll(); // Delete the contents
delete pDataBase2; // Delete the list object
The FindPerson member function of class CPersonList searches the list for a given last name and builds a second list containing pointers to any found CPerson objects. The function returns a pointer to the list of found objects.
The TestFindPerson function in DMTEST.CPP simply creates a new list and searches it for two different last names.
The first search looks for the name “Banipuli,” which is not in the database. When FindPerson returns an empty list, TestFindPerson uses the CPersonList member function IsEmpty, inherited from the base class of CPersonList, CObList, to detect the list's empty condition.
The second search looks for “Smith,” a name that is in the database several times. This time FindPerson returns a list containing pointers to two CPerson objects. These contain the names “Mary Smith” and “Al Smith.”
Because the search was successful, TestFindPerson uses the predefined “dump context” afxDump to dump the contents of the found list to the standard output. Recall that, at the beginning of the main function, the afxDump object's SetDepth member function was called to specify that not only the list object be dumped but the list's content objects as well.
The afxDump object is used here as a simple way to display the contents of the found list.. Note that afxDump only works if the _DEBUG flag is defined, so you only get the dump during debugging.
The output of this dump is shown with the full program output below (for a release version of the program).
Create a person list and fill it with persons
Serialize the person list
List contains:
Meyers, Brian 236-1234
Hart, Mary 287-0987
Jones, Steve 344-9865
Smith, Al 435-4505
Smith, Mary 435-8159
Delete the list and all its elements
List is Empty
Deserialize the data from disk into a new list
List contains:
Hart, Mary 287-0987
Jones, Steve 344-9865
Smith, Al 435-4505
Smith, Mary 435-8159
Test the FindPerson function
Looking for the name Banipuli
No matching persons
Looking for the name Smith
Found matching persons
Delete the list and all its elements
End of program
You've already seen the code to serialize the database to disk and to deserialize it from disk in the steps on pages 42-44. (This topic was also discussed earlier, in “How to Serialize the List” on page 42.) This section shows how to use CFile and CArchive objects. The next section shows how to handle exceptions.
Recall that to serialize a CPersonList you:
1.Create a CFile object and call its Open member function.
2.Create a CArchive object, passing the CFile to it as an argument.
3.Use the overloaded insertion (<<) or extraction (>>) operator to pass data through the archive object. The one you use depends on whether you are writing or reading the list.
4.When you finish, close the archive object and then the file object.
The objects used in this process are a CFile object, a CArchive object, and, optionally, a CFileException object. The CFileException object is used to return information about any file errors that may have occurred during the attempt to open the file. For more information on exceptions, see “Exception Handling” on page 61.
The Microsoft Foundation Class Library provides class CFile and two derived classes, CStdioFile and CMemFile. CFile is for binary files, CStdioFile is for buffered text files as defined in STDIO.H, and CMemFile is for memory-based files. You can use these classes or your own classes derived from them.
A file object, of any of these classes, handles file opening, closing, and other file operations. The DMTEST program uses class CFile and its Open, Close, and GetStatus member functions. For additional CFile capabilities, see the Class Libraries Reference.
You can create a CFile object on the frame of a function or in the heap. The code in DMTEST.CPP typically declares pointers to CFile objects and allocates the objects dynamically with new. See the OpenForReading function in step 2.
Before you open the CFile object, you can check that the file exists by calling the CFile object's GetStatus member function. Pass the filename and a preconstructed CFileStatus object. CFileStatus is a Microsoft Foundation Class that contains status information about the file when GetStatus returns. If the call to GetStatus fails, it returns FALSE. You can examine the contents of the CFileStatus object to see why. For more information about CFileStatus, see the Class Libraries Reference.
In DMTEST.CPP, if GetStatus returns FALSE, it means the file doesn't exist. The inclusive OR assignment operator, |=, is used to add the mode CFile::modeCreate to the existing access parameter, nAccess, before calling Open.
You need an opened CFile object to pass to the constructor of your CArchive object, as in ReadDataBase. The arguments to the Open member function of class CFile are a filename, the access mode, and an optional CFileException object. The filename argument is a null-terminated string. The access mode argument is one of the access modes defined in class CFile, such as CFile::modeRead or CFile::modeWrite. The optional CFileException argument is an object that you construct before you pass it to Open. When Open returns, this object contains information about any exception that is thrown. For more information on this argument, see “Exception Handling” on this page.
Be sure to close your CArchive object before you call CFile::Close.
A CArchive object uses a CFile object to establish a connection with a disk file. Once the connection is made, you can use the CArchive object as a stream, much as you would use the standard C++ iostreams, cin and cout.
Pass an already-opened CFile object to the CArchive constructor when your archive object is created. A CArchive object must be initialized for loading (reading) or storing (writing). It can't be used for both. To initialize the archive for reading, pass TRUE as the second argument to the constructor. For writing, pass FALSE. For example, in ReadDataBase, the archive object is constructed like this:
CArchive archive( pFile, TRUE );
Class CArchive overloads the insertion (<<) and extraction (>>) operators. To write data with a CArchive opened for writing, do this:
archive << pDataBase;
The overloaded insertion operator causes the Serialize member functions of the CPersonList object and its CPerson elements to write the appropriate data to the file specified when the archive was opened.The serialization process could throw a CArchiveException. For an explanation of exceptions and how they're handled, see the next section.
This section explains the use of the Microsoft Foundation Class Library's exception-handling mechanism as used in DMTEST.CPP. For more information about exception handling, see Chapter 12, and see the Class Libraries Reference under class CException.
Some of the code in the Microsoft Foundation Class Library generates exceptions when errors occur. The Microsoft Foundation Classes provide a mechanism for handling exceptions in an object-oriented fashion. Certain error-prone operations, such as memory allocation, file processing, and archive processing, detect errors and “throw” exceptions. When an exception is thrown, an exception object of the appropriate class is constructed. To process exceptions, you set up “exception frames” with a set of predefined macros. (For more information about how to set up exception frames, see “The Exception Frames in the Main Function” on page 63.)
You can then “catch” the exception at any level of the hierarchy of exception frames in your program. For example, suppose that, in your program, function A calls function B, which calls function C. Suppose further that you set up exception frames in functions A and C. If an exception is thrown in function C, you can choose to catch it in C, or you can catch it in A. To view this mechanism diagrammatically, see Figure 2.7.
You can also explicitly throw exceptions yourself, as a way of selecting the level at which your program responds to an error.
The best guideline for handling exceptions is to catch them only if there is something useful to do about them. For example, if you can recover from the error that generated an exception, or if you can at least clean up, by all means catch the exception. But if, for example, your program throws a memory-allocation exception and there is no way you can free enough memory to make the allocation attempt succeed if tried again, there is little point in catching the exception. Sometimes, as in DMTEST.CPP, the only useful response to an exception is to alert the user before the program terminates.
The following sections explain how exception handling is used in DMTEST.CPP.
The Exception Frames in the Main Function
To process an exception, set up an “exception frame” around the code that can throw an exception. An exception frame specifies a region of code in which you wish to catch exceptions. It also specifies blocks of actions to take if an exception is caught. Use the TRY, CATCH, AND_CATCH, and END_CATCH macros, provided by the Microsoft Foundation Class Library.
Enclose the code that can throw an exception in a TRY block. For example, in DMTEST.CPP, TRY blocks enclose the calls that open files and create archives. The code that actually throws the exception could be deep in the function call chain. For instance, main calls OpenForWriting, which calls the CFile member function Open, which could throw an exception. Those calls, and any exceptions they throw, are enclosed in the TRY block in main. For example, in DMTEST.CPP:
TRY
{
pFile = OpenForWriting( szFileName );
WriteDataBase( pFile, pDataBase );
}
Enclose code to handle, or respond to, a particular exception type in a CATCH block. In DMTEST.CPP, code to handle a file exception is enclosed like this:
CATCH( CFileException, theException )
{
// Code to handle the exception
}
The CATCH macro specifies two arguments:
The exception type (in this case class CFileException)
An exception object (e, the actual exception thrown)
For more information about the exception classes—CException, CFileException, CArchiveException, and so on—see the exception classes in the Class Libraries Reference and see Chapter 12.
If, as in DMTEST.CPP, a TRY block encloses code that could throw more than one exception type, you can use the AND_CATCH macro to set up additional CATCH blocks for the TRY block. In DMTEST.CPP, a CATCH block is used to catch file exceptions and an AND_CATCH block is used to catch archive exceptions. The AND_CATCH macro takes the same kinds of arguments as CATCH.
Be sure to end the TRY/CATCH exception frame with the END_CATCH macro.
The main function in DMTEST.CPP uses two exception frames, one for serialization and one for deserialization.
The Exception Object Passed to CFile's Open Member Function
The code for OpenForWriting and OpenForReading shows the Open member function of class CFile being called. Open takes three arguments (although the third is optional). The third argument is an exception object, created previously, used to pass back exception information for the call. The exception object argument is a pointer to a CFileException object, created dynamically in the heap with the call
CFileException* theException = new CFileException;
When Open returns, you can examine the member variables of object e. You can access public member variables and member functions of class CFileException to examine the cause of the exception or to convert it to a standard error code—for example:
TRACE( " Cause: %d\n", theException -> m_cause );
The m_cause member variable of a CFileException object contains a code identifying the error type. These error types are defined in an enum declaration in class CFileException. Open also returns a Boolean value that you can examine in the usual way to test the results of the function call.
The THROW and THROW_LAST Macros
In OpenForReading, the call to Open looks like this:
if( !pFile -> Open( rFileName, CFile::modeRead, theException ) )
{
delete pFile;
TRACE( " Threw file exception in OpenForReading\n" );
THROW( theException );
}
Note that the THROW macro is invoked here inside a nested scope, which is delineated by the braces of the if block. The if block, executed if the Open call fails, cleans up by deleting the CFile object, pFile. Otherwise, it defers further processing of the error condition for handling by the caller of OpenForReading. However, because the code is in a nested scope, not the scope of OpenForReading, it's necessary to throw the exception out of that scope, where it can be available to main when OpenForReading returns.
You can also use the THROW macro to throw your own exceptions. These can be of predefined types, such as CFileException, or of types you derive from CException or any of its derived classes.
The THROW_LAST macro is used in ReadDataBase. In that function, a TRY/CATCH exception frame does some local handling of archive exceptions, then uses THROW_LAST to pass the exceptions on for further handling by main.
If deserialization of the data fails in midstream, a partially complete person list could be left over, which results in memory leakage. The local exception frame catches the CArchiveException, deletes any partially completed data, and then invokes the THROW_LAST macro to throw the same exception again so it can be caught again later.
You can use the exception-handling mechanism provided by the Microsoft Foundation Class Library to considerable advantage. For more information about exceptions, see Chapter 12.
Now that your files are complete, the next section shows you how to compile the DMTEST program.