按月归档: 4月 2007

简单快速的哈夫曼编码

作者:Hatem Mostafa
译者:happyparrot

下载源代码

介绍

本文描述在网上能够找到的最简单,最快速的哈夫曼编码。本方法不使用任何扩展动态库,比如STL或者组件。只使用简单的C函数,比如:memset,memmove,qsort,malloc,realloc和memcpy。

因此,大家都会发现,理解甚至修改这个编码都是很容易的。

背景

哈夫曼压缩是个无损的压缩算法,一般用来压缩文本和程序文件。哈夫曼压缩属于可变代码长度算法一族。意思是个体符号(例如,文本文件中的字符)用一个特定长度的位序列替代。因此,在文件中出现频率高的符号,使用短的位序列,而那些很少出现的符号,则用较长的位序列。

编码使用

我用简单的C函数写这个编码是为了让它在任何地方使用都会比较方便。你可以将他们放到类中,或者直接使用这个函数。并且我使用了简单的格式,仅仅输入输出缓冲区,而不象其它文章中那样,输入输出文件。

bool CompressHuffman(BYTE *pSrc, int nSrcLen, BYTE *&pDes, int &nDesLen);
bool DecompressHuffman(BYTE *pSrc, int nSrcLen, BYTE *&pDes, int &nDesLen);

要点说明

速度

为了让它(huffman.cpp)快速运行,我花了很长时间。同时,我没有使用任何动态库,比如STL或者MFC。它压缩1M数据少于100ms(P3处理器,主频1G)。

压缩

压缩代码非常简单,首先用ASCII值初始化511个哈夫曼节点:

CHuffmanNode nodes[511];

for(int nCount = 0; nCount < 256; nCount++)
nodes[nCount].byAscii = nCount;

然后,计算在输入缓冲区数据中,每个ASCII码出现的频率:

for(nCount = 0; nCount < nSrcLen; nCount++)
nodes[pSrc[nCount]].nFrequency++;

然后,根据频率进行排序:

qsort(nodes, 256, sizeof(CHuffmanNode), frequencyCompare);

现在,构造哈夫曼树,获取每个ASCII码对应的位序列:

int nNodeCount = GetHuffmanTree(nodes);

构造哈夫曼树非常简单,将所有的节点放到一个队列中,用一个节点替换两个频率最低的节点,新节点的频率就是这两个节点的频率之和。这样,新节点就是两个被替换节点的父节点了。如此循环,直到队列中只剩一个节点(树根)。

// parent node
pNode = &nodes[nParentNode++];

// pop first child
pNode->pLeft = PopNode(pNodes, nBackNode--, false);

// pop second child
pNode->pRight = PopNode(pNodes, nBackNode--, true);

// adjust parent of the two poped nodes
pNode->pLeft->pParent = pNode->pRight->pParent = pNode;

// adjust parent frequency
pNode->nFrequency = pNode->pLeft->nFrequency + pNode->pRight->nFrequency;

这里我用了一个好的诀窍来避免使用任何队列组件。我先前就直到ASCII码只有256个,但我分配了511个(CHuffmanNode nodes[511]),前255个记录ASCII码,而用后255个记录哈夫曼树中的父节点。并且在构造树的时候只使用一个指针数组 (ChuffmanNode *pNodes[256])来指向这些节点。同样使用两个变量来操作队列索引(int nParentNode = nNodeCount;nBackNode = nNodeCount –1)。

接着,压缩的最后一步是将每个ASCII编码写入输出缓冲区中:

int nDesIndex = 0;

// loop to write codes
for(nCount = 0; nCount < nSrcLen; nCount++)
{
*(DWORD*)(pDesPtr+(nDesIndex>>3)) |=
nodes[pSrc[nCount]].dwCode << (nDesIndex&7);
nDesIndex += nodes[pSrc[nCount]].nCodeLength;
}
  • (nDesIndex>>3): >>3 以8位为界限右移后到达右边字节的前面
  • (nDesIndex&7): &7 得到最高位.

注意:在压缩缓冲区中,我们必须保存哈夫曼树的节点以及位序列,这样我们才能在解压缩时重新构造哈夫曼树(只需保存ASCII值和对应的位序列)。

解压缩

解压缩比构造哈夫曼树要简单的多,将输入缓冲区中的每个编码用对应的ASCII码逐个替换就可以了。只要记住,这里的输入缓冲区是一个包含每个ASCII 值的编码的位流。因此,为了用ASCII值替换编码,我们必须用位流搜索哈夫曼树,直到发现一个叶节点,然后将它的ASCII值添加到输出缓冲区中:

int nDesIndex = 0;
DWORD nCode;
while(nDesIndex < nDesLen)
{
nCode = (*(DWORD*)(pSrc+(nSrcIndex>>3)))>>(nSrcIndex&7);
pNode = pRoot;
while(pNode->pLeft)
{
pNode = (nCode&1) ? pNode->pRight : pNode->pLeft;
nCode >>= 1;
nSrcIndex++;
}
pDes[nDesIndex++] = pNode->byAscii;
}
  • (nDesIndex>>3): >>3 以8位为界限右移后到达右边字节的前面
  • (nDesIndex&7): &7 得到最高位.

源文件: Huffman.cpp Huffman.h 请见本文提供的源代码压缩包。

Read: 1624

从编程员的角度理解 NTFS 2000:流和硬链接

简介

自 1994 年以来,有关 Microsoft(R) Windows NT(R) 的完全面向对象版的神话已流传一段时日了。Cairo — 传说中的 OS 版本的代码名 — 从未在 Redmond 的实验室以外得以实现。自有 Cairo 起,它的一些基本思想就不时地被公之于众。

Cairo 背后的基本思想是:文件和文件夹应成为对象和对象的集合。文件夹的内容不必局限于基础文件系统存储机制,您可将那些对象作为独立的、单独的项目,访问并复 制它们。文件和文件夹对象将用方法和属性的术语展示可编程的 API,这些术语既可以是标准的,也可以是由拥有者或作者定义的。

而我们今天所拥有的,是一个在某些内部结构中注册文件和文件夹的文件系统,当文件和文件夹在磁盘中移动时,它会被复制。文件和文件夹具有一套固定的 功能,而这些功能太少了,不能满足现代应用程序的需求。作为工作区的一部分,在过去的几年中,我们提供了几项技术用于向文件和文件夹添加附加信息。 Shell 和 namespace 扩展名、desktop.ini 文件、FileSystemObject 和“Shell 自动对象模型”就是几个例子。不过,所有这些功能仅仅是少量的和局部的解决方案。它们完全不能成为对 Windows 的文件系统进行有机的重新设计的基点。因为向前兼容性是一个严肃的问题,所以 Windows 仍然采用建立在文件分配表 (FAT) 上的旧式文件系统,它的诞生日期可追溯到 Microsoft MS-DOS(R) 2.0 版!即使最近做了更多的改进,如支持高容量的硬盘,FAT 对于存储文件和文件夹信息来说,仍然是一种不太合适的方法。

几年来的实践经验表明,我们遇到的最重要的限制是必须处理程序员正确管理并识别文件所需的附加信息。最近,有人请我检索 Word 97 文档的实际创 建日期。您可能认为这是一项简单的工作,因为创建日期是一个可以通过某些 API 功能轻易检索到的属性。这只是部分正确的。试着在不同的机器上、甚至相同的文件夹中复制相同的 Word 文件,然后比较两个副本的创建日期。奇怪的是,它们并不相同!当复制文件时,您创建了一个带有表明何时进行创建的时间标记的全新文件。当继续处理副本时, 您丢失了关于何时初创文件的潜在有价值的信息。

幸好,Word 文档在内部的 SummaryInformation 字段保留此信息。因此,在我的情况中,我得以解决了该问题并成功地通告了客户。如果是 Access 或文本文件,那么我的努力就白费了。

对于 Windows NT,Microsoft 引入了称为 NTFS 的新式文件系统。在它所有引人注目的功能中,B 树结构尤为显著,它加速了大文件夹上的文件检索、基于文件的安全、记录、增强的文件系统可恢复性以及比 FAT 或 FAT32 更好地利用磁盘空间。(顺便说明,Windows 2000 提供对 FAT32 卷的完全支持和访问。)

自从它们被 Windows 3.1 采用以后,NTFS 卷还具有另一个通常被忽视的功能:它们支持多个数据流流入单个文件。对于 Windows 2000,流支持再次被加强了,并增加了其他一些顺手的功能,以帮助您无缝地处理文件。让我们来见识一下 NTFS 2000 — 与 Windows 2000 同步的 NTFS 版 — 的主要功能吧。

NTFS 2000 概述

如果多个数据流不是 NTFS 2000 卷文件的独占功能,那么还有其他几种功能要求使用 Windows 2000。它们是:

  • 文件和目录加密
  • 每个用户、每卷的磁盘配额
  • 重新分析点和分级存储管理
  • 装入点
  • 硬链接
  • 更改记录

在 Windows 2000 安装过程中,要求您指定是否想将 Windows 2000 卷转换为 NTFS 2000。不过,只在机器作为域控制器时,才要求使用 NTFS 2000 文件系统。您可以在任何时候,通过使用命令行实用程序 convert.exe,将 FAT 分区转换为 NTFS:

 CONVERT volume /FS:NTFS [/V]

其中 volume 参数指定驱动器号,后面跟着一个冒号。它也可以是一个装入点或一个卷名。/FS:NTFS 选项指明该卷必须被转换为 NTFS。最后,如果您希望以详细模式运行实用程序,则使用 /V。当您运行 convert.exe 时,它进行初始化,然后请求您重新启动。重新启动之后,转换立即生效。

除了上述列出的所有功能外,Windows 2000 整个文件夹管理的显著方面,是它提供给 desktop.ini 文件的全面而稍微扩展的支持。在本文的其余部分中,我将主要侧重于流和硬链接。然而表 1 概述了涉及 NTFS 2000 其他关键功能的要点。

表 1. NTFS 2000 的主要功能

 

多文件流

在 NTFS 文件系统下,每个文件可拥有多个数据流。值得指出的是,流并不是 NTFS 2000 的特性,而是从 Windows NT 3.1 起就已存在了。当您读取位于非 NTFS 卷中的文件内容时(如:Windows 98 机器上的磁盘分区),您只能访问一个数据流。从而,您认为它是该文件的真正而“唯一”的内容。此类主流没有名称,而且是非 NTFS 文件系统能够处理的唯一的流。但当您在 NTFS 卷上创建文件时,情形就不同了。请参见图 1 来理解一下大的图形吧。

图 1. 多流文件的结构

多流文件是所有嵌入相同文件系统项目的单流文件的一类集合。它们看上去无疑像唯一的、基本的单元,然而包含一系列独立的子单元,您可以分别创建、删 除、修改它们。有一些常见的编程环境,其中的流是绰绰有余的。但是,如果您打算使用它们,请记住一旦您将多流文件复制到非 NTFS 存储设备(如 CD、软盘或非 NTFS 磁盘分区)上,则所有多余的流便丢失,且不可恢复。遗憾的是,这种兼容性问题使得流在实际应用中不那么受欢迎。对于设计并限定只在 NTFS 卷上运行的服务器端应用程序来说,流是一个出色的工具,可被沿用于建立杰出的、具有创造性的解决方案。

流的基本原理

当您在非 NTFS 卷上复制多流文件时,只复制了主流文件。这意味着您丢失了多余的数据,因为即使您将该文件复制回 NTFS 磁盘,它们也不会再次出现了。现在,假设您专门在 NTFS 机器上工作,让我们看看如何创建命名流。在代码示例 1 您可看到 Windows Script Host (WSH),以及 Microsoft Visual Basic(R) Scripting Edition (VBScript) 文件,它演示如何从 NTFS 文件中读写流。

要想在文件中识别命名流,您应遵守特殊的命名规则,并在文件名末尾加上一个冒号,然后是流的名称。例如,要想访问 test.txt 文件上的 VersionInfo 流,您应使用以下文件名:

 Test.txt:VersionInfo

与操纵文件的任何 Microsoft Win32(R) API 函数一起使用这个文件名。要想访问 VersionInfo 流的内容,将该名称传递给 CreateFile(),然后用 ReadFile()WriteFile() 照常完成读写。如果您想要检查某个特定的流是否存在于文件中,按如下所示编写文件流的名称,并使用 CreateFile() 检查它是否存在:

HANDLE hfile = CreateFile(szFileStreamName, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);CloseHandle(hfile);if (hfile == NULL) MessageBox(hWnd, "Error", NULL, MB_OK);

要想对流进行处理,您不必是一个熟练的 C++ 程序员。您也可以在 Visual Basic 中、甚至在脚本代码中利用流,如代码示例 1 所示。使这种透明性称为可能的关键因素是,所有低级 Win32 API 函数,特别是 CreateFile(),均支持 NTFS 分区上基于流的文件名。如果您试图在非 NTFS 分区上,例如在 Windows 98 机器上,打开称为 Test.txt:VersionInfo 的文件,您将会得到“未找到文件”的出错消息。请注意,问题的实质是,只是含有该文件的卷的文件系统,而不是调用应用程序所驻留的 Windows 平台或磁盘分区类型。换言之,您也可以通过连接的 Windows 98 机器,成功地访问 NTFS 分区上共享文件夹中特定的命名流。此外,应考虑到,即使对长文件名来说,冒号也不是有效的字符。因此,当 CreateFile() 遇到文件名中的冒号时,会知道它具有特殊的含义。

代码示例 1 所示,您也可以与 VBScript 一起使用流,因为 FileSystemObject 对象模式非常有效地运用 CreateFile() 来打开、写入、创建和测试文件。在示例代码中,我创建的文本文件带有空数据、0 长度的主流以及所需的任意多个命名流。请试着运行演示程序并创建两个流。可以将它们命名为 VersionInfoVersionInfoEx。Windows shell 中没有任何迹象有助于您推断出在某一特定的文件中有多个流的存在。在图 2 中,您可看到 test.txt文件在“Windows 资源管理器”中的样子。

图 2. 一个文件的长度可为 0,但具有命名流。

Size 列只显示了未命名的主流的大小,甚至在属性对话框中也无法获取关于流的更多信息。只有在 NTFS 卷上,Windows 2000 属性对话框中,您才有唯一的机会能够读到所有文件的相关信息,包括文本文件。单击摘要选项卡,然后输入,比如,一个作者名,如图 3 所示。

顺便提一句,由于 Windows 2000 的 shell 用户界面的改进,此类名称可在特定的作者列中显示出来。有关详细信息,请参阅位于 http://msdn.microsoft.com/msdnmag/(英文)上 MSDN Magazine 中的首要问题。

图 3. NTFS 卷上 .txt 文件的相关附加信息

嗨,等一下。尽管摘要信息是您为 Word 或 Excel 文档设置的一般数据,但它更无疑是文档本身的一部分。能不能将它与文本文件相结合而又不改变纯文本的内容呢?当然能。Shell 通过流来完成它!应用那些改变后,立即尝试将该文件复制到另一个非 NTFS 分区中。将出现如图 4 所示的对话框。

图 4. Windows 2000 关于可能的流数据丢失的预警。

事实证明 test.txt 文件包含一个带文档摘要信息的流。当您试图将带有附加信息的文件复制到不支持该文件的卷中时,系统会有所察觉。在非 NTFS 分区中,只复制了未命名的主流,其余的则被废除。因此,如果目标文件不相符,则基于流的文件几乎不会被交换。

流备份和枚举

是否有办法 — 一种或两种 API 函数 — 来枚举某一特定文件拥有的所有流呢?是的,有。但它并非那么简单而直观。Win32 备份 API 函数(BackupReadBackupWrite 等等),可被用来枚举文件中的流。不过,它们用起来有点怪异,而且看上去更像一个工作区,而不是有效的最终的解决方案。

其思路是,当您想要备份一个文件或整个文件夹时,您需要打包并存储全部可能存在的信息。因此,当需要尝试枚举文件中的流时,BackupRead() 是您最好的朋友。我将重点介绍该函数的原型:

BOOL BackupRead( HANDLE hFile, LPBYTE lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, BOOL bAbort, BOOL bProcessSecurity, LPVOID *lpContext );

为了我们的目的,您此处可忽略诸如上下文和安全等方面。hFile 参数必须通过调用 CreateFile() 获得,而 lpBuffer 应指向 WIN32_STREAM_ID 数据结构:

typedef struct _WIN32_STREAM_ID { DWORD dwStreamId; DWORD dwStreamAttributes; LARGE_INTEGER Size; DWORD dwStreamNameSize; WCHAR cStreamName[ANYSIZE_ARRAY]; } WIN32_STREAM_ID, *LPWIN32_STREAM_ID;

这种结构的前 20 个字节表示每个流的标题。流的名称紧随 dwStreamNameSize 字段后面出现,名称后面跟着流的内容。因为传统的文件内容可被视为流 — 尽管是未命名的流,所以要想枚举所有的流,您只需进行循环,直到 BackupRead 返回 False。实际上,BackupRead 应该能读取所有与给定的文件或文件夹相关的信息:

WIN32_STREAM_ID sid;ZeroMemory(&sid, sizeof(WIN32_STREAM_ID));DWORD dwStreamHeaderSize = (LPBYTE)&sid.cStreamName - (LPBYTE)&sid+ sid.dwStreamNameSize;bContinue = BackupRead(hfile, (LPBYTE) &sid, dwStreamHeaderSize, &dwRead, FALSE, FALSE, &lpContext);

上面的这个小段是在流的标题中读到的关键代码。如果该操作是成功的,即可尝试读取该流的实际名称:

WCHAR wszStreamName[MAX_PATH]; BackupRead(hfile, (LPBYTE) wszStreamName, sid.dwStreamNameSize, &dwRead, FALSE, FALSE, &lpContext);

在访问下一个流之前,首先要调用 BackupSeek(),向前移动备份指示器:

BackupSeek(hfile, sid.Size.LowPart, sid.Size.HighPart, &dw1, &dw2, &lpContext);

在多数情况下,您可将流视为常规文件 — 如,要删除流,可以用 DeleteFile()。如果想要刷新流的内容,只需使用 ReadFile()WriteFile()。没有正式的和得到支持的方法来移动或重新命名流。在本文的最后部分,我将利用本代码建立一个 NTFS 2000 专用的 Windows shell 扩展,将新的属性页添加到所有带流信息的文件中。同时,让我们来快速浏览一下 NTFS 的另一个特性。

硬链接

您知道快捷方式吗? — 就是那些小 .lnk 文件,它们多数散布在桌面上,用于引用其他内容。毫无疑问,快捷方式是一个有用的特性,但也存在一些缺点。首先,如果使来自不同文件夹的多个快捷方式指向 同一个目标,您实际上拥有了同一个 — 幸好较小 — 文件的多个副本。更为重要的是,快捷方式的目标对象会随着时间而更改。它可能会被移动、删除或仅仅是重新命名。您的快捷方式情形如何呢?它们能否检测并跟 踪到那些更改,从而正确地(自动)更新呢?很遗憾,它们不能。其主要原因是,快捷方式是应用程序级的功能。从系统的角度来看,它们只不过是用户定义的文 件,当您想要打开它们时,只需做一些额外的工作即可。考虑到拥有快捷方式是一种特权,您可能会决定也将其分配给其他文件类。假如这样做有意义,您可以创建 属于自己的、扩展名不是 .lnk 的快捷方式类。完成该任务的是在类节点下叫做 IsShortcut 的注册表项。假设您想让 .xyz 文件作为快捷方式。通过在 HKEY_CLASSES_ROOT 下创建 .xyz 节点来注册该文件类,并使其指向另一个节点,通常是 xyzfile。然后将空的 REG_SZ 项目添加到:

HKEY_CLASSES_ROOTxyzfile

这样就做完了。

其他操作系统,尤其是 Posix 和 OS/2,具有在系统级上起作用的类似功能。特别是 OS/2,将它们称为 shadows。 硬链接是给定文件在系统级上的快捷方式。通过创建现有文件的硬链接,您既没有复制该文件,也没有复制对其基于文件的引用(即,快捷方式)。相反,您将信息 添加到 NTFS 级上它的目录项中。物理文件在原始位置上原封未动。简言之,您现在可使用两个或更多个名称来访问相同的内容了!

硬链接使您免于保留同一个文件的多个(除需要外)副本,使负责管理不同路径名的系统处理单一的物理内容。这就极大地简化了您的工作,并节省了宝贵的 磁盘空间。另外,硬链接作为系统级的快捷方式,始终指向正确的目标文件 — 无论您是否重新命名或移动了它。由于该链接存储在文件系统级,因此所有的更改都自动而透明地得到应用。值得注意的是,硬链接必须在相同的 NTFS 卷中被创建。比如,您不能让驱动器 C: 上的硬链接,指向驱动器 D: 上的文件。

为了听起来更熟悉,可以将硬链接想象为文件的别名。您可以使用任何一个别名访问该文件,只有当删除了所有别名以后,该文件才能被删除。(别名的作用正如引用计数一样。)因为硬链接是别名,所以使它们的内容同步是不成问题的。

CreateHardLink() 是用于创建硬链接的 API 函数。其原型如下所示:

BOOL CreateHardLink( LPCTSTR lpFileName, LPCTSTR lpExistingFileName, LPSECURITY_ATTRIBUTES lpSecurityAttributes ); 

在旧的 MIND 一文所含的代码中(请参阅 “Windows 2000 for Web Developers” 的 MIND(英文),1999 年 3 月),我提供了一个 COM 对象,使您能够通过脚本代码创建硬链接。代码示例 2 显示了使用它来创建给定文件的硬链接的 VBScript 程序。尽管很容易找出一个文件有多少个硬链接,然而没有枚举所有硬链接的工具。API 函数 GetFileInformationByHandle() 填充了 BY_HANDLE_FILE_INFORMATION 结构,其 nNumberOfLinks 字段向您发出关于枚举的通知。枚举所有链接文件的名称稍微困难一点。基本上,您必须扫描整个卷,并且为每一个文件跟踪分配给它的唯一 ID。当您遇到现有的 ID 时,就已经找到该文件的一个硬链接了。文件的唯一 ID 是由系统分配的,并被存储在 BY_HANDLE_FILE_INFORMATIONnFileIndexHigh nFileIndexLow 字段中。

享受 NTFS 功能

对于向文件添加附加信息,而又不改变或损坏原始格式,同时不占用磁盘空间来说,流的作用特别重要。当然,流会占用其本身的空间,然而 “Windows 资源管理器”似乎没有觉察到这一点。流对于“Windows 资源管理器”来说是不可见的,所以尽管看上去似乎有足够的可用磁盘空间,但实际上可用的磁盘空间已经降低到危险的程度了。您可以将附加(不可见的)信息添 加给任何文件,包括文本和可执行文件。

另一方面,硬链接是聚集共享信息的杰出资源。您只有一个真正的、可以从各个不同的路径访问的信息库。要知道,硬链接对于 Windows NT 技术来说,并不是一个全新的概念。自从 Windows NT 一出台,就有了硬链接。然而,直到有了 Windows 2000,Microsoft 才提供了创建硬链接的公用函数。每个文件至少有一个到其自身的链接,因而 GetFileInformationByHandle 总会返回大于零的链接数。您不能将硬链接设置到目录,而只能设置到文件。

流和硬链接有个共同的实际问题,就是它们从 shell 得到的支持极为有限。为了补救这一问题,我编写了一个 shell 扩展,以提供有关给定文件的流和硬链接的信息。图 5 图示了它的外观和感观。

图 5. 流选项卡显示了关于流和硬链接的信息。

Shell 扩展的源代码用 BackupRead() API 函数枚举流。只需通过调用 DeleteFile(),选定的流的内容即可被删除。编辑流按钮运行代码示例 1 中的脚本代码,通过它,您可以添加或更新流。同样地,创建硬链接按钮运行代码示例 2 中的代码,以创建附加的链接。只有在刷新后,用户界面才反映出所有的更改。最后应注意,要记住如果您删除了硬链接(即删除了文件),只要被删除的文件仍在“回收站”里,则链接的总数就不会被更新。

摘要

在本文中,我只是粗浅地介绍了 NTFS 2000,侧重于其主要功能,如流和硬链接。如果您想对 Windows 2000 文件系统的新功能有更为广泛的了解,我建议您参阅“A File System for the 21st Century: Previewing the Windows NT 5.0 File System(21 世纪的文件系统:预览 Windows NT 5.0 文件系统)”一文,它是由 Jeff Richter 和 Luis Cabrera 于 1998 年 11 月为 MSJ 撰写的,(http://www.microsoft.com/msj/1198/ntfs/ntfs.htm(英文))。该文并未涉猎一些引人注目的话题,尤其是稀疏流和重新分析点,不过,如果您对此文感兴趣的话,请告知我们,我们会进一步帮助您。

代码示例 1

代码示例 2

Read: 1073

VC++中用内存映射文件

软件的开发过程中,有时需要控制一些程序使他们不能同时运行,也就是多个程序间互斥运行(还包括禁止同一程序运行多个实例)。针对这一问题,我们在Visual C++6.0中利用内存映射文件实现了多个程序间的互斥运行。内存映射文件可以创建一个没有和磁盘文件相联系的内存对象,将文件的信息映射到一个进程的地址空间上,我们可以访问该文件中的数据,就如同它位于内存中一样。同时,在程序设计中可以给内存映射文件对象起一个名字,这个名字在整个系统中是唯一的,这个名字可以在多个进程之间共享,通过名字共享能实现进行信息交换,进而实现多个程序间的互斥运行。

在讲述具体的编程方法之前,让我们先介绍和内存映射文件操作有关的几个重要的函

  1. CreateFileMapping的函数为指定的文件创建一个文件映射对象,该函数的原形如下:

    HANDLE CreateFileMapping(HANDLE hFile,//用于映射的文件句柄 LPSECURITY?ATTRIBUTES FileMappingAttributes,//内存映射文件的安全描述符 DWORD Flprotect,//文件映射对象的最大长度的高32位 DWORD dwMaximumSizelow,//最大长度的低32位 LPCTSTR IPNAME//指定这个内存映射文件的名字)

    值得注意的是,参数如果是OXFFFFFFFF,将在操作系统虚拟内存页面替换文件中创建文件映射对象,而不是使用磁盘文件,同时必须给出这个映射对象的大小。

  2. NAO VUEWIFFILE函数将文件的视图映射到一个进程地址空间上,返回LPVOID类型的内存指针。通过它,就可以直接访问文件视图中的信息。

LPVOID MAP VIEWLFFILE(HANDLE HFILEMAPPINGOBUCT,//映射文件对象句柄 DWORD DWDESIREDACCESS,//访问模式 DWORD DWFILEOFFSETHIGH,//文件偏移地址的高32位 DWORD DWFILEOFFSETHIGH,//文件偏移地址的低32位 DWORD DWNUMBEROFBYTESTOMAP//映射视图的大小)

在Visual c++6.0中我们用默认方式生成基于对话框的应用程序,在程序的初始化阶段,在CwinApp生类的Initln_stance函数的开始处,添加以下代码:

(//创建内存映射文件对象,mu_texRunning是其名字,所有需要互斥运行 //的程序都使用这个名字(这些代码对于需要互斥运行的程序是通用的)HANDLE hMap=CreateFileMapping((HANDLE)0Xffffffffnull,PAGE_READWRLTE,0,128,"Mu_texRunning") if(hMap==NULL)//如果创建失败 (AfxMessageBox("创建用于互斥运行的内存映文件对象失败!”,MB?OK MB??ICLNSTOP);

return FALSE;//退出此程序)

//如果已经存在这个同名对象,说明已有需要互斥的其他程序运行了

else if(GetLastError( )==ER_ROR_ALREADY_EXISTS)

(LPVOID ipMen=MapViewOFFile(hMap,FILE_MAP_WRITE,0,0,0);

Cstring str=(char*)ipMem;//获得已在运行的程序的描述信息

UnmapViewofFile(lpMem);//解除映射图

CloseHandle(hMap);//关闭此对象

AfxMessageBox(str,MB_ok MB_ICONSTOP);显示有关的描述信息

Return FALSE;//退出此程序)

Else//经过上面的检查,说明这是第一个运行的互斥程序

(LPVOID ip_mem=MapViewofFile(hMap,FILE_MAP_WRITE,0,0,0);

//这里可写入该程序运行的描述信息,上面的错误提示就是这信信息

strcpy((char*)lpMem,"xxx程序正在运行!”);

UnmapViewofFile(lpMem);//解除映射图)

//下面可以继续执行函数INITIN?STANCE原有的代码了

AfxEnableControl_comtainer();

//当程序运行结束了,要记住调用CHANDIE(HMAP)关闭这个对象句柄,//这里可以在Initinstance函数最后returnFALSE之前调用

CloseHandle(hMap);//关闭内存映文件对象句柄 RETURN false;)以上的程序在Visual C++6.中已调试通过。其他非对话框类型的程序可以在各自的初始化和终止阶段添加类似的代码,只是如果内存映射文件对象的句柄hMap可能在不同函数中使用,那就要将其定义成CwinApp生类的成员变量或是全局变量了。

Read: 820

如何将一个文件分割成多个小文件

   你也许会遇到到这样一个问题?当你有一个较大的软件,而无法用一张软盘将其全部拷下时,你也许会想到该将它分解开,分盘拷回去后,再将它们合并起来。现在的这种分割工具很多,你想自己动手做一个适合自己的分割工具么?下面就让我用以前用VC做的一个<袖珍文件分割器>的例程来告诉你吧!程序运行后界面如下图:

基本构成思想:文件分割的基本思想比我之前发表的另一篇文章<如何将多个文件合并为一个可执行程序>的构成思想简单多了,它主要也分为分割文件和合并分割后的文件二大部分。分割文件,将原文件按指定分割大小进行等分,然后顺序读取其指定分割大小数据后到写到各自的新建文件中。合并文件,将各分割后的文件顺序读取后,写入到一个文件中既可。

1、分割文件时:打开文件,读取指定的分割大小一段数据,写入到一新建文件中,接着再读同样大小的一段数据,再写入到一新建文件中……,直到读出文件最后一部分数据,写入到最后一个新建文件中。对每一个分割后的新建文件名,采用原文件名前加数字信息的方法,按分割的顺序,按个加上一数字标识信息,以便合并时使用。
分割文件的部分代码实现如下:
//文件分割涵数
int CFileSpltDlg::SplitMe()
{
…… (省略:此部分代码实现省略掉)
//分割文件
do {
//动态建立一个新建文件名的前的数字
name = _ltoa(l, buff, 10);
name += _T("_");
CString newpath;

//判断选择目录未尾是否已有""符
if(m_targetpath.Right(1)==’\’)
newpath = m_targetpath;
else
newpath = m_targetpath + _T("\");
if (!destFile.Open(newpath + name + m_SourceFile.GetFileName(),
CFile::modeWrite |
CFile::shareExclusive |
CFile::typeBinary |
CFile::modeCreate, &ex)) {
TCHAR szError[1024];
ex.GetErrorMessage(szError, 1024);
::AfxMessageBox(szError);
m_SourceFile.Close();
return 1;
}
do {
dwRead = m_SourceFile.Read(buffer, nCount);
destFile.Write(buffer, dwRead);
}//当文件小于指定要分割的大小时
while (dwRead > 0 && destFile.GetLength() < newlen);
destFile.Close();

l++;
UpdateWindow();
}while (dwRead > 0);
m_SourceFile.Close();
return 0;

2、合并文件时:和上面分割所采用的方法相反,将各个分割后的小文件读出后,按其分割后文件名前数字大小的顺序,按个写入到新建的文件中,这一新建文件的名字,为去掉分割后文件前面数字部分后的文件名(既原文件名)。
合并文件的部分代码实现如下:
// 文件合并涵数
int CFileSpltDlg::MergeMe()
{
…… (省略:此部分代码实现省略掉)
//开始合并文件
do {
//自动定位分割文件名前的数字信息
pref = _ltoa(l, buff, 10);
pref += _T("_");
//打开新的分割文件
if (!m_SourceFile.Open(newpath + pref + m_filename,
CFile::modeRead |
CFile::shareExclusive |
CFile::typeBinary, &ex)) {
TCHAR szError[1024];
ex.GetErrorMessage(szError, 1024);
destFile.Close();
m_path = _T("");
m_filename = _T("");
newpath = _T("");
UpdateData(FALSE);
return 0;
}
else
//形成一个新的文件名
name = _T(newpath + pref + m_filename);
do {//写入到目标文件中
dwRead = m_SourceFile.Read(buffer, nCount);
destFile.Write(buffer, dwRead);
}while (dwRead > 0);

m_SourceFile.Close();

l++;
UpdateWindow();
}while (l < 500);//little bit dirty solution, but you can always improve it!…

return 0;
}

以上各部分代码的具体实现,请在下载例程后,参看源代码既可。

联系方式:
地址:陕西省西安市劳动路2号院六单元
邮编:710082
作者EMAIL:jingzhou_xu@163.net
如何将一个文件分割成多个小文件

Read: 767

用Visual C++操作INI文件

在我们写的程序当中,总有一些配置信息需要保存下来,以便完成程序的功能,最简单的办法就是将这些信息写入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:studstudent.ini 文件中.

CString strName,strTemp;
int nAge;
strName="张三";
nAge=12;
::WritePrivateProfileString("StudentInfo","Name",strName,"c:\stud\student.ini");

  此时c:studstudent.ini文件中的内容如下:

   [StudentInfo]
Name=张三

  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 值.

为在VC++中, \ 才表示一个 .

   3.也可将INI文件放在程序所在目录,此时 lpFileName 参数为: ".\student.ini".

 

Read: 755