Home | Overview | How Do I | Tutorial
The article Serialization: Making a Serializable Class shows how to make a class serializable. Once you have a serializable class, you can serialize objects of that class to and from a file via a CArchive object. This article explains:
You can let the framework create the archive for your serializable document or explicitly create the CArchive object yourself. You can transfer data between a file and your serializable object by using the << and >> operators for CArchive or, in some cases, by calling the Serialize
function of a CObject-derived class.
A CArchive object provides a type-safe buffering mechanism for writing or reading serializable objects to or from a CFile object. Usually the CFile object represents a disk file; however, it can also be a memory file (CSharedFile object), perhaps representing the Clipboard.
A given CArchive object either stores (writes, serializes) data or loads (reads, deserializes) data, but never both. The life of a CArchive object is limited to one pass through writing objects to a file or reading objects from a file. Thus, two successively created CArchive objects are required to serialize data to a file and then deserialize it back from the file.
When an archive stores objects to a file, the archive attaches the CRuntimeClass name to the objects. Then, when another archive loads objects from a file to memory, the CObject-derived objects are dynamically reconstructed based on the CRuntimeClass of the objects. A given object may be referenced more than once as it is written to the file by the storing archive. The loading archive, however, will reconstruct the object only once. The details about how an archive attaches CRuntimeClass information to objects and reconstructs objects, taking into account possible multiple references, are described in Technical Note 2.
As data is serialized to an archive, the archive accumulates the data until its buffer is full. Then the archive writes its buffer to the CFile object pointed to by the CArchive object. Similarly, as you read data from an archive, it reads data from the file to its buffer and then from the buffer to your deserialized object. This buffering reduces the number of times a hard disk is physically read, thus improving your application’s performance.
There are two ways to create a CArchive object:
The most common, and easiest, way is to let the framework create a CArchive object for your document on behalf of the Save, Save As, and Open commands on the File menu.
Here is what the framework does when the user of your application issues the Save As command from the File menu:
Serialize
function defined in your CDocument-derived class, passing it a reference to the CArchive object.Your document’s Serialize
function then writes data to the CArchive object, as explained shortly. Upon return from your Serialize
function, the framework destroys the CArchive object and then the CFile object.
Thus, if you let the framework create the CArchive object for your document, all you have to do is implement the document’s Serialize
function that writes and reads to and from the archive. You also have to implement Serialize
for any CObject-derived objects that the document’s Serialize
function in turn serializes directly or indirectly.
Besides serializing a document via the framework, there are other occasions when you may need a CArchive object. For example, you might want to serialize data to and from the Clipboard, represented by a CSharedFile object. Or, you may want to use a user interface for saving a file that is different from the one offered by the framework. In this case, you can explicitly create a CArchive object. You do this the same way the framework does, using the following procedure.
To explicitly create a CArchive object
CFile theFile;
theFile.Open(..., CFile::modeWrite);
CArchive archive(&theFile, CArchive::store);
The second argument to the CArchive constructor is an enumerated value that specifies whether the archive will be used for storing or loading data to or from the file. The Serialize
function of an object checks this state by calling the IsStoring function for the archive object.
When you are finished storing or loading data to or from the CArchive object, close it. Although the CArchive (and CFile) objects will automatically close the archive (and file), it is good practice to explicitly do so since it makes recovery from errors easier. For more information about error handling, see the article Exceptions: Catching and Deleting Exceptions.
archive.Close();
theFile.Close();
CArchive provides << and >> operators for writing and reading simple data types as well as CObjects to and from a file.
To store an object in a file via an archive
CArchive ar(&theFile, CArchive::store);
WORD wEmployeeID;
...
ar << wEmployeeID;
To load an object from a value previously stored in a file
CArchive ar(&theFile, CArchive::load);
WORD wEmployeeID;
...
ar >> wEmployeeID;
Usually, you store and load data to and from a file via an archive in the Serialize
functions of CObject-derived classes, which you must have declared with the DECLARE_SERIALIZE macro. A reference to a CArchive object is passed to your Serialize
function. You call the IsLoading function of the CArchive object to determine whether the Serialize
function has been called to load data from the file or store data to the file.
The Serialize
function of a serializable CObject-derived class typically has the following form:
void CPerson::Serialize(CArchive& ar)
{
CObject::Serialize(ar);
if (ar.IsStoring())
{
// TODO: add storing code here
}
else
{
// TODO: add loading code here
}
}
The above code template is exactly the same as the one AppWizard creates for the Serialize
function of the document (a class derived from CDocument). This code template helps you write code that is easier to review, because the storing code and the loading code should always be parallel, as in the following example:
void CPerson:Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar << m_strName;
ar << m_wAge;
}
else
{
ar >> m_strName;
ar >> m_wAge;
}
}
The library defines << and >> operators for CArchive as the first operand and the following data types and class types as the second operand:
CObject* | SIZE and CSize | float |
WORD | CString | POINT and CPoint |
DWORD | BYTE | RECT and CRect |
double | LONG | CTime and CTimeSpan |
int | COleCurrency | COleVariant |
COleDateTime | COleDateTimeSpan |
Note Storing and loading CObjects via an archive requires extra consideration. For more information see Storing and Loading CObjects via an Archive below.
The CArchive << and >> operators always return a reference to the CArchive object, which is the first operand. This enables you to chain the operators, as illustrated below:
BYTE bSomeByte;
WORD wSomeWord;
DWORD wSomeDoubleWord;
...
ar << bSomeByte << wSomeWord << wSomeDoubleWord;
Storing and loading CObjects via an archive requires extra consideration. In certain cases, you should call the Serialize
function of the object, where the CArchive object is a parameter of the Serialize
call, as opposed to using the << or >> operator of the CArchive. The important fact to keep in mind is that the CArchive >> operator constructs the CObject in memory based on CRuntimeClass information previously written to the file by the storing archive.
Therefore, whether you use the CArchive << and >> operators, versus calling Serialize
, depends on whether you need the loading archive to dynamically reconstruct the object based on previously stored CRuntimeClass information. Use the Serialize
function in the following cases:
Caution If you load the object using the Serialize
function, you must also store the object using the Serialize
function. Don’t store using the CArchive << operator and then load using the Serialize
function, or store using the Serialize
function and then load using CArchive >> operator.
The following example illustrates the cases:
class CMyObject : public CObject
{
// ...Member functions
public:
CMyObject() { }
virtual void Serialize( CArchive& ar ) { }
// Implementation
protected:
DECLARE_SERIAL( CMyObject )
};
class COtherObject : public CObject
{
// ...Member functions
public:
COtherObject() { }
virtual void Serialize( CArchive& ar ) { }
// Implementation
protected:
DECLARE_SERIAL( COtherObject )
};
class CCompoundObject : public CObject
{
// ...Member functions
public:
CCompoundObject();
virtual void Serialize( CArchive& ar );
// Implementation
protected:
CMyObject m_myob; // Embedded object
COtherObject* m_pOther; // Object allocated in constructor
CObject* m_pObDyn; // Dynamically allocated object
//..Other member data and implementation
DECLARE_SERIAL( CCompoundObject )
};
IMPLEMENT_SERIAL(CMyObject,CObject,1)
IMPLEMENT_SERIAL(COtherObject,CObject,1)
IMPLEMENT_SERIAL(CCompoundObject,CObject,1)
CCompoundObject::CCompoundObject()
{
m_pOther = new COtherObject; // Exact type known and object already
//allocated.
m_pObDyn = NULL; // Will be allocated in another member function
// if needed, could be a derived class object.
}
void CCompoundObject::Serialize( CArchive& ar )
{
CObject::Serialize( ar ); // Always call base class Serialize.
m_myob.Serialize( ar ); // Call Serialize on embedded member.
m_pOther->Serialize( ar ); // Call Serialize on objects of known exact type.
// Serialize dynamic members and other raw data
if ( ar.IsStoring() )
{
ar << m_pObDyn;
// Store other members
}
else
{
ar >> m_pObDyn; // Polymorphic reconstruction of persistent
// object
//load other members
}
}
In summary, if your serializable class defines an embedded CObject as a member, you should not use the CArchive << and >> operators for that object, but should call the Serialize
function instead. Also, if your serializable class defines a pointer to a CObject (or an object derived from CObject) as a member, but constructs this other object in its own constructor, you should also call Serialize
.