串行化:串行化一个对象
“Serialization:Making a Serializable Class”这篇文章展示了如何制作一个可串行化的类。当你拥有一个可串行化的类,你就可以通过CArchive的对象将这个类的对象串行化到(或解串行化从)一个文件。这篇文章阐述了:
·什么是CArchive对象
·创建CArchive的两种方法
·如何使用CArchive的<<和>>运算符
·通过archive保存和读取CObjcet的对象(archive指的是CArchive的对象,可以被理解为一个缓冲区)
你可以让框架为你的课串行化的文档(document)创建archive,或是自己显式(explicitly)地 创建CArchive的对象。你可以使用CArchive的<<和>>运算符完成文件和可串行化类的对象之间的数据交换, 或者,在某些情况下调用从CObject类派生的类的Serialize函数。
什么是CArchive类的对象
一个CAchive类的对象为可串行化的对象在文件(与CFile类的对象相关的文件)中的读写提供了一种支持多种类型(type-safe)的缓冲机制,通常CFile对象代表一个磁盘文件;然而,也可以是一个内存文件(CSharedFile的对象),可能代表剪切板。
一个给定的CArchive对象可以储存(写入,串行化)数据或读取(读取,解串行化)数据,不过不能同时进行。
CArchive对象的生命周期仅限于经历一次将对象写入到文件中或从文件中读取一个对象。因此,两个先后创建的CArchive对象需要串行化数据到文件中然后从文件中解串行化数据。
当一个archive将对象存储到文件中时,archive将CRuntimeClass名附到对象上。之后,当下一个archive将文件中的 对象读取到内存中的时候,基于对象的CRuntimeClass,对象会被动态地重新构造。当一个对象被用来储存的archive 写入到文件中,它就可以被多次读取。然而,用来读取文件的archive只会重新构造对象一次。考虑到你可能会需要更 多的参考资料,关于一个archive如何将CRuntimeClass的信息附到对象上、如何重新构造对象的细节,在 Technical Note 2 中有相关的描述。
当数据被串行化到一个archive中时,archive会堆积数据直到缓冲区装满。然后archive将缓冲区数据写入到CArchive 对象指向的CFile对象中。类似的,当你从archive中读取数据时,archive从文件中将数据读取到缓冲区中,然后将数据 填充到你需要解串行化的对象中。这个缓冲区减少硬盘物理读取的次数,从而提高了应用程序的性能。
两种创建CArchive对象的方法
有两种方法可以创建一个CArchive对象:
·通过框架隐性地创建CArchive对象
·明确地创建CArchive对象
通过框架隐性地创建CArchive对象
最常见的,也是最简单的方法就是让框架为你的文档创建一个CArchive对象,框架会根据文件菜单下的保存、保存为、和打开命令来判断用户的操作。
下面是当用户通过文件菜单发出“保存为”命令时框架所做的事:
1.显示“保存为对话框”,并得到用户输入的文件名。
2.将用户命名的文件作为一个CFile的对象打开。3.创建一个指向CFile对象的CArchive对象。在创建CArchive对象的时候,框架将对象的模式设为“store”(写入,串行化), 而不是“load”(读取,解串行化)。
4.调用你的从CDocument派生的类(就是CXXXDoc类,你的文档类)的Serialize函数,并将上述CArchive对象的引用作 为Serialize函数的参数。
你的文档类的Serialize函数会把数据写入到CArchive的对象中,下面会解释这点。当你的Serialize函数返回的时候,框架 自动销毁CArchive对象,然后销毁CFile对象。
因此,如果你让框架为你的文档创建CArchive对象,你要做的只是实现你的文档类的Serialize函数,它被用来对archive进行读写。你也得实现你的从CObject类派生的类的Serialize函数从而让你的Document类的 Serialize函数直接或间接地调用(其实在制作可串行化的类的时候就完成了)。
明确地创建一个CArchive类的对象
除了通过框架对文档进行串行化,还有其他时候你也许也会需要一个CArchive的对象。比如,你可能想要 串行化数据到(从)剪切板(由CSharedFile的对象表示)。或者你可能想用一个不同于框架的用户界面 来保存文件。在这种情形下,你可以明确地创建CArchive对象。你需要和框架做的一样,使用下面的步骤。
明确地创建CArchive对象
1.构造一个CFile对象或者一个从CFile类派生的类的对象。
2.将CFile对象作为参数传给CArchive的构造函数,就像下面的例子一样:
CFile theFile;
theFile.Open(..., CFile::modeWrite);
CArchive archive(&theFile, CArchive::store);
CArchive构造函数的第二个参数是一个用来指定archive是用来存储还是读取数据的枚举值。一个对象的 Serialize函数通过调用CArchive对象的IsStoring函数来为archive确定读写状态。
当你完成了储存(读取)数据到(从)一个CArchive对象,关闭archive对象。虽然CArchive(还有CFile) 对象会自动关闭archive(CFile对象),明确地这样做是很好的做法,因为这样会使从错误中恢复过来更 加容易。更多关于错误处理的信息,请看文章Exceptions: Catching and Deleting Exceptions.
关闭CArchive对象
·下面的例子表明了如何关闭CArchive对象:
archive.Close();
theFile.Close();
使用CArchive的<<和>>运算符
CArchive提供了<<和>>运算符用于写入(读取)简单的数据类型以及CObject对象到(从)文件中。
通过archive将数据保存到文件中
·下面的例子展示了如何通过archive把数据储存到文件中
CArchive ar(&theFile, CArchive::store);
WORD wEmployeeID;
...
ar << wEmployeeID;
从一个永久保存的文件中读取数据
·下面的例子展示了如何从一个文件中读取数据
CArchive ar(&theFile, CArchive::load);
WORD wEmployeeID;
...
ar >> wEmployeeID;
通常,你通过CObject派生类的Serialize函数中的CArchive的对象储存(读取)数据到(从)一个文件中,这个 从CObject类派生的类中必须得有DECLARE_SERIAIL宏。一个CArchive对象的引用会被传到你的Serialize函数中 作为参数。你调用CArchive对象的IsStoring函数来确定Serialize函数是用来储存还是读取数据到(从)一个 文件。
一个可串行化的从CObject类派生的类一般有下面的格式:
void CPerson:Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar << m_strName;
ar << m_wAge;
}
else
{
ar >> m_strName;
ar >> m_wAge;
}
}
框架为CArchive定义的<<和>>运算符为第一操作数,下面的数据类型和类的类型为第二操作数:
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
注意:通过archive读取或储存CObject类对象需要额外的考虑。更多相关的信息请看下面的“通过archive储存 和读取CObject对象”
CArchvie的<<和>>运算符总是返回CArchive对象的引用,也就是第一操作数。这是你可以连续使用运算符,就像 下面展示的一样:
BYTE bSomeByte;
WORD wSomeWord;
DWORD wSomeDoubleWord;
...
ar << bSomeByte << wSomeWord << wSomeDoubleWord;
通过archive储存和读取CObject对象
通过archive读取或储存CObject类对象需要额外的考虑。在某些情况下,你应该调用该对象的Serialize函数,它 的参数是一个CArchive对象的引用,不同于使用CArchive的<<和>>运算符。需要牢记在心的是CArchive的>>运算符 是基于之前通过archive写入到文件中的CRuntimeClass的信息来构造CObject对象的。
因此,使用CArchive的<<和>>运算符,或是调用Serialize函数,取决于你是否需要让archive对象基于CRuntimeClass 信息动态地重新构造对象。在下面的情况下,要使用Serialize函数:
·在解序列化对象的时候,你事先知道确切的对象的类型。
·在解序列化对象的时候,你事先为它分配了内存。
警告:如果你用Serialize函数读取对象,你必须用Serialize函数储存对象。不要用<<运算符储存却用Serialize函数 读取,或是用Serialize函数储存,却用>>运算符读取。
下面的例子说明了这种情况:
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. // 内部的成员得调用Serialize函数
m_pOther->Serialize( ar ); // Call Serialize on objects of known exact type. // 对于知道确切类型的对象调用Serialize函数
// 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
}
}
总之,如果你的可序列化的类定义了一个内部的从CObject派生的类的对象作为类成员,你不应该使用<<和>>运算符,
而应该调用Serialize函数。同样的,如果你的可序列化的类定义了一个指向CObject类(或是一个从CObject派生的类)
的指针作为类成员,但是用这个成员对象自己的构造函数构造这个成员,你也应该调用Serialize函数。