Création de chaînes au format C (sans les imprimer)

101

J'ai une fonction qui accepte une chaîne, c'est-à-dire:

void log_out(char *);

En l'appelant, je dois créer une chaîne formatée à la volée comme:

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

Comment faire cela dans ANSI C?


Seulement, puisque sprintf()renvoie un int, cela signifie que je dois écrire au moins 3 commandes, comme:

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

Un moyen de raccourcir cela?

pistache
la source
1
J'espère que le prototype de fonction est vraiment: extern void log_out (const char *, ...); car si ce n'est pas le cas, l'appel à lui est erroné (trop d'arguments). Il devrait prendre un pointeur const car il n'y a aucune raison pour log_out () de modifier la chaîne. Bien sûr, vous pourriez dire que vous voulez passer une seule chaîne à la fonction - mais vous ne pouvez pas. Une option est alors d'écrire une version varargs de la fonction log_out ().
Jonathan Leffler

Réponses:

91

Utilisez sprintf .

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

Écrire des données formatées dans une chaîne Compose une chaîne avec le même texte que celui qui serait imprimé si format était utilisé sur printf, mais au lieu d'être imprimé, le contenu est stocké sous forme de chaîne C dans le tampon pointé par str.

La taille du tampon doit être suffisamment grande pour contenir toute la chaîne résultante (voir snprintf pour une version plus sûre).

Un caractère nul de fin est automatiquement ajouté après le contenu.

Après le paramètre format, la fonction attend au moins autant d'arguments supplémentaires que nécessaire pour le format.

Paramètres:

str

Pointeur vers un tampon dans lequel la chaîne C résultante est stockée. Le tampon doit être suffisamment grand pour contenir la chaîne résultante.

format

Chaîne C qui contient une chaîne de format qui suit les mêmes spécifications que format dans printf (voir printf pour plus de détails).

... (additional arguments)

Selon la chaîne de format, la fonction peut s'attendre à une séquence d'arguments supplémentaires, chacun contenant une valeur à utiliser pour remplacer un spécificateur de format dans la chaîne de format (ou un pointeur vers un emplacement de stockage, pour n). Il doit y avoir au moins autant de ces arguments que le nombre de valeurs spécifiées dans les spécificateurs de format. Les arguments supplémentaires sont ignorés par la fonction.

Exemple:

// Allocates storage
char *hello_world = (char*)malloc(13 * sizeof(char));
// Prints "Hello world!" on hello_world
sprintf(hello_world, "%s %s!", "Hello", "world");
Akappa
la source
35
Yikes! Si possible, utilisez les fonctions de variation «n». Ie snprintf. Ils vous feront compter vos tailles de buffer et ainsi vous assurer contre les dépassements.
dmckee --- ex-moderator chaton
7
Oui, mais il a demandé une fonction ANSI C et je ne sais pas si snprintf est ansi ou même posix.
akappa
7
Ah. J'ai manqué ça. Mais je suis sauvé par la nouvelle norme: les variantes 'n' sont officielles dans C99. FWIW, YMMV, etc.
dmckee --- ex-moderator chaton
1
le snprintf n'est pas le moyen le plus sûr de procéder. Vous devriez aller avec snprintf_s. Voir msdn.microsoft.com/en-us/library/f30dzcf6(VS.80).aspx
joce
2
@Joce - la famille de fonctions C99 snprintf () est assez sûre; cependant la famille snprintf_s () a un comportement différent (en particulier en ce qui concerne la manière dont la tronction est gérée). MAIS - La fonction _snprintf () de Microsoft n'est pas sûre - car elle peut potentiellement laisser le tampon résultant inachevé (C99 snprintf () se termine toujours).
Michael Burr
16

Si vous avez un système compatible POSIX-2008 (n'importe quel Linux moderne), vous pouvez utiliser la fonction sûre et pratique asprintf(): il aura malloc()suffisamment de mémoire pour vous, vous n'avez pas à vous soucier de la taille maximale de la chaîne. Utilisez-le comme ceci:

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

C'est l'effort minimum que vous pouvez obtenir pour construire la chaîne de manière sécurisée. Le sprintf()code que vous avez donné dans la question est profondément imparfait:

  • Il n'y a pas de mémoire allouée derrière le pointeur. Vous écrivez la chaîne dans un emplacement aléatoire de la mémoire!

  • Même si tu avais écrit

    char s[42];

    vous seriez en grande difficulté, car vous ne pouvez pas savoir quel chiffre mettre entre parenthèses.

  • Même si vous aviez utilisé la variante "sûre" snprintf(), vous courriez toujours le risque que vos chaînes soient tronquées. Lors de l'écriture dans un fichier journal, c'est une préoccupation relativement mineure, mais elle a le potentiel de couper précisément les informations qui auraient été utiles. En outre, il coupera le caractère de fin de ligne de fin, collant la ligne de journal suivante à la fin de votre ligne écrite sans succès.

  • Si vous essayez d'utiliser une combinaison de malloc()et snprintf()de produire un comportement correct dans tous les cas, vous vous retrouvez avec environ deux fois plus de code que ce que j'ai donné asprintf()et reprogrammez fondamentalement la fonctionnalité de asprintf().


Si vous cherchez à fournir un wrapper log_out()qui peut prendre une printf()liste de paramètres de style elle-même, vous pouvez utiliser la variante vasprintf()qui prend a va_listcomme argument. Voici une implémentation parfaitement sûre d'un tel wrapper:

//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 - réintégrer monica
la source
2
Notez que cela asprintf()ne fait ni partie de la norme C 2011 ni partie de POSIX, pas même POSIX 2008 ou 2013. Cela fait partie de TR 27431-2: voir Utilisez-vous les fonctions «sûres» du TR 24731?
Jonathan Leffler
fonctionne comme du charme! J'étais confronté au problème, lorsque la valeur "char *" n'était pas correctement imprimée dans les journaux, c'est-à-dire que la chaîne formatée n'était pas appropriée d'une manière ou d'une autre. le code utilisait, "asprintf ()".
parasrish le
11

Il me semble que vous voulez pouvoir facilement passer une chaîne créée à l'aide d'un formatage de style printf à la fonction que vous avez déjà qui prend une chaîne simple. Vous pouvez créer une fonction wrapper à l'aide des fonctionnalités stdarg.het vsnprintf()(qui peuvent ne pas être facilement disponibles, selon votre compilateur / plateforme):

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

Pour les plates-formes qui ne fournissent pas une bonne implémentation (ou une implémentation) de la snprintf()famille de routines, j'ai utilisé avec succès un domaine presque public snprintf()de Holger Weiss .

Michael Burr
la source
Actuellement, on peut envisager d'utiliser vsnprintf_s.
fusionner le
3

Si vous avez le code log_out(), réécrivez-le. Très probablement, vous pouvez faire:

static FILE *logfp = ...;

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

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

Si des informations de journalisation supplémentaires sont nécessaires, elles peuvent être imprimées avant ou après le message affiché. Cela permet d'économiser l'allocation de mémoire et les tailles de tampon douteuses et ainsi de suite. Vous devez probablement initialiser logfpà zéro (pointeur nul) et vérifier s'il est nul et ouvrir le fichier journal le cas échéant - mais le code existant log_out()devrait de toute façon s'occuper de cela.

L'avantage de cette solution est que vous pouvez simplement l'appeler comme s'il s'agissait d'une variante de printf(); en effet, il s'agit d'une variante mineure sur printf().

Si vous n'avez pas le code log_out(), demandez-vous si vous pouvez le remplacer par une variante telle que celle décrite ci-dessus. La possibilité d'utiliser le même nom dépendra de la structure de votre application et de la source ultime de la log_out()fonction actuelle . S'il se trouve dans le même fichier objet qu'une autre fonction indispensable, vous devrez utiliser un nouveau nom. Si vous ne parvenez pas à la répliquer exactement, vous devrez utiliser une variante comme celles données dans d'autres réponses qui allouent une quantité appropriée de mémoire.

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

De toute évidence, vous appelez maintenant la log_out_wrapper()place de log_out()- mais l'allocation de mémoire et ainsi de suite est effectuée une fois. Je me réserve le droit de surallouer de l'espace d'un octet inutile - je n'ai pas revérifié si la longueur renvoyée par vsnprintf()inclut le null de fin ou non.

Jonathan Leffler
la source
3

N'utilisez pas sprintf.
Il débordera votre String-Buffer et plantera votre programme.
Utilisez toujours snprintf

À M
la source
0

Je ne l'ai pas fait, alors je vais simplement vous indiquer la bonne réponse.

C a des dispositions pour les fonctions qui prennent des nombres non spécifiés d'opérandes, en utilisant l'en- <stdarg.h>tête. Vous pouvez définir votre fonction comme void log_out(const char *fmt, ...);, et obtenir l' va_listintérieur de la fonction. Ensuite, vous pouvez allouer de la mémoire et appeler vsprintf()avec la mémoire allouée, le format et va_list.

Vous pouvez également l'utiliser pour écrire une fonction analogue à sprintf()celle qui allouerait de la mémoire et retournerait la chaîne formatée, la générant plus ou moins comme ci-dessus. Ce serait une fuite de mémoire, mais si vous vous déconnectez, cela n'a peut-être pas d'importance.

David Thornley
la source
-2

http://www.gnu.org/software/hello/manual/libc/Variable-Arguments-Output.html donne l'exemple suivant pour imprimer sur stderr. Vous pouvez le modifier pour utiliser votre fonction de journal à la place:

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

Au lieu de vfprintf, vous devrez utiliser vsprintf où vous devez fournir un tampon adéquat pour imprimer.

Lothar
la source