Visual C++中对象的序列化与文件I/O研究

持久性和序列化

  持久性是对象所有的保存和加载其状态数据的能力。具有这种能力的对象能够在应用程序结束之前以某种方式将当前的对象状态数据记录下来,当程序再次运行时,通过对这些数据的读取而恢复到上一次任务结束时的状态。由于绝大多数的MFC类是直接或间接由MFC的CObject类派生出来的,因此这些MFC类都具有保存和加载对象状态的能力,是具有持久性的。在使用应用程序向导生成文档/视结构的程序框架时,就已经为应用程序提供了用于对象状态数据保存和加载的基本代码。

  为实现对象的持久性,通常多以字节流的形式将记录对象状态的数据存放到磁盘上,这种将状态数据保存到磁盘和从磁盘恢复到内存的过程称为序列化。序列化是MFC的一个重要概念,是MFC文档/视图结构应用程序能进行文档打开、保存等操作的基础。当在MFC框架程序中装载或保存一个文件时,除了打开文件以供程序读写外,还传递给应用程序一个相关的CArchive对象,并以此实现对持久性数据的序列化。

  大多数MFC应用程序在实现对象的持久性时并非直接用MFC的CFile类对磁盘文件进行读写(有关CFile类的详细介绍将在下一节进行),而是通过使用CArchive对象并由其对CFile成员函数进行调用来执行文件I/O操作。CArchive类支持复合对象的连续二进制形式的输入输出。在构造一个CArchive对象或将其连接到一个表示打开的文件的CFile对象后,可以指定一个档案是被装载还是被保存。MFC允许使用操作符“<<”和“>>”来对多种原始数据类型进行序列化。这些原始数据类型包括BYTE,WORD,LONG,DWORD,float,double,int,unsigned int,short和char等。

  对于其他由MFC类来表示的非原始数据类型如CString对象等的序列化则可以通过对“<<”和“>>”运算符的重载来解决,可以用此方式进行序列化的MFC类和结构有CString,CTime,CTimeSpan,COleVariant,COleCurreny,COleDateTime,COleDateTimeSpan,CSize,CPoint,CRect,SIZE,POINT和RECT等。除了操作符“<<”和“>>”之外还可以调用CArchive类成员函数Read()和Write()来完成序列化。下面这段代码展示了通过操作符对int型变量VarA、VarB的序列化过程:

// 将VarA、VarB存储到档案中
CArchive ar (&file, CArchive::store);
ar << VarA << VarB;
……
// 从档案装载VarA、VarB
CArchive ar (&file, CArchive::load)
ar >> VarA >> VarB;


  CArchive类仅包含有一个数据成员m__pDocument。在执行菜单上的打开或保存命令时,程序框架将会把该数据成员设置为要被序列化的文档。另外需要特别注意的是:在使用CArchive类时,要保证对CArchive对象的操作与文件访问权限的统一。

  在本文下面将要给出的示例程序中,将对绘制连线所需要的关键点坐标和坐标个数等持久性对象进行序列化。其中文档类的成员变量m_nCount和m_ptPosition[100]分别记录了当前点的个数和坐标,初始值为0。当鼠标点击客户区时将对点的个数进行累加,并保存当前点的坐标位置。随后通过Invalidate()函数发出WM_PAINT消息通知窗口对客户区进行重绘,在重绘代码中对这些点击过的点进行绘图连线:

void CSample04View::OnLButtonDown(UINT nFlags, CPoint point)
{
 // 获取指向文档类的指针
 CSample04Doc* pDoc = GetDocument();
 // 保存当前鼠标点击位置
 pDoc->m_ptPosition[pDoc->m_nCount] = point;
 if (pDoc->m_nCount < 100)
  pDoc->m_nCount++;
 // 刷新屏幕
 Invalidate();
 CView::OnLButtonDown(nFlags, point);
}
……
void CSample04View::OnDraw(CDC* pDC)
{
 CSample04Doc* pDoc = GetDocument();
 ASSERT_VALID(pDoc);
 // 对点击的点进行连线绘图
 pDC->MoveTo(pDoc->m_ptPosition[0]);
 for (int i = 1; i < pDoc->m_nCount; i++)
  pDC->LineTo(pDoc->m_ptPosition[i]);
}



  从上述程序代码不难看出,为了能保存绘制结果需要对文档类的成员变量m_nCount和m_ptPosition[100]进行序列化处理。而文档类成员函数Serialize()则通过Archive类为这些持久性对象的序列化提供了功能上的支持。下面的代码完成了对持久性对象的保存和加载:

if (ar.IsStoring())
{
 // 存储持久性对象到档案
 ar << m_nCount;
 for (int i = 0; i < m_nCount; i++)
  ar << m_ptPosition[i];
}
else
{
 // 从档案装载持久性对象
 ar >> m_nCount;
 for (int i = 0; i < m_nCount; i++)
  ar >> m_ptPosition[i];
}


自定义持久类

  为了使一个类的对象成为持久的,可以自定义一个持久类,将持久性数据的存储和加载的工作交由自定义类自己去完成。这种处理方式也更加符合面向对象的程序设计要求。可以通过下面几个基本步骤来创建一个能序列化其成员变量的自定义持久类:

  1. 直接或间接从CObject类派生出一个新类。

  2. 在类的声明部分包含MFC的DECLARE_SERIAL宏,该宏只需要将类名作为参数。

  3. 重载基类的Serialize()函数,并添加对数据成员进行序列化的代码。

  4. 如果构造函数没有一个空的缺省的构造函数(不含任何参数),为其添加一个。

  5. 在类的实现部分,添加MFC的IMPLEMENT_SERIAL宏。该宏需要三个参数:类名,基类名和一个方案号。其中方案号是一个相当于版本号的整数,每当改变了类的序列化数据格式后就应当及时更改此数值。
根据上述步骤不难对上一小节中的序列化代码进行封装,封装后的持久类CPosition负责对类成员变量m_nCount和m_ptPosition[100]的序列化,封装后的代码如下:

// CPosition类声明部分:
class CPosition : public CObject
{
 DECLARE_SERIAL(CPosition)
 CPosition();
 int m_nCount;
 CPoint m_ptPosition[100];
 void Serialize(CArchive& ar);
 CPoint GetValue(int index);
 void SetValue(int index, CPoint point);
 virtual ~CPosition();
};
……
// CPosition类实现部分:
IMPLEMENT_SERIAL(CPosition, CObject, 0)
CPosition::CPosition()
{
 // 对类成员进行初始化
 m_nCount = 0;
 for (int i = 0; i < 100; i++)
  m_ptPosition[i] = CPoint (0, 0);
}
CPosition::~CPosition()
{
}
void CPosition::SetValue(int index, CPoint point)
{
 // 设置指定点的坐标值
 m_ptPosition[index] = point;
}
CPoint CPosition::GetValue(int index)
{
 // 获取指定点的坐标值
 return m_ptPosition[index];
}
void CPosition::Serialize(CArchive &ar)
{
 CObject::Serialize(ar);
 if (ar.IsStoring())
 {
  // 存储持久性对象到档案
  ar << m_nCount;
  for (int i = 0; i < m_nCount; i++)
  ar << m_ptPosition[i];
 }
 else
 {
  // 从档案装载持久性对象
  ar >> m_nCount;
  for (int i = 0; i < m_nCount; i++)
   ar >> m_ptPosition[i];
 }
}


  在创建了自定义持久类CPosition后,可以通过该类对鼠标点击过的点的坐标进行管理,由于序列化的工作已由类本身完成,因此只需在文档类的Serialize()函数中对CPosition的Serialize()成员函数进行调用即可:

void CSample04Doc::Serialize(CArchive& ar)
{
 // 使用定制持久类
 m_Position.Serialize(ar);
 if (ar.IsStoring())
 {
 }
 else
 {
 }
}


文件I/O

  虽然使用CArchive类内建的序列化功能是保存和加载持久性数据的便捷方式,但有时在程序中需要对文件处理过程拥有更多的控制权,对于这种文件输入输出(I/O)服务的需求,Windows提供了一系列相关的API函数,并由MFC将其封装为CFile类,提供了对文件进行打开,关闭,读,写,删除,重命名以及获取文件信息等文件操作的基本功能,足以处理任意类型的文件操作。CFile类是MFC文件类的基类,支持无缓冲的二进制输入输出,也可以通过与CArchive类的配合使用而支持对MFC对象的带缓冲的序列化。

  CFile类包含有一个公有型数据成员m_hFile,该数据成员包含了同CFile类对象相关联的文件句柄。如果没有指定句柄,则该值为CFile::hFileNull。由于该数据成员所包含的意义取决于派生的类,因此一般并不建议使用m_hFile。

  通过CFile类来打开文件可以采取两种方式:一种方式是先构造一个CFile类对象然后再调用成员函数Open()打开文件,另一种方式则直接使用CFile类的构造函数去打开一个文件。下面的语句分别演示了用这两种方法打开磁盘文件“C:\TestFile.txt”的过程:

// 先构造一个实例,然后再打开文件
CFile file;
file.Open(“C:\\TestFile.txt”, CFile::modeReadWrite);
……
// 直接通过构造函数打开文件
CFile file(“C:\\TestFile.txt”, CFile::modeReadWrite);


  其中参数CFile::modeReadWrite是打开文件的模式标志,CFile类中与之类似的标志还有十几个,现集中列表如下:

文件模式标志
说明
CFile::modeCreate 创建方式打开文件,如文件已存在则将其长度设置为0
CFile::modeNoInherit 不允许继承
CFile::modeNoTruncate 创建文件时如文件已存在不对其进行截断
CFile::modeRead 只读方式打开文件
CFile::modeReadWrite 读写方式打开文件
CFile::modeWrite 写入方式打开文件
CFile::shareCompat 在使用过程中允许其他进程同时打开文件
CFile::shareDenyNone 在使用过程中允许其他进程对文件进行读写
CFile::shareDenyRead 在使用过程中不允许其他进程对文件进行读取
CFile::shareDenyWrite 在使用过程中不允许其他进程对文件进行写入
CFile::shareExclusive 取消对其他进程的所有访问
CFile::typeBinary 设置文件为二进制模式
CFile::typeText 设置文件为文本模式


  这些标志可以通过“或”运算符而同时使用多个,并以此来满足多种需求。例如,需要以读写方式打开文件,如果文件不存在就创建一个新的,如果文件已经存在则不将其文件长度截断为0。为满足此条件,可用CFile::modeCreate、CFile::modeReadWrite和CFile::modeNoTruncate等几种文件模式标志来打开文件:

CFile file ("C:\\TestFile.txt", CFile::modeCreate | CFile::modeReadWrite | CFile::modeNoTruncate);

  在打开的文件不再使用时需要将其关闭,即可以用成员函数Close()关闭也可以通过CFile类的析构函数来完成。当采取后一种方式时,如果文件还没有被关闭,析构函数将负责隐式调用Close()函数去关闭文件,这也表明创建在堆上的CFile类对象在超出范围后将自动被关闭。由于调用了对象的析构函数,因此在文件被关闭的同时CFile对象也被销毁,而采取Close()方式关闭文件后,CFile对象仍然存在。所以,在显式调用Close()函数关闭一个文件后可以继续用同一个CFile对象去打开其他的文件。

  文件读写是最常用的文件操作方式,主要由CFile类成员函数Read()、Write()来实现。其函数原型分别为:

UINT Read( void* lpBuf, UINT nCount );
void Write( const void* lpBuf, UINT nCount );


  参数lpBuf为指向存放数据的缓存的指针,nCount为要读入或写入的字节数,Read()返回的为实际读取的字节数,该数值小于或等于nCount,如果小于nCount则说明已经读到文件末尾,可以结束文件读取,如继续读取,将返回0。因此通常可以将实际读取字节数是否小于指定读取的字节数或是否为0作为判断文件读取是否到达结尾的依据。下面这段代码演示了对文件进行一次性写入和循环多次读取的处理过程:

// 创建、写入方式打开文件
CFile file;
file.Open("C:\\TestFile.txt", CFile::modeWrite | CFile::modeCreate);
// 写入文件
memset(WriteBuf, 'a', sizeof(WriteBuf));
file.Write(WriteBuf, sizeof(WriteBuf));
// 关闭文件
file.Close();
// 只读方式打开文件
file.Open("C:\\TestFile.txt", CFile::modeRead);
while (true)
{
// 读取文件数据
int ret = file.Read(ReadBuf, 100);
……
// 如果到达文件结尾则中止循环
if (ret < 100)
break;
}
// 关闭文件
file.Close();


  Write()和Read()函数执行完后将自动移动文件指针,因此不必再显示调用Seek()函数去定位文件指针。包含有文件定位函数的完整代码如下所示:

// 创建、写入方式打开文件
CFile file;
file.Open("C:\\TestFile.txt", CFile::modeWrite | CFile::modeCreate);
// 写入文件
memset(WriteBuf, 'a', sizeof(WriteBuf));
file.SeekToBegin();
file.Write(WriteBuf, sizeof(WriteBuf));
// 关闭文件
file.Close();
// 只读方式打开文件
file.Open("C:\\TestFile.txt", CFile::modeRead);
while (true)
{
// 文件指针
static int position = 0;
// 移动文件指针
file.Seek(position, CFile::begin);
// 读取文件数据
int ret = file.Read(ReadBuf, 100);
position += ret;
……
// 如果到达文件结尾则中止循环
if (ret < 100)
break;
}
// 关闭文件
file.Close();


  小结

  持久性和文件I/O是程序保持和记录数据的一种重要方法,这两种不同的处理方法虽然功能上有些接近但实现过程却大不相同。而且这两种处理方法各有优势,读者在编程过程中应根据实际情况而灵活选用。

你可能感兴趣的:(C++)