深入解析VisualC++文件系统编程
文件系统的查找、读写、删除等操作在程序设计中出现非常频繁。如何在较短的时间内找到实际项目中最佳解决方案,对前人工作积累的高效的代码进行分析和重用就显得十分重要。以目前Windows桌面开发中流行平台Visual C++为例。下文是笔者在多年实际项目开发中,积累的文件系统编程方面的一些经验,以此抛砖引玉,并对某些特殊问题进行详细分析。
1.文件的查找
MFC类库中的CFileFind类是专门的文件查找类。下面代码演示了该类的使用方法:
CStringstrFileName;
CFileFind fileFinder;
BOOL bFinded = fileFinder.FindFile("C:\winnt\system32\*.dll");
while(bFinded)
{
bFinded = fileFinder.FindNextFile();
strFileName = fileFinder.GetFileTitle();
}
2.文件的读写
文件的读写编程非常普遍,其最常见方法是直接使用MFC类库的CFile声明一个对象,而后用这个对象的指针做参数声明一个CArchive对象,就可对复杂数据类型操作了。如下例示:
//对文件进行写操作
CString strTemp;
CFile mFile;
mFile.Open("c:\\try.TRY",CFile::modeCreate|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();
实际编程中,VC++中是以追加方式向文本文件写入数据情况较多。在c语言中,追加数据相当简单,设定writefile函数的写参数为“a+”就可以了。在用MFC中的CStdioFile类进行文件操作,读写时,参数modeNoTruncate的意思是不截取。测试中发现,WriteString写字符串之前要首先将指针定位到文件末尾:
CString str = "software week\r\n";
CStdioFilefile(strFile,CFile::modeCreate|CFile::modeNoTruncate|CFile::modeWrite);
file.SeekToEnd();//先定位到文件尾部
file.WriteString(strTmp);
file.Close;
3.文件复制、移动和删除操作
MFC中不提供这些操作具体类,要采用API中文件操作的相关函数如CopyFile()、CreateDirectory()、DeleteFile()、MoveFile()等。其调用形式简单,用法可详细参考MSDN。下面以具体应用中的临时目录和文件删除为例进行说明:
VC++只提供了删除一个空目录的函数,但实际中往往要求删除其下很多文件,需要开发用户自定义函数以实现这一功能。代码如下所示:
//函数功能: 删除指定路径下的指定文件,支持通配符
//参数: lpstrName:被删除的文件;strCurrentPath:找到的文件路径
void DelDirectoryPointFiles(LPSTRlpstrName, CString strCurrentPath)
{
//删除指定路径下的指定文件,支持通配符
//lpstrName:被删除的文件;strCurrentPath:找到的文件路径
WIN32_FIND_DATAwinFileData;
HANDLEhSearch;
charszHome[MAX_PATH];
DWORDdwRightWrong;
DWORDdwNameLength;
//当前的程序路径
dwRightWrong= GetCurrentDirectory(MAX_PATH, szHome);
dwRightWrong= SetCurrentDirectory(strCurrentPath);
//保存程序执行路径,然后把当前路径设定为需要查找的路径
hSearch= ::FindFirstFile(lpstrName, &winFileData);
if(hSearch!= INVALID_HANDLE_VALUE)
{
dwNameLength= lstrlen(winFileData.cFileName);
DeleteFile(winFileData.cFileName);
while(::FindNextFile(hSearch,&winFileData))
{
//寻找下一个符合条件的文件,找到一个删除一个
dwNameLength= lstrlen(winFileData.cFileName);
DeleteFile(winFileData.cFileName);
}
FindClose(hSearch);// 关闭查找句柄
}
dwRightWrong= SetCurrentDirectory(szHome);
}
至于删除子目录,则需要使用递归循环。读者可考虑用文件查找类CFileFind,利用IsDots(),IsDirectory()函数作为判断条件,进行编程实现。
4.临时文件的使用
在软件安装过程中,你常会发现C:\winnt\Temp目录下有大量的扩展名为tmp的文件,这就是大部分商业化软件在安装运行时所建立的临时文件。临时文件应用也相当普遍,与普通文件操作不同的只是其文件名获取要调用函数GetTempFileName()。其第一个参数是建立此临时文件的路径,第二个参数是建立临时文件名的前缀,第四个参数用于得到建立的临时文件名。代码示例如下:
charszTempPath[_MAX_PATH], szTempFile[_MAX_PATH];
GetTempPath(_MAX_PATH, szTempPath);
GetTempFileName(szTempPath, _T("app_"), 0, szTempfile);
CFile tempFile(szTempfile,CFile:: modeCreate|CFile:: modeWrite);
static const TCHAR sz[] = _T("software week");
tempFile.Write(sz, lstrlen(sz));
tempFile.Close();
5.文件的打开/保存对话框
当用户对所选择的文件打开或存储操作时,要用到文件打开/保存对话框。MFC的类CFileDialog用于实现这种功能。构造CFileDialog对象时,如果在参数中指定了OFN_ALLOWMULTISELECT风格,则在此对话框中可以进行多选操作。此时要重点注意为此CFileDialog对象的m_ofn.lpstrFile分配一块内存,用于存储多选操作所返回的所有文件路径名,如果不进行分配或分配的内存过小就会导致操作失败。演示代码如下:
CFileDialogmFileDlg(TRUE,NULL,NULL,
OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT|OFN_ALLOWMULTISELECT,
"All Files (*.*)|*.*||", AfxGetMainWnd());
CString str(" ",5000);
mFileDlg.m_ofn.lpstrFile = str.GetBuffer(5000);
str.ReleaseBuffer();
POSITION mPos = mFileDlg.GetStartPosition();
CString pathName(" ",MAX_PATH);
CFileStatus status;
while(mPos!=NULL)
{
pathName=mFileDlg.GetNextPathName(mPos);
CFile::GetStatus(pathName, status );
}
VC经常需要选择一个文件夹,但并没有现成的函数。需要使用SHBrowseForFolder,代码如下:
#include<Shlobj.h>
intSelFolder(HWND hParent, CString &strFolder)
{
strFolder.Empty();
LPMALLOC lpMalloc;
if (::SHGetMalloc(&lpMalloc) !=NOERROR) return 0;
char szDisplayName[_MAX_PATH];
char szBuffer[_MAX_PATH];
BROWSEINFO browseInfo;
browseInfo.hwndOwner = hParent;
browseInfo.pidlRoot = NULL; // 设桌面目录为根目录
browseInfo.pszDisplayName = szDisplayName;
browseInfo.lpszTitle = "选择目录";
browseInfo.ulFlags =BIF_RETURNFSANCESTORS|BIF_RETURNONLYFSDIRS;
browseInfo.lpfn = NULL;
browseInfo.lParam = 0;
LPITEMIDLIST lpItemIDList;
if ((lpItemIDList =::SHBrowseForFolder(&browseInfo)) != NULL)
{ // 得到列表框中所选择的子项的目录路径
if (::SHGetPathFromIDList(lpItemIDList,szBuffer))
{
if (szBuffer[0] == '\0') return 0;
// 返回所得到的目录路径.szBuffer
strFolder = szBuffer;
return TRUE;
}
else return TRUE; // strResult 为空
// 释放资源
lpMalloc->Free(lpItemIDList);
lpMalloc->Release();
}
return TREU;
}
6.配置信息INI文件操作
在我们编程中,总有一些配置信息需要保存下来。最常用的简单办法就是将这些信息写入INI文件中。在程序初始化时读入,在程序结束时保存到INI文件中。具体应用如下:
将信息写入到INI文件中所用的WINAPI函数原型为:
BOOLWritePrivateProfileString(
LPCTSTRlpAppName, // INI文件中字段名
LPCTSTRlpKeyName, // lpAppName下的子键名,通俗讲就是变量名
LPCTSTRlpString, // 键值,也就是变量的值,不过必须为LPCTSTR型或CString型
LPCTSTRlpFileName // 完整的INI文件名
);.
将信息从INI文件中读入到程序中的变量所用的WINAPI函数原型为:
DWORDGetPrivateProfileString(
LPCTSTRlpAppName, // INI文件中字段名
LPCTSTRlpKeyName, // lpAppName下的子键名,通俗讲就是变量名
LPCTSTRlpDefault, //如果INI文件中没有前两个参数指定的字段名或键名,则将此值赋给变量
LPTSTRlpReturnedString, //目的缓存器的CString对象
DWORDnSize, //目的缓存器大小
LPCTSTRlpFileName //完整INI文件名
);
读入整型值要用另一个WINAPI函数:UINTGetPrivateProfileInt( LPCTSTR lpAppName, LPCTSTR lpKeyName, INT nDefault,LPCTSTR lpFileName ); 其参数意义与上相同。
以实现最近使用的多个文件名保存为例,运用以上函数进行循环写入多个值,示例代码如下:
CStringstrTemp, strTempA;
int i,nCount =10;
//需要对10个文件名进行保存
for(i=0; i<nCount; i++)
{
strTemp.Format("%d",i);
strTempA= 文件名; //文件名可以从数组,列表框等处取得
::WritePrivateProfileString("UseFileName","FileName"+strTemp,strTempA,
"c:\\usefile\\usefile.ini");
}
//将文件总数写入,以便读出
strTemp.Format("%d",nCount);
::WritePrivateProfileString("FileCount","Count",strTemp,"c:\\usefile\\usefile.ini");
读出代码和上述类似。另外要补充以下注意事项:INI文件的路径必须完整,文件名的前各级子目录必须存在,否则写入不成功,函数返回 FALSE 值。文件名路径中必须为” \\”,VC++中 “\\ “才表示一个”\”。如果INI文件要放在程序所在目录中,此时参数lpFileName 为 ".\\student.ini"。
7.目录的日期和时间修改
在Windows环境下开发某些具有数据备份和恢复等功能的程序,在拷贝文件及其目录时把文件和目录的所有属性、包括日期和时间完全备份并还原。但Win32只提供修改文件时间的API函数,没有关于修改目录时间的任何描述。但仔细分析Windows提供的备份功能时发现,它实现的这一功能是从备份有关的Win32 API入手的。步骤如下:以FILE_FLAG_BACKUP_SEMANTICS属性调用Win32 API函数CreateFile()打开目录,再调用修改文件时间功能的SetFileTime() 函数即可。由此,在数据备份和恢复软件中,所有目录(包括根目录)都可以完全恢复原来的日期和时间了。
BOOLSetDirTime(char* pDirName,SYSTEMTIME newstime) //修改指定目录的时间
{
HANDLE hDir;
//打开目录的Win32API调用
hDir = CreateFile(DirName,GENERIC_READ |GENERIC_WRITE, // 必须写方式打开
FILE_SHARE_READ|FILE_SHARE_DELETE,NULL,OPEN_EXISTING,// 打开现存的目录
FILE_FLAG_BACKUP_SEMANTICS,// 只有这样才能打开目录
NULL);
if(hDir == INVALID_HANDLE_VALUE)
return FALSE; // 打开失败时返回
FILETIME lpCreationTime; // 目录的创建时间
FILETIME lpLastAccessTime; //最近一次访问目录的时间
FILETIME lpLastWriteTime; //最近一次修改目录的时间
SystemTimeToFileTime(&newstime,&lpCreationTime);
SystemTimeToFileTime(&newstime,&lpLastAccessTime);
SystemTimeToFileTime(&newstime,&lpLastWriteTime);
// 修改目录时间的Win32API函数调用
BOOL bVal = SetFileTime(hDir,&lpCreationTime, &lpLastAccessTime, &lpLastWriteTime);
CloseHandle(hDir); //关闭目录
return bVal;
}
8.运行时程序文件自删除
在共享软件的开发过程中,要求程序在识出恶意破解时或注册未成功时,在运行时就将其自身可执行文件删除。其原理为:如果文件的HANDLE打开,文件删除就会失败。由于HANDLE4是Win操作系统的硬编码,对应于EXE的文件映像(IMAGE)。缺省情况下,操作系统假定没有任何调用就会关闭文件映像区的句柄(HANDLE)。而现在,此HANDLE被关闭,删除文件就解除了文件所对应的句柄。所以CloseHandle(HANDLE(4))的运用十分巧妙。
由于UnmapViewOfFile解除了另外一个对应IMAGE的HANDLE,而且解除了IMAGE在内存的映射。由此以后的任何代码都不可以引用IMAGE映射地址内的任何代码。否则操作系统将报错。而现在代码在UnmapViewOfFile后则刚好没有引用到任何IMAGE内的代码。另外,在ExitProcess之前,EXE文件就将被删除。由于Win9x/NT/2K保护这些被映射到内存的Win32 IMAGE不被删除,所以进程虽然存在,但主线程所在的磁盘EXE文件已经没了。
HMODULEhModule = GetModuleHandle(0);
charbuffer[MAX_PATH]; //存放EXE文件名
GetModuleFileName(hModule,buffer, sizeof(buffer)); //得到可执行的EXE文件名
CloseHandle((HANDLE)4);
_asm
{
lea eax, buffer
push 0
push 0
push eax
push ExitProcess
push hModule
push DeleteFile
push UnmapViewOfFile
ret
}
上述代码调用ret返回到了UnmapViewOfFile,也就是堆栈里的偏移0所指的地方,当进入UnmapViewOfFile的流程时,栈里见到的是返回地址DeleteFile和hModule。当返回DeleteFile时,得到了ExitProcess的地址,即返回地址和参数EAX,而EAX为buffer即EXE的文件名。
以上源码在Windows和VC6.0+SP5平台的实际项目工程中经过验证,运行效果良好。读者可在充分消化吸收的基础上,举一反三,更好的应用于自身的软件开发中。