3

在一次采访中,我被要求(除其他外)实现以下功能:

int StrPrintF(char **psz, const char *szFmt, ...);

类似于sprintf,除了不是已经分配的存储,函数必须自己分配它,并在*psz变量中返回。此外,*psz可能指向一个已经分配的字符串(在堆上),它可能会在格式化期间使用。自然,该字符串必须通过适当的方式释放。

返回值应该是新创建的字符串的长度,或者错误时为负值。

这是我的实现:

int StrPrintF(char **psz, const char *szFmt, ...)
{
    va_list args;
    int nLen;

    va_start(args, szFmt);

    if ((nLen = vsnprintf(NULL, 0, szFmt, args)) >= 0)
    {
        char *szRes = (char*) malloc(nLen + 1);
        if (szRes)
            if (vsnprintf(szRes, nLen + 1, szFmt, args) == nLen)
            {
                free(*psz);
                *psz = szRes;
            }
            else
            {
                free(szRes);
                nLen = -1;
            }
        else
            nLen = -1;
    }

    va_end(args);
    return nLen;
}

问题作者声称此实现存在错误。不仅仅是在特定深奥系统上可能失败的标准违规,而是一个“真正的”错误,它可能在大多数系统上偶然失败。

它也与使用 ofint而不是适合内存能力的类型无关,例如size_tor ptrdiff_t。说,字符串的大小是“合理的”。

我真的不知道这个错误可能是什么。恕我直言,所有指针算术都可以。我什至不认为两个后续调用会vsnprintf产生相同的结果。恕我直言,所有可变参数处理的东西也是正确的。va_copy不需要(这是使用 的被调用者的责任va_list)。也在x86上va_copy并且va_end没有意义。

如果有人能发现(潜在的)错误,我将不胜感激。

编辑:

查看答案和评论后 - 我想添加一些注释:

  • 自然地,我已经使用各种输入构建并运行了代码,包括在调试器中一步一步地观察变量状态。如果不先亲自尝试,我永远不会寻求帮助。我没有看到任何问题,没有堆栈/堆损坏等。我还在调试版本中运行它,启用了调试堆(它不能容忍堆损坏)。
  • 我假设函数是使用有效参数调用的,即psz是一个有效的指针(不要与 混淆*psz),szFmt是一个有效的格式说明符,并且所有可变参数都被评估并对应于格式字符串。
  • 根据标准,free用指针调用是可以的。NULL
  • vsnprintf使用NULL指针和大小 = 0可以调用。它应该返回结果字符串长度。MS 版本虽然不完全符合标准,但在这种特定情况下也是如此。
  • vsnprintf不会超过指定的缓冲区大小,包括 0 终止符。意味着 - 它并不总是放置它。
  • 请把编码风格放在一边(如果你不喜欢它——我很好)。
4

5 回答 5

9

不需要 va_copy(这是使用 va_list 的被调用者的责任)

不太对。vsnprintf我在 C11 标准中没有找到任何这样的要求。它确实在脚注中这样说:

由于函数 vfprintf、vfscanf、vprintf、vscanf、vsnprintf、vsprintf 和 vsscanf 调用 va_arg 宏,返回后的 arg 的值是不确定的

当您调用vsnprintf时,va_list可以通过值或引用传递(就我们所知,它是一种不透明的类型)。所以第一个vsnprintf实际上可以修改va_list和破坏第二个。推荐的方法是使用va_copy.

事实上,根据这篇文章,它不会在 x86 上以这种方式发生,但它会在 x64 上发生。

于 2012-04-09T08:14:09.610 回答
1

vsnprintf 的第一个参数不应为空,根据:

http://msdn.microsoft.com/en-us/library/1kt27hek(v=vs.80).aspx

编辑 1:如果 *psz 为空,则不应释放它!

于 2012-04-09T06:25:35.717 回答
1

第一次调用 vsnprintf() 实际上是尝试获取最终字符串的长度。但是,它有一个副作用!它也将变量参数移动到列表中的下一个参数。因此,对 vsnprintf() 的下一次调用没有捕获列表中的第一个参数。简单的技巧是从第一个 vsnprintf() 获得长度后重新设置变量参数列表以重新开始。也许还有另一种方法可以做得更好,但是,是的,这就是问题所在。

于 2014-04-09T17:58:34.620 回答
0

此外,*psz 可能指向一个已经分配的字符串(在堆上),它可能会在格式化期间使用。

为了*psz潜在地可重用,需要一些指示它是垃圾还是有效的堆指针。如果没有函数参数表明,您可以假设 NULL 哨兵值的唯一合理约定....即,如果*psz不是 NULL,那么您可以重用它,前提是您希望格式化的数据可以适合相同的空间。由于该函数没有给出任何先前分配的内存量的指示,您可以: - 使用 realloc 并信任它以避免不必要的缓冲区移动 - 推断最小预先存在的缓冲区大小strlen()- 这意味着如果你'说写一个长字符串然后一个短字符串然后将原始长字符串写入缓冲区,最后一个操作将不必要地替换缓冲区。

显然 realloc 是一个更好的选择。

int StrPrintF(char **psz, const char *szFmt, ...)
{
     va_list args;
     int nLen;
     va_start(args, szFmt);
     if ((nLen = vsnprintf(NULL, 0, szFmt, args)) >= 0)
     {
         char *szRes = (char*) realloc(psz, nLen + 1);
                             // ^ realloc does a fresh allocation is *psz == NULL
         if (szRes)
             vsnprintf(*psz = szRes, nLen + 1, szFmt, args); // can't fail
                       // ^ note the assignment....
         else
             nLen = -1;
     }
     va_end(args);
     return nLen;
} 

还要注意 - 来自 Linux 手册页printf()- 如果你sprintf()没有返回有用的长度,你必须获取/编写一个实现......

关于 snprintf() 的返回值,SUSv2 和 C99 相互矛盾:当使用 size=0 调用 snprintf() 时,SUSv2 规定未指定的返回值小于 1,而 C99 在这种情况下允许 str 为 NULL,并给出返回值(一如既往)作为在输出字符串足够大的情况下将写入的字符数。

于 2012-04-09T07:12:44.837 回答
-1

没有直接给你答案:检查你的输入。

于 2012-04-09T06:21:15.257 回答