创建C格式化字符串(不是打印它们)

148

我有一个接受字符串的函数,即:

void log_out(char *);

在调用它时,我需要即时创建一个格式化的字符串,如下所示:
int i = 1;
log_out("some text %d", i);

如何在 ANSI C 中实现这个功能?


但是,由于 sprintf() 返回一个 int 值,这意味着我至少需要编写 3 条命令,例如:

char *s;
sprintf(s, "%d\t%d", ix, iy);
log_out(s);

有什么方法可以缩短这个吗?

1
我相信函数原型确实是: extern void log_out(const char *, ...); 因为如果不是这样,对它的调用就是错误的(参数太多)。它应该接受一个const指针,因为log_out()没有修改字符串的理由。当然,你可能会说你想把一个单独的字符串传递给这个函数 - 但是却不能这么做。一个选择是编写一个变参版本的log_out()函数。 - Jonathan Leffler
8个回答

130

使用sprintf函数。(这并不安全,但是OP要求一个ANSI C的答案。请查看评论以获取更安全的版本。)

int sprintf ( char * str, const char * format, ... );

将格式化的数据写入字符串。该函数返回一个与使用printf打印相同文本的字符串,但不是直接打印输出,而是存储在由str指向的字符数组缓冲区中。

缓冲区的大小应足够大,以容纳整个结果字符串(请参见snprintf获取更安全的版本)。

内容后会自动添加一个终止空字符。

在格式参数之后,该函数期望提供至少所需数量的额外参数用于格式化。

str

指向存储结果C字符串的缓冲区的指针。该缓冲区应足够大以容纳结果字符串。

format

一个包含格式字符串的 C 字符串,其规范与 printf 中的格式相同(有关详细信息,请参见 printf)。

... (additional arguments)

根据格式字符串的不同,该函数可能会期望一个附加参数序列,每个参数都包含一个值,用于替换格式字符串中的格式说明符(或者是指向存储位置的指针,对于n)。这些参数的数量应与格式说明符中指定的值数量至少相同。函数将忽略多余的附加参数。

示例:

// Allocates storage
char *hello_world = (char*)malloc(13 * sizeof(char));
// Prints "Hello world!" on hello_world
sprintf(hello_world, "%s %s!", "Hello", "world");

46
哎呀!如有可能,请使用“n”变异函数,例如snprintf。它们将使您计算缓冲区大小,并确保防止溢出。 - dmckee --- ex-moderator kitten
7
是的,但他要求一个ANSI C函数,我不确定snprintf是不是ansi甚至posix。 - akappa
8
啊,我没注意到这一点。但是我被新标准拯救了:在C99中,“n”变量是正式的。顺便说一下,你的结果可能会有所不同,等等。 - dmckee --- ex-moderator kitten
2
@Joce - C99的snprintf()函数族非常安全;然而,snprintf_s()函数族确实有一些不同的行为(特别是关于如何处理截断)。但是,微软的_snprintf()函数并不安全,因为它可能会导致结果缓冲区未终止(C99 snprintf()总是终止)。 - Michael Burr
1
从理论的角度来看,这是一个很好的答案,但请提供一些将理论付诸实践的简单示例。 - nbro
显示剩余5条评论

28

如果您有一个符合POSIX-2008标准的系统(任何现代Linux系统),您可以使用安全和方便的asprintf()函数:它会为您分配足够的内存,您无需担心字符串大小的最大值。使用以下方式:

char* string;
if(0 > asprintf(&string, "Formatting a number: %d\n", 42)) return error;
log_out(string);
free(string);

这是最小的努力,可以安全地构建字符串。你在问题中提供的sprintf()代码存在严重缺陷:

  • 指针后面没有分配内存。你正在向随机内存位置写入字符串!

  • 即使您已经编写了

    char s[42];
    

    如果你不知道该在括号中输入什么数字,那么你就会陷入麻烦之中。

  • 即使你使用了“安全”变量 snprintf(),你仍然会有字符串被截断的风险。当写入日志文件时,这只是一个相对较小的问题,但它有可能切断本来有用的信息。此外,它将切断尾随的换行符,将下一条日志行粘贴到你未成功写入的行的末尾。

  • 如果你试图使用 malloc()snprintf() 的组合来在所有情况下产生正确的行为,你最终将得到大约两倍于我给出的 asprintf() 代码,并基本上重新编写 asprintf() 的功能。


如果你想提供一个包装器来接收 printf() 风格参数列表的 log_out(),你可以使用带有 va_list 参数的变量 vasprintf()。下面是这样一个包装器的完全安全的实现:

//Tell gcc that we are defining a printf-style function so that it can do type checking.
//Obviously, this should go into a header.
void log_out_wrapper(const char *format, ...) __attribute__ ((format (printf, 1, 2)));

void log_out_wrapper(const char *format, ...) {
    char* string;
    va_list args;

    va_start(args, format);
    if(0 > vasprintf(&string, format, args)) string = NULL;    //this is for logging, so failed allocation is not fatal
    va_end(args);

    if(string) {
        log_out(string);
        free(string);
    } else {
        log_out("Error while logging a message: Memory allocation failed.\n");
    }
}

2
请注意,asprintf()既不是标准C 2011的一部分,也不是POSIX的一部分,甚至不是POSIX 2008或2013的一部分。它是TR 27431-2的一部分:请参见Do you use the TR 24731 'safe' functions? - Jonathan Leffler
运行得很好!我遇到了一个问题,就是在日志中无法正确打印“char*”值,即格式化字符串出现了问题。代码使用了“asprintf()”。 - parasrish

13

我认为你想要能够轻松地将使用 printf 格式化的字符串传递给已有的接受简单字符串的函数。您可以使用 stdarg.h 工具和 vsnprintf() 创建一个包装函数(根据您的编译器/平台,这可能不容易获得):

#include <stdarg.h>
#include <stdio.h>

// a function that accepts a string:

void foo( char* s);

// You'd like to call a function that takes a format string 
//  and then calls foo():

void foofmt( char* fmt, ...)
{
    char buf[100];     // this should really be sized appropriately
                       // possibly in response to a call to vsnprintf()
    va_list vl;
    va_start(vl, fmt);

    vsnprintf( buf, sizeof( buf), fmt, vl);

    va_end( vl);

    foo( buf);
}



int main()
{
    int val = 42;

    foofmt( "Some value: %d\n", val);
    return 0;
}

对于没有提供良好实现(或任何实现)snprintf() 系列函数的平台,我成功地使用了 Holger Weiss 的一个几乎是公共领域的 snprintf()


目前,可以考虑使用vsnprintf_s。 - amalgamate

6

不要使用sprintf函数。
它会溢出您的字符串缓冲区并导致程序崩溃。
始终使用snprintf函数


3
如果您有 log_out() 的代码,请重写它。很可能,您可以这样做:
static FILE *logfp = ...;

void log_out(const char *fmt, ...)
{
    va_list args;

    va_start(args, fmt);
    vfprintf(logfp, fmt, args);
    va_end(args);
}

如果需要额外的日志信息,可以在显示消息之前或之后打印出来。这样可以节省内存分配、疑虑的缓冲区大小等。您可能需要将logfp初始化为零(空指针),并检查它是否为空,并根据需要打开日志文件 - 但现有的log_out()代码应该已经处理了这个问题。

这种解决方案的优点是,您可以像调用printf()的变体一样直接调用它;事实上,它只是printf()的一个小变体。

如果您没有log_out()的代码,请考虑是否可以用上述方法中的变体替换它。是否可以使用相同的名称取决于您的应用程序框架和当前log_out()函数的最终来源。如果它与另一个不可或缺的函数在同一个目标文件中,您必须使用一个新名称。如果您无法确定如何完全复制它,您将不得不使用其他答案中给出的分配适当数量内存的某些变体。

void log_out_wrapper(const char *fmt, ...)
{
    va_list args;
    size_t  len;
    char   *space;

    va_start(args, fmt);
    len = vsnprintf(0, 0, fmt, args);
    va_end(args);
    if ((space = malloc(len + 1)) != 0)
    {
         va_start(args, fmt);
         vsnprintf(space, len+1, fmt, args);
         va_end(args);
         log_out(space);
         free(space);
    }
    /* else - what to do if memory allocation fails? */
}

显然,现在你需要调用log_out_wrapper()而不是log_out() - 但是内存分配等只需一次。我保留通过过度分配空间一个不必要的字节的权利 - 我还没有双重检查vsnprintf()返回的长度是否包括终止符或不包括。

2

验证和总结:

sprintf与asprintf

asprintf = malloc + sprintf

示例代码

sprintf

int largeEnoughBufferLen = 20;

char *someStr = (char*)malloc(largeEnoughBufferLen * sizeof(char));

sprintf(someStr, "formatted string: %s %s!", "Hello", "world");

// do what you want for formatted string: someStr

free(someStr);

asprintf

char *someStr;

int formattedStrResult = asprintf(&someStr, "formatted string: %s %s!", "Hello", "world");

if(formattedStrResult > 0){
    // do what you want for formatted string: someStr

    free(someStr);
} else {
    // some error
}

作者要求一个 ANSI C 的答案。asprintf 不是 C 标准的一部分。 我们需要在源文件的开头添加 #define __GNU_SOURCE 或者根据 https://dev59.com/N7roa4cB1Zd3GeqPknbX 的建议加上 -D_GNU_SOURCE 标志。 - Dimitri Lesnoff

0

我没有做过这个,所以我只是指出正确的答案。

C语言提供了使用<stdarg.h>头文件来处理不确定数量操作数的函数。你可以将你的函数定义为void log_out(const char *fmt, ...);,并在函数内部获取va_list。然后你可以分配内存,并使用分配的内存、格式和va_list调用vsprintf()

或者,你可以使用这种方法编写类似于sprintf()的函数,该函数将分配内存并返回格式化的字符串,生成方式与上述类似。虽然会有内存泄漏,但如果你只是记录日志,这可能并不重要。


-2

http://www.gnu.org/software/hello/manual/libc/Variable-Arguments-Output.html提供了一个将内容输出到stderr的示例。您可以修改它,以使用您自己的日志函数:

 #include <stdio.h>
 #include <stdarg.h>

 void
 eprintf (const char *template, ...)
 {
   va_list ap;
   extern char *program_invocation_short_name;

   fprintf (stderr, "%s: ", program_invocation_short_name);
   va_start (ap, template);
   vfprintf (stderr, template, ap);
   va_end (ap);
 }

在需要打印输出时,您需要使用vsprintf而不是vfprintf,并提供足够的缓冲区。


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