14

GetModuleFileName()缓冲区和缓冲区大小作为输入;但是它的返回值只能告诉我们复制了多少个字符,以及大小是否不够(ERROR_INSUFFICIENT_BUFFER)。

如何确定保存整个文件名所需的实际缓冲区大小GetModuleFileName()

大多数人使用MAX_PATH,但我记得路径可以超过(默认定义为 260)...

(使用零作为缓冲区大小的技巧不适用于此 API - 我之前已经尝试过)

4

8 回答 8

13

通常的方法是将其大小设置为零,并保证失败并提供分配足够缓冲区所需的大小。分配一个缓冲区(不要忘记空终止的空间)并再次调用它。

在很多情况下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是否可以返回这样的路径。

于 2009-04-30T07:51:42.453 回答
10

实施一些合理的策略来增加缓冲区,例如从 MAX_PATH 开始,然后使每个连续的大小比前一个大 1.5 倍(或减少迭代的 2 倍)。迭代直到函数成功。

于 2009-04-30T09:48:55.587 回答
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 之一,但我还是希望您能享受编码乐趣。

于 2015-01-20T22:05:21.460 回答
1

使用

extern char* _pgmptr

可能会奏效。

从 GetModuleFileName 的文档中:

全局变量 _pgmptr 自动初始化为可执行文件的全路径,可用于检索可执行文件的全路径名。

但如果我读到 _pgmptr:

当程序不是从命令行运行时,_pgmptr 可能会被初始化为程序名(文件的基本名称,不带文件扩展名)或文件名、相对路径或完整路径。

谁知道 _pgmptr 是如何初始化的?如果 SO 支持后续问题,我会将这个问题作为后续问题发布。

于 2013-07-27T14:59:17.870 回答
1

我的示例是“如果一开始没有成功,则将缓冲区的长度加倍”方法的具体实现。它使用字符串(实际上是 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"";
}
于 2017-10-20T21:45:11.787 回答
0

这是 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;
}
于 2019-07-19T14:04:00.243 回答
-1

Windows 无法正确处理超过 260 个字符的路径,因此只需使用 MAX_PATH。您不能运行路径长于 MAX_PATH 的程序。

于 2015-04-08T06:16:16.453 回答
-3

我的方法是使用 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 应用程序)的情况。但以防万一出现上述解决方案的回退。对我来说似乎有点难看,但仍然可以完成工作。

于 2015-01-13T00:05:25.587 回答