两次调用函数会出现段错误(与 char* 到字符串的转换有关)

Calling function twice gives segfault (in connection with char* to string conversion)

我想用 Windows 环境路径扩展字符串 ("%LOCALAPPDATA%/test.txt")。以下函数原则上可以完成这项工作,但使用相同的输出字符串再次调用它(或在调用函数之前为输出字符串分配一些值)会出现段错误。

显然我在将 char* 转换为 std::string 时犯了一些(可能是非常严重的)错误,但我真的不明白这里发生了什么(除了一些内存地址以后不可用)。

#include "processenv.h"

void expandWindowsString(const std::string &input, std::string &output)
{
    const char* in = input.c_str();
    char* out;
    ExpandEnvironmentStrings(in, out, 1024);
    output = out;
}

int main(int argc, char *argv[])
{
    std::string path;
    expandWindowsString("%LOCALAPPDATA%/test.txt", path);
    std::cout << "path is " << path << std::endl;
    //works fine so far, but if I execute the function again (with 'path') or initialising path beforehand with std::string path = "", a segfault occurs.
    expandWindowsString("%LOCALAPPDATA%/test.txt", path); // commenting out this line, makes the code work.
    std::cout << "path is " << path << "\n";
}

如果您阅读 doc

lpDst

A pointer to a buffer that receives the result of expanding the environment variable strings in the lpSrc buffer. Note that this buffer cannot be the same as the lpSrc buffer.

nSize

The maximum number of characters that can be stored in the buffer pointed to by the lpDst parameter. When using ANSI strings, the buffer size should be the string length, plus terminating null character, plus one. When using Unicode strings, the buffer size should be the string length plus the terminating null character.

你会看到你传递了无效的参数,你必须提供buffer,所以:

void expandWindowsString(const std::string &input, std::string &output)
{
    char out[1024];
    ExpandEnvironmentStrings(input.c_str(), out, sizeof (out));
    output = out;
}

或者避免输出参数:

std::string expandWindowsString(const std::string &input)
{
    char out[1024];
    ExpandEnvironmentStrings(input.c_str(), out, sizeof (out));
    return out;
}

请注意,您可能应该检查 ExpandEnvironmentStrings 的 return 值。

正如 documentation 解释的那样:

lpDst

A pointer to a buffer that receives the result of expanding the environment variable strings in the lpSrc buffer.

缓冲区需要由调用者提供,而代码只是传递一个未初始化的指针,同时欺骗系统相信它指向大小为 1024 字节的内存。

一个简单的修复方法是:

void expandWindowsString(const std::string &input, std::string &output)
{
    const char* in = input.c_str();
    char out[1024];
    ExpandEnvironmentStrings(in, out, 1024);
    output = out;
}

还有很大的改进空间,例如

  • 使用 Unicode version
  • 处理错误(如文档中所述)
  • 重复增加输出缓冲区,以防 API 调用 returns 的值大于提供的缓冲区长度
  • 使用 pointer and length
  • 构建 std::string
  • 返回值而不是使用输出参数

至于为什么第二次调用失败:这是一个无意义的问题。通过写入未初始化的指针,代码表现出未定义的行为。有了它,任何结果都是可能的,包括看起来按预期工作的代码。

ExpandEnvironmentStrings 需要一个已分配的缓冲区。您可以确定缓冲区的大小 if you pass a buffer with a size too small to hold the string,如 0.

因此,这里有两个版本,UNICODE 和 ANSI,它们将动态计算大小:

std::wstring expandWindowsString(const std::wstring& input)
{
    std::wstring output;
    do
    {
        auto size = ExpandEnvironmentStringsW(input.c_str(), (LPWSTR)output.c_str(), output.size());
        if (!size || size <= output.size())
            break;

        output.resize(size);
    } while (true);
    output = output.c_str();
    return output;
}

std::string expandWindowsString(const std::string& input)
{
    std::string output;
    do
    {
        auto size = ExpandEnvironmentStringsA(input.c_str(), (LPSTR)output.c_str(), output.size());
        if (!size || size <= output.size())
            break;

        output.resize(size);
    } while (true);
    output = output.c_str();
    return output;
}