来自Windows核心编程 – 第十七章 第三节 – 使用内存映射文件。
本小节会讲到如何使用内存映射文件,在最后会附上一个实例教程。
要使用内存映射文件,需要执行下面三个步骤:
- 创建或打开一个文件内核对象,该对象标识了我们想要用作内存映射文件的那个磁盘文件。
- 创建一个文件映射内核对象来告诉系统文件的大小以及我们打算如何访问文件。
- 高速系统把文件映射对象的部分或全部映射到进程的地址空间中。
用完内存映射文件后,必须执行下面三个步骤来做清理工作:
- 告诉系统从进程地址空间中取消对文件映射内核对象的映射
- 关闭文件映射内核对象
- 关闭文件内核对象
17.3.1 第一步:创建或打开文件内核对象
我们总是调用 CreateFile 函数来创建或打开一个文件内核对象:
1 2 3 4 5 6 7 8 |
HADNLE CreateFile( PCSTR pszFileName, DWORD dwDesiredAccess, DWORD dwShareMode, PSECURITY_ATTRIBUTES psa, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); |
这里总共有七个参数,其中有几个不会详细讲解:
- pszFileName 表示想要创建或打开的文件的名称(可以包含路径,也可以不包含)
- dwDesiredAccess 用来指定打算如何访问文件的内容。稍后会有一个表列出。
- dwShareMode 告诉系统我们打算如何共享这个文件。同样会列表。
- psa 是安全对象。所有的内核对象都有这个参数。
- dwCreationDisposition 参数对文件的含义重大,会有列表给出。
- dwFlagsAndAttributes 参数有两个用途,一个是允许我们设置一些标志来微调与设备之间的通信,另一个是如果设备是一个文件,我们还能够设置文件的属性。后面我们会介绍通信标志和文件属性。
- hTemplateFile 既可以标识一个已经打开的文件句柄,也可以是NULL.
以上是几个参数的作用,接下来会对他们进行详细的讲解。
第一个参数:略…这有啥好讲的…文件名。
第二个参数是如何访问文件的内容,一般是以下四个值之一:
dwDesiredAccess 值 | 含义 |
0 | 既不能读取文件也内容,也不能写入数据。 如果想取得文件的属性,可以传入0,即NULL |
GENERIC_READ | 可以读取文件 |
GENERIC_WRITE | 可以写入文件 |
GENERIC_READ | GENERIC_WRITE | 既可以读取,也可以写入。 |
要注意的是,对内存映射文件来说,必须以只读或者读/写方式来打开文件。
第三个参数告诉系统我们打算如何共享这个文件:
dwShareMode 值 | 含义 |
0 | 其他任何试图打开文件的操作都会失败,不共享。 |
FILE_SHARE_READ | 其他任何试图通过 GENERIC_WRITE 来打开文件的操作都会失败 只读共享 |
FILE_SHARE_WRITE | 其他任何试图通过 GENERIC_READ 来打开文件的操作都会失败 唯写共享 |
FILE_SHARE_READ | FILE_SHARE_WRITE | 其他任何试图打开文件的操作都会成功,读写共享。 |
第四个参数:参考内核对象这一章:http://blog.tk-xiong.com/archives/518
第五个参数:列表如下:
dwCreationDisposition 值 | 含 义 |
CREATE_NEW | 创建一个新文件,如果同名文件已经存在,则会调用失败 |
CREATE_ALWAYS | 告诉CreateFile 无论同名文件存在与否都会创建一个新文件。 如果已存在则会覆盖原来的文件 |
OPEN_EXISTING | 打开一个已有的文件,如果文件不存在,那么会调用失败 |
OPEN_ALWAYS | 打开一个已有的文件,如果文件存在会直接打开 如果文件不存在,会创建一个新的文件 |
TRUNCATE_EXISTING | 打开一个已有的文件并将文件大小截断为0,如果文件不存在,则调用失败。 |
第六个参数:dwFlagsAndAttributes 参数的作用是设置一些标志来微调设备间的通信,如果设备是一个文件,我们还能够设置文件的一些属性。
通信标志都是一些信号,告诉系统我们打算以何种方式来访问设备。这样系统就可以对缓存算法进行优化。
下面我们会下你介绍通信标志,然后再介绍文件属性。
1. CreateFile 的高速缓存标志 —— 主要关注文件系统对象
FILE_FLAG_NO_BUFFERING 这个标志表示在访问文件的时候不要使用任何数据缓存。为了提高性能,系统在访问磁盘的时候会对数据进行缓存。我们通常不指定这个标志,于是告诉缓存管理器就能够将文件系统中最近访问的那部分保存在内存中。这样如果我们先从文件中读取几个字节,然后再读取几个字节,那么文件的数据可能在我们第二次读取之前就已经载入到了内存中,这样我们就只需要访问一次硬盘了,这样显著提高了性能。
但是要注意的是,速度的提升是从文件中读取超过实际需要的数据量来达到的。如果我们不再从文件中读取数据,可能会浪费内存。通过指定 FILE_FLAG_NO_BUFFERING 标志,告诉告诉告诉缓存器我们不希望它对任何数据进行缓存——我们自己对数据进行缓存。这个标志可以提高应用程序的性能和内存的使用效率。由于文件系统会将文件数据直接写入到我们提供的缓存中,因此我们必须遵循一定的规则:
- 在访问文件的时候,使用的偏移量必须正好是磁盘卷的扇区大小的整数倍。
- 读取/写入文件的字节数必须正好是扇区大小的整数倍。
- 必须确保缓存在进程地址空间中的起始地址正好是扇区大小的整数倍。
但是有一种情况下必须用 FILE_FLAG_NO_BUFFERING 这个参数。首先我们要知道的是,为了对一个文件进行管理,高速缓存器必须为该文件保存一些内部数据结构,文件越大,所需要的结构也就越多。在处理非常大的时候,高速缓存器可能无法分配文件所需的内部数据结构,从而导致文件打开失败。所以为了访问非常大的文件,我们必须设置这个标志来打开文件。
FILE_FLAG_SEQUENTIAL_SCAN 标志 和 FILE_FLAG_RANDOM_ACCESS 标志 只有当我们允许对文件数据进行缓存的时候,这些标志才有用。
如果指定了 FILE_FLAG_SEQUENTIAL_SCAN 标志,那么系统会认为我们将顺序地访问文件。当我们从文件中读取数据时,系统从文件中实际读取的数据量会超过我们所要求的数量。这个过程减少了对硬盘的访问速度并提高了应用程序的运行速度。但是如果我们重置文件指针,那么系统所花费的额外时间以及在内存中的数据就都狼给了。
如果我们经常需要重置文件指针的话,可以指定 FILE_RANDOM_ACCESS 标志,这个标志告诉系统不要提前读取文件数据。
FILE_FLAG_WRITE_THROUGH 这是最后一个与高速缓存有关的标志。它禁止对文件写入操作进行缓存以减少丢失数据的可能性。当我们指定这个标志的时候,系统会对所有文件的修改直接写入到磁盘中。但是系统仍然在内部的缓存中保存文件数据,这样文件读取操作会继续使用缓存中的数据,而不必直接从磁盘读取数据。如果用这个标志来打开网络服务器上的文件,那么只有在数据已经被写入到服务器磁盘后,各个Windows文件写入函数才会返回到调用线程。
2. CreateFile 的其他标志
FILE_FLAG_DELETE_ON_CLOSE 使用这个标志可以让文件系统在文件所有句柄都被关闭后,删除该文件。这个标志通常和 FILE_ATTRIBUTE_TEMPORARY 属性一起使用。当这两个标志一起使用的时候,系统可以创建一个临时文件,向文件中写入 数据,从文件中读取数据,最后关闭文件。当关闭文件的时候,系统会自动删除该文件。
FILE_FLAG_BACKUP_SEMANTICS 这个标志一般用于备份和恢复文件。在打开或创建任何文件之前,为了确保试图打开文件 或 创建文件的进程具有所需访问的特权,系统通常会执行安全性检查。但是备份和恢复软件有一定的特殊性,它们会跳过某些文件安全性检查。当我们指定了这个标志的时候,系统会检查调用者的存取令牌是否具备对文件和目录进行备份/恢复特权。如果调用者具备相应的特权,那么系统会允许它打开文件。我们也可以用这个标志来打开一个目录的句柄。
FILE_FLAG_POSIX_SEMANTICS 在Windows中,文件名会保留最初命名时使用的大小写,而在查找文件的时候文件名是不区分大小写的。但是POSIX子系统要求在查找文件的时候区分大小写。这个标志让CreateFile 在创建文件或打开文件的时候,以区分大小写的方式来查找文件名。在使用 FILE_FLAG_POSIX_SEMANTICS 的时候要极其小心,如果在创建一个文件的时候用这个标志,那么Windows应用程序可能无法访问该文件。
FILE_FLAG_OPEN_REPARSE_POINT 它告诉系统忽略文件的重解析属性。
重解析属性允许一个文件系统过滤器对打开文件、读取文件、写入文件以及关闭文件这些行为修改。
FILE_FLAG_OPEN_NO_RECALL 这个标志告诉系统不要将文件内容从脱机存储器(比如磁盘)恢复到关联存储器(比如硬盘)。
FILE_FLAG_OVERLAPPED 这个标志告诉我们以异步方式来访问。
3. 文件属性标志
文件属性标志 | 含义 |
FILE_ATTRIBUTE_ARCHIVE | 文件是一个存档文件。应用程序用这个标志来讲文件标记为待备份或待删除,一般创建一个新文件的时候会自动设置这个标志。 |
FILE_ATTRIBUTE_ENCRYPTED | 文件是经过加密的。 |
FILE_ATTRIBUTE_HIDDEN | 文件是隐藏的,它不会出现在通常的目录清单中。 |
FILE_ATTRIBUTE_NORMAL | 文件没用其他属性。只有单独使用的时候,这个标志才有效 |
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED | 内容索引服务不会对文件进行索引。 |
FILE_ATTRIBUTE_OFFLINE | 文件虽然存在,单文件内容已经被转移到脱机存储器中。 |
FILE_ATTRIBUTE_READONLY | 文件是只读的。可以读取文件,但不能写入或删除文件。 |
FILE_ATTRIBUTE_SYSTEM | 文件是操作系统的一部分,或专供操作系统使用。 |
FILE_ATTRIBUTE_TEMPORARY | 文件数据只会使用一小段时间。问了将访问时间降至最低,会尽量将文件数据保存在内存中而不是保存在磁盘中。 |
如果我们要创建临时文件的话,那么应该使用 FILE_ATTRIBUTE_TEMPORARY 标志。当我们把这个标志和前面介绍到的 FILE_FLAG_DELETE_ON_CLOSE 标志组合使用的时候,可以提高系统的性能。当我们关闭的时候,会直接删除它。
第七个参数:刚才讲了那么久…我们还在讨论CreateFile的参数还记得吗…
接下来是hTemplateFile,它既可以标识一个已经打开的文件的句柄,也可以是NULL。
如果hTemplateFile标识一个文件句柄,那么CreateFile会完全忽略 dwFlagsAndAttributes 参数,并转而使用 hTemplateFile 所标识的文件的属性。为了能够让函数以这种方式工作,hTemplateFile标识的文件必须是一个已经用GENERIC_READ 标志打开的文件。
如果CreateFile要打开已有文件而不是创建新文件,那么它会忽略hTemplateFile参数。
如果CreateFile成功地创建或打开了文件或设备,那么它会返回文件或设备句柄。
如果CreateFile失败了,那么它会返回 INVALID_HANDLE_VALUE
大多数以句柄为返回值的Windows函数在失败的时候会返回NULL,但是在CreateFile返回的却是 INVALID_HANDLE_VALUE (-1) 。所以如果要判断是否创建成功的正确代码如下:
1 2 3 4 5 6 7 8 9 |
HADNLE hFile = CreateFile(...); if(hFile == INVALID_HANDLE_VALUE) { //Not Create } else { //Success } |
17.3.2 第二步:创建文件映射内核对象
调用CreateFile时为了告诉操作系统文件映射的物理存储器所在的位置。传入路径是文件在磁盘上所在的位置,文件映射对象的物理存储器来自该文件。现在我们就要告诉系统文件映射内存对象需要多大的物理存储器。
这个步骤需要调用 CreateFileMapping 函数:
1 2 3 4 5 6 7 |
HANDLE CreateFileMapping( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD fdwProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName); |
六个参数,讲述其中四个…
第一个参数 hFile:
是需要映射到进程地址空间的文件的句柄。该句柄是前面调用CreateFile的时候返回的。
第二个参数是安全对象。传递NULL就好了
第三个参数是一个保护属性。一般还是下面五个属性:
保护属性 | 含义 |
PAGE_READONLY | 完成对文件映射对象的映射时,可以读取文件中的数据。在调用CreateFile时必须传入 GENERIC_READ |
PAGE_READWRITE | 可以读/写文件数据。调用CreateFile时必须传入 GENERIC_READ | GENERIC_WRITE |
PAGE_WRITECOPY | 可以读写,并且写入操作会创建一个副本。调用CreateFile时必须传入GENERIC_READ 或者 GENERIC_READ|GENERIC_WRITE |
PAGE_EXECUTE_READ | 可以读可运行,在调用CreateFile时必须传入 GENERIC_READ 和 GENERIC_EXECUTE |
PAGE_EXECUTE_READWRITE | 可读可写运行,在调用CreateFile时必须传入 GENERIC_READ、GENERIC_WRITE 和 GENERIC_EXECUTE. |
除了前面提到的页面保护属性,我们还可以把五种段属性与 CreateFileMapping 的 fdwProtect 参数按位或起来。这个“段”只不过是内存映射的另一种叫法。
这些段属性的第一个是 SEC_NOCACHE ,它告诉系统不要对内存映射文件的页面进行缓存。因此如果把数据写入文件,那么与通常的情况相比,系统会更加频繁地更新磁盘上的文件。这个标志和 PAGE_NOCACHE 保护属性相似,主要是给驱动程序开发人员使用的。普通应用程序一般不会用到。
第二个段属性是 SEC_IMAGE,它告诉系统要映射的文件是一个PE文件映像。当系统把文件映射到进程地址空间的时候,系统会检查文件的内容并决定应该给各页面指定何种保护属性。
第三个段属性 和 第四个段属性分别是 SEC_RESERVE 和 SEC_COMMIT 这里不讲。
最后一个段属性是 SEC_LARGE_PAGES,它告诉Windows要为内存映射文件使用大页面内存。只有当用于PE映像文件或内存映射文件的时候,这个属性才是有效的。
接下里两个参数是很重要的,分别是 dwMaximumSizeHigh 和 dwMaximumSizeLow。
因为CreateFileMapping函数的主要目的是为了确保有足够的物理存储器可供文件映射对象使用。这两个参数告诉系统内存映射文件的最大大小。以字节为单位。由于Windows支持最大文件大小可以用64位整数表示,因此这里必须使用过两个32位值。其中 High表示的是高32位…Low表示的是低32位。对小于4GB的文件来说,dwMaximumSizeHigh这个参数永远都是0.
如果想要用当前的文件大小创建一个文件映射对象,那么只要传0给这两个参数就可以了。如果想要读取文件或者在不改变文件大小的前提下访问文件,那么同样需要传0给这两个参数。
如果想要给文件追加数据,那么在选择文件最大大小的时候应该留有余地了。
要注意的是,如果当前文件大小是0,那么我们就不能给这两个参数传递0.因为这样做就相当于告诉系统我们想要一个大小为0的内存映射对象。CreateFileMapping函数会认为这样是错误的,并返回NULL.
最后一个参数是 pszName,是一个以0为终止符的字符串,用来给文件映射内存对象指定一个名称。这个名称用来在不同的进程间共享文件映射对象。通常不需要共享的话,传递NULL即可。
如果无法创建文件映射对象的话,我们会返回NULL…
17.3.3 第三步:将文件的数据映射到进程的地址空间
前一步我们创建了文件映射对象,接下来我们要为文件的数据预定一块地址空间并将文件的数据作为物理存储器调拨给区域。这一步通过 MapViewOfFile 来实现:
1 2 3 4 5 6 |
PVOID MapViewOfFile( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap); |
五个参数…
第一个参数 hFileMappingObject 是文件映射对象的句柄,他是之前调用 CreateFileMapping 或 OpenFileMapping 函数返回的。
第二个参数 dwDesiredAccess 表示想要如何访问文件数据…是的,我们必须再次指定我们打算如何访问文件的数据。可以指定下表中的5个值之一:
保护属性 | 含义 |
FILE_MAP_WRITE | 可以读取和写入文件,在调用CreateFileMapping的时候必须传入PAGE_READWRITE属性。 |
FILE_MAP_READ | 可以读取文件。在调用CreateFileMapping的时候可以传入 PAGE_READONLY 或 PAGE_READWRITE 保护属性。 |
FILE_MAP_ALL_ACCESS | 等同于 FILE_MAP_WRITE | FILE_MAP_READ | FILE_MAP_COPY |
FILE_MAP_COPY | 可以读取和写入文件。写入操作会导致系统为该页面创建一份副本。在调用 CreateFileMapping 时必须传入 PAGE_WRITECOPY 保护属性。 |
FILE_MAP_EXECTUE | 可以将文件中的数据作为代码来执行。在调用CreateFileMapping 时可以传递 PAGE_EXECUTE_READWRITE 或者 PAGE_EXECUTE_READ 保护属性。 |
剩下的三个参数与预定地址空间区域和给区域调拨物理存储器有关。当我们把一个文件映射到进程的地址空间的时候,不必一下子映射整个文件。可以每次只把文件的一小部分映射到地址空间中。文件中被映射到进程地址空间中的部分被称为视图,其名称 MapViewOfFile(文件的映射视图) 便源于此了。
把文件的一个视图映射到进程的地址空间的时候,必须告诉系统两件事:
第一,必须告诉系统应该把数据文件中的哪个字节映射到视图中的第一个字节。这是通过参数dwFileOffsetHigh 和 dwFileOffsetLow来指定的。要注意的是文件的偏移量必须是系统分配粒度的整数倍(Win – 64KB)
第二,我们必须告诉系统要把数据文件中的多少映射到地址空间中去。这和预订地址空间时需要指定区域的大小一样。参数 dwNumberOfBytesToMap 用来指定大小。如果指定的大小为0,那么系统会试图把文件中从偏移量开始到文件末尾的所有部分都映射到视图中。
注意这一点:无论整个文件映射对象有多大,MapViewOfFile 只需要找到一块足够大的地址空间区域来容纳指定的视图。
如果在调用MapViewOfFile 的时候 指定了 FILE_MAP_COPY 标志,那么系统会从页交换文件中调拨物理存储器,调拨的物理存储器的大小由 dwNumberOfBytesToMap 参数决定。对文件映射视图进程操作时,只要我们不执行读取数据之外的任何操作,系统就不会用到从页交换文件中调拨的页面。但是一旦哪个线程写入文件映射视图中的任何内存地址,系统就会从页交换文件中已调拨的页面中选择一个页面,把原始数据复制到页交换文件中的页面,然后把复制的页面映射到进程的地址空间中。此后,各线程都会访问数据的副本,而不是访问或修改原始的数据了。
系统对原始数据进行复制是,会把页面的保护属性从 PAGE_WRITECOPY 改成 PAGE_READWRITE 。
17.3.4 第四步:从进程的地址空间撤销对文件数据的映射
不再需要把文件的数据映射到进程的地址空间中时,可以调用下面的函数来释放内存区域:
1 |
BOOL UnmapViewOfFile(PVOID pvBaseAddress); |
这个函数唯一的参数 pvBaseAddress 用来指定区域的基地址,它必须和 MapViewOfFile函数 的返回值相同。
出于对速度的考虑,系统会对文件数据的页面进行缓存处理,这样在处理文件映射视图的时候就不需要随时更新磁盘上的文件。如果需要确保所做的修改已经被写入到磁盘中,那么可以调用 FlushViewOfFile ,这个函数用来强制系统把部分或者全部修改过的数据写回到磁盘中:
1 2 3 |
BOOL FlushViewOfFile( PVOID pvAddress, SIZE_T dwNumberOfBytesToFlush); |
两个参数,一个是内存映射文件视图中第一个字节的地址。函数会把传入的地址向下取整到页面大小的整数倍。
第二个参数表示要刷新的字节数,会把这个参数向上取整到页面大小的整数倍。
如果没有修改任何数据的话,会直接返回。
UnmapViewOfFile 有一个特征是需要牢记的。如果试图最初是用 FILE_MAP_COPY 标志映射的,那么对文件数据的修改实际上是对保存在也交换文件中的文件数据的副本的修改。如果在这里种情况下调用 UnmapViewOfFile 函数的话,不会对磁盘文件进行任何更新,但它会释放页交换文件中的页面,从而导致数据丢失。
如果希望保留修改过的数据的话,就必须进行额外的操作了。
例如:可以为同一个文件 用PAGE_READWRITE 创建另一个文件映射对象,并用 FILE_MAP_WRITE 标志把这个新的 文件映射对象 映射到进程的地址空间中。然后可以在第一个视图中查找具有 PAGE_READWRITE 保护属性的页面,只要找到一个具有该保护属性的页面,就可以对其内容进行检查,并决定是否需要将修改过的数据写入文件。如果不写入的话继续查找就好了,直到文件尾截止。如果想保存的话,把数据页面从第一个视图复制到第二个视图即可。由于第二个视图的保护属性是 PAGE_READWRITE,所以会更新文件位于磁盘上的实际内容。
17.3.5 第五步和第六步:关闭文件映射对象 和 文件对象
调用两次 CloseHandle 函数,每次关闭一个句柄。
我们必须关闭自己打开的任何内核对象,不然会再进程继续运行的过程中引起资源泄露。
整个过程大致如下:
1 2 3 4 5 6 7 8 9 |
HANDLE hFile = CreateFile(...); HANDLE hFileMapping = CreateFileMapping(hFile, ...); PVOID pvFile = MapViewOfFile(hFileMapping, ...); //Use the memory-mapped file. UnmapViewOfFile(pvFile); CloseHandle(hFileMapping); CloseHandle(hFile); |
当然还有另外一种方法,不过这里就不列举了。提一下:引用计数的使用。
17.3.6 这里以 File Reverse 示例程序为例来进行内存映射文件的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
#include <windows.h> //3300 为基础 #define IDB_Base 3300 #define IDB_Browse IDB_Base+1 #define IDB_Reverse IDB_Base+2 #define IDC_Text IDB_Base+3 //文本框 HWND hStatic; //文件Reverse用到 TCHAR szPathname[MAX_PATH]; OPENFILENAME ofn = {OPENFILENAME_SIZE_VERSION_400}; inline void chMB(PCSTR szMsg) { char szTitle[MAX_PATH]; GetModuleFileNameA(NULL, szTitle, _countof(szTitle)); MessageBoxA(GetActiveWindow(), szMsg, szTitle, MB_OK); } BOOL FileReverse(PCTSTR pszPathname, PBOOL pbIsTextUnicode) { // 假定是Unicode // Assume text is Unicode *pbIsTextUnicode = FALSE; // 读写方式打开存在的文件 // Open the file for reading and writing. HANDLE hFile = CreateFile(pszPathname, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); //判断是否打开成功 - 注意这里不是和 NULL 比较 if(hFile == INVALID_HANDLE_VALUE) { chMB("File could not be opened."); return(FALSE); } // 获取文件大小 (我假定整个文件是可以被映射的) // Get the size of the file (I assume the whole file can be mapped). DWORD dwFileSize = GetFileSize(hFile, NULL); // 创建 文件映射对象,这个文件映射对象比文件的大小 多一个字符大小,这样就可以在文件末尾放一个值为0的字符使之成为字符串 // 因为我不知道文件格式是 ANSI 或者是 Unicode 字符串,我假定最糟糕的情况并添加WCHAR的大小而不是CHAR的大小。 // Create the file-mapping object. The file-mapping object is 1 character // bigger than the file size so that a zero character can be placed at the // end of the file to terminate the string (file). Because I don't yet know // if the file contains ANSI or Unicode characters, I assume worst case // and add the size of a WCHAR instead of CHAR. HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, dwFileSize + sizeof(WCHAR), NULL); //判断文件映射对象是否创建成功了 if(hFileMap == NULL) { chMB("File map could not be opened."); CloseHandle(hFile); return(FALSE); } // 获取 映射文件视图到进程地址空间 的第一个字符的地址 // Get the address where the first byte of the file is mapped into memory. PVOID pvFile = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0); //判断是否映射成功 if(pvFile == NULL) { chMB("Could not map view of file."); CloseHandle(hFileMap); CloseHandle(hFile); return(FALSE); } // 判断 缓冲区 是ANIS 或 Unicode // Does the buffer contain ANSI or Unicode? int iUnicodeTestFlags = -1; // Try all tests *pbIsTextUnicode = IsTextUnicode(pvFile, dwFileSize, &iUnicodeTestFlags); if(!*pbIsTextUnicode) { // 下面的文件操作,我们显示地使用ANSI的函数,因为我们正在处理ANSI的文件 // For all the file manipulations below, we explicitly use ANSI // functions because we are processing an ANSI file. // 把一个零字符放在文件的最后 // Put a zero character at the very end of the file. PSTR pchANSI = (PSTR)pvFile; pchANSI[dwFileSize / sizeof(CHAR)] = 0; //反转文件 // Reverse the contents of the file. _strrev(pchANSI); // 转换所有的 \n\r 组合 回到 \r\n 以保存正常的行尾序列 // Convert all "\n\r" combinations back to "\r\n" to // preserve the normal end-of-line sequence pchANSI = strstr(pchANSI, "\n\r"); // Find first "\r\n". //找到第一个 "\r\n" while(pchANSI != NULL) { // 我们发现了有一个出现 // We have found an occurrence.... *pchANSI++ = '\r'; // Change '\n' to '\r'. *pchANSI++ = '\n'; // Change '\r' to '\n'. pchANSI = strstr(pchANSI, "\n\r"); // Find the next occurrence. //发现下一个 } } else { // 下面的文件操作,我们显示地使用Unicode的函数,因为我们正在处理Unicode的文件 // For all the file manipulations below, we explicitly use Unicode // functions because we are processing a Unicode file. // 把一个零字符放在文件的最后 // Put a zero character at the very end of the file. PWSTR pchUnicode = (PWSTR)pvFile; pchUnicode[dwFileSize / sizeof(WCHAR)] = 0; if((iUnicodeTestFlags & IS_TEXT_UNICODE_SIGNATURE) != 0) { // 如果第一个字符是Unicode BOM(字节顺序标志) - 0xFEFF // 那么保持这个字符在文件的开始。 // If the first character is the Unicode BOM (byte-order-mark), // 0xFEFF, keep this character at the beginning of the file. pchUnicode++; } // 反转文件 // Reverse the contents of the file. _wcsrev(pchUnicode); // 转换所有的 \n\r 组合 回到 \r\n 以保存正常的行尾序列 // Convert all "\n\r" combinations back to "\r\n" to // preserve the normal end-of-line sequence. pchUnicode = wcsstr(pchUnicode, L"\n\r"); // Find first '\n\r'. while(pchUnicode != NULL) { // We have found an occurrence.... *pchUnicode++ = L'\r'; // Change '\n' to '\r'. *pchUnicode++ = L'\n'; // Change '\r' to '\n'. pchUnicode = wcsstr(pchUnicode, L"\n\r"); // Find the next occurrence. } } // 在退出前清除所有东西 // Clean up everything before exiting. UnmapViewOfFile(pvFile); CloseHandle(hFileMap); // 移除在前面添加的最后的补零字符 // Remove trailing zero character added earlier. SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN); SetEndOfFile(hFile); CloseHandle(hFile); return(TRUE); } //定义消息处理函数 LRESULT CALLBACK WinSunProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ); INT WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { //定义窗口类 WNDCLASS wndcls; wndcls.cbClsExtra = 0; wndcls.cbWndExtra = 0; wndcls.hbrBackground = (HBRUSH)GetStockObject(COLOR_WINDOWFRAME); wndcls.hCursor = LoadCursor(NULL, IDC_ARROW); wndcls.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndcls.hInstance = hInstance; wndcls.lpfnWndProc = WinSunProc; wndcls.lpszClassName = TEXT("NormalWindow"); wndcls.lpszMenuName = NULL; wndcls.style = CS_HREDRAW | CS_VREDRAW; //注册窗口类 ATOM nAtom = 0; nAtom = RegisterClass(&wndcls); if(nAtom == 0) { MessageBox(NULL, TEXT("窗口注册失败!"), TEXT("创建窗口失败"), MB_OK); return FALSE; } //创建窗口 HWND hwnd; hwnd = CreateWindow(TEXT("NormalWindow"), TEXT("RevrseFile"), WS_OVERLAPPEDWINDOW, 300, 300, 350, 180, NULL, NULL, hInstance, NULL); if(hwnd == NULL) { MessageBox(NULL, TEXT("窗口创建失败!"), TEXT("创建窗口失败"), MB_OK); return FALSE; } //显示窗口 ShowWindow(hwnd, SW_SHOWNORMAL); //刷新窗口 UpdateWindow(hwnd); BOOL bRet; MSG msg; while(bRet = GetMessage(&msg, hwnd, 0, 0)) { if(bRet == -1) { // handle the error and possibly exit return -1; } else { TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam; } LRESULT CALLBACK WinSunProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { BOOL bIsUnicode; switch(uMsg) { case WM_CREATE: //窗口创建完毕后会发出这个消息,我们可以在这里创建控件 //创建文本框 hStatic = CreateWindow(TEXT("Edit"), TEXT("文件名称 - 地址"), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_MULTILINE | ES_READONLY, 10, 20, 200, 100, hwnd, (HMENU)IDC_Text, NULL, NULL); //创建按钮按钮 CreateWindow(TEXT("Button"), TEXT("Browse"), WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 220, 20, 100, 40, hwnd, (HMENU)(IDB_Browse), NULL, NULL); //创建按钮按钮 CreateWindow(TEXT("Button"), TEXT("Reverse"), WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 220, 80, 100, 40, hwnd, (HMENU)(IDB_Reverse), NULL, NULL); break; case WM_PAINT: //处理重绘消息 HDC hDC; PAINTSTRUCT ps; hDC = BeginPaint(hwnd, &ps); //在这里画图 EndPaint(hwnd, &ps); break; case WM_COMMAND://处理命令消息 switch(LOWORD(wParam)) { case IDB_Browse: ofn.hwndOwner = hwnd; ofn.lpstrFile = szPathname; ofn.lpstrFile[0] = 0; ofn.nMaxFile = _countof(szPathname); ofn.lpstrTitle = TEXT("Select file for reversing"); ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST; GetOpenFileName(&ofn); //选择文件 SetWindowText(hStatic, ofn.lpstrFile); break; case IDB_Reverse: if(FileReverse(ofn.lpstrFile, &bIsUnicode)) { MessageBox(NULL, ofn.lpstrFile, TEXT("文件内容已翻转"), MB_OK); } else { MessageBox(NULL, TEXT("文件翻转失败咯..."), TEXT("发生了错误"), MB_OK); } break; default: return DefWindowProc(hwnd, uMsg, wParam, lParam);; } break; case WM_CLOSE: //处理Close消息 if(IDYES == MessageBox(hwnd, TEXT("是否真的结束?"), TEXT("Message"), MB_YESNO)) { DestroyWindow(hwnd); } break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } return 0; } |
以上代码在 VS2013 – Win10 是亲测可用的。
如有疑问请留言。