最近闲来无聊,想写一个简单的文件操作的类。但是由于经验尚浅,对于类的设计总是把握的不是太好。
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、使用网上说的方法,用一个临时文件,超出文件尾的数据,写到另一个内存映射对象中,操作结束时,将数据写回到原文件中。
其中第二种方法最易实现。