Створення рядків у форматі C (не друкуючи їх)


101

У мене є функція, яка приймає рядок, тобто:

void log_out(char *);

Викликаючи це, мені потрібно створити відформатований рядок на льоту, як:

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

Як це зробити в ANSI C?


Тільки, оскільки sprintf()повертає int, це означає, що я повинен написати щонайменше 3 команди, наприклад:

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

Будь-який спосіб скоротити це?


1
Я вірю, що прототип функції дійсно є: extern void log_out (const char *, ...); тому що якщо це не так, виклик до нього помилковий (занадто багато аргументів). Потрібно брати покажчик const, оскільки немає причини, щоб log_out () міняв рядок. Звичайно, ви можете сказати, що ви хочете передати одну функцію функції, але не можете. Один із варіантів - це написати версію varargs функції log_out ().
Джонатан Леффлер

Відповіді:


91

Використовуйте спринт .

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

Запис відформатованих даних у рядок Складає рядок із тим самим текстом, який був би надрукований, якби формат був використаний у printf, але замість того, щоб друкуватись, вміст зберігається як рядок C у буфері, вказаному str.

Розмір буфера повинен бути достатньо великим, щоб містити всю отриману рядок (див. Snprintf для більш безпечної версії).

Після змісту автоматично додається закінчуючий нульовий символ.

Після параметра формату функція очікує щонайменше стільки додаткових аргументів, скільки потрібно для формату.

Параметри:

str

Вказівник на буфер, де зберігається отриманий C-рядок. Буфер повинен бути досить великим, щоб містити отриманий рядок.

format

Рядок C, що містить рядок формату, який відповідає тим же специфікаціям, що й формат у printf (деталі див. У printf).

... (additional arguments)

Залежно від рядка формату, функція може очікувати послідовності додаткових аргументів, кожен з яких містить значення, яке буде використано для заміни специфікатора формату в рядку формату (або вказівника на місце зберігання для n). Цих аргументів повинно бути як мінімум стільки, скільки кількість значень, зазначених у специфікаторах формату. Додаткові аргументи ігноруються функцією.

Приклад:

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

35
Yikes! Якщо це можливо, використовуйте функції n 'варіації. Тобто snprintf. Вони змусять вас рахувати розміри буфера і тим самим убезпечити від перевищення.
dmckee --- кошеня колишнього модератора

7
Так, але він попросив функцію ANSI C, і я не дуже впевнений, чи snprintf - це ansi чи навіть posix.
akappa

7
Ага. Я пропустив це. Але мене врятував новий стандарт: 'n' варіанти офіційні в C99. FWIW, YMMV тощо
dmckee --- кошеня колишнього модератора

1
snprintf - не найбезпечніший спосіб пройти. Ви повинні піти з snprintf_s. Дивіться msdn.microsoft.com/en-us/library/f30dzcf6(VS.80).aspx
Joce

2
@Joce - сімейство функцій C99 snprintf () досить безпечно; однак сімейство snprintf_s () має таку різну поведінку (особливо щодо способу обробки обрізання). АЛЕ - функція _snprintf () Microsoft не є безпечною, оскільки може потенційно залишити буфер незакінченим (C99 snprintf () завжди припиняється).
Майкл Берр

16

Якщо у вас є сумісна з POSIX-2008 система (будь-який сучасний Linux), ви можете використовувати безпечну та зручну asprintf()функцію: це буде malloc()достатньо для вас пам'яті, вам не потрібно турбуватися про максимальний розмір рядка. Використовуйте його так:

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

Це мінімальні зусилля, які ви можете докласти, щоб безпечно сконструювати струну. sprintf()Код ви дали в питанні глибоко зіпсований:

  • За вказівником не виділена пам'ять. Ви записуєте рядок у випадкове місце в пам'яті!

  • Навіть якби ти написав

    char s[42];

    у вас виникнуть глибокі проблеми, оскільки ви не можете знати, яке число поставити в дужки.

  • Навіть якби ви використовували "безпечний" варіант snprintf(), ви все одно матимете небезпеку, що ваші струни будуть вкорочені. Під час запису в файл журналу це є відносно незначною проблемою, але він має потенціал точно вирізати інформацію, яка була б корисною. Крім того, він буде відрізати символ кінцевої лінії, приклеюючи наступний рядок журналу до кінця вашого невдало записаного рядка.

  • Якщо ви намагаєтеся використовувати комбінацію malloc()та snprintf()виробляти правильну поведінку у всіх випадках, ви отримуєте приблизно вдвічі більше коду, ніж я дав asprintf(), і в основному перепрограмуєте функціонал asprintf().


Якщо ви шукаєте, як забезпечити обгортку, log_out()яка може приймати printf()сам список параметрів стилю, ви можете використовувати варіант, vasprintf()який бере va_listаргумент. Ось ідеально безпечна реалізація такої обгортки:

//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
Зауважте, що asprintf()це не є частиною стандарту C 2011, не є частиною POSIX, навіть не POSIX 2008 або 2013. Це частина TR 27431-2: див. Чи використовуєте ви TR 24731 "безпечні" функції?
Джонатан Леффлер

працює як шарм! Я зіткнувся з проблемою, коли значення "char *" не надходило належним чином надрукованим у журналах, тобто відформатований рядок якось не підходив. код використовувався, "asprintf ()".
парашутувати

11

Мені здається, ви хочете, щоб ви могли легко передати рядок, створений за допомогою форматування printf у функції, які у вас уже є, яка займає просту рядок. Ви можете створити функцію обгортки, використовуючи stdarg.hзасоби та vsnprintf()(які можуть бути недоступні, залежно від вашого компілятора / платформи):

#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()сімейства процедур, я успішно використовував майже публічний домен snprintf()від Holger Weiss .


В даний час можна подумати про використання vsnprintf_s.
об'єднати

3

Якщо у вас є код log_out(), перепишіть його. Швидше за все, ви можете зробити:

static FILE *logfp = ...;

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

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

Якщо потрібна додаткова інформація про реєстрацію, вона може бути надрукована до або після відображення повідомлення. Це економить розподіл пам’яті та сумнівні розміри буфера тощо, тощо. Можливо, вам потрібно ініціалізуватись logfpдо нуля (нульовий покажчик) і перевірити, чи він є нульовим, і відкрити файл журналу, якщо це доречно - але код у існуючомуlog_out() повинен мати справу з цим у будь-якому випадку.

Перевага цього рішення полягає в тому, що ви можете просто назвати його так, ніби це був варіант printf(); Дійсно, це незначний варіант наprintf() .

Якщо у вас немає коду log_out(), подумайте, чи можете ви замінити його на такий варіант, як описаний вище. Чи можна використовувати те саме ім’я, буде залежати від вашої програми та кінцевого джерела поточної log_out()функції. Якщо він знаходиться в тому ж файлі об'єкта, що і інша необхідна функція, вам доведеться використовувати нове ім’я. Якщо ви не можете розробити, як точно його повторити, вам доведеться використовувати такий варіант, як той, який наведено в інших відповідях, що виділяє відповідний об'єм пам'яті.

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

Очевидно, ви тепер викликаєте log_out_wrapper()замість цього, log_out()- але розподіл пам'яті та інше робиться один раз. Я залишаю за собою право надмірно розподіляти простір одним непотрібним байтом - я не перевіряв, чи повертається довжина, vsnprintf()включаючи закінчуючий нуль, чи ні.


3

Не використовуйте sprintf.
Це переповнить ваш String-Buffer і розбить вашу програму.
Завжди використовуйте snprintf


0

Я цього не робив, тому просто збираюся вказати на правильну відповідь.

C має положення щодо функцій, які приймають невизначене число операндів, використовуючи <stdarg.h>заголовок. Ви можете визначити свою функцію як void log_out(const char *fmt, ...);і отримати va_listвнутрішню функцію. Тоді ви можете виділити пам'ять та зателефонувати vsprintf()за допомогою виділеної пам'яті, формату та va_list.

Ви також можете скористатися цією функцією, щоб записати функцію, аналогічну цій, sprintf()яка б розподіляла пам'ять і повертала відформатовану рядок, генеруючи її більш-менш, як вище. Це було б витоком пам'яті, але якщо ви просто виходите з системи, це може не мати значення.


-2

http://www.gnu.org/software/hello/manual/libc/Variable-Arguments-Output.html дає наступний приклад для друку на stderr. Ви можете змінити його, щоб замість цього використовувати функцію журналу:

 #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 вам потрібно буде використовувати vsprintf там, де вам потрібно надати адекватний буфер для друку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.