MFC【6】文件I/O和串行化

时间:2021-10-25 15:45:32

文件输入和输出(I/O)服务是所有操作系统的主要工作。Microsoft Windows提供了各种API函数用来读、写和操作磁盘文件。MFC将这些桉树和CFile类融合在面对对象的模型里。其中CFile类允许把文件当做对象,并用CFile成员函数,如Read和Write,对它们进行操作。CFile具有MFC编程人员实现第几文件I/O所需要的所有工具。

尽管用CFile对象实现磁盘文档的读写并没有错,但大部分MFC应用程序不会这么做,而是用CArchive对象。通过MFC实现巧妙的运算符重载,大部分数据都可以串行化为CArchive,即作为一个字节流输出,或从一个CArchive并行化为初始状态。另一方面,如果CArchive对象挂在在CFile对象上,被串行化为该CArchive的数据就一透明方式写到磁盘上。此后,您还可以吧文件有关的CArchive并行化,最终重建一这种方式存档的数据。

6.1 CFile类

CFile是比较简单的类,它封装了Win32 API用来处理文件I/O的那部分。m_hFile,保存着与CFile对象向关联的文件句柄。一个受保护的CString数据成员,名为m_strFileName,保存这文件的名称。成员函数GetFilePath,GetFileName和GetFileTitle可以用来提取整个文件名或文件名的一部分。

6.1.1 打开、关闭和创建文件

用CFile打开文件有两种方法。

第一种方法是构造一个没有初始化的CFile对象并调用CFile::Open。

    CFile file;
file.Open(_T("File.txt"),CFile::modeReadWrite);

CFile::Open返回一个BOOL值,表示是否成稿打开文件,下面的示例是利用该返回值确定定义文件是否被打开:

    CFile file;
if(file.Open(_T("File.txt"),CFile::modeReadWrite)){
//It worked!
}

如果CFile::Open返回零,并且向知道调用失败的原因,则创建一个CFileException对象并把它的地址传送到Open的第三个参数中。

    CFile file;
CFileException e;
if(file.Open(_T("File.txt"),CFile::modeReadWrite),&e){
//It worked!
}
else{
//Open failed. Tell the user why.
e.ReportError();
}

如果Open失败,则它用描述失败的本质信息将CFileException对象初始化。ReportError在该信息的基础上显示一条错误信息。通过检查CFileException的公用数据成员m_cause,您可以知道导致失败的原因。CFileException的文档资料包含一个完整的错误代码列表。

第二种方法是用CFile的构造函数打开文件。

CFile file(_T("File.txt"),CFile::modeReadWrite);

如果文件不能打开,CFile的构造函数会引发一个CFileException.因此,利用CFile::CFile打开文件的代码通常使用try和catch来俘获错误:

    try{
CFile file(_T("File.txt"),CFile::modeReadWrite);
.
.
.
}
catch(CFileException *e){
//Something went wrong.
e->ReportError();
e->Delete();
}

是否删除MFC发送给您的CFileException对象,决定权在你。这就是在处理异常后该示例调用Delete删除异常对象的原因。不想调用Delete的唯一场合是您要用throw重新发送异常。

如果创建一个新文件,而不是打开一个现存文件,则要在CFile::Open或CFile构造函数的第二个参数中包含一个CFile::modeCreate标志:

    CFile file(_T("File.txt"),CFile::modeReadWrite|CFile::modeCreate);

如果用这种方法创建的文件已存在,则截取它的长度到0,。如果要创建一个不存在的文件,或要在文件存在但没有截取是打开文件,则也要包含一个CFile::modeNoTruncate标志:

    CFile file(_T("File.txt"),CFile::modeReadWrite|CFile::modeCreate|CFile::modeNoTruncate);

在默认方式下,用CFile::Open或CFile::CFile打开文件会获得该文件的独占访问权,也就是说,其他人不能在打开该文件。如果有必要,在打开文件是可以指定共享模式,明确地允许其他人访问该文件。表6-1是可选的4种共享模式。

表6-1 4种共享模式

共享模式 说明
CFile::shareDenyNone 非独占访问权式打开文件
CFile::shareDenyRead 禁止读访问权
CFile::shareDenyWrite 禁止写访问权
CFile::shareExclusive 禁止度写访问权(默认值)

另外,还可以指定表6-2中的三种读/写访问权之一。

表6-2 3种读/写访问权

访问权模式 说明
CFile::modeReadWrite 请求读写访问权限
CFile::modeRead 只请求读访问权限
CFile::modeWrite 值请求写访问权限

这些选项的常见用法是允许任一客户读取文件,但禁止往文件上写:

CFile file(_T("File.txt"),CFile::modeRead|CFile::shareDenyWrite);

如果执行上面的语句是该文件已经打开,则这次调用失败,并且CFile会发送一个CFileException,其m_cause等于CFileException::sharingViolation。

关闭打开的文件有用两种方式。如果要显式关闭文件,则对响应的CFile对象调用CFile::Close: file.Close();

可以用CFile的析构函数关闭文件。如果文件还没有关闭,类的析构函数则调用Close。这就是说,在对上创建的CFile对象在失效后会自动关闭。在下面的实例中,当程序执行到try块接洽诶的花括号,文件关闭

try{
CFile file(_T("File.txt",CFile::modeReadWrite);
.
.
.
//CFile::~Cfile close the file.
}

又是编程人员先是调用Close的原因是:关闭当前处于打开状态的文件,一遍用同一个CFile对象打开另一个文件。

6.1.2读和写

可以用CFile::Read读一个具有访问权限的打开文件。可以用CFile::Write写一个具有写访问权限的打开文件。下面的市里分配了一个4KB的文件I/O缓冲区并一次读取文件4KB内容。为了使程序清晰,省略错误检查。

    BYTE buffer[0x1000];
CFile file(_T("File.txt"),CFile::mode
DWORD dwBytesRemaining = file.GetLength();
while(dwBytesRemaining){
UINT nBytesRead = file.Read(buffer,sizeof(buffer));
dwBytesRemaining -= nBytesRead;
}

未读字节数保存在dwRytesRemaining中,它的初始值为CFile::GetLength返回的文件尺寸。每次调用Read后,dwBytesRemaining都要减去从文件读取的字节数(nBytesRead)。执行while循环知道dwBytesRemaining变成0。

  下面的示例是在前端代码的基础上发展的,它调用::CharLowerBuff将从文件中读取的所有大写字符转化成小写,并调用CFile::Write把转化后的征文写回文件。为了使程序清晰,再次省略错误检查。

    BYTE buffer[0x1000];
CFile file(_T("File.txt"),CFile::mode
DWORD dwBytesRemaining = file.GetLength();
while(dwBytesRemaining){
DWORD dwPosition = file.GetPosition();
UINT nBytesRead = file.Read(buffer,sizeof(buffer));
::CharLowerBuff((LPTSTR)buffer,nBytesRead);
file.Seek(dwPosition,CFile::begin);
file.Write(buffer,nBytesRead);
dwBytesRemaining -= nBytesRead;
}

该示例调用CFile函数GetPosition和Seek操作文件指针,即执行下一个读或写的位置,使修改后的数据覆盖源文件。Seek用第二个参数确定第一个参数中的字节偏移值是相对于文件起始位置(CFile::begin),结束位置(CFile::end)还是当前位置(CFile::current)。如果要快速定位到文件的开始位置或结束位置,可以调用CFile::SeekToBegin或CFile::SeekToEnd。

如果在文件I/O过程途中有错误发生,Read,Write和其它CFile函数就会发送一个CFileException.CFileException::m_cause告诉您引发错误的原因。例如:试图往已满的磁盘上写文件会引发CFileException,其中m_cause等于CFileException::diskFull。试图在文件范围之外读取数据会引发CFileException,其m_cause等于CFileException::diskFull。试图在文件范围之外读取数据会引发CFileException,其m_cause等于CFileException::endOfEile。下面就是讲小写正文转化为大写的历程,其中还包括检查代码:

    BYTE buffer[0x1000];
try{
CFile file(_T("File.txt"),CFile::mode
DWORD dwBytesRemaining = file.GetLength();
while(dwBytesRemaining){
DWORD dwPosition = file.GetPosition();
UINT nBytesRead = file.Read(buffer,sizeof(buffer));
::CharLowerBuff((LPTSTR)buffer,nBytesRead);
file.Seek(dwPosition,CFile::begin);
file.Write(buffer,nBytesRead);
dwBytesRemaining -= nBytesRead;
}
}
catch(CFileException* e){
e->ReportError();
e->Delete();
}

如果不补货CFile成员函数发送的一场,MFC会替你捕获它们。MFC给为处理的一场配有默认处理程序,该程序调用ReportError显示一条描述错误的信息。然而,一般情况下,最好捕获文件I/O异常,防止代码的关键部分遗漏。

6.1.3 CFile派生类

CFile是整个MFC类家族的根类。其家族成员及彼此间的关系如下:

CFile

  -CMemFile

    -CSharedFile

  -COleStreamFile

    -CMonikerFile

      -CAsyncMonikerFile

        -CDataPathProterty

          -CCachedDataPathProperty

  -CSocketFile

  -CStdioFile

    -CInternetFile

      -CGopherFile

      -CHttpFile

CFile家族某些成员的作用尽在于给某些文件煤体提供文件式界面。例如:CMemFile和CSharedFile允许内存块可以向文件那样读和写。在第19张介绍到的MFC函数,COleDataObject::GetFileData,根据这个便利的抽象,可以允许OLE放下剪贴板的目标和用户,从而调用CFile::Read检索内存中的数据信息。CSocketFile对TCP/IP套接字进行了类似的抽象。MFC编程人员有事吧CSocketFile对象放在CSocket对象和CArchive对象之间,这样就可以用C++的插入和提取运算符对打开的套接字进行读写了。COleStreamFile使留对象,即表示字节流的COM对象,看上去像一个普通文件。对于支持对象链表和嵌入OLE的MFC应用程序,这种方法很重要。

CStdioFile将变成接口简化为文本文件。它在由CFile继承来的类中值添加了两个成员函数:一个用来读取正文行的ReadString函数,一个用来输出正文行的WriteString函数。对CStdioFile来说,一行正文就是由回车符和换行符定界的字符串。ReadString读取当前文件位置到下一个回车符间的所有数据,可以包含或不包含回车符。WriteString输出正文字符串,并还在文件中写一个回车符和换行符。下面的代码段打开一个文本文件File.txt,并将它的内容专放在调试输出窗口:

try{
CString string;
CStdioFile file(_T("File.txt"),CFile::modeRead);
while(file.ReadString(string))
   TRACE(_T("%s\n"),string);
}
catch(CFileException *e){
e->ReportError();
e->Delete();
}
}

同Read和Write一样,如果有错误发生,是的ReadString和WriteString无法执行任务,则这两个函数引发异常。

6.1.4 枚举文件和文件夹

CFile包含一对静态的成员函数,Rename和Remove.可以用着两个函数重命名和删除文件。但是,他不包含用来枚举文件和文件夹的函数。因此,您只好求助于Windows API。

枚举文件和文件夹的关键在于一对API函数,::FileFirstFile和::FindNextFile。如果给定一个绝对或相对文件名(例如:“C:\\*.*"或”*.*“),::FindFirstFile打开一个”查找句柄“,并把它返回给调用者。::FindNextFile利用该句柄枚举文件系统对象。常见的方法是:枚举一开始,线调用::FindFirstFile,然后反复调用::FindNextFile直到枚举结束。每次成功地调用::FindFirstFile或::FindFirstFile或::FindNextFile(也就是说,调用::FindFirstFile时,返回值是INVALID_HANDLE_VALUE外的任意值;或者调用::FindNextFile时,返回值是个非NULL值)都会在WIN32_FIND_DATA结构中填充文件或目录信息。WIN32_FIND_DATA是这样用ANSI代码定义的:

typedef struct_WIN32_FIND_DATAA{
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
CHAR cFileName[MAX_PATH];
CHAR cAlternateFileName[];
}WIN32_FIND_DATAA;
typedef WIN32_FIND_DATAA WIN32_FIND_DATA;

如果要确定由WIN32_FIND_DATA结构表示的这一项是文件还是目录,检测dwFileAttributes字段FILE_ATTRIBUTE_DIRECTORY标识:

if(fd.dwFileAttributes &FILE_ATTRIBUTE_DIRECTORY){

//It's a directory.

}

else{

//It's a file.

}

cFileName和cAlternateFileName字段保留着文件或目录名。cFileName包含常的文件名,cAlternateFileName包含段的文件名。当枚举完成时,您应该关闭有::FindFirstFile和::FindClose返回的任一句柄。

作为示范,下面的历程美居乐当前目录下的所有文件,并把它们的文件名写到了调试输出窗口:

WIN32_FIND_DATA fd;
HANDLE hFind =::FindFirstFile(_T("*.*"),&fd); if(hFind!=INVALID_HANDLE_VALUE){
  do{
    if(!(fd.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY))
      TRACE(_T("%s\n"),fd.cFileName);
  }while(::FindNextFile(hFind,&fd));
  ::FindClose(hFind);
}

如果要枚举当前目录下的所有子目录,则需要稍微改动一下:

WIN32_FIND_DATA fd;
HANDLE HfIND=::FindFirstFile(_T("*.*"),&FD);
if(hFind!=INVALID_HANDLE_VALUE{
  do{
    fd.dwFileAttribute & FILE_ATTRIBUTE_DIRECTORY)
      TRACE(_T("%s\n"),fd,cFileName);
  }while(::FindNextFile(hFind,&fd));
  ::FindClose(hFind);
}

如何把给定目录”和它的子目录“下的所有目录枚举出来。下面的函数枚举除当前目录和子目录中的所有目录,并把每个目录的名称写到调试输出窗口。一单遇到目录,EnumerateFolders便会进入该目录并会递归调用自身。

void EnumerateFolders()
{
WIN32_FIND_DATA fd;
HANDLE hFind = ::FindFirstFile(_T("*.*"),&fd);
if(hFind != INVALID_HANDLE_VALUE){
  do{
    if(fd.dwFileAttributes &FILE_ATTRIBUTE_DIRECTORY){
    CString name = fd.cFileName;
    if(name != _T(".")&&name!=_T("..")){
         TRACE(_T("%s \n"),fd.cFileName);
          ::SetCurrentDircetory(fd.cFileName);
          EnumerateFolders();
          ::SetCurrentDircetory(_T(".."));
      }
    }
  }while(::FindNextFile(hFind,&fd));
  ::FindClose(hFind);
}
}

如果要使用该函数,线导航到枚举其实是所在的目录,然后调用EnumerateFolders.下面的语句美居乐驱动器C中的所有目录:

::SetCurrentDirctory(_T("C:\\"));
EnumerateFolders();

6.2串行化和CArchive类

尽管MFC的CFile类简化了文件数据的读写,但大部分MFC应用程序并不直接和CFile对象发生相互作用。它们借助于CArchive对象完成读写工作,而CArchive对象又转而利用CFile函数实现文件I/O。MFC重载<<和>>运算符。这两个运算符和CArchive一起简化了串行化和并行化过程。串行化和并行化的根本目的在于把应用程序持久性数据保存到磁盘上或再从磁盘都会需要的数据。

串行化是MFC编程中的一个重要概念,因为在文档/视图应用程序中打开并保存文件式MFC的基本功能。在使用文档/视图应用程序时,如果在应用程序的File菜单中选中Open和Save,MFC就会打开文件进行读或写,并传递给应用程序一个指向CArchive对象的引用。接着,应用程序又将持久性数据串行化为档案,或把档案并行化为数据,这样就把一个完整的文档保存在磁盘上或重新把文档读取出来了。

6.2.1串行化基础

假定一个CFile对象,名为file,代表一个打开的文件,该文件具有写访问权,并且您想在文件上写一对整数,名为a和b。为了实现这个要求,一种方法是对没一个整数都调用CFile::Write:

file.Write(&a,sizeof(a));
file.Write(&b,sizeof(b));

另一种方法是创建一个CArchive对象,并把它与该CFile对象关联起来,然后运用<<运算符把整数串行化到档案中:

CArchive ar(&file,CArchive::store);
ar<<a<<b;

CArchive对象也可以用来读取数据。假定file再次代表一个打开的文件,并且该文件具有读访问权,下面的小代码段将CArchive对象挂接到文件上,并从文件中读取整数,或将整数”并行化“:

CArchive ar(&file,CArchive::load);
ar>>a>>b;

MFC允许多种基本数据类型以这种方式串行化,包括BYTE,WORD,LONG,DWORD,float,double,int,unsigned int , short和char。

MFC还重载<<和>>运算符,以便串行化或并行化CString和其它某些MFC类表示的非基本数据类型。如果string是一个CString对象,ar是一个CArchive对象,可以按如下方式吧字符串写入档案:

ar<<string;

上面的运算符掉个方向就可以从档案中读取字符串了。

ar>>string;

可以用这种方式串行化的类型包括:CString,CTime,CTimeSpan,COleVariant,COleCurrency,COleDateTime,COleDateTimeSpan,CSize,CPoint和CRect.类型为SIZE,POINT和RECT的结构也可以串行化。

您能够创建自己的课串行化类,使它们与CArchive的插入和提取运算符一起工作。并且为了是这些类工作,您也不必自己重载任何运算符。

示范:假定您以编写了一个绘图程序,它给出用户用CLine类实例化的线。在假定CLine是直接或间接由CObject派生来的课串行化类。如果pLines是CLine指针数组,nCount是一个整型数,保存数组中的指针的个数,而ar是一个CArchive对象,您可以按下面的方式将每个CLine存档,并同时对CLines进行记数:

ar<<nCount;
for(int i=;i<nCount;i++)
  arr<<pLines[i];

相反地,也可以根据档案中的信息创建CLines,并用下面的语句将pLine初始化为CLine指针;

ar<<nCount;
for(int i=;i<nCount;i++)
  arr<<pLines[i];

如果数据串行化或并行化时有错误发生,MFC会发送一个异常。异常的类型取决于错误的性质。如果由于内存不足,串行化请求失败(例如:入股哦内存太少,不足以创建一个正在并行化的对象的实例),MFC会发送一个CMemoryException。如果由于文件I/O出错,请求失败,则MFC发送一个CFileException.如果发生了其它错误,MFC会发送一个CArchiveException。

6.2.2编写可串行化类

如果一个对象支持串行化,那么他一定是课串行化类的实例。您可以按照以下五个步骤编写课串行化类:

1.直接或间接得到CObject的派生类。

2.在类的说明中写入MFC的DECLARE_SERIAL宏。DECLARE_SERIAL只接受一个参数:类名。

3.重载基本类的Serialize函数,并串行化派生类的数据成员。

4.如果派生类没有默认的构造函数(该函数没有参数),则添加一个。因为对象并行化时,MFC要用默认构造函数在浮动标签上创建对象,并用从档案取回的值设置对象数据成员的初始值,所以这一步是非常必要的。

5.在类的实例中写入MFC的IMPLEMENT_SERIAL宏。IMPLEMENT_SERIAL宏接受三个参数:类名,基本类名和模式好。”模式号“是一个整型值,等于版本号。只要修改了累的串行化数据格式,模式号也要随之改变。

假定您编写了一个简单的类来代表线,名为CLine。该类有两个CPoint数据成员,它们存储着线的两个端点,并且您想添加串行化支持。最初,类的声明是这样的:

class CLine
{
protected:
  CPoint m_ptFrom;
  CPoint m_ptTo;
public:
CLine(CPoint from,CPoint to){m_ptFrom=from;m_ptTo = to;}
};

做到使该类可串行化

class CLine:public CObject
{
  DECLARE_SERIAL(CLine)
  protected:
    CPoint m_ptFrom;
    CPoint m_ptTo;
  public:
    CLine(){}//Required!
    CLine(CPoint from,CPoint to){m_ptFrom = from;m_ptTo = to;}
    void Serialize(CArchive&ar);
};
void CLine::Serialize(CArchive&ar);
{
  CObject::Serialize(ar);
  if(ar.IsStoring())
    ar<<m_ptFrom<<m_ptTo;
  else//Loading,not storing
    arr>>m_ptFrom>>m_ptTo;  
}

在实例类的过程中

IMPLEMENT_SERIAL(CLine,CObject,)

说明:这个宏为动态的CObject派生类对象生成必要的C++代码,使它能够在运行时访问类名及其在继承关系中的位置。在.CPP模块中使用IMPLEMENT_SERIAL宏,然后一次性地连接生成的目标代码。目前版本号为1.

该类实例呗要求串行化或并行化时,MFC调用实例的CLine::Serialize函数。在将自己的数据成员串行化之前,CLine::Serialize调用CObject::Serialize串行化基本类的数据成员。在这个示例中,基本类的Serialize函数并不起作用,但是如果您编写的类使间接由CObject派生来的,那么情况可能就不同了。基本类调用返回之后,CLine::Seralize调用CArchive::IsStoring决定数据流的方向。如果返回值为非0只,则数据串行化到档案中;如果为0,则数据被串行化读出。CLine::Serialize根据该返回值决定是用<<运算符想档案中写数据能,还是用>>运算符从档案中读取数据。

6.2.3给可串行化类分配版本号:可配置版本模式

编写课串行化类时,MFC用您制定的模式号制定一个粗略的版本控制方式。在向档案写数据是,MFC用模式号标记该类的实例;而在读回数据时,MFC将档案记录中的模式号和应用程序中使用着的该类对象的模式号做比较,如果两模式号不匹配,则MFC发送一个CArchiveException,其m_cause等于CArchiveException::badSchema.没有得到处理的该类异常会初始MFC显示一个消息框,提示”文件格式不对“。如果每次修改对象的串行化存储格式都能做到增加模式号,那么久不怕这种无心操作试图把磁盘中存的老版本对象读入内存里新的版本对象了。

有一个问题经常会突然在使用了可串行化类的应用程序中出现,就是向下兼容性。换句话说,就是如何并行化在老版本应用程序中创建的对象。如果兑现的持久存储格式岁应用程序的版本的更新发生了变化,这时您可能希望新版本应用程序对两种格式都能读。但是一旦MFC发现不配套的模式号,它将发送异常。鉴于MFC的结构的特点,最好按照MFC的方式处理异常并终止串行化过程。

可视化模式也就此产生了。可视化模式知识包含VERSIONABLE_SCHEMA标志的模式号。标志告诉MFC应用程序针对某一类能够处理多种串行化的数据格式。这种模式禁止CArchiveException,并允许应用程序对不同的模式号有判断地相应。使用了可视化模式的应用程序可以提供用户希望的向下兼容性。

如果要编写一个具有MFC可视化模式支持的可串行化类,一般需要两部:

1.将IMPLEMENT_SERIAL宏中的模式号于值VERSIONABLE_SCHEMA相或;

2.如果从档案加载对象时需要调用CArchive::GetObjectSchema,则要修改类的Serialize函数,并相应地调整其并行化例程。GetObjectSchema返回要进行并行化对象的模式号。

调用GetObjectSchema时要注意几个规则。首先,只有对象在被并行化时才能调用。其次,必须在读取档案对象数据之前调用。再者,它只能被调用一次。如果GetObjectSchema在调用Serialize前后被滴啊用了两次,则返回-1.

假定在应用程序的第2版本中,您要修改CLine类,想添加一个成员变量,用来保存线的颜色。下面是修改后的类的声明:

class CLine:public CObject
{
  DECLARE_SERIAL(CLine)
  protected:
    CPoint m_ptFrom;
    CPoint m_ptTo;
    COLORREF m_clrLine;//Line color(new in version 2)
  public:
    CLine(){}
    CLine(CPoint from,CPoint to,COLORREF color)
      {m_ptFrom = from; m_ptTo = to; m_clrLine = color}
    void Serialize(CArchive &ar);
};

因为线的颜色是持久性属性,所以您想修改CLine::Serialize,使它在串行化m_ptFrom和m_ptTo。to之外还能串行化m_clrLine。这意味着要把CLine的模式号增加到2。使用原类时按一下方式调用MFC的IMPLEMENT_SERIAL宏:

IMPLEMENT_SERIAL(CLine,CObject,1)

但是在修改后的类中,应该这样调用IMPLEMENT_SERIAL:

IMPLEMENT_SERIAL(CLine,CObject,2|VERSIONABLE_SCHEMA)

更新后的程序在读取CLine对象时,如果对象的模式号是1,MFC也不会发送CArchive异常,因为模式号中有VERSIONABLE_SCHEMA标志。但是它会了解到:由于模式号从1变为2,两个模式实际上是不同的。

最后一步是修改CLine::Serialize,使它根据GetObjectSchema不同的返回值并行化CLine。原Serialize函数如下:

新函数如下:

void CLine::Serialize(CArchive&ar)
{
  CObject::Serialize(ar);
  if(ar.IsStoring())
    ar<<m_ptFrom<<m_ptTo<<m_clrLine;
  else{
      UINT nSchema = ar.GetObjectSchema();
      switch(nSchema){
      case ://Version 1 CLine
          ar>>m_ptFrom>>m_ptTo;
          m_clrLine=RGB(,,);//Default color
          break;
      case ://Version 2 CLine
          ar>>m_ptFrom>>m_ptTo>>m_clrLine;
          break;
      default://Unknown version
        AfxThrowArchiveException(CArchiveException::badSchema);
        break;        
      }
    }
}

CLine对象写到档案上时,它的格式总是CLine的第2个版本。但是读取CLine是,根据GetObjectSchema返回值的不同,它又被当做CLine版本1或版本2读回。如果模式号为1,则对象按老方式读取,并把m_clrLine设置为默认值。如果对象模式号为2,则对象所有数据成员,包括m_clrLine,都要从档案中读取出来。其它模式号回导致CArchiveException,表示不能识别版本号(如果发生异常,可能是因为程序错了或档案毁坏了。如果将来还要修改CLine,则要把模式号增加到3并给新模式添加一个case程序段。

6.2.4串行化工作过程

看实际中数据进出档案的串行化和并行化过程,可以帮您进一步理解MFC的运作过程及其体系结构。MFC通过直接把原始数据类型如int何DWORD复制到档案中从而实现了该类型数据的串行化。为了便于理解,下例引用MFC源代码文件Arccore.cpp中的程序,说明了对于DWORD,CArchive中插入运算符的使用方法:

CArchive&CArchive::operator<<(DWORD dw)
{
  if(m_lpBufCur+sizeof(DWORD)>M_LPbUFmAX)
    fLUSH();
  if(!(m_nMode&bNoByteSwap))
    _AfxByteSwap(dw.m_lpBufCur);
  else
    *(DWORD*)m_lpBufCur = dw;
  m_lpBufCur += sizeof(DWORD);
  return *this;
}

为了提高效率,CArchive对象把接受到的数据保存在内部缓冲区。m_lpBufCur指向该缓冲区中数据的当前位置。如果缓冲区太满而不能再保存一个DWORD,它就会在DWORD复制给它之前被清空。对于挂接在CFile上的CArchive对象,CArchive::Flush会将当前缓冲区中的内容写入文件。

  CString,CRect以及其他从MFC类中生成的非原始数据类型一不同的方式串行化。例如:对于串行化CString,先输出字符数后输出字符本身。可以用CArchive::Write执行写入操作。下列从Arccore.cpp中节选的程序段说明了一个包含不足255个字符的CString是如何被串行化的:

CArchive&AFXAPI operator<<(CArchive&ar,const CString&string)
{
.
.
.
  if(string.GetData()->nDataLength<)
  {
    ar<<(BYTE)string.GetDate()->nDataLength;
  }
  .
  .
  .
  ar.Write(string.m_pchData,
      string.GetData()->nDataLength *sizeof(TCHAR));
  return ar;
}

CArchive::Write将指定量的数据复制到档案的内部缓冲区,并在必要的时候为防止溢出而清空缓冲区。偶尔,如果用<<运算符串行化给档案的CString包含Unicode字符,MFC就会在字符之前给档案写入一个特殊的3位标记。这就使得MFC可以表示串行化的字符串字符的类型,以便在字符串从档案中并行化时在必要的情况下将字符串换为用户希望的格式。换句话说,由Unicode应用程序串行化一个字符串让ANSI应用程序并行化它,或者相反,都是完全可以的。

在给档案串行化CObject指针时可以看到更有趣的情况。下面是Afx.inl中相关的代码:

_AFX_INLINE CArchive&AFXAPI operator<<(CArchive&ar,const CObject*pOb)
{ar.WriteObject(pOb);return ar;}

您可以看到,<<运算符调用了CArchive::WriteObject并给它传递了出现在插入符右边的指针,例如:ar<<pLine;

中的pLine。WriteObject最终会调用对象的Serialize函数来串行化对象的数据成员,但在此之前它要给档案写入附加信息,用来表示所创建对象的类。

例如:假设要串行化的对象是CLine的实例。第一次给档案 串行化CLine时,WriteObject将在档案中插入一个“新类标记”,是16位整形术,其值为-1或0xFFF;接着是对象的16位模式编号,一个16位值表示类名的字符数;最后才是类名资深。WriteObject然后调用CLiine的Serialize函数来串行化CLine的数据成员。

如果给档案写第二个CLine,WriteObject操作就不同了。在给档案写入新类标记是,WriteObject将给内存中的数据库(实际上是数据库 的所银行)。如果以前没有其他类被写入档案中,那么第一个写到磁盘上的CLine会得到索引号1.在请求给档案写入第二个CLine时,WriteObject先检查数据库,看看是否有CLine记录,为了不给档案写入多余的信息,它会写入一个由带有“旧类标记”(0x8000)的类索引号ORed组成的16位值。然后向以前一样调用CLine的Serialize函数。这样写入档案中得嘞的第一个实例由新类标记、模式编号以及类名标记;后来的实例将由一个16位值标记,其中低15位表示上次记录的模式编号和类名。

6-2给出了档案的16位禁止转储描述,其中档案包含两个已串行化版本1的CLine。用下列程序片段内江CLine写入档案中:

//Create two CLines and initialize an array of pointers.
CLine line1(CPoint(,),CPoint(,));
CLine line2(CPoiint(,),CPoint(,));
CLine* pLine[]={&line1,&line2};
int nCount =;
//Serialize the CLines and the CLine count.
ar << nCoun t;
for(int i=;i<nCount;i++)
  ar<<pLines[i];

分解十六进制转存是的每行都代表档案的一个组成成分。Line1包含以下语句执行时写入档案的对象数(2)

ar<<nCount;

Line 2包含了WriteObject写入的定义CLine类的信息。第一个16位值是新类标记;第二个是新类的模式编号(1);第三个保存了类名的长度(5)。第二行中的最后5位保存着类名(CLine)。

Line 1 :02 00 00 00   //Count of CLines in archive

Line 2 :FF FF 01 00 05 00 43 4c 69 63 65 //Tag for first CLine

Line 3 :00 00 00 00 //m_ptFrom.x=0

Line 4 :00 00 00 00 //m_ptFrom.y=0

Line 5 :32 00 00 00 //m_ptTo.x=50

Line 6 :32 00 00 00 //m_ptTo.y=50

Line 7 :01 80 //Tag for second CLine

Line 8 :32 00 00 00 //m_ptFrom.x = 50

Line 9 :32 00 00 00 //m_ptFrom.y=50

Line 10:64 00 00 00 //m_ptTo.x=100

Line 11:00 00 00 00 //m_ptTo.y=0

紧跟着类系的第3到第6行是一个串行化CLine的四个32位值,他们按顺序指定了CLine的m_ptFrom数据成员的x值,y值以及m_ptTo的x值和y值。于此相似到第8到第11行是有关第二个CLine的信息,而第7行是一个16位标记,表示了以后串行化CLine的数据。CLine类的索引号是1,因为它是第一个被加入档案的。16位值0x8001是带有旧类标记的类索引号ORed。

将CLines从档案中读取出来是情况又是怎样呢,假定用下列程序进行并行化CLine:

int nCount;
ar>>nCount;
CLine*pLines=new CLine[nCount];
for(int i=;i<nCount;i++)
  ar>>pLines[i]

在语句ar>>nCount;

被执行时,CArchive进入档案中检索4个字节,并将它们复制给nCount。这样就为从档案中检索CLine的for循环做了准备。每次语句

ar>>pLine[i];

执行时,>>运算符都将调用CArchive::ReadObject并传递一个NULL指针。下面是Afx.inl中的相关程序代码:

_AFX_INLINE CArchive&AFXAPI operator>>(CArchive&ar, CObject*&pOb)
{pOb=ar.ReadObject(NULL);return ar;}

ReadObject调用零一个CArchive函数ReadClass 来确定即将并行化的对象种类。在 第一次循环过程中,ReadClass从档案中读出一个字,由于是新类标记,它继续从档案中读出了编号了雷鸣。然后ReadClass 比较从档案中得到的模式编号和保存在CRuntimeClass结构中的模式编号,该结构与检索得到的类关联。(DECLARE_SERIAL和IMPLEMENT_SERIAL宏创建了一个静态CRuntimeClass结构,其中包含有关类的重要信息,包括类名和模式编号。MFC维持着一个CRuntimeClass结构的链接列表。可以用来查找特定类的运行时信息。)如果模式好相同,ReadClass就给ReadObject返回一个CRuntimeClass指针,接下来ReadObject将调用通过CRuntimeClass指针CreateObject来创建一个雷的新*,然后调用对象的Serialize函数从档案中给对象数据成员加载数据。由ReadClass返回的指向新的类实例的指针被保存在调用者指定的地方,本例中是pLines[i]的地址处。

从档案中读处类信息过程中,ReadObject也想WriteObject那样在内存中创建了一个数据库。在从档案中读取第二个CLine是,它前面的0x8001标记高速ReadClass可以从数据库中获得ReadObject要求的CRuntimeClass指针。

如果一切顺利的话,串行化过程基本就是这样。但我们跳过了许多细节内容,如:MFC执行的多种出错检查,对NULL对象指针的特殊处理,以及同一个对象的多个引用等。

如果从档案中读出的模式编号与保存在相应的CRuntimeClass中的模式编号不匹配怎么办?数如不同版本的模式。MFC首先检查保存在CRuntimeClass内模式编号中的VERSIONABLE_SCHEMA标志。如果标志不存在,MFC产生CArchiveException。到此为止,串行化过程就完了。这是除了显示一个错误消息意外无事可做,如果您不自己处理异常事件的话,MFC就会带您做。但是如果VERSIONABLE_SCHEMA标志存在,MFC就会跳过AfxThrowArchiveException,将模式编号保存在应用程序通过调用GetObjectSchema可以检索到的地方。这就说明了为什么VERSIONABLE_SCHEMA和GetObjectSchema是对可串行化类型成功变笨编号的关键。

6.2.5串行化CObject

MFC为CObject指针重载了CArchive的插入和提取运算符,但对CObject没有重载。这就意味着下面的语句有效:

CLine*pLine=new CLine(CPoint(,),CPoint(,));
ar<<pLine;

而下面的语句无效:

CLine line(CPoint(,),CPoint(,));
ar<<line;

也就是说,CObject可以用指针而不能用值串行化。

通过值而不是指针串行化CObject的一种方法是按下列程序代码进行串行化和并行化:

//Serialize.
CLine line((CPoint(,),CPoint(,));
ar<<&line;
//Deserialize.
CLine* pLine;
ar>>pLine;
CLine line =*pLine;//Assumes CLine has a copy constructor.
delete pLine;

更通用 的方法是直接调用其他类的Serialize函数,如下:

//Serialize
CLine line(CPoint(,),CPoint(,));
line.Serialize(ar);
//Deserialize.
CLine line;
line.Serialize(ar);

虽然直接调用Serialize完全合法,但您应  该意识到这样做就意味着对于瑶串行化的对象没有可编版本号的模式 。在使用<<运算符串行化一个对象指针时,MFC给档案写入对象的模式编号,而如果直接调用Serialize,就不会这样。如果调用GetObjectSchema来检索没有记录模式的对象的模式编号,它会返回-1并且并行化处理的结果将依赖于Serialize处理意外模式编号的能力。