现在的位置: 首页 > 综合 > 正文

文件IO和串行化

2013年09月06日 ⁄ 综合 ⁄ 共 8546字 ⁄ 字号 评论关闭

      文件输入和输出服务是所有操作系统的主要工作,不必惊奇,MicroSoft Windows提供了各种API函数用来读、写和操作磁盘文件。MFC将这些函数封装在CFile类里,CFile允许把“文件”当做对象,并用CFile的成员函数(如:read、write等)对文件进行操作。CFile具有MFC编程人员实现低级文件IO所需要的所有操作。

      编写文件IO最主要的是为了支持文档的存储和加载,尽管用CFile对象实现磁盘文档的读写没有什么困难,但大部分MFC应用程序不会这么做,而是用CArchive对象。

1CFile类

      CFile是比较简单的类,它封装了Win32用来处理文件IO的那部分。在多于25个的成员函数中有用来打开和关闭文件的函数、读写文件数据的函数、删除和重命名文件的函数、检索文件信息的函数。它的public成员数据之一m_hFile保存了与CFile对象相关联的文件的句柄、protected成员m_strFileName保存着文件的名称。成员函数GetFilePath、GetFileName、GetFileTitle可以用来提取整个文件名或文件名的一部分。例如:如果完整的文件名和路径名为C:/personal/File.txt,那么GetFilePath返回整个字符串,GetFileName返回File.txt,GetFileTitle返回File。

      但是如果细讲这些函数,就等于忽略了对编程人员来说CFile拥有的重要功能,即用来读写磁盘文件的函数。下面的几部分简要介绍了CFile的使用方法,以及错误发生时CFile的通知方式(如果从来没有用过C++异常处理,现在开始吧)。

1.1打开关闭和创建文件

      用CFile打开文件有2中方法,第一种方法是构造一个没有初始化的CFile对象并调用CFile::Open。下面的代码段就用了这个方法打开一个具有读写访问权的文件File.txt。因为函数的第一个参数没有给出路径名,如果该文件不在当前目录下,Open就会失败。

CFile file;

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

{

      //It Worked

}

CFile::Open返回一个BOOL值,表示是否成功打开文件。返回非零值意味着文件打开了,零意味着文件没有打开。如果CFile::Open返回零,并且你想知道调用失败的原因,则创建一个CFileException对象并把它的地址传送到Open函数的第三个参数中。

 

如果Open失败,则它用“描述失败本质的信息”将CFileException对象初始化。ReportError在该信息的基础上显示一条错误信息通过检查CFileException的公用数据成员m_cause,你可以找出导致失败的原因。详细信息见msdn

      第二种方法是用CFile的构造函数打开文件。不必构造一个空的CFile对象并调用Open,可以这样创建一个CFile对象,并用一个语句打开文件:CFile file("File.txt",CFile::modeReadWrite);如果文件不能打开,CFile的构造函数会引发一个CFileException。因此,利用CFile的构造函数打开文件通常使用try和catch块来俘获错误。

 

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

      如果要创建一个新文件而不是打开一个现存文件,则要在CFile::Open或CFile::CFile函数的第二个参数中包含一个CFile::modeCreate标志。CFile file("File.txt",CFile::modeReadWrite | CFile::modeCreate);如果用这种方法创建的文件已经存在,则清空它的内容。

      如果要创建一个不存在的文件 或 要在文件存在但没有被截去时打开该文件,则要包含一个CFile::modeNoTruncate标志:CFile file("File.txt",CFile::modeReadWrite | CFile::modeCreate | CFile::modeNoTruncate);按这种方式打开文件基本上总是成功的,因为如果该文件不存在,他能自动生成。

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

CFile::shareDenyNone没有读写访问限制、CFile::shareDenyRead禁止读访问权、CFile::shareDenyWrite禁止写访问权、CFile::shareExclusive禁止读写访问权(默认值)。还要3种读写访问权CFile::modeReadWrite请求读写访问权、CFile::modeRead请求读访问权、CFile::modeWrite请求写访问权。这些选项的常见用法是允许任一客户读取文件,但禁止往文件上写。

      关闭打开的文件有2种方式。如果要显式关闭文件,则对应的CFile对象调用Close函数。如果你喜欢,可以用CFile的析构函数关闭文件。如果文件还没有关闭,类的析构函数则调用Close。这就是说,在堆上创建的CFile对象在失效后会自动关闭。有时程序员显示调用Close的原因是:关闭当前处于打开状态的文件,以便用同一个CFile对象打开另一个文件。

1.2读和写

      可以用CFile::Read读一个具有访问权的打开文件。可以用CFile::Write写一个具有写访问权的打开文件。下面的示例分配一个4KB的文件IO缓冲,并一次读取4KB内容,转换为小写字母后再写回文件。如下:

 

      如果在文件IO过程中有错误发生,Read、Write、和其它CFile函数就会发送一个CFileException。CFileException::m_Cause告诉你引发错误的原因。例如:试图向已满的磁盘上写文件会引发CFileException,其中m_Cause等于CFileException::diskFull。试图在文件范围之外读取数据会引发CFileException,其中m_Cause等于CFileException::endOfFile.

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

1.3枚举文件和文件夹

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

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

typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
TCHAR cFileName[ MAX_PATH ];
TCHAR cAlternateFileName[ 14 ];
} WIN32_FIND_DATA, *PWIN32_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和::FindNextFile返回的句柄。

下面的代码枚举了当前目录下的所有文件,并把它们的名字写入文件1.txt中

 

更有趣的问题是:如果把给定目录“和它的子目录”下的所有目录枚举出来,下面的函数枚举出当前目录和其子目录中的所有目录,并把每个目录的名字写到文件中,秘诀是递归。

2串行化和CArchive类

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

2.1串行化基础

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

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

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

另一种方法是:创建一个CArchive对象,并把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、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的结构也可以串行化。

      也许MFC串行化机制最强大的一面是:你能够创建自己的可串行化类,使他们与CArchive的插入和提前运算符一起工作。并且为了使这些类工作,你也不必自己重载任何运算符。什么原因呢?因为MFC为指向CObject派生类的实例的指针重载了<<和>>运算符。

2.2编写可串行化的类

      如果一个对象支持串行化,那么它一定是可串行化类的实例,可以按照下面的5个步骤编写可串行化的类:

1、直接或间接把类从CObject派生出来

2、在类的头文件中写入MFC的DECLARE_SERIAL宏。DECLARE_SERIAL只接收一个参数:类名。

3、重载基类的Serial函数,并串行化该派生类的数据成员

4、如果派生类没有默认的构造函数(该构造函数没有参数),则添加一个。因为对象并行化时,MFC要调用默认构造函数创建临时对象,并把从文件中取回的值设置为对象的数据成员的初始值,所以这一步是非常重要的。

5、在类的实现中写入MFC的IMPLEMENT_SERIAL宏,IMPLEMENT_SERIAL接收3个参数:该类名、基类名、版本号。只要修改了类的串行化数据格式,版本号也要随之改变。至于如何分配可串行化类的版本号在下节介绍。

头文件代码如下:

 

源文件代码如下:

 

      经过这5个步骤,类就可以串行化了目前版本号为1,如果后来又给CLine添加了一个持久性数据成员,则要把版本号增加到2,这样主结构就能根据程序的不同版本区别串行化到磁盘的CLine对象了。否则,磁盘上版本为1的CLine就可能被读入内存中版本为2的CLine,从而可能造成严重损失。

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

2.3串行化CObject

      在结束本部分之前,对有关串行化CObject还有些建议。MFC为CObject指针重载了CArchive的插入和提取运算符,但对CObject没有重载,这就意味着下面的语句有效:

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

而以下语句无效:

CLine Line(CPoint(0,0),CPoint(50,100));
ar<<Line;

也就是说,CObject可以用指针而不能用串行化。通常这并不是问题,但如果你要编写可串行化的类而该类又使用其它可串行化的类作为内部数据成员时,串行化这些数据成员就出现麻烦了。

      通过值而不是指针串行化CObject的一种方法是按下列程序代码进行串行化和并行化://Serialize.
CLine Line(CPoint(0,0),CPoint(50,100));
ar<<&Line;.

 

//DeSerialize.
CLine* pLine;
ar>>pLine;
CLine line=*pLine;//Assumes CLine has a copy constructor.
delete pLine;

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

//Serialize.
CLine line(CPoint(0,0),CPoint(50,100));
line.Serialize(ar);
//DeSerialize.
CLine line;
line.Serialize(ar);

 

 

 

 

抱歉!评论已关闭.