Crear cadenas con formato C (no imprimirlas)

101

Tengo una función que acepta una cadena, es decir:

void log_out(char *);

Al llamarlo, necesito crear una cadena formateada sobre la marcha como:

int i = 1;
log_out("some text %d", i);

¿Cómo hago esto en ANSI C?


Solo que, dado que sprintf()devuelve un int, esto significa que tengo que escribir al menos 3 comandos, como:

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

¿Alguna forma de acortar esto?

pistacho
fuente
1
Confío en que el prototipo de la función sea realmente: extern void log_out (const char *, ...); porque si no es así, la llamada es errónea (demasiados argumentos). Debería tomar un puntero constante porque no hay razón para que log_out () modifique la cadena. Por supuesto, podría estar diciendo que desea pasar una sola cadena a la función, pero no puede. Una opción es entonces escribir una versión varargs de la función log_out ().
Jonathan Leffler

Respuestas:

91

Utilice sprintf .

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

Escribir datos formateados en una cadena Compone una cadena con el mismo texto que se imprimiría si se usara formato en printf, pero en lugar de imprimirse, el contenido se almacena como una cadena C en el búfer apuntado por str.

El tamaño del búfer debe ser lo suficientemente grande para contener toda la cadena resultante (consulte snprintf para obtener una versión más segura).

Un carácter nulo de terminación se agrega automáticamente después del contenido.

Después del parámetro de formato, la función espera al menos tantos argumentos adicionales como sea necesario para el formato.

Parámetros:

str

Puntero a un búfer donde se almacena la cadena C resultante. El búfer debe ser lo suficientemente grande para contener la cadena resultante.

format

Cadena C que contiene una cadena de formato que sigue las mismas especificaciones que el formato en printf (consulte printf para obtener más detalles).

... (additional arguments)

Dependiendo de la cadena de formato, la función puede esperar una secuencia de argumentos adicionales, cada uno de los cuales contiene un valor que se utilizará para reemplazar un especificador de formato en la cadena de formato (o un puntero a una ubicación de almacenamiento, para n). Debe haber al menos tantos de estos argumentos como el número de valores especificados en los especificadores de formato. La función ignora los argumentos adicionales.

Ejemplo:

// Allocates storage
char *hello_world = (char*)malloc(13 * sizeof(char));
// Prints "Hello world!" on hello_world
sprintf(hello_world, "%s %s!", "Hello", "world");
Akappa
fuente
35
¡Ay! Si es posible, utilice las funciones de variación 'n'. Es decir, snprintf. Le harán contar el tamaño de su búfer y, por lo tanto, lo asegurarán contra sobrecostos.
dmckee --- ex-moderador gatito
7
Sí, pero pidió una función ANSI C y no estoy tan seguro de si snprintf es ansi o incluso posix.
Akappa
7
¡Ah! Me lo perdí. Pero estoy rescatado por el nuevo estándar: las variantes 'n' son oficiales en C99. Fwiw, tu caso es distinto, etc
dmckee --- ex-moderador gatito
1
los snprintf no son la forma más segura de hacerlo. Deberías ir con snprintf_s. Ver msdn.microsoft.com/en-us/library/f30dzcf6(VS.80).aspx
joce
2
@Joce: la familia de funciones C99 snprintf () es bastante segura; sin embargo, la familia snprintf_s () tiene algunos comportamientos diferentes (especialmente con respecto a cómo se maneja la trunción). PERO: la función _snprintf () de Microsoft no es segura, ya que puede dejar el búfer resultante sin terminar (C99 snprintf () siempre termina).
Michael Burr
16

Si tiene un sistema compatible con POSIX-2008 (cualquier Linux moderno), puede usar la función segura y conveniente asprintf(): tendrá malloc()suficiente memoria para usted, no necesita preocuparse por el tamaño máximo de cadena. Úselo así:

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

Este es el esfuerzo mínimo que puede realizar para construir la cuerda de forma segura. El sprintf()código que dio en la pregunta es profundamente defectuoso:

  • No hay memoria asignada detrás del puntero. ¡Está escribiendo la cadena en una ubicación aleatoria en la memoria!

  • Incluso si hubieras escrito

    char s[42];

    estaría en serios problemas, porque no puede saber qué número poner entre paréntesis.

  • Incluso si hubiera utilizado la variante "segura" snprintf(), aún correría el peligro de que sus cadenas se truncaran. Al escribir en un archivo de registro, esa es una preocupación relativamente menor, pero tiene el potencial de cortar con precisión la información que hubiera sido útil. Además, cortará el carácter final de la línea final, pegando la siguiente línea de registro al final de su línea escrita sin éxito.

  • Si intenta usar una combinación de malloc()y snprintf()producir un comportamiento correcto en todos los casos, terminará con aproximadamente el doble de código del que he dado asprintf()y básicamente reprogramará la funcionalidad de asprintf().


Si está buscando proporcionar un contenedor log_out()que pueda tomar una printf()lista de parámetros de estilo, puede usar la variante vasprintf()que toma a va_listcomo argumento. Aquí hay una implementación perfectamente segura de dicho contenedor:

//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");
    }
}
cmaster - reinstalar a monica
fuente
2
Tenga en cuenta que asprintf()no forma parte del estándar C 2011 ni de POSIX, ni siquiera POSIX 2008 o 2013. Forma parte de TR 27431-2: consulte ¿Utiliza las funciones 'seguras' de TR 24731?
Jonathan Leffler
funciona como un encanto! Me estaba enfrentando al problema, cuando el valor "char *" no se imprimía correctamente en los registros, es decir, la cadena formateada no era apropiada de alguna manera. el código estaba usando, "asprintf ()".
Parasrish
11

Me parece que desea poder pasar fácilmente una cadena creada con el formato de estilo printf a la función que ya tiene que toma una cadena simple. Puede crear una función contenedora utilizando las stdarg.hinstalaciones y vsnprintf()(que pueden no estar disponibles, dependiendo de su compilador / plataforma):

#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;
}

Para las plataformas que no proporcionan una buena implementación (o ninguna implementación) de la snprintf()familia de rutinas, he utilizado con éxito un dominio casi público snprintf()de Holger Weiss .

Michael Burr
fuente
Actualmente, se podría considerar usar vsnprintf_s.
amalgama
3

Si tiene el código log_out(), vuelva a escribirlo. Lo más probable es que puedas hacer:

static FILE *logfp = ...;

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

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

Si se necesita información de registro adicional, se puede imprimir antes o después del mensaje que se muestra. Esto ahorra asignación de memoria y tamaños de búfer dudosos y así sucesivamente. Probablemente necesite inicializar logfpa cero (puntero nulo) y verificar si es nulo y abrir el archivo de registro según corresponda, pero el código existente log_out()debería lidiar con eso de todos modos.

La ventaja de esta solución es que simplemente puede llamarla como si fuera una variante de printf(); de hecho, es una variante menor de printf().

Si no tiene el código log_out(), considere si puede reemplazarlo con una variante como la descrita anteriormente. Si puede usar el mismo nombre dependerá de su marco de aplicación y de la fuente última de la log_out()función actual . Si está en el mismo archivo de objeto que otra función indispensable, tendrá que usar un nuevo nombre. Si no puede averiguar cómo replicarlo exactamente, tendrá que usar alguna variante como las que se dan en otras respuestas que asignen una cantidad adecuada de memoria.

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? */
}

Obviamente, ahora llama al en log_out_wrapper()lugar de log_out(), pero la asignación de memoria y así sucesivamente se realiza una vez. Me reservo el derecho de sobreasignar espacio por un byte innecesario; no he verificado dos veces si la longitud devuelta por vsnprintf()incluye el nulo de terminación o no.

Jonathan Leffler
fuente
3

No use sprintf.
Desbordará su String-Buffer y bloqueará su programa.
Utilice siempre snprintf

Tom
fuente
0

No he hecho esto, así que solo voy a señalar la respuesta correcta.

C tiene disposiciones para funciones que toman números no especificados de operandos, usando el <stdarg.h>encabezado. Puede definir su función como void log_out(const char *fmt, ...);y obtener el va_listinterior de la función. Luego, puede asignar memoria y llamar vsprintf()con la memoria asignada, formato y va_list.

Alternativamente, puede usar esto para escribir una función análoga a la sprintf()que asignaría memoria y devolvería la cadena formateada, generándola más o menos como arriba. Sería una pérdida de memoria, pero si acaba de cerrar la sesión, puede que no importe.

David Thornley
fuente
-2

http://www.gnu.org/software/hello/manual/libc/Variable-Arguments-Output.html da el siguiente ejemplo para imprimir en stderr. Puede modificarlo para usar su función de registro en su lugar:

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

En lugar de vfprintf, deberá usar vsprintf donde debe proporcionar un búfer adecuado para imprimir.

Lothar
fuente