C formatlı dizeler oluşturma (bunları yazdırmamak)


101

Bir dizeyi kabul eden bir fonksiyonum var, yani:

void log_out(char *);

Çağırırken, anında aşağıdaki gibi biçimlendirilmiş bir dize oluşturmam gerekiyor:

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

Bunu ANSI C'de nasıl yaparım?


Yalnızca, sprintf()bir int döndürdüğü için bu, en az 3 komut yazmam gerektiği anlamına gelir, örneğin:

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

Bunu kısaltmanın bir yolu var mı?


1
İşlev prototipinin gerçekten olduğuna inanıyorum: extern void log_out (const char *, ...); çünkü değilse, ona yapılan çağrı hatalıdır (çok fazla argüman). Log_out () 'un dizeyi değiştirmesi için bir neden olmadığından, bir const işaretçisi alıyor olmalıdır. Elbette, işleve tek bir dize geçirmek istediğinizi söylüyor olabilirsiniz, ancak yapamazsınız. O zaman bir seçenek, log_out () işlevinin bir varargs sürümünü yazmaktır.
Jonathan Leffler

Yanıtlar:


91

Sprintf kullanın .

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

Biçimlendirilmiş verileri dizeye yaz Eğer biçim printf üzerinde kullanılmışsa yazdırılacak olanla aynı metinle bir dizge oluşturur, ancak içerik yazdırılmak yerine str ile gösterilen arabellekte bir C dizesi olarak depolanır.

Tamponun boyutu, ortaya çıkan tüm dizgiyi içerecek kadar büyük olmalıdır (daha güvenli bir sürüm için snprintf'e bakın).

İçeriğin arkasına otomatik olarak sonlandırıcı bir boş karakter eklenir.

Format parametresinden sonra işlev, format için en az gerektiği kadar ek argüman bekler.

Parametreler:

str

Ortaya çıkan C-string'in depolandığı bir tampona işaretçi. Tampon, ortaya çıkan dizeyi içerecek kadar büyük olmalıdır.

format

Printf'deki biçimle aynı özellikleri izleyen bir biçim dizesi içeren C dizesi (ayrıntılar için bkz. Printf).

... (additional arguments)

Biçim dizesine bağlı olarak, işlev, her biri biçim dizesindeki bir biçim belirticisini (veya n için bir saklama konumuna işaretçi) değiştirmek için kullanılacak bir değer içeren bir dizi ek bağımsız değişken bekleyebilir. Biçim belirteçlerinde belirtilen değerlerin sayısı kadar bu bağımsız değişkenlerden en az biri olmalıdır. Ek argümanlar işlev tarafından yok sayılır.

Misal:

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

35
Eyvah! Mümkünse 'n' varyasyon işlevlerini kullanın. Yani snprintf. Tampon boyutlarınızı saymanızı sağlayacak ve böylece aşımlara karşı sigorta sağlayacaktır.
dmckee - eski moderatör kedi yavrusu

7
Evet, ancak bir ANSI C işlevi istedi ve ben snprintf'in ansi mi yoksa posix mi olduğundan pek emin değilim.
akappa

7
Ah. Onu özledim. Ama yeni standart tarafından kurtarıldım: 'n' varyantları C99'da resmi. FWIW, YMMV, vb.
dmckee - eski moderatör yavru kedi

1
snprintf, gitmenin en güvenli yolu değildir. Snprintf_s ile gitmelisiniz. Bkz. Msdn.microsoft.com/en-us/library/f30dzcf6(VS.80).aspx
joce

2
@Joce - C99 snprintf () işlev ailesi oldukça güvenlidir; ancak snprintf_s () ailesinin çok farklı davranışları vardır (özellikle trunction'ın nasıl işlendiğine ilişkin). AMA - Microsoft'un _snprintf () işlevi güvenli değildir - sonuçta ortaya çıkan arabelleği sonlandırılmadan bırakabilir (C99 snprintf () her zaman sona erer).
Michael Burr

16

POSIX-2008 uyumlu bir sisteminiz varsa (herhangi bir modern Linux), güvenli ve kullanışlı asprintf()işlevi kullanabilirsiniz: malloc()Sizin için yeterli bellek olacak , maksimum dizi boyutu konusunda endişelenmenize gerek yok. Bunu şu şekilde kullanın:

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

Bu, dizgiyi güvenli bir şekilde inşa etmek için alabileceğiniz minimum çabadır. sprintf()Söz konusu verdi kod derinden kusurlu olduğunu:

  • İşaretçinin arkasında ayrılmış bellek yok. Dizeyi hafızada rastgele bir yere yazıyorsunuz!

  • Yazmış olsan bile

    char s[42];

    Başınız büyük belaya girer, çünkü parantez içine hangi sayıyı koyacağınızı bilemezsiniz.

  • "Güvenli" değişkeni kullanmış olsanız bile snprintf(), dizelerinizin kesilmesi tehlikesini yine de yaşarsınız. Bir günlük dosyasına yazarken, bu görece önemsiz bir sorundur, ancak yararlı olabilecek bilgileri kesin olarak kesme potansiyeline sahiptir. Ayrıca, sonraki günlük satırını başarısız bir şekilde yazılan satırın sonuna yapıştırarak sondaki son satır karakterini keser.

  • Eğer bir arada kullanmaya çalışırsanız malloc()ve snprintf()her durumda doğru davranış üretmek için, size kabaca iki katı kadar kod olarak ben verdik daha ile bitirmek asprintf()ve temelde işlevselliğini yeniden programlamak asprintf().


Eğer bir sarmalayıcı sağlamayı arıyorsanız log_out()bir alabilir printf()stil parametre listesi kendisi, Varyantı kullanabilirsiniz vasprintf()bir alan va_listbağımsız değişken olarak. İşte böyle bir sarmalayıcının tamamen güvenli bir uygulaması:

//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
Bunun asprintf()ne standart C 2011'in ne de POSIX'in bir parçası olmadığını, POSIX 2008 veya 2013'ün bile olmadığını unutmayın. TR 27431-2'nin bir parçasıdır: bkz . TR 24731 'güvenli' işlevlerini kullanıyor musunuz?
Jonathan Leffler

cazibe gibi çalışıyor! "Char *" değeri günlüklere düzgün şekilde yazdırılmadığında, yani biçimlendirilmiş dize bir şekilde uygun olmadığında sorunla karşı karşıyaydım. kod "asprintf ()" kullanıyordu.
parasrish

11

Bana, printf-tarzı biçimlendirme kullanılarak oluşturulmuş bir dizgeyi, basit bir dizge alan, zaten sahip olduğunuz işleve kolayca geçirebilmeyi istediğiniz gibi geliyor. stdarg.hTesisleri kullanarak bir sarmalayıcı işlevi oluşturabilirsiniz ve vsnprintf()(derleyicinize / platformunuza bağlı olarak hemen bulunmayabilir):

#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()Rutin ailesinin iyi bir uygulamasını (veya herhangi bir uygulamasını) sağlamayan platformlar için , Holger Weiss'ten neredeyse herkese açık bir alanısnprintf() başarıyla kullandım .


Şu anda, vsnprintf_s kullanmayı düşünebilirsiniz.
amalgamı

3

Kodunuz varsa log_out(), yeniden yazın. Büyük olasılıkla şunları yapabilirsiniz:

static FILE *logfp = ...;

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

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

Ekstra günlük bilgisi gerekiyorsa, bu gösterilen mesajdan önce veya sonra yazdırılabilir. Bu, bellek tahsisini ve şüpheli arabellek boyutlarını vb. Azaltır. Muhtemelen logfpsıfıra (boş gösterici) başlatmanız ve boş olup olmadığını kontrol etmeniz ve günlük dosyasını uygun şekilde açmanız gerekir - ancak mevcut koddaki kodlog_out() gerekir bununla ilgileniyor olmalıdır.

Bu çözümün avantajı, sanki bir varyantıymış gibi adlandırabilmenizdir printf(); gerçekten de küçük bir değişkendir printf().

Kodunuz log_out()yoksa, onu yukarıda belirtilen gibi bir varyantla değiştirip değiştiremeyeceğinizi düşünün. Aynı adı kullanıp kullanamayacağınız, uygulama çerçevenize ve mevcut log_out()işlevin nihai kaynağına bağlı olacaktır . Başka bir vazgeçilmez işlevle aynı nesne dosyasındaysa, yeni bir ad kullanmanız gerekir. Tam olarak nasıl kopyalayacağınızı çözemezseniz, uygun miktarda bellek ayıran diğer yanıtlarda verilenler gibi bazı değişkenler kullanmanız gerekecektir.

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

Açıkçası, şimdi log_out_wrapper()yerine çağırıyorsunuz log_out()- ancak bellek ayırma ve benzeri işlemler bir kez yapılır. Gereksiz bir bayt fazla alan tahsis etme hakkını saklı tutuyorum - döndürülen uzunluğun vsnprintf()sonlandırıcı boş değeri içerip içermediğini iki kez kontrol etmedim .


3

Sprintf kullanmayın.
String-Buffer'ınızı taşacak ve Programınızı çökertecektir.
Her zaman snprintf kullan


0

Bunu yapmadım, bu yüzden sadece doğru cevabı göstereceğim.

C, <stdarg.h>başlığı kullanarak belirtilmemiş sayıda işlenen alan işlevler için hükümlere sahiptir . Sen olarak işlev tanımlayabilirsiniz void log_out(const char *fmt, ...);ve almak va_listişlev içinde. Ardından bellek ayırabilir ve vsprintf()ayrılan bellek, format ve ile arama yapabilirsiniz va_list.

Alternatif olarak, sprintf()bellek ayıracak ve biçimlendirilmiş dizgeyi döndürerek, yukarıdaki gibi aşağı yukarı oluşturacak bir işlev yazmak için bunu kullanabilirsiniz . Bu bir hafıza sızıntısı olabilir, ancak sadece çıkış yapıyorsanız önemli olmayabilir.


-2

http://www.gnu.org/software/hello/manual/libc/Variable-Arguments-Output.html , stderr'e yazdırmak için aşağıdaki örneği verir. Bunun yerine günlük işlevinizi kullanmak için bunu değiştirebilirsiniz:

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

Vfprintf yerine, yazdırmak için yeterli bir tampon sağlamanız gereken vsprintf'i kullanmanız gerekecektir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.