Membuat string berformat C (tidak mencetaknya)

101

Saya memiliki fungsi yang menerima string, yaitu:

void log_out(char *);

Dalam memanggilnya, saya perlu membuat string berformat dengan cepat seperti:

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

Bagaimana saya melakukan ini di ANSI C?


Hanya saja, karena sprintf()mengembalikan int, ini berarti saya harus menulis setidaknya 3 perintah, seperti:

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

Adakah cara untuk mempersingkat ini?

pistacchio.dll
sumber
1
Saya percaya bahwa prototipe fungsinya benar-benar: extern void log_out (const char *, ...); karena jika tidak, panggilan ke sana salah (terlalu banyak argumen). Ini harus mengambil pointer const karena tidak ada alasan untuk log_out () untuk mengubah string. Tentu saja, Anda mungkin mengatakan bahwa Anda ingin meneruskan satu string ke fungsi - tetapi tidak bisa. Salah satu opsinya adalah menulis versi varargs dari fungsi log_out ().
Jonathan Leffler

Jawaban:

91

Gunakan sprintf .

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

Menulis data yang diformat ke string Membuat string dengan teks yang sama yang akan dicetak jika format digunakan pada printf, tetapi bukannya dicetak, konten disimpan sebagai string C di buffer yang ditunjukkan oleh str.

Ukuran buffer harus cukup besar untuk menampung seluruh string yang dihasilkan (lihat snprintf untuk versi yang lebih aman).

Karakter null penghentian secara otomatis ditambahkan setelah konten.

Setelah parameter format, fungsi tersebut mengharapkan setidaknya argumen tambahan sebanyak yang diperlukan untuk format.

Parameter:

str

Pointer ke buffer tempat C-string yang dihasilkan disimpan. Buffer harus cukup besar untuk menampung string yang dihasilkan.

format

String C yang berisi string format yang mengikuti spesifikasi yang sama seperti format di printf (lihat printf untuk detailnya).

... (additional arguments)

Bergantung pada format string, fungsi mungkin mengharapkan urutan argumen tambahan, masing-masing berisi nilai yang akan digunakan untuk menggantikan penentu format dalam format string (atau penunjuk ke lokasi penyimpanan, untuk n). Harus ada setidaknya argumen ini sebanyak jumlah nilai yang ditentukan dalam penentu format. Argumen tambahan diabaikan oleh fungsi.

Contoh:

// Allocates storage
char *hello_world = (char*)malloc(13 * sizeof(char));
// Prints "Hello world!" on hello_world
sprintf(hello_world, "%s %s!", "Hello", "world");
akappa
sumber
35
Astaga! Jika memungkinkan, gunakan fungsi variasi 'n'. Yaitu snprintf. Mereka akan membuat Anda menghitung ukuran buffer Anda dan dengan demikian memastikan terhadap overruns.
dmckee --- kucing mantan moderator
7
Ya, tapi dia meminta fungsi ANSI C dan saya tidak begitu yakin apakah snprintf adalah ansi atau bahkan posix.
akappa
7
Ah. Saya melewatkan itu. Tapi saya diselamatkan oleh standar baru: varian 'n' resmi di C99. FWIW, YMMV, etc.
dmckee --- mantan moderator kucing
1
snprintf bukanlah cara teraman untuk melakukannya. Anda harus menggunakan snprintf_s. Lihat msdn.microsoft.com/en-us/library/f30dzcf6(VS.80).aspx
joce
2
@Joce - keluarga fungsi snprintf () C99 cukup aman; namun keluarga snprintf_s () memang memiliki perilaku yang berbeda (terutama mengenai bagaimana trunction ditangani). TAPI - Fungsi _snprintf () Microsoft tidak aman - karena berpotensi meninggalkan buffer yang dihasilkan tidak diselesaikan (C99 snprintf () selalu berakhir).
Michael Burr
16

Jika Anda memiliki sistem yang sesuai dengan POSIX-2008 (semua Linux modern), Anda dapat menggunakan fungsi yang aman dan nyaman asprintf(): Ini akan memiliki malloc()cukup memori untuk Anda, Anda tidak perlu khawatir tentang ukuran string maksimum. Gunakan seperti ini:

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

Ini adalah upaya minimum yang bisa Anda lakukan untuk membuat string dengan cara yang aman. The sprintf()kode yang Anda berikan dalam pertanyaan secara mendalam cacat:

  • Tidak ada memori yang dialokasikan di belakang penunjuk. Anda menulis string ke lokasi acak dalam memori!

  • Bahkan jika Anda telah menulis

    char s[42];

    Anda akan berada dalam masalah besar, karena Anda tidak tahu angka apa yang harus dimasukkan ke dalam tanda kurung.

  • Bahkan jika Anda telah menggunakan varian "aman" snprintf(), Anda masih akan menghadapi bahaya jika string Anda terpotong. Saat menulis ke file log, itu adalah masalah yang relatif kecil, tetapi berpotensi memotong dengan tepat informasi yang seharusnya berguna. Juga, itu akan memotong karakter garis akhir, menempelkan baris log berikutnya ke akhir baris yang tidak berhasil Anda tulis.

  • Jika Anda mencoba menggunakan kombinasi malloc()dan snprintf()untuk menghasilkan perilaku yang benar dalam semua kasus, Anda akan mendapatkan kode kira-kira dua kali lebih banyak daripada yang saya berikan asprintf(), dan pada dasarnya memprogram ulang fungsionalitas asprintf().


Jika Anda ingin menyediakan pembungkus log_out()yang dapat mengambil printf()daftar parameter gaya itu sendiri, Anda dapat menggunakan varian vasprintf()yang mengambil a va_listsebagai argumen. Berikut adalah implementasi yang sangat aman dari pembungkus seperti itu:

//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 - kembalikan monica
sumber
2
Perhatikan bahwa asprintf()itu bukan bagian dari standar C 2011 atau bagian dari POSIX, bahkan bukan POSIX 2008 atau 2013. Ini adalah bagian dari TR 27431-2: lihat Apakah Anda menggunakan fungsi 'aman' TR 24731?
Jonathan Leffler
bekerja seperti pesona! Saya menghadapi masalah, ketika nilai "char *" tidak dicetak dengan benar dalam log, yaitu string yang diformat tidak sesuai. kode telah menggunakan, "asprintf ()".
parasrish
11

Kedengarannya bagi saya seperti Anda ingin dapat dengan mudah meneruskan string yang dibuat menggunakan pemformatan gaya printf ke fungsi yang sudah Anda miliki yang membutuhkan string sederhana. Anda dapat membuat fungsi pembungkus menggunakan stdarg.hfasilitas dan vsnprintf()(yang mungkin tidak tersedia, tergantung pada kompiler / platform Anda):

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

Untuk platform yang tidak menyediakan implementasi yang baik (atau implementasi apa pun) dari rangkaian snprintf()rutinitas, saya telah berhasil menggunakan domain hampir publik snprintf()dari Holger Weiss .

Michael Burr
sumber
Saat ini, seseorang mungkin mempertimbangkan untuk menggunakan vsnprintf_s.
menggabungkan
3

Jika Anda memiliki kode untuk log_out(), tulis ulang. Kemungkinan besar, Anda dapat melakukan:

static FILE *logfp = ...;

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

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

Jika ada informasi logging tambahan yang diperlukan, yang dapat dicetak sebelum atau sesudah pesan ditampilkan. Ini menghemat alokasi memori dan ukuran buffer yang meragukan dan seterusnya dan seterusnya. Anda mungkin perlu menginisialisasi logfpke nol (penunjuk nol) dan memeriksa apakah itu nol dan membuka file log yang sesuai - tetapi kode yang ada log_out()harus menangani itu juga.

Keuntungan dari solusi ini adalah Anda dapat dengan mudah menyebutnya sebagai varian dari printf(); memang, ini adalah varian kecil pada printf().

Jika Anda tidak memiliki kodenya log_out(), pertimbangkan apakah Anda dapat menggantinya dengan varian seperti yang diuraikan di atas. Apakah Anda dapat menggunakan nama yang sama akan bergantung pada kerangka aplikasi Anda dan sumber utama dari log_out()fungsi saat ini . Jika ada di file objek yang sama dengan fungsi lain yang sangat diperlukan, Anda harus menggunakan nama baru. Jika Anda tidak dapat mengetahui cara mereplikasi dengan tepat, Anda harus menggunakan beberapa varian seperti yang diberikan dalam jawaban lain yang mengalokasikan jumlah memori yang sesuai.

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

Jelas, Anda sekarang memanggil log_out_wrapper()alih - alih log_out()- tetapi alokasi memori dan seterusnya dilakukan sekali. Saya berhak untuk mengalokasikan ruang secara berlebihan dengan satu byte yang tidak perlu - saya belum memeriksa ulang apakah panjang yang dikembalikan oleh vsnprintf()termasuk penghentian null atau tidak.

Jonathan Leffler
sumber
3

Jangan gunakan sprintf.
Ini akan meluap-luap String-Buffer Anda dan merusak Program Anda.
Selalu gunakan snprintf

Tom
sumber
0

Saya belum melakukan ini, jadi saya hanya akan menunjukkan jawaban yang benar.

C memiliki ketentuan untuk fungsi yang mengambil jumlah operan yang tidak ditentukan, menggunakan <stdarg.h>header. Anda dapat mendefinisikan fungsi Anda sebagai void log_out(const char *fmt, ...);, dan mendapatkan di va_listdalam fungsi tersebut. Kemudian Anda dapat mengalokasikan memori dan memanggil vsprintf()dengan memori yang dialokasikan, format, dan va_list.

Bergantian, Anda dapat menggunakan ini untuk menulis fungsi analog sprintf()yang akan mengalokasikan memori dan mengembalikan string yang diformat, menghasilkannya kurang lebih seperti di atas. Ini akan menjadi kebocoran memori, tetapi jika Anda baru saja keluar, itu mungkin tidak masalah.

David Thornley
sumber
-2

http://www.gnu.org/software/hello/manual/libc/Variable-Arguments-Output.html memberikan contoh berikut untuk dicetak ke stderr. Anda dapat memodifikasinya untuk menggunakan fungsi log Anda:

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

Alih-alih vfprintf, Anda perlu menggunakan vsprintf di mana Anda perlu menyediakan buffer yang memadai untuk mencetak.

lothar
sumber