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

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

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

有办法缩短吗?

245032 次浏览

使用 Sprintf。(这是不安全的,但 OP 要求一个 ANSI C 答案。有关安全版本,请参阅注释。)

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

将格式化数据写入字符串组成具有相同文本的字符串 如果在 printf 上使用格式,那么它将被打印出来,但是 在打印时,内容以 C 字符串的形式存储在缓冲区中 由 str 指示。

缓冲区的大小应该足以包含整个 结果字符串(更安全的版本见 snprintf)。

属性之后自动追加终止空字符 内容。

在 format 参数之后,函数期望至少有同样多的 格式所需的额外参数。

参数:

str

指向存储 C 字符串的缓冲区的指针 应该足够大以包含生成的字符串。

format

C 字符串,它包含一个格式化字符串,后面跟着相同的 规格格式为 printf (详见 printf)。

... (additional arguments)

根据格式字符串的不同,函数可能需要 其他参数,每个参数都包含一个用于替换 格式字符串中的格式说明符(或指向存储的指针) 这些参数应该至少有一样多 格式说明符中指定的值的数目 参数被函数忽略。

例如:

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

我还没做过这个,所以我要指出正确的答案。

对于使用 <stdarg.h>头接受未指定数目的操作数的函数,C 有一些规定。您可以将函数定义为 void log_out(const char *fmt, ...);,并获得函数内部的 va_list。然后,您可以分配内存并使用分配的内存、格式和 va_list调用 vsprintf()

或者,您可以使用它来编写一个类似于 sprintf()的函数,该函数将分配内存并返回格式化的字符串,或多或少地像上面那样生成它。这将导致内存泄漏,但是如果您只是登出,那么这可能无关紧要。

在我看来,您似乎希望能够轻松地将使用 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()

Http://www.gnu.org/software/hello/manual/libc/variable-arguments-output.html 给出了下面的例子来打印到 stderr,你可以修改它来使用 log 函数来代替:

 #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。

如果你有 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()返回的长度是否包括结束 null。

如果您有一个符合 POSIX-2008的系统(任何现代 Linux) ,您可以使用安全和方便的 asprintf()函数: 它将 malloc()足够的内存为您,您不需要担心最大字符串大小。像这样使用它:

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()的功能。


如果您希望提供一个 log_out()包装器,它可以接受 printf()样式参数列表本身,那么您可以使用变量 vasprintf(),它接受 va_list作为参数。下面是这种包装器的一个绝对安全的实现:

//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");
}
}

别用 sprintf。
它将溢出您的字符串缓冲区并使您的程序崩溃。
一定要使用 snprintf

核实及摘要:

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

(咒语)

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
}