自动内存分配的sprintf()函数?

52

我正在寻找一个类似于 sprintf() 的函数实现,可以自动分配所需的内存。因此,我想说:

char *my_str = dynamic_sprintf("Hello %s, this is a %.*s nice %05d string", a, b, c, d);

my_str接收的是一个分配的内存块的地址,该内存块保存了sprintf()的结果。

在另一个论坛上,我读到可以这样解决:

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

int main()
{
    char    *ret;
    char    *a = "Hello";
    char    *b = "World";
    int     c = 123;

    int     numbytes;

    numbytes = sprintf((char *)NULL, "%s %d %s!", a, c, b);
    printf("numbytes = %d", numbytes);

    ret = (char *)malloc((numbytes + 1) * sizeof(char));
    sprintf(ret, "%s %d %s!", a, c, b);

    printf("ret = >%s<\n", ret);
    free(ret);

    return 0;
}

但是当 sprintf() 函数使用空指针时,会立即导致段错误。

那么有什么想法、解决方案或技巧吗?提供一个类似于 sprintf() 的小型解析器并放入公共领域的实现就足够了,然后我就可以自己完成了。

非常感谢!


1
谁给你那个建议可能是想让你使用 snprintf 而不是 sprintf - R.. GitHub STOP HELPING ICE
可能是使用snprintf避免缓冲区溢出的重复问题。 - user9645477
8个回答

52

以下是从Stack Overflow获取的原始回答。正如其他人所提到的,你需要使用snprintf而不是sprintf。确保snprintf的第二个参数为0。这将防止snprintf写入作为第一个参数的NULL字符串。

第二个参数是必需的,因为它告诉snprintf输出缓冲区中没有足够的空间可供写入。当没有足够的空间可用时,snprintf返回如果有足够的空间可用它将要写入的字节数。

以下是从该链接复制的代码...

char* get_error_message(char const *msg) {
    size_t needed = snprintf(NULL, 0, "%s: %s (%d)", msg, strerror(errno), errno) + 1;
    char  *buffer = malloc(needed);
    sprintf(buffer, "%s: %s (%d)", msg, strerror(errno), errno);
    return buffer;
}

4
你不应该将 needed 增加1以考虑到终止的空字符吗? - beldaz
5
一开始没有注意到第一行末尾的 +1(它在可见区域之外):size_t needed = snprintf(...) + 1;。该行代码的作用是将格式化后的字符串长度加1,以便为字符串终止符留出空间。 - user2421739
2
我有点担心在这里传递NULL是否会引发未定义的行为,所以我进行了检查,并确认它是由C标准明确允许的 - 请参见https://dev59.com/02jWa4cB1Zd3GeqPoC1f#57646312。 - Mark Amery
3
从技术上讲,这段代码是不安全的,因为在调用sprintf之前,errno可能会在snprintf调用之间发生变化,而sprintf没有受到缓冲区溢出的保护。你应该对两个调用都使用snprintf,并且应该在第一次调用之前将errno保存到一个本地变量中。 - chqrlie

37

GNU和BSD都有asprintfvasprintf,专门为你设计的工具。它将为您分配内存,如果内存分配出错,则返回null。

asprintf在分配字符串时会采用正确的方法--首先测量大小,然后尝试使用malloc进行分配。如果分配失败,则返回null。除非您有自己的内存分配系统,不允许使用malloc,否则asprintf是最佳的工具。

代码如下:

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

int main()
{
    char*   ret;
    char*   a = "Hello";
    char*   b = "World";
    int     c = 123;

    int err = asprintf(&ret, "%s %d %s!", a, c, b );
    if (err == -1) {
        fprintf(stderr, "Error in asprintf\n");
        return 1;
    }

    printf("ret = >%s<\n", ret);
    free(ret);

    return 0;
}

10
asprintf() 是我选择的函数,但不幸的是它不是标准的,也不具备可移植性,这很糟糕! - the-shamen
1
@the-shamen - 你所要求的定义上来说是非标准且不可移植的。如果需要,可以获取asprintf的源代码并将其引入到您的项目中,或者独立重新实现它。 - bstpierre
19
我没有听说过可以返回指针的 asprintf() 函数。GNU、BSD(以及 gnulib 和 libstrl 提供的函数)中提供的函数与相应的 printf() 调用具有相同的返回值,并且将一个指向指针的指针作为第一个参数。因此,如果 ret == -1,则 char *s; int ret = asprintf(&s, "%s %d %s!", a, c, b); 会出错。只是想知道,有哪些系统/库提供了像这个答案中一样返回指针的 asprintf() 函数? - binki

20

如果您可以接受GNU/BSD扩展,那么问题已经有了答案。您可以使用asprintf()(以及构建包装函数的vasprintf())。

但是根据手册,snprintf()vsnprintf()是POSIX所规定的,并且可以用于构建自己简单版本的asprintf()vasprintf()

int
vasprintf(char **strp, const char *fmt, va_list ap)
{
    va_list ap1;
    int len;
    char *buffer;
    int res;

    va_copy(ap1, ap);
    len = vsnprintf(NULL, 0, fmt, ap1);

    if (len < 0)
        return len;

    va_end(ap1);
    buffer = malloc(len + 1);

    if (!buffer)
        return -1;

    res = vsnprintf(buffer, len + 1, fmt, ap);

    if (res < 0)
        free(buffer);
    else
        *strp = buffer;

    return res;
}

int
asprintf(char **strp, const char *fmt, ...)
{
    int error;
    va_list ap;

    va_start(ap, fmt);
    error = vasprintf(strp, fmt, ap);
    va_end(ap);

    return error;
}

你可以运用预处理器的魔力,仅在不支持这些函数的系统上使用你的版本。


1
你只能将 va_list 变量传递给一个函数。如果你想像在 vasprintf() 中那样两次使用 vsnprintf(),你应该使用 va_copy() - Ilia K.
1
如果在从vasprintf()返回之前添加va_end(ap1)(例如在调用vsnprintf()后立即添加),那么就会这样。 - Ilia K.
1
你忘记在vasprintf的结尾设置strp了。 - 5andr0
代码中有一些错误处理(它处理了calloc的失败),但如果vsnprintf失败,它仍然会崩溃:如果第一个vsnprintf返回小于-1的错误代码,则大小计算将会回绕。如果第二次调用vsnprintf失败,则缓冲区会泄漏。 - user2421739
1
@user2421739,我修复了这个问题,因为这个答案已经相当老了。 - Marco Bonelli

11
  1. 如果可能的话,使用snprintf -- 它提供了一种简单的方法来测量将要生成的数据的大小,以便您可以分配空间。
  2. 如果您真的无法这样做,另一种可能性是使用fprintf将内容打印到临时文件中,以获取大小,然后分配内存,并使用sprintf。但是snprintf肯定是首选方法。

6

GLib库提供了一个g_strdup_printf函数,如果可以连接到GLib,则完全符合您的要求。从文档中可以看出:

与标准C的sprintf()函数类似,但更安全,因为它计算所需的最大空间并分配内存以容纳结果。当不再需要返回的字符串时,应使用g_free()释放。


你好,谢谢!但这只是glibc,我需要一个跨平台的解决方案。所以也许自己做会更好? - the-shamen
3
GLib(GTK +的基础)与GNU C库(glibc)不同,但它与glibc的asprintf函数相当。 - Pavel Šimerda
1
GLib是跨平台的。 - Jules G.M.

3

POSIX.1 (也称为IEEE 1003.1-2008)提供了open_memstream函数:

char *ptr;
size_t size;
FILE *f = open_memstream(&ptr, &size);
fprintf(f, "lots of stuff here\n");
fclose(f);
write(1, ptr, size); /* for example */
free(ptr);

open_memstream(3)至少在Linux和macOS上可用,已经有些年头了。 open_memstream(3)的反义词是fmemopen(3),它使缓冲区的内容可供读取。

如果您只想要一个简单的sprintf(3),那么广泛实现但非标准的asprintf(3)可能是您想要的。


0

这是我(v3)的https://dev59.com/_W865IYBdhLWcg3whe5f#10388547版本 - 更加通用一些 [请注意,我是一个C++新手,使用时自行决定风险 :P]:

#include <cstdarg>

char* myFormat(const char* const format...) {
  // `vsnprintf()` changes `va_list`'s state, so using it after that is UB.
  // We need the args twice, so it is safer to just get two copies.
  va_list args1;
  va_list args2;
  va_start(args1, format);
  va_start(args2, format);

  size_t needed = 1 + vsnprintf(nullptr, 0, format, args1);

  // they say to cast in C++, so I cast…
  // https://dev59.com/jm435IYBdhLWcg3w-1V_#5099675
  char* buffer = (char*) malloc(needed);

  vsnprintf(buffer, needed, format, args2);

  va_end(args1);
  va_end(args2);

  return buffer;
}

char* formatted = myFormat("Foo %s: %d", "bar", 456);
Serial.println(formatted); // Foo bar: 456

free(formatted); // remember to free or u will have a memory leak!

1
这段代码无法正常工作,因为你两次使用了 args 且没有使用 va_end()。你必须使用 va_copy 或者 va_start()va_end() 两次。此外,使用 vsprintf 是有风险的。为什么不使用 vsnprintf 两次以确保安全呢? - chqrlie
这个不起作用。好吧,至少对于我的 Foo bar: 456 示例和我测试过的其他示例是有效的。“你必须使用 va_copy 或 va_start() 和 va_end() 两次。”我是新手,只是复制粘贴了一些代码,像瞎子一样修改了它,但它不知何故就起作用了 xD“此外,使用 vsprintf 是有风险的。为什么不使用 vsnprintf 两次来保险呢?”我不知道。我修改的示例使用了 snprintf 和 sprintf。我将它们更改为 "v" 版本以便于参数。建议使用两次 snprintf,但我是新手。你能更好地修改它吗?请修改,我会很乐意使用更好的版本 :) - MacDada
@chqrlie 好的,我现在学了更多的C++,明白你的意思了:D 我刚刚更新了这个例子,修复了UB和内存泄漏问题;-) - MacDada

0
/*  casprintf print to allocated or reallocated string

char *aux = NULL;
casprintf(&aux,"first line\n");
casprintf(&aux,"seconde line\n");
printf(aux);
free(aux);
*/
int vcasprintf(char **strp,const char *fmt,va_list ap)
{
  int ret;
  char *strp1;
  char *result;
  if (*strp==NULL)
     return vasprintf(strp,fmt,ap);

  ret=vasprintf(&strp1,fmt,ap); // ret = strlen(strp1) or -1
  if (ret == -1 ) return ret;
  if (ret==0) {free(strp1);return strlen(*strp);}

  size_t len = strlen(*strp);
  *strp=realloc(*strp,len + ret +1);
  memcpy((*strp)+len,strp1,ret+1);
  free(strp1);
  return(len+ret);
}

int casprintf(char **strp, const char *fmt, ...)
{
 int ret;
 va_list ap;
 va_start(ap,fmt);
 ret =vcasprintf(strp,fmt,ap);
 va_end(ap);
 return(ret);
}

欢迎来到 Stack Overflow。虽然您的代码可能提供了问题的答案,但请添加一些上下文说明,以便其他人了解它的作用和原因。 - Theo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接