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


101

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

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()函数的varargs版本。
乔纳森·莱夫勒

Answers:


91

使用sprintf

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

将格式化的数据写入字符串如果在printf上使用format,则使用与要打印的文本组成的字符串组成一个字符串,但是内容将作为C字符串存储在由str指向的缓冲区中,而不是被打印。

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

内容后面会自动附加一个终止的空字符。

在format参数之后,该函数至少需要与format一样多的附加参数。

参数:

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

35
kes!如果可能的话,请使用'n'变化函数。即snprintf。它们将使您计算缓冲区大小,从而确保不会发生溢出。
dmckee ---前主持人小猫,

7
是的,但是他要求使用ANSI C函数,我不确定snprintf是ansi还是posix。
akappa

7
啊。我错过了。但是我对新标准感到宽慰:“ n”变体在C99中是官方的。FWIW,YMMV等
dmckee ---前主持人小猫

1
snprintf并不是最安全的方法。您应该使用snprintf_s。参见msdn.microsoft.com/en-us/library/f30dzcf6(VS.80).aspx
09年

2
@Joce-C99 snprintf()系列函数非常安全;但是snprintf_s()系列确实具有不同的行为(特别是关于如何处理截断的行为)。但是-Microsoft的_snprintf()函数并不安全-因为它可能会使生成的缓冲区未终止(C99 snprintf()总是终止)。
迈克尔·伯

16

如果您拥有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];

    您将陷入困境,因为您不知道将哪个数字放在括号中。

  • 即使您使用了“ safe”变体snprintf(),您仍然会面临字符串被截断的危险。在写入日志文件时,这是一个相对较小的问题,但是它有可能精确地切断有用的信息。而且,它将切断结尾的结尾字符,将下一条日志行粘贴到未成功写入的行的末尾。

  • 如果尝试在所有情况下使用的组合malloc()snprintf()产生正确的行为,那么最终得到的代码大约是我给出的两倍asprintf(),并且基本上对的功能进行了重新编程asprintf()


如果您正在寻找一种包装,log_out()printf()本身可以使用样式参数列表,则可以使用vasprintf()将a 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");
    }
}

2
请注意,asprintf()它既不是标准C 2011的一部分,也不是POSIX的一部分,甚至不是POSIX 2008或2013的一部分。它不是TR 27431-2的一部分:请参阅是否使用TR 24731的“安全”功能?
乔纳森·勒夫勒

作品就像魅力!我正面临一个问题,当“ char *”值未正确记录在日志中时,即格式化的字符串不适合使用。代码正在使用“ asprintf()”。
parasrish

11

在我看来,您希望能够轻松地将使用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()例程系列的平台,我已经成功地使用snprintf()Holger Weiss 的几乎公共领域


当前,可以考虑使用vsnprintf_s。
amalgamate 2013年

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()包含终止null。


3

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


0

我还没有这样做,所以我只想指出正确的答案。

C使用<stdarg.h>标头规定了使用未指定数量的操作数的函数。您可以将函数定义为void log_out(const char *fmt, ...);,并获取va_list函数内部。然后,您可以分配内存并vsprintf()使用已分配的内存,格式和进行调用va_list

或者,您可以使用它来编写类似于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,在这里需要提供足够的缓冲区以供打印。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.