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.
A CPerson object is designed to manage the name and phone number of one person. A CPerson object is constructed from the CPerson class. CPerson is derived publicly from class CObject.
In effect, a CPerson object is created from a stock of existing components: a string class, a time class, and a general object class (CObject, from which CPerson is derived). The new CPerson class automatically inherits a great deal of functionality from CObject and then adds to its inheritance. CPerson also relies heavily on the built-in capabilities of CString and CTime. These component classes encapsulate specialized kinds of data storage, control access to that data, and cooperate in the ability of CPerson to serialize itself and to provide diagnostic information.
The result of creating CPerson from library components is that you write less code, and the code encapsulated by the component objects comes fully tested. This leaves you more time to focus on high-level design issues and reduces debugging and maintenance time and costs.
The CPerson class declaration given above requires some explanation. The discussion that follows explains how to construct CPerson objects, how CPerson data is stored and accessed, how to test a new object for validity, how to serialize a CPerson object, and how to get a diagnostic dump of a CPerson object during debugging.
A CPerson object is constructed when one of its constructors is invoked. CPerson has two constructors, one with parameters and one without. It also has a copy constructor and an overloaded assignment operator.
You can use a constructor with parameters to construct CPerson objects in your program. To initialize objects created this way, the public constructor for CPerson takes initialization arguments, which you supply at construction time.
Constructor Without Parameters
The parameterless constructor of class CPerson is used internally by the class to support serialization, but you must supply it in your class declaration.
How to Construct CPerson Objects
You can construct a CPerson object in two ways:
You can construct a CPerson object on the frame of a function (as a local variable) as follows:
void f()
{
CPerson thePerson( "Smith", "Mary", "435-8159" );
// Other function code
}
You can construct a CPerson object dynamically on the heap, using the C++ new operator, as follows:
CPerson* pPerson = new CPerson( "Smith", "Mary", "435-8159");
The copy constructor is a special constructor that takes a C++ reference to a CPerson object as its argument. The copy constructor copies the data members of the person object whose copy constructor has been invoked into the object passed as an argument. This allows you to make duplicates of CPerson objects if you need to. For an important discussion, see the shaded box “Copy Constructors” on page 30.
The Overloaded Assignment Operator
Class CPerson overloads the C++ assignment operator (=) to provide correct assignment of one person object to another. For an important discussion, see the shaded box “Copy Constructors” on page 30.
For any class derived from CObject that will be serialized, the Microsoft Foundation Class Library requires that you define a constructor with no arguments. This constructor must at least put the object into a valid state so that it can be safely deleted. Usually this means setting all the member variables to some default null state. If you forget to define a constructor with no arguments for a serializable class, you will get a compiler error at the line that contains the IMPLEMENT_SERIAL macro.
Summary: For serializable classes, you must define a constructor with no arguments.
The constructor with no arguments is used only internally for serialization. The declaration inside class CPerson looks like this:
CPerson();
In addition to the required constructor with no arguments, you may also declare a constructor that takes arguments to initialize the member variables of the object, as in CPerson:
CPerson( const char* pszLastName,
const char* pszFirstName,
const char* pszPhoneNum );
This practice of defining several variations on the constructor is common in C++ programming. You must declare at least one public constructor.
Objects constructed on the frame of a function are allocated when the function is called. At the time of allocation, the constructor is invoked and the object initialized. When the function completes, the destructors of any objects allocated on the frame are invoked automatically to destroy the objects.
Copy Constructors
In general, it is good C++ practice to also supply a copy constructor and an overloaded assignment operator with your data classes. A copy constructor creates a duplicate of the current object. An overloaded assignment operator allows you to assign one object to another with the C++ assignment operator. You'll often want to duplicate an object or assign one to another.
However, in C++ the semantics of copying can be quite subtle. There is one case in which the constructors provided by a class are not invoked to initialize a new object—when an object is initialized with another object of its class. By default in this case, C++ performs a “memberwise” copy of an object's data members. This may not be what you want, since, for example, copying a pointer to a null-terminated C string or array copies an address, not the string's data. This can lead to errors if the original object's destructor frees the memory that the copied object points to. For more information, see the C++ Tutorial in your Microsoft C/C++ package.
CPerson is an example of a class that does provide copy and assignment services. The data members of CPerson are CString objects, which themselves provide a copy constructor and an overloaded assignment operator. Thus, you can safely copy and assign CPerson objects.
You can construct objects dynamically on the heap at any time. Use the new operator to allocate the space. When you call new, the object's constructor is invoked automatically and the object is initialized. The new operator returns a pointer to the object. However, unlike allocation on the frame, allocation on the heap requires that the programmer explicitly deallocate the object with the C++ delete operator.
In the case of classes such as CPerson, it is good practice for created objects to live beyond the scope of the function where they are created, so objects are most often created with the new operator.
How CPerson Data Is Stored and Accessed
CPerson uses two Microsoft Foundation Classes to store its data in member variables. This section explains the use of classes CString and CTime.
CString is used to store the first and last names and the phone number. The declarations of these member variables looks like this:
CString m_pszLastName;
CString m_pszFirstName;
CString m_pszPhoneNumber;
CPerson uses the Microsoft Foundation Class CString to store its data in the m_pszLastName, m_pszFirstName, and m_pszPhoneNumber member variables. The names of these variables follow the Microsoft Foundation Class Library convention of prefixing member variable names with “m_”.
The CString class is used because CString objects are dynamic. A CString object encapsulates a string that can automatically grow up to approximately 32,000 characters. CStrings also have the ability to serialize themselves, so when it is time to serialize a CPerson object, you can simply rely upon each CString in the object to serialize itself without needing to know about the internal structure of the CString. This is a considerable advantage.
CTime is used to store date and time information. The modification time variable in CPerson is declared like this:
CTime m_modTime;
The CPerson class also uses a CTime member variable to represent the date and time of the last modification of each CPerson object. The time and date are set when the object is created, and modified whenever any of the other member variables are changed. For example, the SetLastName member function looks like this:
void CPerson::SetLastName( const char* pszName )
{
m_pszLastName = pszName;
m_modTime = CTime::GetCurrentTime();
}
In the body of this function, the first line sets the last name value. The second line sets the modification time. This operation is transparent to the user of the class. It demonstrates one of the virtues of providing a controlled interface to a class, as discussed in the next section.
Like the name and phone number information, the modification date is serialized with the CPerson object.
Given a CPerson object, how do you examine and update its data? The class provides four pairs of member functions to set and get the values of a CPerson object's member variables. For example, use the SetLastName member function to set a new value for a person object's last name member. Typically, the values are set all at once by the public constructor, which takes arguments and loads them into the member variables. But you can also modify the object's data at any time with the “Set” and “Get” member functions.
It's common in object-oriented programming to define such data-access functions. Notice that the member functions are defined as public to invite use, while the member variables are defined as protected to prevent outside use. Such controlled access to protected member variables helps to ensure the data's integrity.
For example, in the CPerson class, all the member functions that set the member variables also set the modification-date member variable to reflect the time of the change. If it were possible to access member variables directly from outside the object, a person object could be updated without updating its modification date. The data could become invalid without this kind of secure encapsulation.
Class CPerson demonstrates some of the facilities provided by the Microsoft Foundation Classes for testing the validity of objects. The class uses the ASSERT_VALID macro and overrides the AssertValid member function of class CObject.
Along with the ASSERT macro, which is discussed in Chapter 4, these facilites allow you to test your assumptions. Before you use an object, it's wise to test the validity of its internal state. For example, if you have an object that represents a stack data structure, you can confirm that the top and bottom of the stack are in a valid relationship: the top is either “above” the bottom or equal to it (in the case of an empty stack). Similarly, a pointer to an object must point to a valid area of memory.
During debugging, you can use these assumption-testing facilities freely. If an assumption fails the test, the program asserts, prints a diagnostic message, and halts. When you build the program for release, the assumption testing code is not compiled.
CPerson demonstrates the form of these facilities, but you'll need to wait until Chapter 4 for a more meaningful example. In CPerson, the ASSERT_VALID macro typically tests whether the this pointer is NULL within a member function. The override of the AssertValid member function simply calls its base class. In class CDataBase in Chapter 4, you'll see some more serious testing of assumptions.
For more information about assumption testing, see Chapter 11 of the cookbook.
How to Serialize a CPerson Object
The ability to serialize data to and from the disk is probably the most important attribute of the CPerson class. To enable serialization, you can derive your class from the CObject class, use the DECLARE_SERIAL and IMPLEMENT_SERIAL macros, and override the virtual Serialize member function. The version of Serialize that is defined for CObject can work with data in the CObject class only. When you override Serialize for your class, you extend the capability of the function so that it can handle the data in your class as well as the data in CObject.
The Serialize function takes a CArchive object as its argument. CArchive is a Microsoft Foundation Class that provides a context for reading and writing object data to and from a disk file. An archive uses a class's overloaded insertion and extraction operators (<< and >>) to write and read object data to and from the storage media. Notice that even though an archive uses the same overloaded operators as the general-purpose I/O stream objects (such as cin and cout) provided with Microsoft C, a CArchive object is different from an I/O stream:
A CArchive object handles data in binary form, which the computer can process efficiently.
General-purpose I/O streams handle data in textual form, which makes it easy for humans to interpret.
An individual CArchive object can be created for reading or for writing, but not for both at the same time. Thus, each CArchive object maintains internal status information that indicates whether it is for loading (reading) or for storing (writing) data. The Serialize function checks that status in the CArchive object passed to it as an argument to determine whether to read or write the object data.
The code for the Serialize member function in CPerson was shown on page 27. Notice that it uses the TRACE macro to print out a debugging message indicating that the function has been called. The TRACE macro is designed so that it is activated when you build a debug version of your program, but deactivated when you build a release version. Thus, you can sprinkle TRACE messages liberally throughout your code to monitor program execution during development, and they will be deactivated automatically when you build a version of your program to ship. This means that you don't have to go back and comment the messages out or bracket them with #ifdef _DEBUG and #endif statements.
When a CPerson object is serialized, the following actions occur:
1.The CPerson object's Serialize member function is called.
In the example program in this chapter, the CPersonList object that contains a database of CPerson objects calls Serialize for each object in the list.
2.The Serialize member function immediately calls Serialize for its base class, which in this example is CObject. The base class's data is thus written to disk.
By calling the base class version of Serialize first, you ensure that all the contents of the base class portion of your object are correctly serialized. If the
base class is itself a derived class, the Serialize function for the base class of CPerson is also called. Thus Serialize is called for all classes in the hierarchy above your class. Figure 2.3 shows this sequence for CPerson.
3.The CPerson object's Serialize member next writes its own data to disk.
To prepare, the Serialize function calls the IsStoring member function for the CArchive object. If the archive is for storing data, then each member variable of the CPerson object is written with the << insertion operator. If, on the other hand, the archive is for reading, the >> extraction operator is used to read each member variable. The insertion and extraction operators perform all the operations necessary to make sure that the member variables are correctly written or read.
Notice that the member variables are extracted in the same order that they were inserted. This ensures that each member variable is matched with the correct data.
Any serializable object can be written to disk with a single line of code. As you'll see later, a collection or list of serializable objects can also be serialized simply and with minimal code. Figure 2.3 shows the steps taken as a CPerson object is serialized.
How to Dump a CPerson Object's Data
The previous section described how to override the Serialize member function to read and write object contents to and from a CArchive object. The Dump member function performs a similar function, but instead of writing out binary data to a CArchive object, Dump writes a textual representation of the object data to a CDumpContext object. A CDumpContext object is typically used for debugging output during program development. It is similar to the general I/O streams in that it is often directed to the screen or a log file, but a CDumpContext object can be used only for output, not for input. A CDumpContext object's output cannot be formatted.
The Dump function writes the contents of an object, including descriptive labels, to a diagnostic context. If you compare it to the Serialize function, you will see three main differences:
Serialize operates on a CArchive object and Dump uses a CDumpContext object.
Dump writes out descriptive text labels along with the textual representation of the value of each member variable, while Serialize reads and writes only the binary value of the member variables.
Dump is a write-only operation.
The code for the Dump member function of CPerson is shown on page 26. Notice that it is bracketed with an #ifdef _DEBUG/#endif block so that it will not be included in a release version of your program.
Notice also that, like Serialize, the first statement of Dump calls the base class's version of the function. This ensures that the contents of the base class portion of the object get dumped first. (That is, if the base class has any member variables, they're written out before member variables of the derived class.) Then the rest of the Dump function uses the insertion operator to send descriptive labels and the contents of each member variable of the CPerson class. Once again, the insertion operator does all the hard work.
The Microsoft Foundation Class Library provides a predefined CDumpContext object named afxDump. You can use this object as the argument to the Dump function. The afxDump dump context object writes the dump information to standard output. For a Windows program, afxDump uses the Windows function OutputDebugString to route the dump information to the debugger if present, or to the auxiliary (AUX) device if not. This occurs only if tracing is enabled. For more information, see Technical Note 7 in file TN007.TXT in your distribution disks. The afxDump object is available only in debug mode, but class CDumpContext can be used for programs in release mode.
For example, if you had a CPerson object, you could dump it to the predefined dump context with the following code. The DMTEST code also calls the SetDepth member function of class CDumpContext to specify that all data of all objects is to be dumped. This call is discussed again in step 1 of “Test the Data Model” on page 49 and “How FindPerson Is Tested” on page 58.
CPerson myPerson( "Smith", "Mary", "223-9175" );
myPerson.AssertValid(); // See if object contains valid data
myPerson.Dump( afxDump );
The output looks like this:
Last Name: Smith
First Name: Mary
Phone #: 223-9175
Modification date: Fri Jul 19 13:36:30 1999