操作文件基本上是每个应用程序都必须做的事情。除了必要的配置信息外,用户的工作最终都要以文件的形式保存到磁盘上。保存和获取这些信息可以使用独立的磁盘文件,也可以使用系统自带的数据库——注册表。
本章首先介绍底层操作文件的API函数和MFC中对 应的CFile类;然后介绍一些与操作文件相关的逻辑驱动器和目录方面的知识,包括驱动器的格式化和卷标设置、目录的创建和删除等;接着,本章介绍使用 API函数和ATL库中的CRegKey类操作注册表的方法;本章还重点讨论了内存映射文件在读写磁盘文件和建立共享内存方面的应用;本章最后介绍一个多 线程的文件分割系统的开发过程。
8.1 文件操作
文件的输入输出(I/O)服务是操作系统的重要部 分。Windows提供了一类API函数来读、写和管理磁盘文件。MFC将这些函数转化为一个面向对象的类——CFile,它允许将文件视为可以由 CFile成员函数操作的对象,如Read和Write等。CFile类实现了程序开发者执行底层文件I/O需要的大部分功能。
并不是在任何时候使用CFile类都是方便的,特别是要与底层设备(如COM口、设备驱动)进行交互的时候,所以本节主要讨论管理文件的API函数。事实上,了解这些函数之后,自然就会使用CFile类了。
8.1.1 创建和读写文件
使用API函数读写文件时,首先要使用 CreateFile函数创建文件对象(即打开文件),调用成功会返回文件句柄;然后以此句柄为参数调用ReadFile和WriteFile函数,进行 实际的读写操作;最后调用CloseHandle函数关闭不再使用的文件对象句柄。
1.打开和关闭文件
CreateFile是一个功能相当强大的函数,Windows下的底层设备差不多都是由它打开的。它可以创建或打开文件、目录、物理磁盘、控制台缓冲区、邮槽和管道等。调用成功后,函数返回能够用来访问此对象的句柄,其原型如下:
HANDLE CreateFile (
LPCTSTR lpFileName, // 要创建或打开的对象的名称
DWORD dwDesiredAccess, // 文件的存取方式
DWORD dwShareMode, // 共享属性
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全属性
DWORD dwCreationDisposition, // 文件存在或不存在时系统采取的行动
DWORD dwFlagsAndAttributes, // 新文件的属性
HANDLE hTemplateFile // 一个文件模板的句柄
);
各参数含义如下。
(1)lpFileName参数是要创建或打开的对 象的名称。如果打开文件,直接在这里指定文件名称即可;如果操作对象是第一个串口,则要指定“COM1”为文件名,然后就可以像操作文件一样操作串口了; 如果要打开本地电脑上的一个服务,要以“"""."服务名称"”为文件名,其中的“.”代表本地机器;也可以使用CreateFile打开网络中其他主机 上的文件,此时的文件名应该是“""主机名"共享目录名"文件名”。
(2)dwDesiredAcces参数是访问方式,它指定了要对打开的对象进行何种操作。指定GENERIC_READ标志表示以只读方式打开;指定GENERIC_WRITE标志表示以只写方式打开;指定这两个值的组合,表示要同时对打开的对象进行读写操作。
(3)dwShareMode参数指定了文件对象的共享模式,表示文件打开后是否允许其他代码以某种方式再次打开这个文件,它可以是下列值的一个组合:
l 0 不允许文件再被打开。C语言中的fopen函数就是这样打开文件的
l FILE_SHARE_DELETE 允许以后的程序代码对文件删除文件(Win98系列的系统不支持这 个标志)
l FILE_SHARE_READ 允许以后的程序代码以读方式打开文件
l FILE_SHARE_WRITE 允许以后的程序代码以写方式打开文件
(4)dwCreationDisposition参数指定了当文件已存在或者不存在时系统采取的动作。在这里设置不同的标志就可以决定究竟是要打开文件,还是要创建文件。参数的可能取值如下:
l CREATE_ALWAYS 创建新文件。如果文件存在,函数会覆盖这个文件,清除存在的属性
l CREATE_NEW 创建新文件。如果文件存在,函数执行失败
l OPEN_ALWAYS 如果文件已经存在,就打开它,不存在则创建新文件
l OPEN_EXISTING 打开存在的文件。如果文件不存在,函数执行失败
l TRUNCATE_EXISTING 打开文件并将文件截断为零,当文件不存在时函数执行失败
(5)dwFlagsAndAttributes参数用来指定新建文件的属性和标志。文件属性可以是下面这些值的组合:
l FILE_ATTRIBUTE_ARCHIVE 标记归档属性
l FILE_ATTRIBUTE_HIDDEN 标记隐藏属性
l FILE_ATTRIBUTE_READONLY 标记只读属性
l FILE_ATTRIBUTE_READONLY 标记系统属性
l FILE_ATTRIBUTE_TEMPORARY 临时文件。操作系统会尽量把所有文件的内容保持在内 存中以加快存取速度。使用完后要尽快将它删除
此参数还可同时指定对文件的操作方式,下面是一些比较常用的方式:
l FILE_FLAG_DELETE_ON_CLOSE 文件关闭后系统立即自动将它删除
l FILE_FLAG_OVERLAPPED 使用异步读写文件的方式
l FILE_FLAG_WRITE_THROUGH 系统不会对文件使用缓存,文件的任何改变都会被系统 立即写入硬盘
(6)hTemplateFile参数指定了一个文件模板句柄。系统会复制该文件模板的所有属性到当前创建的文件中。Windows 98系列的操作系统不支持它,必须设为NULL。
打开或创建文件成功时,函数返回文件句柄,失败时返回INVALID_HANDLE_VALUE(-1)。如果想再详细了解失败的原因,可以继续调用GetLastError函数。
用不同的参数组合调用CreateFile函数可以完成不同的功能,例如,下面的代码为读取数据打开了一个存在的文件。
HANDLE hFile;
hFile = ::CreateFile("myfile.txt", // 要打开的文件
GENERIC_READ, // 要读这个文件
FILE_SHARE_READ, // 允许其他程序已只读形式再次打开它
NULL, // 默认安全属性
OPEN_EXISTING, // 仅仅打开存在的文件(如果不存不创建)
FILE_ATTRIBUTE_NORMAL, // 普通文件
NULL); // 没有模板
if(hFile == INVALID_HANDLE_VALUE)
{ ……// 不能够打开文件 }
仅当当前目录中存在名称为myfile.txt的文 件时,上面的CreateFile才能执行成功。由于为dwCreationDisposition参数指定了OPEN_EXISTING,所以当要打开 的文件不存在时,CreateFile返回INVALID_HANDLE_VALUE,而不会创建这个文件。如果想创建一个文件以便向里面写入数据,可以 使用下面的代码:
HANDLE hFile;
hFile = CreateFile("myfile.txt", // 要创建的文件
GENERIC_WRITE, // 要写这个文件
0, // 不共享
NULL, // 默认安全属性
CREATE_ALWAYS, // 如果存在就覆盖
FILE_ATTRIBUTE_NORMAL, // 普通文件
NULL); // 没有模板
if(hFile == INVALID_HANDLE_VALUE)
{ ……// 不能够打开文件 }
要关闭打开的文件,直接以CreateFile返回的文件句柄调用CloseHandle函数即可。
2.移动文件指针
系统为每个打开的文件维护一个文件指针,指定对文件 的下一个读写操作从什么位置开始。随着数据的读出或写入,文件指针也随之移动。当文件刚被打开时,文件指针处于文件的头部。有时候需要随机读取文件内容, 这就需要先调整文件指针,SetFilePointer函数提供了这个功能,原型如下:
DWORD SetFilePointer (
HANDLE hFile, // 文件句柄
LONG lDistanceToMove, // 要移动的距离
PLONG lpDistanceToMoveHigh, // 移动距离的高32位,一般设置为NULL
DWORD dwMoveMethod // 移动的模式
);
dwMoveMethod参数指明了从什么地方开始移动,可以是下面的一个值:
l FILE_BEGIN 开始移动位置为0,即从文件头部开始移动
l FILE_CURRENT 开始移动位置是文件指针的当前值
l FILE_END 开始移动位置是文件的结尾,即从文件尾开始移动
函数执行失败返回-1,否则返回新的文件指针的位置。
文件指针也可以移动到所有数据的后面,比如现在文件的长度是100 KB,但还是可以成功的将文件指针移动到1000 KB的位置。这样做可以达到扩展文件长度的目的。
SetEndOfFile函数可以截断或者扩展文件。该函数移动指定文件的结束标志(end-of-file,EOF)到文件指针指向的位置。如果文件扩展,旧的EOF位置和新的EOF位置间的内容是未定义的。SetEndOfFile函数的用法如下:
BOOL SetEndOfFile(HANDLE hFile );
截断或者扩展文件时,要首先调用SetFilePointer移动文件指针,然后再调用SetFilePointer函数设置新的文件指针位置为EOF。
3.读写文件
读写文件的函数是ReadFile和WriteFile,这两个函数既可以同步读写文件,又可以异步读写文件。而函数ReadFileEx和WriteFileEx只能异步读写文件。
从文件读取数据的函数是ReadFile,向文件写入数据的函数是WriteFile,操作的开始位置由文件指针指定。这两个函数的原型如下:
BOOL ReadFile(
HANDLE hFile, // 文件句柄
LPVOID lpBuffer, // 指向一个缓冲区,函数会将读出的数据返回到这里
DWORD nNumberOfBytesToRead, // 要求读入的字节数
LPDWORD lpNumberOfBytesRead, // 指向一个DWORD类型的变量,
// 用于返回实际读入的字节数
LPOVERLAPPED lpOverlapped // 以便设为NULL
);
BOOL WriteFile (hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped);
当用WriteFile写文件时,写入的数据通常被Windows暂时保存在内部的高速缓存中,等合适的时候再一并写入磁盘。如果一定要保证所有的数据都已经被传送,可以强制使用FlushFileBuffers函数来清空数据缓冲区,函数的惟一参数是要操作的文件句柄。
BOOL FlushFileBuffers (HANDLE hFile );
4.锁定文件
当对文件数据的一致性要求较高时,为了防止程序在写入的过程中其他进程刚好在读取写入区域的内容,可以对已打开文件的某个部分进行加锁,这就可以防止其他进程对该区域进行读写。加锁和解锁的函数是LockFile和UnlockFile,它们的原型如下:
BOOL LockFile(
HANDLE hFile, // 文件句柄
DWORD dwFileOffsetLow, // 加锁的开始位置
DWORD dwFileOffsetHigh,
DWORD nNumberOfBytesToLockLow, // 加锁的区域的大小
DWORD nNumberOfBytesToLockHigh
);
UnlockFile ( hFile, dwFileOffsetLow, dwFileOffsetHigh,
nNumberOfBytesToUnlockLow, nNumberOfBytesToUnlockHigh);
dwFileOffsetLow和 dwFileOffsetHigh参数组合起来指定了加锁区域的开始位置,nNumberOfBytesToLockLow和 nNumberOfBytesToLockHigh参数组合起来指定了加锁区域的大小。这两个参数都指定了一个64位的值,在Win32中,只使用32位 就够了。
如果加锁文件的进程终止,或者文件关闭时还未解锁,操作系统会自动解除对文件的锁定。但是,操作系统解锁文件花费的时间取决于当前可用的系统资源。因此,进程终止时最好显式地解锁所有已锁定的文件,以免造成这些文件无法访问。
8.1.2 获取文件信息
1.获取文件类型
Windows下的许多对象都称之为文件,如果想知道一个文件句柄究竟对应什么对象,可以使用GetFileType函数,原型如下:
DWORD GetFileType(HANDLE hFile);
函数的返回值说明了文件类型,可以是下面的一个值:
l FILE_TYPE_CHAR 指定文件是字符文件,通常是LPT设备或控制台
l FILE_TYPE_DISK 指定文件是磁盘文件
l FILE_TYPE_PIPE 指定文件是套节字,一个命名的或未命名的管道
l FILE_TYPE_UNKNOWN 不能识别指定文件,或者函数调用失败
2.获取文件大小
如果确定操作的对象是磁盘文件,还可以使用GetFileSize函数取得这个文件的长度。
DWORD GetFileSize(
HANDLE hFile, // 文件句柄
LPDWORD lpFileSizeHigh // 用于返回文件长度的高字。可以指定这个参数为NULL
);
函数执行成功将返回文件大小的低双字,如果lpFileSizeHigh参数不是NULL,函数将文件大小的高双字放入它指向的DWORD变量中。
如果函数执行失败,并且 lpFileSizeHigh是NULL,返回值将是INVALID_FILE_SIZE;如果函数执行失败,但lpFileSizeHigh不是 NULL,返回值是INVALID_FILE_SIZE,进一步调用GetLastError会返回不为NO_ERROR的值。
如果返回值是INVALID_FILE_SIZE, 应用程序必须调用GetLastError来确定函数调用是否成功。原因是,当lpFileSizeHigh不为NULL或者文件大小为 0xffffffff时,函数虽然调用成功了,但依然会返回INVALID_FILE_SIZE。这种情况下,GetLastError会返回 NO_ERROR来响应成功。
3.获取文件属性
如果要查看文件或者目录的属性,可以使用GetFileAttributes函数,它会返回一系列FAT风格的属性信息。
DWORD GetFileAttributes(LPCTSTR lpFileName); // lpFileName指定了文件或者目录的名称
函数执行成功,返回值包含了指定文件或目录的属性信息,可以是下列取值的组合:
l FILE_ATTRIBUTE_ARCHIVE 文件包含归档属性
l FILE_ATTRIBUTE_COMPRESSED 文件和目录被压缩
l FILE_ATTRIBUTE_DIRECTORY 这是一个目录
l FILE_ATTRIBUTE_HIDDEN 文件包含隐含属性
l FILE_ATTRIBUTE_NORMAL 文件没有其他属性
l FILE_ATTRIBUTE_READONLY 文件包含只读属性
l FILE_ATTRIBUTE_SYSTEM 文件包含系统属性
l FILE_ATTRIBUTE_TEMPORARY T 文件是一个临时文件
这些属性对目录也同样适用。INVALID_FILE_ATTRIBUTES(0xFFFFFFFF)是函数执行失败后的返回值。
下面是快速检查某个文件或目录是否存在的自定义函数,可以将它用在自己的工程中。
BOOL FileExists(LPCTSTR lpszFileName, BOOL bIsDirCheck)
{ // 试图取得文件的属性
DWORD dwAttributes = GetFileAttributes(lpszFileName);
if(dwAttributes == 0xFFFFFFFF)
return FALSE;
if ((dwAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY)
{ if (bIsDirCheck)
return TRUE;
else
return FALSE;
}
else
{ if (!bIsDirCheck)
return TRUE;
else
return FALSE;
}
}
第2个参数bIsDirCheck指定要检查的对象是目录还是文件。
与GetFileAttributes相对应的函数是SetFileAttributes,这个函数用来设置文件属性。
BOOL SetFileAttributes(
LPCTSTR lpFileName, // 目标文件名称
DWORD dwFileAttributes // 要设置的属性值
);
8.1.3 常用文件操作
1.拷贝文件
拷贝文件的函数是CopyFile和CopyFileEx,其作用都是复制一个存在的文件到一个新文件中。CopyFile函数的用法如下:
BOOL CopyFile(
LPCTSTR lpExistingFileName, // 指定已存在的文件的名称
LPCTSTR lpNewFileName, // 指定新文件的名称
BOOL bFailIfExists // 如果指定的新文件存在是否按出错处理
);
CopyFileEx函数的附加功能是允许指定一个回调函数,在拷贝过程中,函数每拷贝完一部分数据,就会调用回调函数。用户在回调函数中可以指定是否停止拷贝,还可以显示进度条来指示拷贝的进度。
2.删除文件
删除文件的函数是DeleteFile,仅有的参数是要删除文件的名称。
BOOL DeleteFile(LPCTSTR lpFileName);
如果应用程序试图删除不存在的文件,DeleteFile将执行失败。如果目标文件是只读的,函数也会执行失败,出错代码为ERROR_ACCESS_DENIED。为了删除只读文件,先要去掉其只读属性。
DeleteFile函数可以标识一个文件为“关闭时删除”。因此,直到最后一个到此文件的句柄关闭之后,文件才会被删除。
下面的自定义函数RecursiveDelete示例了如何删除指定目录下的所有文件和子目录。
void RecursiveDelete(CString szPath)
{ CFileFind ff; // MFC将查找文件的API封装到了CFileFind类。读者可参考下面的框架使用这个类
CString strPath = szPath;
// 说明要查找此目录下的所有文件
if(strPath.Right(1) != """")
strPath += """";
strPath += "*.*";
BOOL bRet;
if(ff.FindFile(strPath))
{ do
{ bRet = ff.FindNextFile();
if(ff.IsDots()) // 目录为“.”或者“..”?
continue;
strPath = ff.GetFilePath();
if(!ff.IsDirectory())
{ // 删除此文件
::SetFileAttributes(strPath, FILE_ATTRIBUTE_NORMAL);
::DeleteFile(strPath);
}
else
{ // 递归调用
RecursiveDelete(strPath);
// 删除此目录(RemoveDirectory只能删除空目录)
::SetFileAttributes(strPath, FILE_ATTRIBUTE_NORMAL);
::RemoveDirectory(strPath);
}
}
while(bRet);
}
}
用DeleteFile函数删除的文件不会被放到回收站,它们将永远丢失,所以请小心使用RecursiveDelete函数。
3.移动文件
移动文件的函数是MoveFile和MoveFileEx函数。它们的主要功能都是用来移动一个存在的文件或目录。MoveFile函数用法如下:
BOOL MoveFile(
LPCTSTR lpExistingFileName, // 存在的文件或目录
LPCTSTR lpNewFileName // 新的文件或目录
);
当需要指定如何移动文件时,请使用MoveFileEx函数。
BOOL MoveFileEx(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName, DWORD dwFlags);
dwFlags参数可以是下列值的组合:
l MOVEFILE_DELAY_UNTIL_REBOOT 函数并不马上执行,而是在操作系统下一此重新启动时才移动文件。在AUTOCHK执行之后,系统立即移动文件,这是在创建任何分页文件之前进行的。因此,这个值使函数能够删除上一次运行时使用的分页文件。只有拥有管理员权限的用户才可以使用这个值
l MOVEFILE_REPLACE_EXISTING 如果目标文件已存在的话,就将它替换掉
l MOVEFILE_WRITE_THROUGH 直到文件实际从磁盘移除之后函数才返回
如果指定了MOVEFILE_DELAY_UNTIL_REBOOT标记,lpNewFileName参数可以指定为NULL,这种情况下,当系统下一次启动时,操作系统会删除lpExistingFileName参数指定的文件。
8.1.4 检查PE文件有效性的例子
PE文件格式是任何可执行模块或者DLL的文件格 式,PE文件以64字节的DOS文件头(IMAGE_DOS_HEADER结构)开始,之后是一小段DOS程序,然后是248字节的NT文件头 (IMAGE_NT_HEADERS结构)。NT文件头的偏移地址由IMAGE_DOS_HEADER结构的e_lfanew成员给出。
检查文件是不是有效PE文件的一个方法是检查IMAGE_DOS_HEADER和IMAGE_NT_HEADERS结构是否有效。IMAGE_DOS_HEADER结构定义如下:
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // DOS可执行文件标记,为“MZ”。依此识别DOS头是否有效
... // 其他成员,没什么用途
LONG e_lfanew; // IMAGE_NT_HEADERS结构的地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
IMAGE_NT_HEADERS结构定义如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE文件标识,为“PE"0"0”。依此识别NT文件头是否有效
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS,
为了编程方便,Windows为DOS文件标记和PE文件标记都定义了宏标识。
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00
检查文件是否为PE文件的步骤如下:
(1)检验文件头部第一个字的值是否等于IMAGE_DOS_SIGNATURE,是则说明DOS MZ头有效。
(2)一旦证明文件的DOS头有效后,就可用e_lfanew来定位PE头了。
(3)比较PE头的第一个字是否等于IMAGE_NT_SIGNATURE。如果这个值也匹配,那么就认为该文件是一个有效的PE文件。
下面是验证PE文件有效性的代码,在配套光盘的08ValidPE工程下。
BOOL CMyApp::InitInstance()
{ // 弹出选择文件对话框
CFileDialog dlg(TRUE);
if(dlg.DoModal() != IDOK)
return FALSE;
// 打开检查的文件
HANDLE hFile = ::CreateFile(dlg.GetFileName(), GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE)
MessageBox(NULL, "无效文件!", "ValidPE", MB_OK);
// 定义PE文件中的DOS头和NT头
IMAGE_DOS_HEADER dosHeader;
IMAGE_NT_HEADERS32 ntHeader;
// 验证过程
BOOL bValid = FALSE;
DWORD dwRead;
// 读取DOS头
::ReadFile(hFile, &dosHeader, sizeof(dosHeader), &dwRead, NULL);
if(dwRead == sizeof(dosHeader))
{ if(dosHeader.e_magic == IMAGE_DOS_SIGNATURE) // 是不是有效的DOS头?
{ // 定位NT头
if(::SetFilePointer(hFile, dosHeader.e_lfanew, NULL, FILE_BEGIN) != -1)
{ // 读取NT头
::ReadFile(hFile, &ntHeader, sizeof(ntHeader), &dwRead, NULL);
if(dwRead == sizeof(ntHeader))
{ if(ntHeader.Signature == IMAGE_NT_SIGNATURE) // 是不是有效的NT头
bValid = TRUE;
}
}
}
}
// 显示结果
if(bValid)
MessageBox(NULL, "是一个PE格式的文件!", "ValidPE", MB_OK);
else
MessageBox(NULL, "不是一个PE格式的文件!", "ValidPE", MB_OK);
::CloseHandle(hFile);
return FALSE;
}
上述代码简单明确,先利用Windows定义的宏 IMAGE_DOS_SIGNATURE判断DOS头,比较DOS头的e_magic字段;再通过DOS头的e_lfanew字段定位到NT头;最后检查 NT头的Signature字段是不是IMAGE_NT_SIGNATURE(即“PE"0"0”)。
8.1.5 MFC的支持(CFile类)
CFile是一个相当简单的封装了一部分文件I/O 处理函数的类。它的成员函数用于打开和关闭文件、读写文件数据、删除和重命名文件、取得文件信息。它的公开成员变量m_hFile保存了与CFile对象 关联的文件的文件句柄。一个受保护的CString类型的成员变量m_strFileName保存了文件的名称。成员函数GetFilePath、 GetFileName和GetFileTitle能够用来提取整个或者部分文件名。比如,如果完整的文件名是“C:"MyWork" File.txt”,GetFilePath返回整个字符串,GetFileName返回“File.txt”,GetFileTitle返回 “File”。
但是详述这些函数就会忽略CFile类的特色,这就是用来写数据到磁盘和从磁盘读数据的函数。下面简单介绍CFile类用法。
1.打开和创建文件
使用CFile类打开文件有两种方法。
(1)构造一个未初始化的CFile对象,调用CFile::Open函数。下面的部分代码使用这个技术以读写权限打开一个名称为File.txt的文件。
CFile file;
if(file.Open(_T ("File.txt"), CFile::modeReadWrite))
{ // 打开文件成功}
CFile::Open函数的返回值是BOOL类型的变量。如果打开文件出错,还想进一步了解出错的原因,可以创建一个CFileException对象,传递它的地址到Open函数的第3个参数。
CFile file;
CFileException e;
if (file.Open(_T ("File.txt"), CFile::modeReadWrite, &e))
{ // 打开文件成功}
else
{ // 打开文件失败,告诉用户原因
e.ReportError();
}
如果打开失败,CFile::Open函数会使用描 述失败原因的信息初始化一个CFileException对象。ReportError成员函数基于这个信息显示一个出错对话框。可以通过检查 CFileException类的公有成员m_cause找到导致这个错误的原因。
(2)使用CFile类的构造函数。可以将创建文件对象和打开文件合并成一步,如下面代码所示。
CFile file(_T ("File.txt"), CFile::modeReadWrite);
如果文件不能被打开,CFile的构造函数会抛出一个CFileException异常。因此,使用CFile::CFile函数打开文件的代码通常使用try和catch块来捕获错误。
try
{ CFile file(_T ("File.txt"), CFile::modeReadWrite);
}
catch(CFileException* e)
{ // 出错了!
e->ReportError();
e->Delete();
}
删除MFC抛出的异常是程序写作者的责任,所以在程序中处理完异常之后要调用异常对象的Delete函数。
为了创建一个文件而不是打开一个存在的文件,要在CFile::Open或者CFile构造函数的第二个参数中包含上CFile::modeCreate标记,如下代码所示。
CFile file(_T("File.txt"), CFile::modeReadWrite | CFile::modeCreate);
如果以这种方式创建的文件存在,它的长度会被截为0。为了在文件不存在时创建它,存在的时候仅打开而不截去,应再包含上CFile::modeNoTruncate标记,如下面代码所示。
CFile file(_T("File.txt"), CFile::modeReadWrite | CFile::modeCreate | CFile::modeNoTruncate);
默认情况下,由CFile::Open或 CFile::CFile打开的文件使用的是独占模式,即CreateFile API中的第3个参数dwShareMode被设为了0。如果需要,在打开文件时也可以指定一个共享模式,以明确同意其他访问此文件的操作。这里是4个可 以选择的共享模式:
l CFile::shareDenyNone 不独占这个文件
l CFile::shareDenyRead 拒绝其他代码对这个文件进行读操作
l CFile::shareDenyWrite 拒绝其他代码对这个文件进行写操作
l CFile::shareExclusive 拒绝其他代码对这个文件进行读和写操作(默认)
另外,还可以指定下面3个对象访问类型中的一个:
l CFile::modeReadWrite 请求读写访问
l CFile::modeRead 仅请求读访问
l CFile::modeWrite 仅请求写访问
常用的做法是允许其他程序以只读方式打开文件,但是拒绝它们写入数据。
CFile file(_T("File.txt"), CFile::modeReadWrite | CFile::modeCreate | CFile::modeNoTruncate);
如果在上面的代码执行之前,文件已经以可写的方式打开了,这个调用将会失败,CFile类会抛出CFileException异常,异常对象的m_cause成员等于CFileException::sharingViolation。
CFile类的成员函数Close会调用 CloseHandle API关闭应用程序打开的文件对象句柄。如果句柄没有关闭,类的析构函数也会调用Close函数关闭它。显式调用Close函数一般都是为了关闭当前打开 的文件,以便使用同样的CFile对象打开另一个文件。
2.读写文件
CFile类中从文件中读取数据的成员函数是Read。例如,下面的代码申请了一块4KB大小的文件I/O缓冲区,每次从文件读取4KB大小的数据。
BYTE buffer[4096];
CFile file (_T("File.txt"), CFile::modeRead);
DWORD dwBytesRemaining = file.GetLength();
while(dwBytesRemaining)
{ UINT nBytesRead = file.Read(buffer, sizeof(buffer));
dwBytesRemaining -= nBytesRead;
}
文件中未读取的字节数保存在 dwBytesRemaining变量里,此变量由CFile::GetLength返回的文件长度初始化。每次调用Read之后,从文件中读取的字节数 (nBytesRead)会从dwBytesRemaining变量里减去。直到dwBytesRemaining为0整个while循环才结束。
CFile类还提供了Write成员函数向文件写入数据,Seek成员函数移动文件指针,它们都和相关API一一对应。可以通过跟踪程序的执行来查看这些函数的实现代码。