2.文件的打开/保存对话框
让用户选择文件进行打开和存储操作时,就要用到文件打开/保存对话框。MFC的类CFileDialog用于实现这种功能。使用CFileDialog 声明一个对象时,第一个BOOL型参数用于指定文件的打开或保存,当为TRUE时将构造一个文件打开对话框,为FALSE时构造一个文件保存对话框。
在构造CFileDialog对象时,如果在参数中指定了OFN_ALLOWMULTISELECT风格,则在此对话框中可以进行多选操作。此时要重点注意为此CFileDialog对象的m_ofn.lpstrFile分配一块内存,用于存储多选操作所返回的所有文件路径名,如果不进行分配或分配的内存过小就会导致操作失败。下面这段程序演示了文件打开对话框的使用方法。
CFileDialog mFileDlg(TRUE,NULL,NULL,
OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT|OFN_ALLOWMULTISELECT,
"All Files (*.*)|*.*||",AfxGetMainWnd());
CString str(" ",10000);
mFileDlg.m_ofn.lpstrFile=str.GetBuffer(10000);
str.ReleaseBuffer();
POSITION mPos=mFileDlg.GetStartPosition();
CString pathName(" ",128);
CFileStatus status;
while(mPos!=NULL)
{
pathName=mFileDlg.GetNextPathName(mPos);
CFile::GetStatus( pathName, status );
}
3.文件的读写
文件的读写非常重要,下面将重点进行介绍。文件读写的最普通的方法是直接使用CFile进行,如文件的读写可以使用下面的方法:
//对文件进行读操作
char sRead[2];
CFile mFile(_T("user.txt"),CFile::modeRead);
if(mFile.GetLength()<2)
return;
mFile.Read(sRead,2);
mFile.Close();
//对文件进行写操作
CFile mFile(_T("user.txt "), CFile::modeWrite|CFile::modeCreate);
mFile.Write(sRead,2);
mFile.Flush();
mFile.Close();
虽然这种方法最为基本,但是它的使用繁琐,而且功能非常简单。我向你推荐的是使用CArchive,它的使用方法简单且功能十分强大。首先还是用 CFile声明一个对象,然后用这个对象的指针做参数声明一个CArchive对象,你就可以非常方便地存储各种复杂的数据类型了。它的使用方法见下例。
//对文件进行写操作
CString strTemp;
CFile mFile;
mFile.Open("d://dd//try.TRY",CFile::modeCreate|CFile::modeNoTruncate|CFile::modeWrite);
CArchive ar(&mFile,CArchive::store);
ar<
ar.Close();
mFile.Close();
//对文件进行读操作
CFile mFile;
if(mFile.Open("d://dd//try.TRY",CFile::modeRead)==0)
return;
CArchive ar(&mFile,CArchive::load);
ar>>strTemp;
ar.Close();
mFile.Close();
CArchive的 << 和>> 操作符用于简单数据类型的读写,对于CObject派生类的对象的存取要使用ReadObject()和WriteObject()。使用 CArchive的ReadClass()和WriteClass()还可以进行类的读写,如:
//存储CAboutDlg类
ar.WriteClass(RUNTIME_CLASS(CAboutDlg));
//读取CAboutDlg类
CRuntimeClass* mRunClass=ar.ReadClass();
//使用CAboutDlg类
CObject* pObject=mRunClass->CreateObject();
((CDialog* )pObject)->DoModal();
虽然VC提供的文档/视结构中的文档也可进行这些操作,但是不容易理解、使用和管理,因此虽然很多VC入门的书上花费大量篇幅讲述文档/视结构,但我建议你最好不要使用它的文档。关于如何进行文档/视的分离有很多书介绍,包括非常著名的《Visual C++ 技术内幕》。
如果你要进行的文件操作只是简单的读写整行的字符串,我建议你使用CStdioFile,用它来进行此类操作非常方便,如下例。
CStdioFile mFile;
CFileException mExcept;
mFile.Open( "d://temp//aa.bat", CFile::modeWrite, &mExcept);
CString string="I am a string.";
mFile.WriteString(string);
mFile.Close();
4.临时文件的使用
正规软件经常用到临时文件,你经常可以会看到C:/Windows/Temp目录下有大量的扩展名为tmp的文件,这些就是程序运行是建立的临时文件。临时文件的使用方法基本与常规文件一样,只是文件名应该调用函数GetTempFileName() 获得。它的第一个参数是建立此临时文件的路径,第二个参数是建立临时文件名的前缀,第四个参数用于得到建立的临时文件名。得到此临时文件名以后,你就可以用它来建立并操作文件了,如:
char szTempPath[_MAX_PATH],szTempfile[_MAX_PATH];
GetTempPath(_MAX_PATH, szTempPath);
GetTempFileName(szTempPath,_T ("my_"),0,szTempfile);
CFile m_tempFile(szTempfile,CFile:: modeCreate|CFile:: modeWrite);
char m_char='a';
m_tempFile.Write(&m_char,2);
m_tempFile.Close();
5.文件的复制、删除等
MFC中没有提供直接进行这些操作的功能,因而要使用SDK。SDK中的文件相关函数常用的有CopyFile()、CreateDirectory()、DeleteFile()、MoveFile()。它们的用法很简单,可参考MSDN。
*********************************************************************************************************************
×××××××××××××××××××××××××××××××××××××××××××××××××
*********************************************************************************************************************
如何进行文件操作
[1]显示对话框,取得文件名
CString FilePathName;
CFileDialog dlg(TRUE);///TRUE为OPEN对话框,FALSE为SAVE AS对话框
if (dlg.DoModal() == IDOK)
FilePathName=dlg.GetPathName();
相关信息:CFileDialog 用于取文件名的几个成员函数:
假如选择的文件是C:/WINDOWS/TEST.EXE
则(1)GetPathName();取文件名全称,包括完整路径。取回C:/WINDOWS/TEST.EXE
(2)GetFileTitle();取文件全名:TEST.EXE
(3)GetFileName();取回TEST
(4)GetFileExt();取扩展名EXE
[2]打开文件
CFile file("C:/HELLO.TXT",CFile::modeRead);//只读方式打开
//CFile::modeRead可改为 CFile::modeWrite(只写),
//CFile::modeReadWrite(读写),CFile::modeCreate(新建)
例子:
{
CFile file;
file.Open("C:/HELLO.TXT",CFile::modeCreate|Cfile::modeWrite);
.
.
.
}
[3]移动文件指针
file.Seek(100,CFile::begin);///从文件头开始往下移动100字节
file.Seek(-50,CFile::end);///从文件末尾往上移动50字节
file.Seek(-30,CFile::current);///从当前位置往上移动30字节
file.SeekToBegin();///移到文件头
file.SeekToEnd();///移到文件尾
[4]读写文件
读文件:
char buffer[1000];
file.Read(buffer,1000);
写文件:
CString string("自强不息");
file.Write(string,8);
[5]关闭文件
file.Close();
在我们写的程序当中,总有一些配置信息需要保存下来,以便完成程序的功能,最简单的办法就是将这些信息写入INI文件中,程序初始化时再读入.具体应用如下:
一.将信息写入.INI文件中.
1.所用的WINAPI函数原型为:
BOOL WritePrivateProfileString(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
LPCTSTR lpString,
LPCTSTR lpFileName
);
其中各参数的意义:
LPCTSTR lpAppName 是INI文件中的一个字段名.
LPCTSTR lpKeyName 是lpAppName下的一个键名,通俗讲就是变量名.
LPCTSTR lpString 是键值,也就是变量的值,不过必须为LPCTSTR型或CString型的.
LPCTSTR lpFileName 是完整的INI文件名.
2.具体使用方法:设现有一名学生,需把他的姓名和年龄写入 c:/stud/student.ini 文件中.
CString strName,strTemp;
int nAge;
strName="张三";
nAge=12;
::WritePrivateProfileString("StudentInfo","Name",strName,"c://stud//student.ini");
此时c:/stud/student.ini文件中的内容如下:
[StudentInfo]
3.要将学生的年龄保存下来,只需将整型的值变为字符型即可:
strTemp.Format("%d",nAge);
::WritePrivateProfileString("StudentInfo","Age",strTemp,"c://stud//student.ini");
二.将信息从INI文件中读入程序中的变量.
1.所用的WINAPI函数原型为:
DWORD GetPrivateProfileString(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
LPCTSTR lpDefault,
LPTSTR lpReturnedString,
DWORD nSize,
LPCTSTR lpFileName
);
其中各参数的意义:
前二个参数与 WritePrivateProfileString中的意义一样.
lpDefault : 如果INI文件中没有前两个参数指定的字段名或键名,则将此值赋给变量.
lpReturnedString : 接收INI文件中的值的CString对象,即目的缓存器.
nSize : 目的缓存器的大小.
lpFileName : 是完整的INI文件名.
2.具体使用方法:现要将上一步中写入的学生的信息读入程序中.
CString strStudName;
int nStudAge;
GetPrivateProfileString("StudentInfo","Name","默认姓名",strStudName.GetBuffer(MAX_PATH),MAX_PATH,"c://stud//student.ini");
执行后 strStudName 的值为:"张三",若前两个参数有误,其值为:"默认姓名".
3.读入整型值要用另一个WINAPI函数:
UINT GetPrivateProfileInt(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
INT nDefault,
LPCTSTR lpFileName
);
这里的参数意义与上相同.使用方法如下:
nStudAge=GetPrivateProfileInt("StudentInfo","Age",10,"c://stud//student.ini");
三.循环写入多个值,设现有一程序,要将最近使用的几个文件名保存下来,具体程序如下:
1.写入:
CString strTemp,strTempA;
int i;
int nCount=6;
file://共有6个文件名需要保存
for(i=0;i {strTemp.Format("%d",i);
strTempA=文件名;
file://文件名可以从数组,列表框等处取得.
::WritePrivateProfileString("UseFileName","FileName"+strTemp,strTempA,
"c://usefile//usefile.ini");
}
strTemp.Format("%d",nCount);
::WritePrivateProfileString("FileCount","Count",strTemp,"c://usefile//usefile.ini");
file://将文件总数写入,以便读出.
2.读出:
nCount=::GetPrivateProfileInt("FileCount","Count",0,"c://usefile//usefile.ini");
for(i=0;i {strTemp.Format("%d",i);
strTemp="FileName"+strTemp;
::GetPrivateProfileString("CurrentIni",strTemp,"default.fil", strTempA.GetBuffer(MAX_PATH),MAX_PATH,"c://usefile//usefile.ini");
file://使用strTempA中的内容.
}
补充四点:
1.INI文件的路径必须完整,文件名前面的各级目录必须存在,否则写入不成功,该函数返回 FALSE 值.
2.文件名的路径中必须为 // ,因为在VC++中, // 才表示一个 / .
3.也可将INI文件放在程序所在目录,此时 lpFileName 参数为: ".//student.ini".
//----------------------------------------------------------------------------------
/*
类名:CIni
版本:v2.0
最后更新:
v2.0
梦小孩于2004年2月14日情人节
加入高级操作的功能
v1.0
梦小孩于2003年某日
一般操作完成
类描述:
本类可以于.ini文件进行操作
*/
文件 1:
#pragma once
#include "afxTempl.h"
class CIni
{
private:
CString m_strFileName;
public:
CIni(CString strFileName):m_strFileName(strFileName)
{
}
public:
//一般性操作:
BOOL SetFileName(LPCTSTR lpFileName); //设置文件名
CString GetFileName(void); //获得文件名
BOOL SetValue(LPCTSTR lpSection, LPCTSTR lpKey, LPCTSTR lpValue,bool bCreate=true); //设置键值,bCreate是指段名及键名未存在时,是否创建。
CString GetValue(LPCTSTR lpSection, LPCTSTR lpKey); //得到键值.
BOOL DelSection(LPCTSTR strSection); //删除段名
BOOL DelKey(LPCTSTR lpSection, LPCTSTR lpKey); //删除键名
public:
//高级操作:
int GetSections(CStringArray& arrSection); //枚举出全部的段名
int GetKeyValues(CStringArray& arrKey,CStringArray& arrValue,LPCTSTR lpSection); //枚举出一段内的全部键名及值
BOOL DelAllSections();
};
文件 2:
#include "StdAfx.h"
#include "ini.h"
#define MAX_ALLSECTIONS 2048 //全部的段名
#define MAX_SECTION 260 //一个段名长度
#define MAX_ALLKEYS 6000 //全部的键名
#define MAX_KEY 260 //一个键名长度
BOOL CIni::SetFileName(LPCTSTR lpFileName)
{
CFile file;
CFileStatus status;
if(!file.GetStatus(lpFileName,status))
return TRUE;
m_strFileName=lpFileName;
return FALSE;
}
CString CIni::GetFileName(void)
{
return m_strFileName;
}
BOOL CIni::SetValue(LPCTSTR lpSection, LPCTSTR lpKey, LPCTSTR lpValue,bool bCreate)
{
TCHAR lpTemp[MAX_PATH] ={0};
//以下if语句表示如果设置bCreate为false时,当没有这个键名时则返回TRUE(表示出错)
//!*&*none-value*&!* 这是个垃圾字符没有特别意义,这样乱写是防止凑巧相同。
if (!bCreate)
{
GetPrivateProfileString(lpSection,lpKey,"!*&*none-value*&!*",lpTemp,MAX_PATH,m_strFileName);
if(strcmp(lpTemp,"!*&*none-value*&!*")==0)
return TRUE;
}
if(WritePrivateProfileString(lpSection,lpKey,lpValue,m_strFileName))
return FALSE;
else
return GetLastError();
}
CString CIni::GetValue(LPCTSTR lpSection, LPCTSTR lpKey)
{
DWORD dValue;
TCHAR lpValue[MAX_PATH] ={0};
dValue=GetPrivateProfileString(lpSection,lpKey,"",lpValue,MAX_PATH,m_strFileName);
return lpValue;
}
BOOL CIni::DelSection(LPCTSTR lpSection)
{
if(WritePrivateProfileString(lpSection,NULL,NULL,m_strFileName))
return FALSE;
else
return GetLastError();
}
BOOL CIni::DelKey(LPCTSTR lpSection, LPCTSTR lpKey)
{
if(WritePrivateProfileString(lpSection,lpKey,NULL,m_strFileName))
return FALSE;
else
return GetLastError();
}
int CIni::GetSections(CStringArray& arrSection)
{
/*
本函数基础:
GetPrivateProfileSectionNames - 从 ini 文件中获得 Section 的名称
如果 ini 中有两个 Section: [sec1] 和 [sec2],则返回的是 'sec1',0,'sec2',0,0 ,当你不知道
ini 中有哪些 section 的时候可以用这个 api 来获取名称
*/
int i;
int iPos=0;
int iMaxCount;
TCHAR chSectionNames[MAX_ALLSECTIONS]={0}; //总的提出来的字符串
TCHAR chSection[MAX_SECTION]={0}; //存放一个段名。
GetPrivateProfileSectionNames(chSectionNames,MAX_ALLSECTIONS,m_strFileName);
//以下循环,截断到两个连续的0
for(i=0;i
if (chSectionNames[i]==0)
if (chSectionNames[i]==chSectionNames[i+1])
break;
}
iMaxCount=i+1; //要多一个0号元素。即找出全部字符串的结束部分。
arrSection.RemoveAll();//清空原数组
for(i=0;i
chSection[iPos++]=chSectionNames[i];
if(chSectionNames[i]==0)
{
arrSection.Add(chSection);
memset(chSection,0,MAX_SECTION);
iPos=0;
}
}
return (int)arrSection.GetSize();
}
int CIni::GetKeyValues(CStringArray& arrKey,CStringArray& arrValue, LPCTSTR lpSection)
{
/*
本函数基础:
GetPrivateProfileSection- 从 ini 文件中获得一个Section的全部键名及值名
如果ini中有一个段,其下有 "段1=值1" "段2=值2",则返回的是 '段1=值1',0,'段2=值2',0,0 ,当你不知道
获得一个段中的所有键及值可以用这个。
*/
int i;
int iPos=0;
CString strKeyValue;
int iMaxCount;
TCHAR chKeyNames[MAX_ALLKEYS]={0}; //总的提出来的字符串
TCHAR chKey[MAX_KEY]={0}; //提出来的一个键名
GetPrivateProfileSection(lpSection,chKeyNames,MAX_ALLKEYS,m_strFileName);
for(i=0;i
if (chKeyNames[i]==0)
if (chKeyNames[i]==chKeyNames[i+1])
break;
}
iMaxCount=i+1; //要多一个0号元素。即找出全部字符串的结束部分。
arrKey.RemoveAll();//清空原数组
arrValue.RemoveAll();
for(i=0;i
chKey[iPos++]=chKeyNames[i];
if(chKeyNames[i]==0)
{
strKeyValue=chKey;
arrKey.Add(strKeyValue.Left(strKeyValue.Find("=")));
arrValue.Add(strKeyValue.Mid(strKeyValue.Find("=")+1));
memset(chKey,0,MAX_KEY);
iPos=0;
}
}
return (int)arrKey.GetSize();
}
BOOL CIni::DelAllSections()
{
int nSection;
CStringArray arrSection;
nSection=GetSections(arrSection);
for(int i=0;i
if(DelSection(arrSection[i]))
return GetLastError();
}
return FALSE;
}
使用方法:
CIni ini("c://a.ini");
int n;
/*获得值
TRACE("%s",ini.GetValue("段1","键1"));
*/
/*添加值
ini.SetValue("自定义段","键1","值");
ini.SetValue("自定义段2","键1","值",false);
*/
/*枚举全部段名
CStringArray arrSection;
n=ini.GetSections(arrSection);
for(int i=0;i
*/
/*枚举全部键名及值
CStringArray arrKey,arrValue;
n=ini.GetKeyValues(arrKey,arrValue,"段1");
for(int i=0;i
*/
/*删除键值
ini.DelKey("段1","键1");
*/
/*删除段
ini.DelSection("段1");
*/
/*删除全部
ini.DelAllSections();
*/
VC++中以追加方式向文本文件写入数据
在VB、Asp中向文本文件追加数据很容易,只要设定一个参数为ForAppending就行了。
Sub OpenTextFileTest
Const ForReading = 1, ForWriting = 2, ForAppending = 8
Dim fso, f
Set fso = CreateObject("Scripting.FileSystemObject")
Set f = fso.OpenTextFile("c:/testfile.txt", ForWriting, True)
f.Write "Hello world!"
f.Close
End Sub
在c语言中,追加数据也比较简单,好像设定a+参数就可以了。
今天,我要用MFC中的CStdioFile类进行文件操作,读写等。
可是,看了下好像没有简单的方法,
于是在网上看到这样的写法:
CStdioFile file(strFile,CFile::modeCreate|CFile::modeNoTruncate|CFile::modeWrite);
file.WriteString(strTmp);
file.Close;
modeNoTruncate的意思就是不要截取的意思吧
可是,试了下这段代码,并没有起作用,不知道是什么原因。
于是,在WriteString写字符串之前加了个把指针先定位到文件末尾的代码,就可以了
CString strTmp="hehe/r/n";
CStdioFile file(strFile,CFile::modeCreate|CFile::modeNoTruncate|CFile::modeWrite);
file.SeekToEnd();//先定位到文件尾部
file.WriteString(strTmp);
file.Close;
随着Windows 2000和XP的普及,现在的大文件越来越多,而VC6中MFC的CFile类只支持不大于4GB的文件, 原因在于CFile类中使用了32位整型来处理文件,32位数的范围是2的32次方(4GB),超过这个范围的文件CFile就管不了,微软.Net中 VC7的CFile类支持大于4GB的文件,而.Net还不普及,开发桌面应用VC6还是首选,所以我们可以参照VC7写一个CFile的继承类 CFile64,使它支持大于4GB的文件:
class CFile64 : public CFile
{
public:
// Attributes
ULONGLONG GetPosition();
// Overridables
virtual ULONGLONG Seek(LONGLONG lOff, UINT nFrom);
virtual void SetLength(ULONGLONG dwNewLen);
ULONGLONG GetLength() ;
virtual void LockRange(ULONGLONG dwPos, ULONGLONG dwCount);
virtual void UnlockRange(ULONGLONG dwPos, ULONGLONG dwCount);
};
#include "stdafx.h"
#include "file64.h"
// CFile64 implementation
ULONGLONG CFile64::Seek(LONGLONG lOff, UINT nFrom)
{
ASSERT_VALID(this);
ASSERT((HANDLE)m_hFile != INVALID_HANDLE_VALUE);
ASSERT(nFrom == begin || nFrom == end || nFrom == current);
ASSERT(begin == FILE_BEGIN && end == FILE_END && current == FILE_CURRENT);
LARGE_INTEGER liOff;
liOff.QuadPart = lOff;
liOff.LowPart = ::SetFilePointer((HANDLE)m_hFile, liOff.LowPart, &liOff.HighPart,
(DWORD)nFrom);
if (liOff.LowPart == (DWORD)-1)
if (::GetLastError() != NO_ERROR)
CFileException::ThrowOsError((LONG)::GetLastError(), m_strFileName);
return liOff.QuadPart;
}
ULONGLONG CFile64::GetPosition()
{
ASSERT_VALID(this);
ASSERT((HANDLE)m_hFile != INVALID_HANDLE_VALUE);
LARGE_INTEGER liPos;
liPos.QuadPart = 0;
liPos.LowPart = ::SetFilePointer((HANDLE)m_hFile, liPos.LowPart, &liPos.HighPart , FILE_CURRENT);
if (liPos.LowPart == (DWORD)-1)
if (::GetLastError() != NO_ERROR)
CFileException::ThrowOsError((LONG)::GetLastError(), m_strFileName);
return liPos.QuadPart;
}
void CFile64::LockRange(ULONGLONG dwPos, ULONGLONG dwCount)
{
ASSERT_VALID(this);
ASSERT((HANDLE)m_hFile != INVALID_HANDLE_VALUE);
ULARGE_INTEGER liPos;
ULARGE_INTEGER liCount;
liPos.QuadPart = dwPos;
liCount.QuadPart = dwCount;
if (!::LockFile((HANDLE)m_hFile, liPos.LowPart, liPos.HighPart, liCount.LowPart,
liCount.HighPart))
{
CFileException::ThrowOsError((LONG)::GetLastError(), m_strFileName);
}
}
void CFile64::UnlockRange(ULONGLONG dwPos, ULONGLONG dwCount)
{
ASSERT_VALID(this);
ASSERT((HANDLE)m_hFile != INVALID_HANDLE_VALUE);
ULARGE_INTEGER liPos;
ULARGE_INTEGER liCount;
liPos.QuadPart = dwPos;
liCount.QuadPart = dwCount;
if (!::UnlockFile((HANDLE)m_hFile, liPos.LowPart, liPos.HighPart, liCount.LowPart,
liCount.HighPart))
{
CFileException::ThrowOsError((LONG)::GetLastError(), m_strFileName);
}
}
void CFile64::SetLength(ULONGLONG dwNewLen)
{
ASSERT_VALID(this);
ASSERT((HANDLE)m_hFile != INVALID_HANDLE_VALUE);
Seek(dwNewLen, (UINT)begin);
if (!::SetEndOfFile((HANDLE)m_hFile))
CFileException::ThrowOsError((LONG)::GetLastError(), m_strFileName);
}
ULONGLONG CFile64::GetLength()
{
ASSERT_VALID(this);
ULARGE_INTEGER liSize;
liSize.LowPart = ::GetFileSize((HANDLE)m_hFile, &liSize.HighPart);
if (liSize.LowPart == (DWORD)-1)
if (::GetLastError() != NO_ERROR)
CFileException::ThrowOsError((LONG)::GetLastError(), m_strFileName);
return liSize.QuadPart;
}
/
LONGLONG是64位整型,这样在理论上可支持的最大文件为18000000000GB,你也可以根据自己的需要重载CFile的其他函数
摘要: 本文给出了一种方便实用的解决大文件的读取、存储等处理的方法,并结合相关程序代码对具体的实现过程进行了介绍。
引言
文件操作是应用程序最为基本的功能之一,Win32 API和MFC均提供有支持文件处理的函数和类,常用的有Win32 API的CreateFile()、WriteFile()、ReadFile()和MFC提供的CFile类等。一般来说,以上这些函数可以满足大多数场合的要求,但是对于某些特殊应用领域所需要的动辄几十GB、几百GB、乃至几TB的海量存储,再以通常的文件处理方法进行处理显然是行不通的。目前,对于上述这种大文件的操作一般是以内存映射文件的方式来加以处理的,本文下面将针对这种Windows核心编程技术展开讨论。
内存映射文件
内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,只是内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而非系统的页文件,而且在对该文件进行操作之前必须首先对文件进行映射,就如同将整个文件从磁盘加载到内存。由此可以看出,使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。另外,实际工程中的系统往往需要在多个进程之间共享数据,如果数据量小,处理方法是灵活多变的,如果共享数据容量巨大,那么就需要借助于内存映射文件来进行。实际上,内存映射文件正是解决本地多个进程间数据共享的最有效方法。
内存映射文件并不是简单的文件I/O操作,实际用到了Windows的核心编程技术--内存管理。所以,如果想对内存映射文件有更深刻的认识,必须对Windows操作系统的内存管理机制有清楚的认识,内存管理的相关知识非常复杂,超出了本文的讨论范畴,在此就不再赘述,感兴趣的读者可以参阅其他相关书籍。下面给出使用内存映射文件的一般方法:
首先要通过CreateFile()函数来创建或打开一个文件内核对象,这个对象标识了磁盘上将要用作内存映射文件的文件。在用CreateFile ()将文件映像在物理存储器的位置通告给操作系统后,只指定了映像文件的路径,映像的长度还没有指定。为了指定文件映射对象需要多大的物理存储空间还需要通过CreateFileMapping()函数来创建一个文件映射内核对象以告诉系统文件的尺寸以及访问文件的方式。在创建了文件映射对象后,还必须为文件数据保留一个地址空间区域,并把文件数据作为映射到该区域的物理存储器进行提交。由MapViewOfFile()函数负责通过系统的管理而将文件映射对象的全部或部分映射到进程地址空间。此时,对内存映射文件的使用和处理同通常加载到内存中的文件数据的处理方式基本一样,在完成了对内存映射文件的使用时,还要通过一系列的操作完成对其的清除和使用过资源的释放。这部分相对比较简单,可以通过UnmapViewOfFile()完成从进程的地址空间撤消文件数据的映像、通过CloseHandle()关闭前面创建的文件映射对象和文件对象。
内存映射文件相关函数
在使用内存映射文件时,所使用的API函数主要就是前面提到过的那几个函数,下面分别对其进行介绍:
HANDLE CreateFile(LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); |
HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName); |
LPVOID MapViewOfFile(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, DWORD dwNumberOfBytesToMap); |
SYSTEM_INFO sinf; GetSystemInfo(&sinf); DWORD dwAllocationGranularity = sinf.dwAllocationGranularity; |
BOOL UnmapViewOfFile(LPCVOID lpBaseAddress); |
唯一的参数lpBaseAddress指定了返回区域的基地址,必须将其设定为MapViewOfFile()的返回值。在使用了函数 MapViewOfFile()之后,必须要有对应的UnmapViewOfFile()调用,否则在进程终止之前,保留的区域将无法释放。除此之外,前面还曾由CreateFile()和CreateFileMapping()函数创建过文件内核对象和文件映射内核对象,在进程终止之前有必要通过 CloseHandle()将其释放,否则将会出现资源泄漏的问题。
除了前面这些必须的API函数之外,在使用内存映射文件时还要根据情况来选用其他一些辅助函数。例如,在使用内存映射文件时,为了提高速度,系统将文件的数据页面进行高速缓存,而且在处理文件映射视图时不立即更新文件的磁盘映像。为解决这个问题可以考虑使用FlushViewOfFile()函数,该函数强制系统将修改过的数据部分或全部重新写入磁盘映像,从而可以确保所有的数据更新能及时保存到磁盘
使用内存映射文件处理大文件应用示例
下面结合一个具体的实例来进一步讲述内存映射文件的使用方法。该实例从端口接收数据,并实时将其存放于磁盘,由于数据量大(几十GB),在此选用内存映射文件进行处理。下面给出的是位于工作线程MainProc中的部分主要代码,该线程自程序运行时启动,当端口有数据到达时将会发出事件hEvent [0],WaitForMultipleObjects()函数等待到该事件发生后将接收到的数据保存到磁盘,如果终止接收将发出事件hEvent [1],事件处理过程将负责完成资源的释放和文件的关闭等工作。下面给出此线程处理函数的具体实现过程:
…… // 创建文件内核对象,其句柄保存于hFile HANDLE hFile = CreateFile("Recv1.zip", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_FLAG_SEQUENTIAL_SCAN, NULL); // 创建文件映射内核对象,句柄保存于hFileMapping HANDLE hFileMapping = CreateFileMapping(hFile,NULL,PAGE_READWRITE, 0, 0x4000000, NULL); // 释放文件内核对象 CloseHandle(hFile); // 设定大小、偏移量等参数 __int64 qwFileSize = 0x4000000; __int64 qwFileOffset = 0; __int64 T = 600 * sinf.dwAllocationGranularity; DWORD dwBytesInBlock = 1000 * sinf.dwAllocationGranularity; // 将文件数据映射到进程的地址空间 PBYTE pbFile = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_ALL_ACCESS, (DWORD)(qwFileOffset>>32), (DWORD)(qwFileOffset&0xFFFFFFFF), dwBytesInBlock); while(bLoop) { // 捕获事件hEvent[0]和事件hEvent[1] DWORD ret = WaitForMultipleObjects(2, hEvent, FALSE, INFINITE); ret -= WAIT_OBJECT_0; switch (ret) { // 接收数据事件触发 case 0: // 从端口接收数据并保存到内存映射文件 nReadLen=syio_Read(port[1], pbFile + qwFileOffset, QueueLen); qwFileOffset += nReadLen; // 当数据写满60%时,为防数据溢出,需要在其后开辟一新的映射视图 if (qwFileOffset > T) { T = qwFileOffset + 600 * sinf.dwAllocationGranularity; UnmapViewOfFile(pbFile); pbFile = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_ALL_ACCESS, (DWORD)(qwFileOffset>>32), (DWORD)(qwFileOffset&0xFFFFFFFF), dwBytesInBlock); } break; // 终止事件触发 case 1: bLoop = FALSE; // 从进程的地址空间撤消文件数据映像 UnmapViewOfFile(pbFile); // 关闭文件映射对象 CloseHandle(hFileMapping); break; } } … |
// 创建另外一个文件内核对象 hFile2 = CreateFile("Recv.zip", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_FLAG_SEQUENTIAL_SCAN, NULL); // 以实际数据长度创建另外一个文件映射内核对象 hFileMapping2 = CreateFileMapping(hFile2, NULL, PAGE_READWRITE, 0, (DWORD)(qwFileOffset&0xFFFFFFFF), NULL); // 关闭文件内核对象 CloseHandle(hFile2); // 将文件数据映射到进程的地址空间 pbFile2 = (PBYTE)MapViewOfFile(hFileMapping2, FILE_MAP_ALL_ACCESS, 0, 0, qwFileOffset); // 将数据从原来的内存映射文件复制到此内存映射文件 memcpy(pbFile2, pbFile, qwFileOffset); file://从进程的地址空间撤消文件数据映像 UnmapViewOfFile(pbFile); UnmapViewOfFile(pbFile2); // 关闭文件映射对象 CloseHandle(hFileMapping); CloseHandle(hFileMapping2); // 删除临时文件 DeleteFile("Recv1.zip"); |
引言
文件操作是应用程序最为基本的功能之一,Win32 API和MFC均提供有支持文件处理的函数和类,常用的有Win32 API的CreateFile()、WriteFile()、ReadFile()和MFC提供的CFile类等。一般来说,以上这些函数可以满足大多数场合的要求,但是对于某些特殊应用领域所需要的动辄几十GB、几百GB、乃至几TB的海量存储,再以通常的文件处理方法进行处理显然是行不通的。目前,对于上述这种大文件的操作一般是以内存映射文件的方式来加以处理的,本文下面将针对这种Windows核心编程技术展开讨论。
内存映射文件概述
内存文件映射也是Windows的一种内存管理方法,提供了一个统一的内存管理特征,使应用程序可以通过内存指针对磁盘上的文件进行访问,其过程就如同对加载了文件的内存的访问。通过文件映射这种使磁盘文件的全部或部分内容与进程虚拟地址空间的某个区域建立映射关联的能力,可以直接对被映射的文件进行访问,而不必执行文件I/O操作也无需对文件内容进行缓冲处理。内存文件映射的这种特性是非常适合于用来管理大尺寸文件的。
在使用内存映射文件进行I/O处理时,系统对数据的传输按页面来进行。至于内部的所有内存页面则是由虚拟内存管理器来负责管理,由其来决定内存页面何时被分页到磁盘,哪些页面应该被释放以便为其它进程提供空闲空间,以及每个进程可以拥有超出实际分配物理内存之外的多少个页面空间等等。由于虚拟内存管理器是以一种统一的方式来处理所有磁盘I/O的(以页面为单位对内存数据进行读写),因此这种优化使其有能力以足够快的速度来处理内存操作。
使用内存映射文件时所进行的任何实际I/O交互都是在内存中进行并以标准的内存地址形式来访问。磁盘的周期性分页也是由操作系统在后台隐蔽实现的,对应用程序而言是完全透明的。内存映射文件的这种特性在进行大文件的磁盘事务操作时将获得很高的效益。
需要说明的是,在系统的正常的分页操作过程中,内存映射文件并非一成不变的,它将被定期更新。如果系统要使用的页面目前正被某个内存映射文件所占用,系统将释放此页面,如果页面数据尚未保存,系统将在释放页面之前自动完成页面数据到磁盘的写入。
对于使用页虚拟存储管理的Windows操作系统,内存映射文件是其内部已有的内存管理组件的一个扩充。由可执行代码页面和数据页面组成的应用程序可根据需要由操作系统来将这些页面换进或换出内存。如果内存中的某个页面不再需要,操作系统将撤消此页面原拥用者对它的控制权,并释放该页面以供其它进程使用。只有在该页面再次成为需求页面时,才会从磁盘上的可执行文件重新读入内存。同样地,当一个进程初始化启动时,内存的页面将用来存储该应用程序的静态、动态数据,一旦对它们的操作被提交,这些页面也将被备份至系统的页面文件,这与可执行文件被用来备份执行代码页面的过程是很类似的。图1展示了代码页面和数据页面在磁盘存储器上的备份过程:
图1 进程的代码页、数据页在磁盘存储器上的备份
显然,如果可以采取同一种方式来处理代码和数据页面,无疑将会提高程序的执行效率,而内存映射文件的使用恰恰可以满足此需求。
对大文件的管理
内存映射文件对象在关闭对象之前并没有必要撤销内存映射文件的所有视图。在对象被释放之前,所有的脏页面将自动写入磁盘。通过CloseHandle()关闭内存映射文件对象,只是释放该对象,如果内存映射文件代表的是磁盘文件,那么还需要调用标准文件I/O函数来将其关闭。在处理大文件处理时,内存映射文件将表示出卓越的优势,只需要消耗极少的物理资源,对系统的影响微乎其微。下面先给出内存映射文件的一般编程流程框图:
图2 使用内存映射文件的一般流程
而在某些特殊行业,经常要面对十几GB乃至几十GB容量的巨型文件,而一个32位进程所拥有的虚拟地址空间只有232 = 4GB,显然不能一次将文件映像全部映射进来。对于这种情况只能依次将大文件的各个部分映射到进程中的一个较小的地址空间。这需要对上面的一般流程进行适当的更改:
1)映射文件开头的映像。
2)对该映像进行访问。
3)取消此映像
4)映射一个从文件中的一个更深的位移开始的新映像。
5)重复步骤2,直到访问完全部的文件数据。
下面给出一段根据此描述而写出的对大于4GB的文件的处理代码:
// 选择文件 CFileDialog fileDlg(TRUE, "*.txt", "*.txt", NULL, "文本文件 (*.txt)|*.txt||", this); fileDlg.m_ofn.Flags |= OFN_FILEMUSTEXIST; fileDlg.m_ofn.lpstrTitle = "通过内存映射文件读取数据"; if (fileDlg.DoModal() == IDOK) { // 创建文件对象 HANDLE hFile = CreateFile(fileDlg.GetPathName(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { TRACE("创建文件对象失败,错误代码:%d/r/n", GetLastError()); return; } // 创建文件映射对象 HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL); if (hFileMap == NULL) { TRACE("创建文件映射对象失败,错误代码:%d/r/n", GetLastError()); return; } // 得到系统分配粒度 SYSTEM_INFO SysInfo; GetSystemInfo(&SysInfo); DWORD dwGran = SysInfo.dwAllocationGranularity; // 得到文件尺寸 DWORD dwFileSizeHigh; __int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh); qwFileSize |= (((__int64)dwFileSizeHigh) << 32); // 关闭文件对象 CloseHandle(hFile); // 偏移地址 __int64 qwFileOffset = 0; // 块大小 DWORD dwBlockBytes = 1000 * dwGran; if (qwFileSize < 1000 * dwGran) dwBlockBytes = (DWORD)qwFileSize; while (qwFileOffset > 0) { // 映射视图 LPBYTE lpbMapAddress = (LPBYTE)MapViewOfFile(hFileMap,FILE_MAP_ALL_ACCESS, (DWORD)(qwFileOffset >> 32), (DWORD)(qwFileOffset & 0xFFFFFFFF), dwBlockBytes); if (lpbMapAddress == NULL) { TRACE("映射文件映射失败,错误代码:%d/r/n", GetLastError()); return; } // 对映射的视图进行访问 for(DWORD i = 0; i < dwBlockBytes; i++) BYTE temp = *(lpbMapAddress + i); // 撤消文件映像 UnmapViewOfFile(lpbMapAddress); // 修正参数 qwFileOffset += dwBlockBytes; qwFileSize -= dwBlockBytes; } // 关闭文件映射对象句柄 CloseHandle(hFileMap); AfxMessageBox("成功完成对文件的访问"); } |
在本例中,首先通过GetFileSize()得到被处理文件长度(64位)的高32位和低32位值。然后在映射过程中设定每次映射的块大小为1000倍的分配粒度,如果文件长度小于1000倍的分配粒度时则将块大小设置为文件的实际长度。在处理过程中由映射、访问、撤消映射构成了一个循环处理。其中,每处理完一个文件块后都通过关闭文件映射对象来对每个文件块进行整理。CreateFileMapping()、 MapViewOfFile()等函数是专门用来进行内存文件映射处理用的。
1)CreateFile():CreateFile()函数是一个用途非常广泛的函数,在这里的用法并没有什么特殊的地方,但有几点需要注意:一是访问模式参数dwDesiredAccess。该参数设置了对文件内核对象的访问类型,其允许设置的权限可以为读权限GENERIC_READ、写权限GENERIC_WRITE、读写权限GENERIC_READ | GENERIC_WRITE和设备查询权限0。在使用映射文件时,只能打开那些具有可读访问权限的文件,即只能应用GENERIC_READ和 GENERIC_READ | GENERIC_WRITE这两种组合;另一点需要注意的是共享模式参数dwShareMode。该参数定义了对文件内核对象的共享方式,其可能的设置为 FILE_SHARE_READ、FILE_SHARE_WRITE和0,并可对其组合使用。其中,设置为0时不允许共享对象; FILE_SHARE_READ和FILE_SHARE_WRITE分别为在要求只读、只写访问的情况下才允许对象的共享。
由于通过内存映射文件可以在多个进程间共享数据,因此在进行这种应用时应当考虑dwShareMode参数设置对运行结果的影响。
2)CreateFileMapping():该函数的作用是创建一个文件映射内核对象,以告知系统文件映射对象需要多大的物理存储器。创建内存映射文件对象对系统资源几乎没有什么影响,也不会影响进程的虚拟地址空间。除了需要用来表示该对象的内部资源之外通常并不用为其分配虚拟内存,但是如果内存映射文件对象是作共享内存之用的话,就要在创建对象时由系统为内存映射文件的使用在系统页文件中保留足够的空间。
函数第一个参数hFile为标识要映射到进程的地址空间的文件的句柄。虽然由于内存映射文件的物理存储器是来自于磁盘上的文件,而非系统的页文件,使创建内存映射文件就像保留一个地址空间区域并将物理存储器提交给该区域一样。第二个参数为指向文件映射内核对象的SECURITY_ATTRIBUTES结构的指针,由此来决定子进程能否继承得到返回的句柄。通常为其传递NULL 值,以默认的安全属性来禁止返回句柄的被继承。
接下来的参数用于文件被映射后设定文件映像的保护属性。其可能的取值为 PAGE_READONLY、PAGE_READWRITE和PAGE_WRITECOPY。虽然在创建文件映射对象时,系统并不为其保留地址空间区域,也不将文件的存储器映射到该区域。但是,在系统将存储器映射到进程的地址空间中去时,系统必须确切知道应赋予物理存储器页面的保护属性。在设置保护属性时,必须与用CreateFile()函数打开文件时所指定的访问标识相匹配,否则将导致CreateFileMapping()的执行失败。因此这里设置PAGE_READWRITE属性。除了上述三个页面保护属性外,还有4个区(Section)保护属性也可以一起组合使用:
区保护属性 | 说明 |
SEC_COMMIT | 为区中的所有页面在内存中或磁盘页面文件中分配物理存储器 |
SEC_IMAGE | 告知系统,映射的文件是一个可移植的EXE文件映像 |
SEC_NOCACHE | 告知系统,未将文件的任何内存映射文件放入高速缓存,多供硬件设备驱动程序开发人员使用 |
SEC_RESERVE | 对一个区的所有页面进行保留而不分配物理存储器 |