2.13 文件操作

在C语言中,CRT库提供了一组操作文件的函数,比如打开文件fopen、读取文件fread、写入文件fwrite、关闭文件fclose等。在C++中,提供了不同功能的类来支持文件的输入输出,比如ofstream是一个用来写文件的类、ifstream是一个读文件的类、fstream是一个可同时读写操作的文件类,等等。

在Visual C++开发中,除了可以使用上述两种方式来操作文件外,还可以使用Win32 API函数来操作文件,以及MFC类(比如CFile、CStdioFile)来操作文件。

2.13.1 Win32 API操作文件

Visual C++提供了一组API函数来操作文件,比如打开文件、读写文件、复制文件、关闭文件等,并且用一个句柄来表示一个已经打开的文件对象,这个句柄通常称为文件句柄。我们看一下Win32 API中对文件操作的几个常用函数。

(1)CreateFile函数

该函数用来创建或打开一个文件。函数声明如下:

        HANDLE       CreateFile(  LPCTSTR  lpFileName,    DWORD  dwDesiredAccess,  DWORD
    dwShareMode,          LPSECURITY_ATTRIBUTES          lpSecurityAttributes,          DWORD
    dwCreationDisposition, DWORD dwFlagsAndAttributes,  HANDLE  hTemplateFile);

其中参数lpFileName表示要创建或打开的文件的名字;dwDesiredAccess表示对文件的访问权限,是只读还是只写,还是既要读又要写,如果取值为GENERIC_READ表示允许对文件进行读访问;如果为GENERIC_WRITE表示允许对文件进行写访问(可组合使用,比如GENERIC_READ| GENERIC_WRITE),如果为零,表示只允许获取与文件有关的信息;dwShareMode表示文件的共享属性,0表示不共享;FILE_SHARE_READ或FILE_SHARE_WRITE表示允许对文件进行读/写共享访问;lpSecurityAttributes指向一个SECURITY_ATTRIBUTES结构的指针,该结构定义了文件的安全特性;dwCreationDisposition表示文件如何创建,比如:

● CREATE_NEW:新建文件,但如果文件已经存在则会出错。

● CREATE_ALWAYS:创建文件,而且同名文件存在,会改写已经存在的文件。

● OPEN_EXISTING:文件必须已经存在,否则会出错。

● OPEN_ALWAYS:如文件不存在,则创建它。

● TRUNCATE_EXISTING:将现有文件缩短为零长度。

dwFlagsAndAttributes表示文件属性,它通常取值为下列一个或多个宏的组合,比如:

● FILE_ATTRIBUTE_ARCHIVE:标记归档属性。

● FILE_ATTRIBUTE_COMPRESSED:将文件标记为已压缩,或者标记为文件在目录中的默认压缩方式。

● FILE_ATTRIBUTE_NORMAL:默认属性。

● FILE_ATTRIBUTE_HIDDEN:隐藏文件或目录。

● FILE_ATTRIBUTE_READONLY:文件为只读。

● FILE_ATTRIBUTE_SYSTEM:文件为系统文件。

● FILE_FLAG_WRITE_THROUGH:操作系统不得推迟对文件的写操作。

● FILE_FLAG_OVERLAPPED:允许对文件进行重叠操作。

● FILE_FLAG_NO_BUFFERING:禁止对文件进行缓冲处理。文件只能写入磁盘卷的扇区块。

● FILE_FLAG_RANDOM_ACCESS:针对随机访问对文件缓冲进行优化。

● FILE_FLAG_SEQUENTIAL_SCAN:针对连续访问对文件缓冲进行优化。

● FILE_FLAG_DELETE_ON_CLOSE:关闭了上一次打开的句柄后,将文件删除,特别适合临时文件。

hTemplateFile表示一个模板文件的句柄,该文件具有只读访问权限。如果函数执行成功,则返回文件句柄;否则返回INVALID_HANDLE_VALUE,可以调用函数GetLastError来获得错误码。

(2)WriteFile函数

该函数用来向某文件的特定位置写入数据,这个特定位置由文件指示器所确定。这个函数既可以用于同步操作也可以用于异步操作。函数声明如下:

        BOOL WINAPI WriteFile(  HANDLE hFile, LPCVOID lpBuffer, DWORD
    nNumberOfBytesToWrite,
        LPDWORD lpNumberOfBytesWritten,  LPOVERLAPPED lpOverlapped);

其中参数hFile是文件句柄;lpBuffer指向一个缓冲区,该缓冲区中的内容就是要写入到文件中的内容;nNumberOfBytesToWrite为要向文件写入的字节数;lpNumberOfBytesWritten指向一个变量,该变量返回实际写入的字节数;lpOverlapped指向一个OVERLAPPED结构,当文件句柄hFile的打开方式是FILE_FLAG_OVERLAPPED,则该结构被需要。如果函数成功,返回非零;否则返回零。

(3)ReadFile函数

该文件从某个文件中读取数据,读取的位置由当前文件指示器确定。该函数可以用来设计成同步操作或异步操作。函数声明如下:

        BOOL WINAPI ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD
    nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead,  LPOVERLAPPED lpOverlapped);

其中参数hFile是文件句柄;lpBuffer指向一个缓冲区,该缓冲区用来存放从文件中读取的数据;nNumberOfBytesToRead要读取的数据字节数;lpNumberOfBytesRead实际读到的字节数;lpOverlapped指向OVERLAPPED结构,当文件句柄hFile的打开方式是FILE_FLAG_OVERLAPPED,则该结构被需要。如果函数成功,返回非零;否则返回零。

(4)SetFilePointer函数

该函数移动一个打开文件的指针,这个指针也称指示器(也称文件指针,这指针不是C语言中的指针),用来指示文件中的当前位置,以便在一个文件中设置一个新的读取或写入位置。函数声明如下:

        DWORD     SetFilePointer(HANDLE     hFile,     LONG     lDistanceToMove,     PLONG
    lpDistanceToMoveHigh, DWORD dwMoveMethod);

其中参数hFile为文件句柄;lDistanceToMove为偏移量(低位); lpDistanceToMoveHigh为偏移量(高位); dwMoveMethod决定文件指针的起始位置,取值如下:

● FILE_BEGIN:起始位置是文件开头。

● FILE_CURRENT:起始位置是当前位置。

● FILE_END:起始位置是文件末尾。

如果函数成功并且lpDistanceToMoveHigh为NULL,它返回文件指针的DWORD值的低字节序;如果函数成功并且lpDistanceToMoveHigh不为NULL,它返回文件指针的DWORD值的低字节序部分,并且lpDistanceToMoveHigh取值为文件指针的DWORD值的高字节序部分;如果函数失败,它返回INVALID_SET_FILE_POINTER,可以用函数GetLastError来获取错误码。

下列代码用来移动文件指针:

        LARGE_INTEGER li={0};
        li.QuadPart = 22333; //移动的位置
        li.LowPart = SetFilePointer(handle, li.LowPart, &li.HighPart, FILE_BEGIN); //移动文件指针

LARGE_INTEGER是用来定义一个64位的有符号整数值。

        typedef union _LARGE_INTEGER {
            struct {
              DWORD LowPart;
              LONG HighPart;
            };
            struct {
              DWORD LowPart;
              LONG HighPart;
            } u;
            LONGLONG QuadPart;
        } LARGE_INTEGER,  *PLARGE_INTEGER;

LARGE_INTEGER其实是个联合体,它通常可以表示数的范围为-3689348814741910324到+4611686018427387903。

【例2.52】 文件API函数简单应用

(1)新建一个对话框工程。

(2)切换到资源视图,在对话框上删除所有控件,然后添加一个按钮,并添加事件代码如下:

        void CTestDlg::OnBnClickedButton1()
        {
            // TODO:  在此添加控件通知处理程序代码
            HANDLE handle;
            DWORD Num;
            handle  =  ::CreateFile(_T("new.dat"),  GENERIC_READ  |  GENERIC_WRITE,  0,
    NULL, OPEN_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, NULL);
            if (INVALID_HANDLE_VALUE ! = handle)
            {
                ::SetFilePointer(handle,0,0, FILE_BEGIN); //设置文件指示器到开头
                TCHAR Buffer[] = _T("这个字符串是文件里的内容");
                ::WriteFile(handle, Buffer, sizeof(Buffer), &Num, NULL);
                ZeroMemory(Buffer, sizeof(Buffer));
                ::SetFilePointer(handle,0,0, FILE_BEGIN); //设置文件指示器到开头
                ::ReadFile(handle, Buffer, sizeof(Buffer), &Num, NULL);
                AfxMessageBox(Buffer); //显示读取的文件内容
                ::CloseHandle(handle);
            }
        }

(3)保存工程并运行,运行结果如图2-106所示。

图2-106

2.13.2 MFC类操作文件

MFC类库中也提供了对文件进行处理的类,通常用到的是CFile或CStdioFile类。

CFile类是MFC文件类的基类,提供非缓冲方式的二进制磁盘输入、输出功能,但该类只能提供访问本地文件内容的功能,不支持访问网络文件的功能。CFile常见成员如下:

(1)m_hFile句柄

该成员是个句柄,表示一个打开文件的操作系统文件句柄。

(2)hFileNull句柄

该成员也是个句柄,而且是个静态变量,它用来判断CFile对象是否拥有一个有效的句柄。可以用下列代码判断一个文件句柄是否有效:

        if (myFile.m_hFile ! = CFile::hFileNull) // myFile是CFile对象
          ;//处理文件操作
        else
          ;//显示文件句柄无效

(3)构造函数CFile

CFile的构造函数有多种形式:

        CFile( );
        CFile(HANDLE hFile );
        CFile(LPCTSTR lpszFileName, UINT nOpenFlags );

其中参数hFile是由API函数CreateFile成功打开文件后返回的句柄;lpszFileName为需要打开文件的路径字符串,这个路径可以是相对路径,也可以是绝对路径;nOpenFlags表示文件的存取共享模式,比如可以取值如下:

● CFile::modeCreate:创建一个新文件,如果那个文件已经存在,把那个文件的长度重设为0。

● CFile::modeNoTruncate:通常同modeCreate一起用,如果要创建的文件已经存在,并不把它的长度设置为0。

● CFile::modeRead:打开文件仅仅供读。

● CFile::modeReadWrite:打开文件供读写。

● CFile::modeWrite:打开文件只供写。

● CFile::modeNoInherit:阻止这个文件被子进程继承。

● CFile::shareDenyNone:打开这个文件同时允许其他进程读写这个文件。

● CFile::shareDenyWrite:打开文件拒绝其他任何进程写这个文件。

比如,我们可以这样打开一个文件:

        HANDLE hFile = CreateFile(_T("CFile_File.dat"),
          GENERIC_WRITE, FILE_SHARE_READ,
          NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        if (hFile == INVALID_HANDLE_VALUE)
          AfxMessageBox(_T("Couldn't create the file! "));
        else
        {
          // 把文件句柄依附到CFile对象
          CFile myFile(hFile);
          /*
        文件读写操作
            */
          myFile.Close(); //关闭文件句柄
        }

(4)Open函数

该函数用来打开一个文件,函数声明如下:

        BOOL Open( LPCTSTR lpszFileName,  UINT nOpenFlags,  CFileException* pError = NULL );

其中参数lpszFileName为需要打开文件的路径字符串,这个路径可以是相对路径也可以是绝对路径;nOpenFlags表示文件的存取共享模式,它指定文件打开时可以采取的操作。可以使用‘|’号来组合多个选项。比如可以取值如下:

● CFile::modeCreate:创建一个新文件,如果那个文件已经存在,把那个文件的长度重设为0。

● CFile::modeNoTruncate:通常同modeCreate一起用,如果要创建的文件已经存在,并不把它长度设置为0。

● CFile::modeRead:打开文件仅仅供读。

● CFile::modeReadWrite:打开文件供读写。

● CFile::modeWrite:打开文件只供写。

● CFile::modeNoInherit:阻止这个文件被子进程继承。

● CFile::shareDenyNone:打开这个文件同时允许其他进程读写这个文件。

● CFile::shareDenyWrite:打开文件拒绝其他任何进程写这个文件。

pError指向一个文件异常类CFileException的对象。如果打开成功,函数返回非零;否则返回零。

比如下列代码演示了用Open打开文件:

        CFile f;
        CFileException e;
        TCHAR* pszFileName = _T("Open_File.dat");
        if(! f.Open(pszFileName, CFile::modeCreate | CFile::modeWrite, &e))
          TRACE(_T("File could not be opened %d\n"), e.m_cause);

(5)GetFileName函数

该函数得到文件的名字。函数声明如下:

        CString GetFileName();

函数返回文件的名字。

比如,c:\windows\write\下有个文件myfile.txt,函数GetFileName返回的是“myfile.txt”。

(6)GetFilePath函数

该函数得到文件的全路径。函数声明如下:

        CString GetFilePath();

函数返回文件的全路径。

比如,c:\windows\write\下有个文件myfile.txt,则GetFilePath返回的是“c:\windows\write\myfile.txt”。

(7)GetFileTitle函数

该函数得到文件的标题。函数声明如下:

        CString GetFileTitle();

函数返回文件的标题。

比如,c:\windows\write\下有个文件myfile.txt,则GetFileTitle返回的是“myfile”。

(8)GetLength函数

该函数得到文件的长度。函数声明如下:

            ULONGLONG GetLength();

函数返回文件的长度。

比如下列代码返回一个文件的长度:

        CFile* pFile = NULL;
        pFile = new CFile(_T("C:\\WINDOWS\\SYSTEM.INI"),
        CFile::modeRead | CFile::shareDenyNone);
        ULONGLONG dwLength = pFile->GetLength();
        CString str;
        str.Format(_T("Your SYSTEM.INI file is %I64u bytes long."), dwLength);
        pFile->Close();
        delete pFile;
        AfxMessageBox(str);

(9)SeekToBegin函数

该函数重定位当前文件指针到文件的开头。函数声明如下:

        void SeekToBegin( );

(10)SeekToEnd函数

该函数重定位当前文件指针到文件的结尾。函数声明如下:

        void SeekToEnd ( );

比如,可以通过SeekToEnd函数获得文件大小:

        CFile f;
        f.Open(_T("Seeker_File.dat"), CFile::modeCreate |CFile::modeReadWrite);
        f.SeekToBegin(); //移动文件指针到文件开头
        ULONGLONG ullEnd = f.SeekToEnd(); //可以获得文件的大小

(11)Read函数

该函数从文件中读取nCount字节到缓冲区。函数声明如下:

        UINT Read(void* lpBuf, UINT nCount );

其中参数lpBuf指向一个缓冲区,该缓冲区存放从文件中读取的数据;nCount为要读取的文件字节数。函数返回实际读取到的字节数,该值可能小于nCount。

(12)Write函数

该函数向文件写入数据。函数声明如下:

        void Write(  const void* lpBuf, UINT nCount );

其中参数lpBuf指向一个缓冲区,该缓冲区存放要写入到文件中的数据;nCount为要写入的数据的字节数。

(13)Flush函数

该函数强制系统缓存的内容马上写入文件。写数据时先写入内存(系统缓存),然后再写入文件。该函数只是为了确保数据尽快被写入文件,如果不调用flush函数,系统缓存达到一定的数据量也会自动写入磁盘文件,或者在关闭文件的时候也会把缓冲区的数据(如果有)强制写入磁盘文件。如果不是多线程写同一个文件,可以不用flush函数,最后结束前记得close就可以了。

比如下列代码演示了文件读取和写入的过程。

        CFile cfile;
        cfile.Open(_T("Write_File.dat"), CFile::modeCreate | CFile::modeReadWrite);
        char pbufWrite[100];
        memset(pbufWrite, 'a', sizeof(pbufWrite));
        cfile.Write(pbufWrite, 100);
        cfile.Flush();
        cfile.SeekToBegin();
        char pbufRead[100];
        cfile.Read(pbufRead, sizeof(pbufRead));
        ASSERT(0 == memcmp(pbufWrite, pbufRead, sizeof(pbufWrite)));

下面来看个CFile使用的例子,对一个临时文件进行读写。临时文件是程序运行时建立的临时使用的文件,它通常位于C:\Windows\Temp目录下,扩展名为tmp。临时文件的使用方法基本与常规文件一样,只是文件名应该调用API函数GetTempFileName()获得,该函数声明如下:

        UINT   GetTempFileName(   LPCTSTR   lpPathName,   LPCTSTR   lpPrefixString,   UINT uUnique, LPTSTR lpTempFileName);

其中参数lpPathName是临时文件的目录路径,它通常由API函数GetTempPath获得;lpPrefixString是临时文件的文件名前缀;uUnique指定生成文件名的十六进制数字,该参数和参数lpPrefixString一起形成临时文件名,如果该参数为零,则函数会使用系统时间来和前缀形成文件名;lpTempFileName指向一个缓冲区,缓冲区里存放获得的临时文件名。如果函数成功并且uUnique非零,则返回uUnique的值;如果uUnique为零,则返回文件名长度;如果函数失败,则返回零。

【例2.53】 读写一个临时文件

(1)新建一个控制台工程,并在应用程序向导中勾选“MFC”,这样可以在控制台程序中使用MFC库中的类或函数,如图2-107所示。

图2-107

(2)打开Test.cpp,在其中输入代码如下:

        #include "stdafx.h"
        #include "Test.h"
        #ifdef _DEBUG
        #define new DEBUG_NEW
        #endif
        // 唯一的应用程序对象
        CWinApp theApp;
        using namespace std;
        int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
        {
            int nRetCode = 0;
            HMODULE hModule = ::GetModuleHandle(NULL);
            if (hModule ! = NULL)
            {
                // 初始化MFC并在失败时显示错误
                if (! AfxWinInit(hModule, NULL, ::GetCommandLine(), 0))
                {
                    // TODO:  更改错误代码以符合您的需要
                    _tprintf(_T("错误:  MFC初始化失败\n"));
                    nRetCode = 1;
                }
                else
                {
                    // TODO:  在此处为应用程序的行为编写代码。
                    TCHAR szTempPath[_MAX_PATH], szTempfile[_MAX_PATH];
                    GetTempPath(_MAX_PATH, szTempPath);
                    GetTempFileName(szTempPath, _T("my_"), 16, szTempfile);
                    CFile     tmpfile(szTempfile,     CFile::modeCreate     |     CFile::modeReadWrite);
                    char sz[] = "abc\r\n我们大家";
                    tmpfile.Write(sz, strlen(sz));
                    tmpfile.Close();
                }
            }
            else
            {
                // TODO:  更改错误代码以符合您的需要
                _tprintf(_T("错误:  GetModuleHandle失败\n"));
                nRetCode = 1;
            }
            return nRetCode;
        }

上面代码会在“x:\Users\Administrator\AppData\Local\Temp”下新建一个my_10.tmp文件,然后在其中写入数据。x是系统盘符。

(3)保存工程并运行,运行结果如图2-108所示。

图2-108

除了类CFile, MFC中还使用CStdioFile类封装了C++运行时刻文件流的操作,流文件采用缓冲方式,支持文件模式和二进制模式文件操作,默认方式为文本模式。CStdioFile类从CFile类继承,具有如下三个构造函数:

        CStdioFile();
        CStdioFile(FILE* pOpenStream);
        CStdioFile(LPCTSTR lpszFileName, UINT nOpenFlags);

其中参数pOpenStream是FILE指针;lpszFileName为要打开的文件的路径,可以是绝对路径或相对路径;nOpenFlags是打开文件的方式,它可以取值如下:

● CFile::modeCreate:创建新文件,并覆盖已有文件。

● CFile::modeRead:以只读方式打开文件。

● CFile::modeReadWrite:以读/写方式打开文件。

● CFile::modeWrite:以只写方式打开文件。

● CFile::shareExclusive:不允许其他进程读/写文件。

● CFile::typeText:表示以文本方式打开文件。

● CFile::typeBinary:表示以二进制方式打开文件。

比如我们可以这样新建打开一个文件:

        TCHAR* pFileName = _T("CStdio_File.dat");
        CStdioFile f1;
        if(! f1.Open(pFileName,       CFile::modeCreate       |       CFile::modeWrite       |       CFile::typeText))
          TRACE(_T("Unable to open file\n"));
        CStdioFile f2(stdout);
        try
        {
          CStdioFile    f3(    pFileName, CFile::modeCreate    |    CFile::modeWrite    |     CFile::typeText );
        }
        catch(CFileException* pe)
        {
          TRACE(_T("File could not be opened, cause = %d\n"), pe->m_cause);pe->Delete();
        }

类CStdioFile的重要成员函数如下:

(1)ReadString函数

该函数读取一行文本到缓冲区,遇到“0x0D,0x0A”时停止读取,并且去掉硬回车“0x0D”,保留换行符“0x0A”,在字符串末尾添加字符‘\0'。nMax个字符里包含字符‘\0'。函数声明如下:

        LPTSTR ReadString(LPTSTR lpsz, UINT nMax );
        BOOL ReadString( CString& rString);

其中参数lpsz指向一个用户缓冲区,用来存放从文件中读到的数据;nMax为要读取的数据的最大字节数;rString是一个CString对象的引用,用来存放读取到的字符串数据。如果第一个函数成功,返回缓冲区指针,否则返回NULL;第二个函数如果文件未读完返回TRUE,否则返回FALSE。

比如下列代码可以在控制台上输入数据:

        CStdioFile f(stdin);
        TCHAR buf[100]=_T("");
        f.ReadString(buf, 99);

当文件存在多行数据需要逐行读取时,可用函数ReadString(CString& rString),当遇到‘\n’时读取截断,如果文件未读完,返回TRUE,否则返回FALSE,代码如下:

        //逐行读取文件内容,存入strRead
        while(file.ReadString(strRead))
        {
         ...;
        }

(2)WriteString函数

该函数向文件写入数据。函数声明如下:

        void WriteString(LPCTSTR lpsz );

其中参数lpsz指向一段NULL结尾的文本字符串。

【例2.54】 用WriteString向控制台窗口输出文本

(1)新建一个控制台工程,并且在应用程序向导中要勾选“MFC”。

(2)在Test.cpp中输入代码如下:

        #include "stdafx.h"
        #include "Test.h"
        #ifdef _DEBUG
        #define new DEBUG_NEW
        #endif
        // 唯一的应用程序对象
        CWinApp theApp;
        using namespace std;
        int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
        {
            int nRetCode = 0;
            HMODULE hModule = ::GetModuleHandle(NULL);
            if (hModule ! = NULL)
            {
                // 初始化MFC并在失败时显示错误
                if (! AfxWinInit(hModule, NULL, ::GetCommandLine(), 0))
                {
                    // TODO:  更改错误代码以符合您的需要
                    _tprintf(_T("错误:  MFC初始化失败\n"));
                    nRetCode = 1;
                }
                else
                {
                    // TODO:  在此处为应用程序的行为编写代码。
                    setlocale(LC_CTYPE, "chs");
                    CStdioFile f(stdout);
                    TCHAR buf[] = _T("阿凡达test string\n");
                    f.WriteString(buf);
                }
            }
            else
            {
                // TODO:  更改错误代码以符合您的需要
                _tprintf(_T("错误:  GetModuleHandle失败\n"));
                nRetCode = 1;
            }
            return nRetCode;
        }

(3)保存工程并运行,运行结果如图2-109所示。

图2-109