Создание строк в формате 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 *, ...); потому что в противном случае вызов ошибочен (слишком много аргументов). Он должен принимать константный указатель, потому что у log_out () нет причин изменять строку. Конечно, вы можете сказать, что хотите передать в функцию одну строку, но не можете. Один из вариантов - написать varargs-версию функции log_out ().
Джонатан Леффлер,

Ответы:

91

Используйте sprintf .

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
Ой! Если возможно, используйте функции вариации "n". Т.е. snprintf. Они заставят вас подсчитать размер ваших буферов и тем самым застраховать себя от переполнения.
dmckee --- котенок экс-модератора
7
Да, но он попросил функцию ANSI C, и я не уверен, является ли snprintf ansi или даже posix.
akappa
7
Ах. Я пропустил это. Но меня спас новый стандарт: в C99 официальные варианты "n". FWIW, YMMV и т. Д.
dmckee --- котенок экс-модератора
1
snprintf - не самый безопасный способ. Вам следует использовать snprintf_s. См. Msdn.microsoft.com/en-us/library/f30dzcf6(VS.80).aspx
joce
2
@Joce - семейство функций snprintf () C99 довольно безопасно; однако семейство snprintf_s () имеет совершенно другое поведение (особенно в отношении того, как обрабатывается соединение). НО - функция Microsoft _snprintf () небезопасна - она ​​потенциально может оставить результирующий буфер незавершенным (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");
    }
}
cmaster - восстановить монику
источник
2
Обратите внимание, что asprintf()это не входит ни в стандарт C 2011, ни в POSIX, ни даже в POSIX 2008 или 2013. Это часть TR 27431-2: см. Используете ли вы «безопасные» функции TR 24731?
Джонатан Леффлер
работает как шарм! Я столкнулся с проблемой, когда значение «char *» не печаталось должным образом в журналах, т.е. форматированная строка каким-то образом не подходила. код использовал "asprintf ()".
parasrish 09
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, где вам нужно предоставить соответствующий буфер для печати.

Лотар
источник