带有空目标指针意外答案的 vsnprintf

vsnprintf with a null destination pointer unexpected answer

主要问题

我在使用 vsnprintf 时得到了一些意想不到的结果。在下面的代码中,我使用了 snprintf 并传递了一个空目标指针来找出它需要多少space

#define long_string 256
typedef char STRING_VARIABLE [long_string + 1];

void SAFE_snprintf( char * buffer, char * format ,...  )
{
  va_list args;

  va_start(args, format);

  int m = vsnprintf (0, 0, format, args);
  printf("m = %d\n", m);
  if (m < 0)
    {
    perror ("snprintf failed");
    abort ();
    }

  // Allocating memory
  char *bufferString = (char *) malloc (n - 1);
  if (!bufferString)
    {
    perror ("malloc failed");
    abort ();
    } 

  m = vsnprintf (bufferString, (n - 1), format, args);

 if (m < 0)
    {
    perror ("vsnprintf failed");
    abort ();
    }

  strcpy(buffer, bufferString);

  free (bufferString);

  va_end(args);

}

int main(int argc, char * argv[])
{
  char InputString [] = "Hello";
  STRING_VARIABLE bufferStrings;
  char format [] = "%s_test";
  int n = snprintf (0, 0, "%s_test", InputString);

  if (n < 0)
    {
    perror ("vsnprintf failed");
    abort ();
    }

  printf("n = %d", n);
  

  SAFE_snprintf(bufferStrings, format , InputString);
  
  return 0;
}

以上代码returns

n = 7
m = 10

我不确定为什么 snprintf 返回 7(这是正确的),而 vsnprintf 返回 10。我认为这是错误的,当然我的理解在某处有缺陷。

我为什么要这样做?

我想“安全地”使用 snprintf,即避免字符串截断。这个想法是在使用 snprintf 之前确定字符串的大小。这意味着要使用它两次。一旦锻炼了大小,然后分配适当的内存再次使用 snprintf。当然,任何 printf 函数都需要可变数量的输入。所以想使用 vsnprintf 创建一个可变参数函数来执行上述操作。

我知道仍然存在检查传递的原始字符串是否不太长并且不会导致字符串截断的问题。但是对为什么 vsnprintf 没有按预期工作感到困惑

re-useva_list再调用无效

va_list args;
va_start(args, format);
vsnprintf(..., args);
va_end(args); // basically you have to call it

在调用另一个 v* 函数之前再次调用 va_copyva_start

va_list args;
va_start(args, format);
vsnprintf(..., args);
va_end(args);
va_start(args, format);
vsnprintf(..., args);
va_end(args);

va_list args, args2;
va_start(args, format);
vsnprintf(..., args);
va_copy(args2, args);
va_end(args);
vsnprintf(..., args2);
va_end(args2);

您的缓冲区被限制为 256 个字符 long_string 256,因此它没有解决任何问题并且 strcpy(buffer, bufferString); 非常不安全

不要使用 typedef 数组 - 它们 非常 令人困惑。更喜欢结构。

总体而言,您似乎想要实施 asprintf - 请参阅 sprintf() with automatic memory allocation? , https://man7.org/linux/man-pages/man3/asprintf.3.html . Maybe asprintf is going to be standard https://en.cppreference.com/w/c/experimental/dynamic

您的代码中存在多个问题:

  • 您重复使用了 args 而没有使用 va_start()va_copy() 重新启动 va_list
  • 您分配 n - 1 字节而不是 n + 1
  • 使用 strcpy 复制分配的字符串是不安全的,因为组合的字符串可能超过 256 个字节。
  • format 应该有类型 const char *format.
  • 你的类型 STRING_VARIABLE 很混乱。将指针隐藏在 typedef 后面令人困惑,但将数组隐藏在 typedef 后面更糟。

为什么不总是分配内存和 return 指向调用者的指针?一些 C 库有一个函数 asprintf 具有这些语义,这是一个很好的标准化候选者,但委员会花了 30 年才考虑 strdup(),所以自己做是有意义的。

这是一个分配版本:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// allocating version of snprintf
char *SAFE_snprintf(const char *format, ...) {
    // use a local buffer to avoid calling snprintf twice for short strings
    char buf[256];
    va_list args;
    int len;

    va_start(args, format);
    len = vsnprintf(buf, sizeof buf, format, args);
    va_end(args);

    if (len < 0) {
        return NULL;    // errno was set by snprintf
    }

    // Allocating memory
    char *bufferString = (char *)malloc(len + 1);
    if (!bufferString) {
        return NULL;    // errno was set by malloc to EMEM
    }
    if (len < (int)sizeof(buf)) {
        strcpy(bufferString, buf);
    } else {
        va_start(args, format);
        vsnprintf(bufferString, len + 1, format, args);
        va_end(args);
    }
    return bufferString;
}

int main(int argc, char *argv[]) {
    char InputString[] = "Hello";

    int n = snprintf(NULL, 0, "%s_test", InputString);
    printf("n = %d\n", n);
  
    char *str = SAFE_snprintf("%s_test", InputString);
    if (str == NULL) {  
        perror("SAFE_snprintf failed");
        return 1;
    } else {
        printf("len=%zu, str=%s\n", strlen(str), str);
        free(str);
        return 0;
    }
}

如果您的目标只是检测截断,只需将 snprintf 的 return 值与目标数组的长度进行比较:

#include <stdio.h>
#include <stdlib.h>

// safe version of snprintf that detects truncation
// complain on error and truncation
// return -1 on error
// return 1 on case of truncation
// otherwise return 0
int SAFE_snprintf(char *dest, size_t size, const char *format, ...) {
    int len;
    va_start(args, format);
    len = vsnprintf(dest, size, format, args);
    va_end(args);
    if (len < 0) {
        perror("vsnprintf failed");
        return -1;
    } else
    if (len < (int)size) {
        // no error
        return 0;
    } else {
        perror("vsnprintf caused truncation");
        return -1;
    }
}

int main(int argc, char *argv[]) {
    char hello[] = "Hello";
    char long_hello[] = "Pardon my intrusion, I merely want to say hello";
    char name[] = "Mrs Robinson";
    char buf[32];
    int res;

    res = SAFE_snprintf(buf, sizeof buf, "%s %s", hello, name);
    printf("res=%d, buf=%s\n", res, buf);
    res = SAFE_snprintf(buf, sizeof buf, "%s %s", long_hello, name);
    printf("res=%d, buf=%s\n", res, buf);
    return 0;
}