Есть ли способ определить, сколько символов будет написано sprintf? - PullRequest
12 голосов
/ 05 февраля 2009

Я работаю в C ++.

Я хочу написать потенциально очень длинную отформатированную строку, используя sprintf (в частности, защищенную версию с подсчетом, такую ​​как _snprintf_s, но идея та же). Приблизительная длина неизвестна во время компиляции, поэтому мне придется использовать некоторую динамически распределенную память, а не полагаться на большой статический буфер. Есть ли способ определить, сколько символов потребуется для конкретного вызова sprintf, поэтому я всегда могу быть уверен, что у меня достаточно большой буфер?

Мой запасной вариант - я просто возьму длину строки формата, удвою ее и попробую. Если это работает, прекрасно, если это не так, я просто удвою размер буфера и попробую снова. Повторите, пока он не подходит. Не совсем умное решение.

Похоже, что C99 поддерживает передачу значения NULL в snprintf для получения длины. Полагаю, я мог бы создать модуль, чтобы обернуть эту функциональность, если ничего больше, но я не в восторге от этой идеи.

Может быть, вместо этого может работать fprintf для "/ dev / null" / "nul"? Есть другие идеи?

РЕДАКТИРОВАТЬ: В качестве альтернативы, есть ли какой-либо способ «спринт» sprintf, чтобы он взял в середине записи? Если это возможно, он может заполнить буфер, обработать его и начать заполнение с того места, где он остановился.

Ответы [ 7 ]

22 голосов
/ 05 февраля 2009

Справочная страница для snprintf говорит:

   Return value
       Upon  successful  return,  these  functions return the number of
       characters printed (not including the trailing '\0' used to  end
       output to strings).  The functions snprintf and vsnprintf do not
       write more than size bytes (including the  trailing  '\0').   If
       the output was truncated due to this limit then the return value
       is the number of characters (not including  the  trailing  '\0')
       which  would  have  been  written  to the final string if enough
       space had been available. Thus, a return value of size  or  more
       means  that  the  output  was  truncated.  (See also below under
       NOTES.)  If an output error is encountered, a negative value  is
       returned.

Это означает, что вы можете вызвать snprintf с размером 0. Ничего не будет записано, а возвращаемое значение скажет вам, сколько места вам нужно выделить для вашей строки:

int how_much_space = snprintf(NULL, 0, fmt_string, param0, param1, ...);
5 голосов
/ 05 февраля 2009

Как уже упоминалось, snprintf() вернет количество символов, необходимое в буфере для предотвращения усечения вывода. Вы можете просто вызвать его с параметром длины буфера 0, чтобы получить требуемый размер, а затем использовать буфер соответствующего размера.

Для небольшого повышения эффективности вы можете вызвать его с помощью буфера, который достаточно велик для обычного случая, и сделать второй вызов snprintf() только в том случае, если вывод урезан. Чтобы убедиться, что буфер (ы) правильно освобожден в этом случае, я часто буду использовать объект auto_buffer<>, который обрабатывает динамическую память для меня (и имеет буфер по умолчанию в стеке, чтобы избежать выделения кучи в нормальный случай).

Если вы используете компилятор Microsoft, MS имеет нестандартный _snprintf(), который имеет серьезные ограничения, заключающиеся в том, что буфер не всегда завершается нулем и не указывает, насколько большим должен быть буфер.

Чтобы обойти проблему поддержки Microsoft, я использую почти общедоступный домен snprintf() от Holger Weiss .

Конечно, если ваш компилятор не MS или C ++ отсутствует snprintf(), код по ссылке выше должен работать так же хорошо.

2 голосов
/ 05 февраля 2009

Я бы использовал двухэтапный подход. Как правило, большой процент выходных строк будет ниже определенного порога, и только несколько будут больше.

Стадия 1, используйте статический буфер разумного размера, например 4K. Поскольку snprintf() может ограничивать количество записываемых символов, вы не получите переполнение буфера. То, что вы вернете из snprintf(), будет количеством символов, которое написал бы , если бы ваш буфер был достаточно большим.

Если ваш вызов snprintf() возвращает менее 4K, используйте буфер и выйдите. Как уже говорилось, подавляющее большинство звонков должны просто делать это.

Некоторые этого не сделают, и тогда вы перейдете на этап 2. Если вызов snprintf() не помещается в буфер 4K, вы, по крайней мере, теперь знаете, насколько большой буфер вам нужен.

Выделите, с malloc(), достаточно большой буфер, чтобы удержать его, а затем snprintf() снова поместите в этот новый буфер. Когда вы закончите с буфером, освободите его.

Мы работали над системой за несколько дней до snprintf() и получили тот же результат, подключив дескриптор файла к /dev/null и используя fprintf() с этим. Всегда гарантировано, что / dev / null будет принимать столько данных, сколько вы дадите, поэтому мы на самом деле получим из этого размер, а затем выделим буфер при необходимости.

Имейте в виду, что не все системы имеют snprintf() (например, я понимаю, что это _snprintf() в Microsoft C), поэтому вам, возможно, придется найти функцию, которая выполняет ту же работу, или вернуться к решению fprintf /dev/null .

Также будьте осторожны, если данные можно изменить между проверкой размера snprintf() и фактическим snprintf() в буфере (т. Е. Для потоков). Если размеры увеличатся, вы получите повреждение переполнения буфера.

Если вы следуете правилу, что данные, переданные функции, принадлежат этой функции исключительно до ее передачи, это не будет проблемой.

1 голос
/ 04 сентября 2014

Для чего стоит, asprintf - это расширение GNU, которое управляет этой функциональностью. Он принимает указатель в качестве выходного аргумента вместе со строкой формата и переменным числом аргументов и записывает обратно в указатель адрес правильно распределенного буфера, содержащего результат.

Вы можете использовать его так:

#define _GNU_SOURCE
#include <stdio.h>

int main(int argc, char const *argv[])
{
    char *hi = "hello"; // these could be really long
    char *everyone = "world";
    char *message;
    asprintf(&message, "%s %s", hi, everyone);
    puts(message);
    free(message);
    return 0;
}

Надеюсь, это кому-нибудь поможет!

0 голосов
/ 05 февраля 2009

Поскольку вы используете C ++, в действительности нет необходимости использовать любую версию sprintf. Самое простое, что можно сделать, это использовать std :: ostringstream.

std::ostringstream oss;
oss << a << " " << b << std::endl;

oss.str() возвращает std :: string с содержимым того, что вы написали в oss. Используйте oss.str().c_str(), чтобы получить const char *. Это будет намного проще в долгосрочной перспективе и устраняет утечки памяти или переполнения буфера. Как правило, если вас беспокоят проблемы с памятью, подобные тем, которые возникают в C ++, вы не используете язык в полной мере и вам следует переосмыслить свой дизайн.

0 голосов
/ 05 февраля 2009

Взгляните на CodeProject: CString-clone, использующий стандарт C ++ . Он использует решение, которое вы предложили с увеличением размера буфера.

<pre> // ------------------------------------------------------------------------- // FUNCTION: FormatV // void FormatV(PCSTR szFormat, va_list, argList); //<br> // DESCRIPTION: // This function formats the string with sprintf style format-specs. // It makes a general guess at required buffer size and then tries // successively larger buffers until it finds one big enough or a // threshold (MAX_FMT_TRIES) is exceeded. // // PARAMETERS: // szFormat - a PCSTR holding the format of the output // argList - a Microsoft specific va_list for variable argument lists // // RETURN VALUE: // -------------------------------------------------------------------------</p> <pre>void FormatV(const CT* szFormat, va_list argList) { #ifdef SS_ANSI int nLen = sslen(szFormat) + STD_BUF_SIZE; ssvsprintf(GetBuffer(nLen), nLen-1, szFormat, argList); ReleaseBuffer(); #else CT* pBuf = NULL; int nChars = 1; int nUsed = 0; size_type nActual = 0; int nTry = 0; do { // Grow more than linearly (e.g. 512, 1536, 3072, etc) nChars += ((nTry+1) * FMT_BLOCK_SIZE); pBuf = reinterpret_cast<CT*>(_alloca(sizeof(CT)*nChars)); nUsed = ssnprintf(pBuf, nChars-1, szFormat, argList); // Ensure proper NULL termination. nActual = nUsed == -1 ? nChars-1 : SSMIN(nUsed, nChars-1); pBuf[nActual+1]= '\0'; } while ( nUsed < 0 && nTry++ < MAX_FMT_TRIES ); // assign whatever we managed to format this->assign(pBuf, nActual); #endif }

0 голосов
/ 05 февраля 2009

Я искал ту же функциональность, о которой вы говорите, но, насколько мне известно, в C ++ недоступен такой простой метод, как метод C99, потому что C ++ в настоящее время не включает функции, добавленные в C99 как snprintf).

Лучше всего вам использовать строковый объект. Это немного более громоздко, чем четко написанный вызов sprintf, но это сработает.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...