Tworzenie ciągów w formacie C (bez ich drukowania)

101

Mam funkcję, która akceptuje ciąg, czyli:

void log_out(char *);

Nazywając to, muszę w locie utworzyć sformatowany ciąg, taki jak:

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

Jak to zrobić w ANSI C?


Tyle że skoro sprintf()zwraca int, to znaczy, że muszę napisać co najmniej 3 komendy jak:

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

Jakiś sposób, aby to skrócić?

pistacchio
źródło
1
Ufam, że prototyp funkcji to naprawdę: extern void log_out (const char *, ...); ponieważ jeśli tak nie jest, wywołanie jest błędne (zbyt wiele argumentów). Powinien pobierać wskaźnik do stałej, ponieważ nie ma powodu, aby log_out () modyfikował łańcuch. Oczywiście możesz powiedzieć, że chcesz przekazać do funkcji pojedynczy ciąg znaków - ale nie możesz. Jedną z opcji jest wtedy napisanie wersji varargs funkcji log_out ().
Jonathan Leffler

Odpowiedzi:

91

Użyj sprintf .

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

Zapisz sformatowane dane do łańcucha Tworzy łańcuch z tym samym tekstem, który zostałby wydrukowany, gdyby format został użyty w printf, ale zamiast być drukowany, zawartość jest przechowywana jako łańcuch C w buforze wskazanym przez str.

Rozmiar bufora powinien być wystarczająco duży, aby pomieścić cały wynikowy ciąg (bezpieczniejsza wersja znajduje się w snprintf).

Kończący znak pusty jest automatycznie dołączany po treści.

Po parametrze format funkcja oczekuje co najmniej tylu dodatkowych argumentów, ile potrzeba do formatu.

Parametry:

str

Wskaźnik do buforu, w którym przechowywany jest wynikowy ciąg C. Bufor powinien być wystarczająco duży, aby zawierał wynikowy ciąg.

format

Ciąg C, który zawiera ciąg formatu, który jest zgodny z tymi samymi specyfikacjami, co format w printf (szczegóły w printf).

... (additional arguments)

W zależności od ciągu formatu funkcja może oczekiwać sekwencji dodatkowych argumentów, z których każdy zawiera wartość, która ma zostać użyta do zastąpienia specyfikatora formatu w ciągu formatu (lub wskaźnika do lokalizacji magazynu dla n). Argumentów tych powinno być co najmniej tyle, ile jest wartości określonych w specyfikatorach formatu. Dodatkowe argumenty są ignorowane przez funkcję.

Przykład:

// Allocates storage
char *hello_world = (char*)malloc(13 * sizeof(char));
// Prints "Hello world!" on hello_world
sprintf(hello_world, "%s %s!", "Hello", "world");
akappa
źródło
35
Yikes! Jeśli to możliwe, użyj funkcji wariacji „n”. Tj. Snprintf. Sprawią, że policzysz rozmiary buforów, a tym samym zabezpieczysz się przed przekroczeniami.
dmckee --- ex-moderator kitten
7
Tak, ale poprosił o funkcję ANSI C i nie jestem pewien, czy snprintf to ansi, czy nawet posix.
akappa
7
Ach. Tęsknie za tym. Ale ratuje mnie nowy standard: warianty 'n' są oficjalne w C99. FWIW, YMMV itp.
dmckee --- kociak ex-moderator
1
snprintf nie jest najbezpieczniejszym sposobem. Powinieneś iść z snprintf_s. Zobacz msdn.microsoft.com/en-us/library/f30dzcf6(VS.80).aspx
joce
2
@Joce - rodzina funkcji snprintf () C99 jest całkiem bezpieczna; jednakże rodzina snprintf_s () ma bardzo różne zachowanie (szczególnie w odniesieniu do sposobu obsługi obcinania). ALE - funkcja _snprintf () firmy Microsoft nie jest bezpieczna - ponieważ może potencjalnie pozostawić wynikowy bufor niezakończony (C99 snprintf () zawsze kończy).
Michael Burr
16

Jeśli masz system zgodny z POSIX-2008 (dowolny nowoczesny Linux), możesz skorzystać z bezpiecznej i wygodnej asprintf()funkcji: malloc()wystarczy Ci pamięci, nie musisz martwić się o maksymalny rozmiar łańcucha. Użyj tego w ten sposób:

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

Jest to minimalny wysiłek, jaki możesz wykonać, aby zbudować strunę w bezpieczny sposób. sprintf()Kod dałeś w pytaniu jest głęboko błędna:

  • Za wskaźnikiem nie ma przydzielonej pamięci. Piszesz ciąg w losowym miejscu w pamięci!

  • Nawet jeśli napisałeś

    char s[42];

    miałbyś poważne kłopoty, ponieważ nie możesz wiedzieć, jaką liczbę umieścić w nawiasach.

  • Nawet gdybyś użył „bezpiecznego” wariantu snprintf(), nadal narażasz się na obcięcie sznurków. Podczas zapisywania do pliku dziennika jest to stosunkowo niewielki problem, ale może on potencjalnie odciąć precyzyjnie informacje, które byłyby przydatne. Ponadto odetnie końcowy znak końca linii, przyklejając następny wiersz dziennika do końca nieudanej linii.

  • Jeśli spróbujesz użyć kombinacji malloc()i snprintf()wytworzyć poprawne zachowanie we wszystkich przypadkach, otrzymasz około dwa razy więcej kodu niż podałem asprintf(), i zasadniczo przeprogramujesz funkcjonalność asprintf().


Jeśli zastanawiasz się nad dostarczeniem opakowania, log_out()które może przyjmować printf()samą listę parametrów stylu, możesz użyć wariantu, vasprintf()który przyjmuje va_listjako argument. Oto całkowicie bezpieczna implementacja takiego opakowania:

//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 - przywróć monikę
źródło
2
Zauważ, że asprintf()nie jest to ani część standardowego C 2011, ani POSIX, ani nawet POSIX 2008 czy 2013. Jest to część TR 27431-2: zobacz Czy używasz "bezpiecznych" funkcji TR 24731?
Jonathan Leffler
działa jak urok! Miałem do czynienia z problemem, gdy wartość "char *" nie była poprawnie drukowana w logach, tj. Sformatowany ciąg nie był w jakiś sposób odpowiedni. kod używał „asprintf ()”.
parasrish
11

Wydaje mi się, że chcesz mieć możliwość łatwego przekazania ciągu utworzonego przy użyciu formatowania w stylu printf do funkcji, którą już masz, pobierającej prosty ciąg. Możesz utworzyć funkcję opakowującą, korzystając z stdarg.hudogodnień i vsnprintf()(które mogą nie być łatwo dostępne, w zależności od kompilatora / platformy):

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

W przypadku platform, które nie zapewniają dobrej implementacji (lub jakiejkolwiek implementacji) snprintf()rodziny procedur, z powodzeniem wykorzystałem prawie publiczną domenę snprintf()Holgera Weissa .

Michael Burr
źródło
Obecnie można rozważyć użycie vsnprintf_s.
połącz
3

Jeśli masz kod log_out(), przepisz go. Najprawdopodobniej możesz:

static FILE *logfp = ...;

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

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

Jeśli potrzebne są dodatkowe informacje logowania, można je wydrukować przed lub po wyświetlonym komunikacie. Oszczędza to alokację pamięci i wątpliwe rozmiary buforów i tak dalej i tak dalej. Prawdopodobnie musisz zainicjować logfpdo zera (wskaźnik zerowy) i sprawdzić, czy jest pusty, i odpowiednio otworzyć plik dziennika - ale kod w istniejącym i tak log_out()powinien sobie z tym radzić.

Zaletą tego rozwiązania jest to, że można je po prostu nazwać tak, jakby to był wariant printf(); w rzeczywistości jest to niewielki wariant printf().

Jeśli nie masz kodu log_out(), zastanów się, czy możesz go zastąpić wariantem, takim jak opisany powyżej. To, czy możesz użyć tej samej nazwy, będzie zależeć od struktury aplikacji i ostatecznego źródła bieżącej log_out()funkcji. Jeśli znajduje się w tym samym pliku obiektowym, co inna niezbędna funkcja, musiałbyś użyć nowej nazwy. Jeśli nie możesz dowiedzieć się, jak dokładnie to powtórzyć, będziesz musiał użyć jakiegoś wariantu, takiego jak podane w innych odpowiedziach, który przydziela odpowiednią ilość pamięci.

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

Oczywiście teraz wywołujesz log_out_wrapper()zamiast log_out()- ale alokacja pamięci i tak dalej jest wykonywana raz. Zastrzegam sobie prawo do nadmiernego przydzielania miejsca o jeden niepotrzebny bajt - nie sprawdziłem dwukrotnie, czy długość zwracana przez vsnprintf()zawiera kończący null, czy nie.

Jonathan Leffler
źródło
3

Nie używaj sprintf.
Spowoduje to przepełnienie bufora ciągów i awarię programu.
Zawsze należy używać sprintf

Tomek
źródło
0

Nie zrobiłem tego, więc po prostu wskażę właściwą odpowiedź.

C ma przepisy dla funkcji, które pobierają nieokreśloną liczbę operandów, używając <stdarg.h>nagłówka. Możesz zdefiniować swoją funkcję jako void log_out(const char *fmt, ...);i uzyskać jej va_listwnętrze. Następnie możesz przydzielić pamięć i dzwonić vsprintf()z przydzieloną pamięcią, formatem i va_list.

Alternatywnie, możesz użyć tego do napisania funkcji analogicznej do sprintf()tej, która przydzieliłaby pamięć i zwróciła sformatowany ciąg, generując go mniej więcej tak, jak powyżej. Byłby to wyciek pamięci, ale jeśli się wylogujesz, może to nie mieć znaczenia.

David Thornley
źródło
-2

http://www.gnu.org/software/hello/manual/libc/Variable-Arguments-Output.html podaje następujący przykład drukowania na stderr. Możesz go zmodyfikować, aby zamiast tego używał funkcji dziennika:

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

Zamiast vfprintf będziesz musiał użyć vsprintf, gdzie musisz zapewnić odpowiedni bufor do drukowania.

lothar
źródło