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