将GetModuleFileName()
缓冲区和缓冲区大小作为输入;但是它的返回值只能告诉我们复制了多少个字符,以及大小是否不够(ERROR_INSUFFICIENT_BUFFER
)。
如何确定保存整个文件名所需的实际缓冲区大小GetModuleFileName()
?
大多数人使用MAX_PATH
,但我记得路径可以超过(默认定义为 260)...
(使用零作为缓冲区大小的技巧不适用于此 API - 我之前已经尝试过)
将GetModuleFileName()
缓冲区和缓冲区大小作为输入;但是它的返回值只能告诉我们复制了多少个字符,以及大小是否不够(ERROR_INSUFFICIENT_BUFFER
)。
如何确定保存整个文件名所需的实际缓冲区大小GetModuleFileName()
?
大多数人使用MAX_PATH
,但我记得路径可以超过(默认定义为 260)...
(使用零作为缓冲区大小的技巧不适用于此 API - 我之前已经尝试过)
通常的方法是将其大小设置为零,并保证失败并提供分配足够缓冲区所需的大小。分配一个缓冲区(不要忘记空终止的空间)并再次调用它。
在很多情况下MAX_PATH
就足够了,因为许多文件系统限制了路径名的总长度。但是,可以构造超过 的合法且有用的文件名MAX_PATH
,因此查询所需的缓冲区可能是个好建议。
不要忘记最终从提供它的分配器返回缓冲区。
编辑:弗朗西斯在评论中指出,通常的食谱不适用于GetModuleFileName()
. 不幸的是,弗朗西斯在这一点上是绝对正确的,我唯一的借口是在提供“通常”的解决方案之前我没有去查证。
我不知道那个 API 的作者在想什么,除了它可能在引入时MAX_PATH
确实是最大的可能路径,使正确的配方变得容易。只需在长度不少于MAX_PATH
字符的缓冲区中进行所有文件名操作。
哦,是的,不要忘记自 1995 年左右以来的路径名称允许使用 Unicode 字符。因为 Unicode 占用更多空间,所以任何路径名都可以在前面\\?\
明确请求MAX_PATH
删除对该名称的字节长度限制。这使问题复杂化。
MSDN 在标题为File Names, Paths, and Namespaces的文章中对路径长度有这样的说法:
最大路径长度
在 Windows API(以下段落中讨论的一些例外情况)中,路径的最大长度为
MAX_PATH
,定义为 260 个字符。本地路径按以下顺序构建:驱动器号、冒号、反斜杠、由反斜杠分隔的组件和终止空字符。例如,驱动器 D 上的最大路径是“D:\<some 256 character path string><NUL>
”,其中“<NUL>
”表示当前系统代码页的不可见终止空字符。(此处使用的字符<
>
是为了清晰起见,不能作为有效路径字符串的一部分。)注意 Windows API 中的文件 I/O 函数将“
/
”转换为“\
”,作为将名称转换为 NT 样式名称的一部分,除非使用\\?\
以下部分中详细说明的“ ”前缀。Windows API 有许多函数也有 Unicode 版本,以允许最大总路径长度为 32,767 个字符的扩展长度路径。这种类型的路径由由反斜杠分隔的组件组成,每个组件都达到 函数
lpMaximumComponentLength
参数中返回的值GetVolumeInformation
。要指定扩展长度的路径,请使用“\\?\
”前缀。例如,“\\?\D:\<very long path>
”。(此处使用的字符<
>
是为了清晰起见,不能作为有效路径字符串的一部分。)注意 32,767 个字符的最大路径是近似的,因为
\\?\
系统在运行时可能会将“”前缀扩展为更长的字符串,并且此扩展适用于总长度。"
\\?\
" 前缀也可以用于根据通用命名约定 (UNC) 构建的路径。要使用 UNC 指定此类路径,请使用“\\?\UNC\
”前缀。例如,“\\?\UNC\server\share
”,其中“server”是机器的名称,“share”是共享文件夹的名称。这些前缀不用作路径本身的一部分。它们表明应该以最少的修改将路径传递给系统,这意味着您不能使用正斜杠来表示路径分隔符,也不能使用句点来表示当前目录。此外,您不能将“\\?\
”前缀与相对路径一起使用,因此相对路径仅限于MAX_PATH
之前针对不使用“”的路径所述的字符\\?\
使用API创建目录时,指定路径不能太长,不能附加8.3文件名(即目录名不能超过
MAX_PATH
-12)。shell 和文件系统有不同的要求。可以使用 shell 用户界面可能无法处理的 Windows API 创建路径。
所以一个简单的答案是分配一个大小的缓冲区MAX_PATH
,检索名称并检查错误。如果合适,你就完成了。否则,如果它以“ \\?\
”开头,则获取大小为 64KB 左右的缓冲区(上面的短语“32,767 个字符的最大路径是近似的”在这里有点麻烦,所以我留下一些细节以供进一步研究)然后再试一次。
溢出MAX_PATH
但不以“”开头\\?\
似乎是“不可能发生”的情况。同样,接下来要做什么是您必须处理的细节。
对于以“”开头的网络名称的路径长度限制也可能存在一些混淆\\Server\Share\
,更不用说内核对象名称空间中以“ \\.\
”开头的名称了。上面的文章没有说,我不确定这个API是否可以返回这样的路径。
实施一些合理的策略来增加缓冲区,例如从 MAX_PATH 开始,然后使每个连续的大小比前一个大 1.5 倍(或减少迭代的 2 倍)。迭代直到函数成功。
虽然 API 证明了糟糕的设计,但解决方案实际上非常简单。很简单,但很遗憾,它必须是这样,因为它可能需要多次内存分配,这有点消耗性能。以下是解决方案的一些关键点:
您不能真正依赖不同 Windows 版本之间的返回值,因为它在不同 Windows 版本(例如 XP)上可能具有不同的语义。
如果提供的缓冲区太小而无法容纳字符串,则返回值是包括 0 终止符在内的字符数。
如果提供的缓冲区足够大以容纳字符串,则返回值是不包括 0 终止符的字符数。
这意味着如果返回值正好等于缓冲区大小,你仍然不知道它是否成功。可能还有更多数据。或不。最后,只有当缓冲区长度实际上大于要求时,您才能确定成功。可悲...
因此,解决方案是从一个小缓冲区开始。然后我们调用 GetModuleFileName 传递确切的缓冲区长度(以 TCHAR 为单位)并将返回结果与它进行比较。如果返回结果小于我们的缓冲区长度,则表示成功。如果返回结果大于或等于我们的缓冲区长度,我们必须用更大的缓冲区再试一次。冲洗并重复直到完成。完成后,我们制作缓冲区的字符串副本 (strdup/wcsdup/tcsdup),清理并返回字符串副本。该字符串将具有正确的分配大小,而不是我们临时缓冲区的可能开销。请注意,调用者负责释放返回的字符串(strdup/wcsdup/tcsdup mallocs 内存)。
请参阅下面的实现和使用代码示例。我已经使用这个代码十多年了,包括在企业文档管理软件中可能有很多很长的路径。代码当然可以通过各种方式进行优化,例如首先将返回的字符串加载到本地缓冲区(TCHAR buf[256])。如果该缓冲区太小,您可以启动动态分配循环。其他优化是可能的,但这超出了这里的范围。
实现和使用示例:
/* Ensure Win32 API Unicode setting is in sync with CRT Unicode setting */
#if defined(_UNICODE) && !defined(UNICODE)
# define UNICODE
#elif defined(UNICODE) && !defined(_UNICODE)
# define _UNICODE
#endif
#include <stdio.h> /* not needed for our function, just for printf */
#include <tchar.h>
#include <windows.h>
LPCTSTR GetMainModulePath(void)
{
TCHAR* buf = NULL;
DWORD bufLen = 256;
DWORD retLen;
while (32768 >= bufLen)
{
if (!(buf = (TCHAR*)malloc(sizeof(TCHAR) * (size_t)bufLen))
{
/* Insufficient memory */
return NULL;
}
if (!(retLen = GetModuleFileName(NULL, buf, bufLen)))
{
/* GetModuleFileName failed */
free(buf);
return NULL;
}
else if (bufLen > retLen)
{
/* Success */
LPCTSTR result = _tcsdup(buf); /* Caller should free returned pointer */
free(buf);
return result;
}
free(buf);
bufLen <<= 1;
}
/* Path too long */
return NULL;
}
int main(int argc, char* argv[])
{
LPCTSTR path;
if (!(path = GetMainModulePath()))
{
/* Insufficient memory or path too long */
return 0;
}
_tprintf("%s\n", path);
free(path); /* GetMainModulePath malloced memory using _tcsdup */
return 0;
}
说了这么多,我想指出您需要非常了解 GetModuleFileName(Ex) 的各种其他警告。32/64 位/WOW64 之间存在不同的问题。此外,输出不一定是完整的长路径,但很可能是短文件名或受路径别名的影响。我希望当你使用这样一个函数时,目标是为调用者提供一个可用的、可靠的完整的、长的路径,因此我建议确实确保返回一个可用的、可靠的、完整的、长的绝对路径,以这样的方式它可以在各种 Windows 版本和体系结构之间移植(同样是 32/64 位/WOW64)。如何有效地做到这一点超出了这里的范围。
虽然这是现有的最糟糕的 Win32 API 之一,但我还是希望您能享受编码乐趣。
使用
extern char* _pgmptr
可能会奏效。
从 GetModuleFileName 的文档中:
全局变量 _pgmptr 自动初始化为可执行文件的全路径,可用于检索可执行文件的全路径名。
但如果我读到 _pgmptr:
当程序不是从命令行运行时,_pgmptr 可能会被初始化为程序名(文件的基本名称,不带文件扩展名)或文件名、相对路径或完整路径。
谁知道 _pgmptr 是如何初始化的?如果 SO 支持后续问题,我会将这个问题作为后续问题发布。
我的示例是“如果一开始没有成功,则将缓冲区的长度加倍”方法的具体实现。它使用字符串(实际上是 a wstring
,因为我希望能够处理 Unicode)作为缓冲区来检索正在运行的可执行文件的路径。为了确定它何时成功检索到完整路径,它检查从返回GetModuleFileNameW
的值与返回的值wstring::length()
,然后使用该值调整最终字符串的大小以去除多余的空字符。如果失败,则返回一个空字符串。
inline std::wstring getPathToExecutableW()
{
static const size_t INITIAL_BUFFER_SIZE = MAX_PATH;
static const size_t MAX_ITERATIONS = 7;
std::wstring ret;
DWORD bufferSize = INITIAL_BUFFER_SIZE;
for (size_t iterations = 0; iterations < MAX_ITERATIONS; ++iterations)
{
ret.resize(bufferSize);
DWORD charsReturned = GetModuleFileNameW(NULL, &ret[0], bufferSize);
if (charsReturned < ret.length())
{
ret.resize(charsReturned);
return ret;
}
else
{
bufferSize *= 2;
}
}
return L"";
}
这是 std::wstring 的另一个解决方案:
DWORD getCurrentProcessBinaryFile(std::wstring& outPath)
{
// @see https://msdn.microsoft.com/en-us/magazine/mt238407.aspx
DWORD dwError = 0;
DWORD dwResult = 0;
DWORD dwSize = MAX_PATH;
SetLastError(0);
while (dwSize <= 32768) {
outPath.resize(dwSize);
dwResult = GetModuleFileName(0, &outPath[0], dwSize);
dwError = GetLastError();
/* if function has failed there is nothing we can do */
if (0 == dwResult) {
return dwError;
}
/* check if buffer was too small and string was truncated */
if (ERROR_INSUFFICIENT_BUFFER == dwError) {
dwSize *= 2;
dwError = 0;
continue;
}
/* finally we received the result string */
outPath.resize(dwResult);
return 0;
}
return ERROR_BUFFER_OVERFLOW;
}
Windows 无法正确处理超过 260 个字符的路径,因此只需使用 MAX_PATH。您不能运行路径长于 MAX_PATH 的程序。
我的方法是使用 argv,假设您只想获取正在运行的程序的文件名。当您尝试从不同的模块获取文件名时,已经描述了没有任何其他技巧的唯一安全方法,可以在此处找到实现。
// assume argv is there and a char** array
int nAllocCharCount = 1024;
int nBufSize = argv[0][0] ? strlen((char *) argv[0]) : nAllocCharCount;
TCHAR * pszCompleteFilePath = new TCHAR[nBufSize+1];
nBufSize = GetModuleFileName(NULL, (TCHAR*)pszCompleteFilePath, nBufSize);
if (!argv[0][0])
{
// resize memory until enough is available
while (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
delete[] pszCompleteFilePath;
nBufSize += nAllocCharCount;
pszCompleteFilePath = new TCHAR[nBufSize+1];
nBufSize = GetModuleFileName(NULL, (TCHAR*)pszCompleteFilePath, nBufSize);
}
TCHAR * pTmp = pszCompleteFilePath;
pszCompleteFilePath = new TCHAR[nBufSize+1];
memcpy_s((void*)pszCompleteFilePath, nBufSize*sizeof(TCHAR), pTmp, nBufSize*sizeof(TCHAR));
delete[] pTmp;
pTmp = NULL;
}
pszCompleteFilePath[nBufSize] = '\0';
// do work here
// variable 'pszCompleteFilePath' contains always the complete path now
// cleanup
delete[] pszCompleteFilePath;
pszCompleteFilePath = NULL;
我还没有遇到 argv 不包含文件路径(Win32 和 Win32-console 应用程序)的情况。但以防万一出现上述解决方案的回退。对我来说似乎有点难看,但仍然可以完成工作。