利用内存映射处理文件

来源:互联网 发布:sql 行转列 编辑:程序博客网 时间:2024/04/30 15:45

最近闲来无聊,想写一个简单的文件操作的类。但是由于经验尚浅,对于类的设计总是把握的不是太好。

C++毕竟是面向对象,而且自己又学过设计模式(个人觉得这个非常有用),当然也就希望自己设计的类有对象的意味了。

学习的最好方法就是模仿,在我印象中C#.NET框架中的类设计的很好,就借鉴一下。

.NET中操作文件的类如下:

FileStream:提供一个访问文件流对文件进行读写、打开、关闭等操作

StreamReader:用于读取文本信息。他会检查字节标记确定编码方法,当然也可以制定编码方法。

File,FileInfo:提供对文件本身整体的操作,例如,创建、复制、删除、移动等,也负责文件的属性控制,例如查询创建时间

、文件大小等。

编码之前需要做一些前期工作,那就是设计,设计之前我草拟了一些原则:

1、参数不宜超过4个,且每个参数的意义要尽量简单

2、方法尽可能简单,意义明确

3、高内聚、低耦合,例如可将对文件流的编解码代码分离出来

4、每个类尽可能的功能单一,方法数量不宜过多。例如文件读取类StreamReader只提供读取文件的相关操作,至于对文件属性的查询

绝对不能放到这个类中

5、对于节本的读写文件类,应忽略文件结构

6、方法的参数、返回值统一为宽字节,采用unicode编码。

经过一番思考,初期设计如下:

1、FileStream:负责文件的读写、打开、关闭操作。读写都面向字节流,具体来说就是此类负责文件的打开,然后以字节形式读写。

2、Encoding:负责字符的编解码操作。

3、TextReader(TextWriter):此类是个组合类,而不是继承类。此类简单的拥有上面两个类的对象,负责读取(写入)文本文件。

下面是一个接口的设计
class  IFileIOStream
{
public:
 IFileIOStream();
 virtual ~IFileIOStream();
public:
 //方法
 virtual BOOL Open(LPCTSTR lpName,DWORD dwCreate = OPEN_EXISTING, DWORD dwShareMode = 0) = 0;
 virtual BOOL IsOpen() = 0;
 virtual VOID Close() = 0;
 virtual BOOL Flush() = 0;
 virtual DWORD Seek(long lDistanceToMove,DWORD dwMovOrigion) = 0;
 virtual DWORD Read(LPBYTE lpBuffer,DWORD dwLengthToRead) = 0;
 virtual BOOL ReadByte(byte & pByte) = 0;
 virtual DWORD Write(LPBYTE lpBuffer,DWORD dwLengthToWrite) = 0;
 virtual BOOL WriteByte(byte bByte) = 0;
 //属性
public:
 //如果参数lpName为NULL则函数返回文件名的所需空间大小,否则lpName返回文件名,并返回文件名大小
 virtual INT GetName(LPCTSTR lpName) = 0;
 //文件的大小(字节数)
 virtual DWORD GetFileSize() = 0;
 //当前文件指针位置(相对于文件头的偏移)
 virtual DWORD GetPosition() = 0;
 virtual HANDLE GetHandle() = 0;
 INT AddRef();
 INT ReleaseRef();
protected:
 //对象被引用的个数
 INT m_nRefCount;
};

刚开始设计比较简单,实现都是用文件操作的API函数实现的,但是发现效率非常低。例如读取10万字的文本文件,

需要1055毫秒,后来采用内存映射实现,时间缩短到了十分之一。于是干脆把CFileStream用内存映射来实现

class CFileStream:public IFileIOStream
{
public:
 CFileStream(BOOL bOpenToRead = TRUE);
 //CFileStream(VOID);
 //复制构造函数
 CFileStream( CFileStream &anotherFileStream);
 //重载赋值操作符
 const CFileStream & operator=(CFileStream& anotherFileStream);
 virtual ~CFileStream(void);
 //转换操作符。它定义将类类型值转换为其他类型值的转换。
 //下面的定义可以在需要Handle类型的时候,将CFileStream转换为这样的类型
 operator HANDLE() const{return m_hFile;};
public:
 //方法
 BOOL Open(LPCTSTR lpName,DWORD dwCreate = OPEN_EXISTING, DWORD dwShareMode = 0);
 BOOL IsOpen(){return m_bOpen;};
 VOID Close();
 BOOL Flush();
 DWORD Seek(long lDistanceToMove,DWORD dwMovOrigion);
 DWORD Read(LPBYTE lpBuffer,DWORD dwLengthToRead);
 BOOL ReadByte(byte & pByte);
 DWORD Write(LPBYTE lpBuffer,DWORD dwLengthToWrite);
 BOOL WriteByte(byte bByte);
 //属性
public:
 //如果参数lpName为NULL则函数返回文件名的所需空间大小,否则lpName返回文件名,并返回文件名大小
 INT GetName(LPCTSTR lpName);
 //文件的大小(字节数)
 DWORD GetFileSize() {return m_dwSize;}
 //当前文件指针位置(相对于文件头的偏移)
 DWORD GetPosition();
 HANDLE GetHandle(){return m_hFile;}
private:
 BOOL BeginMap(HANDLE hFile);
 VOID CopyFrom(CFileStream &anotherFileStream);
 VOID GrowFileSize();
private:
 HANDLE m_hFile;
 HANDLE m_hMapFile;
 LPBYTE m_pbBegin;
 LPBYTE m_pbData;
 LPBYTE m_pbEnd;
 CFileStream * m_pParentStream;
 TCHAR m_tchName[FILE_MAX_NAME_LENGTH];
 DWORD m_dwSize;
 BOOL m_bOpen;
 BOOL m_bOpenToRead;
};

 

但是在写文件的时候出现了很大的问题,由于内存映射对象的大小事先是确定了的,这样就非常不利于文件的扩展。

例如文件只有4K大小,当写入第4K+1个字节时候就出现了问题!如果用WriteFile这个API,则系统会自动扩展文件大小,而用内存

映射就不行了。

解决办法由三种:

1、当写到文件末尾时候,重新进行映射,将内存映射对象大小增加一个4K,这样可自动扩展文件大小。但是这出现了一个问题,

例如当总共写入4K+1个字节,扩展后原文件大小为8K,且剩下的4K-1全是0,而不像用WriteFile那样在4K+1后有个文件结束符。

这是因为在取消映射时,系统将8K的数据全部写入了文件中。

2、写文件仍然调用WriteFile。这样牺牲了效率

3、使用网上说的方法,用一个临时文件,超出文件尾的数据,写到另一个内存映射对象中,操作结束时,将数据写回到原文件中。

其中第二种方法最易实现。

原创粉丝点击