关于vsnprintf(访谈)

时间:2012-04-09 06:10:20

标签: c printf crt

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

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;
}

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

它与使用int而不是内存功能适合类型无关,例如size_tptrdiff_t。比如说,字符串是“合理”的大小。

我真的不知道这个bug会是什么。所有指针算术都可以恕我直言。我甚至不假设vsnprintf的两个后续调用产生相同的结果。所有可变处理的东西也是正确的恕我直言。不需要va_copy(使用va_list的被调用者负责)。同样在x86上va_copyva_end毫无意义。

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

修改

在查看答案和评论后 - 我想补充一些注意事项:

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

5 个答案:

答案 0 :(得分:8)

  

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

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

  

作为函数vfprintf,vfscanf,vprintf,vscanf,vsnprintf,   vsprintf和vsscanf调用va_arg宏,之后arg的值   回报是不确定的

当您致电vsnprintf时,va_list可以通过值或引用传递(对于我们所知道的,它是一种不透明的类型)。因此,第一个vsnprintf实际上可以修改va_list并破坏第二个内容。建议的方法是使用va_copy制作副本。

事实上,根据this article,它不会在x86上发生,但在x64上会发生。

答案 1 :(得分:1)

根据以下内容,vsnprintf的第一个参数不应为null:

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

编辑1: 如果它是null,你不应该释放* psz!

答案 2 :(得分:1)

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

答案 3 :(得分:0)

  

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

要使*psz具有潜在的可重用性,需要指出它是垃圾还是有效的堆指针。如果没有函数参数指示,你可以假设NULL sentinel值的唯一合理的约定....即如果*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;
} 

请注意 - 来自printf()的Linux联机帮助页 - 如果您的sprintf()没有返回有用的长度,则必须获取/编写实现.... {/ p >

  

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

答案 4 :(得分:-1)

完全没有给你答案:检查你的输入。