2012-04-09 4 views
3

Во время интервью я просил (среди прочего) осуществлять следующие функции:Что касается vsnprintf (интервью)

int StrPrintF(char **psz, const char *szFmt, ...); 

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

Возвращаемое значение должно быть длиной только что созданной строки или отрицательной по ошибке.

Это моя реализация:

int StrPrintF(char **psz, const char *szFmt, ...) 
{ 
    va_list args; 
    int nLen; 

    va_start(args, szFmt); 

    if ((nLen = vsnprintf(NULL, 0, szFmt, args)) >= 0) 
    { 
     char *szRes = (char*) malloc(nLen + 1); 
     if (szRes) 
      if (vsnprintf(szRes, nLen + 1, szFmt, args) == nLen) 
      { 
       free(*psz); 
       *psz = szRes; 
      } 
      else 
      { 
       free(szRes); 
       nLen = -1; 
      } 
     else 
      nLen = -1; 
    } 

    va_end(args); 
    return nLen; 
} 

вопрос автор утверждает, что есть ошибка в этой реализации. Не только стандартное нарушение, которое может терпеть неудачу в конкретных эзотерических системах, но это «настоящая» ошибка, которая случайно может потерпеть неудачу в большинстве систем.

Это также не относится к использованию int вместо подходящего для памяти типа, такого как size_t или ptrdiff_t. Скажем, строки имеют «разумный» размер.

Я действительно не знаю, что может быть ошибкой. Вся арифметика указателя в порядке ИМХО. Я даже не предполагаю, что два последовательных вызова vsnprintf дают тот же результат. Все материалы с вариационной обработкой также правильны ИМХО. va_copy не требуется (это ответственность вызывающего абонента, который использует va_list). Также на x86 va_copy и va_end бессмысленны.

Буду признателен, если кто-то может обнаружить (потенциальную) ошибку.

EDIT:

После проверки ответов и комментариев - я хотел бы добавить некоторые примечания:

  • Естественно я построил и запустить код с различными входами, включая стадию -by-step в отладчике, наблюдая за состоянием переменных. Я никогда не прошу помощи, не пробовав себя в первую очередь. Я не видел никаких проблем, не повреждал кучу/кучу и т. Д. Также я запустил его в сборке отладки, с включенной отладочной кучей (которая непереносима для кучи коррупции).
  • Я предполагаю, что функция вызывается с действительными параметрами, т.е. psz является допустимым указателем (не путать с *psz), szFmt является действительным спецификатором формата, и все VARIADIC параметров оцениваются и соответствуют строке формата.
  • Вызов free с указателем NULL в порядке в соответствии со стандартом.
  • Вызов vsnprintf в порядке с NULL Указатель и размер = 0. Он должен вернуть результирующую длину строки. MS-версия, хотя и не полностью стандартная, делает то же самое в этом конкретном случае.
  • vsnprintf не будет превышать указанный размер буфера, включая 0-терминатор. Значит - он не всегда его размещает.
  • Пожалуйста, отложите стиль кодирования в сторону (если вам это не нравится - отлично со мной).
+0

Вы пробовали компилировать и запускать некоторый тестовый код на вашем компьютере? –

+0

Проблема с 'snprintf()', но я не уверен, что она применяется к 'vsnprintf()' ... –

+0

Я считаю, что документы для 'vsnprintf' указывают, что вывод * всегда * с нулевым завершением , поэтому ваш вызов 'vsnprintf (NULL, ...)' может вызвать ошибку seg. –

ответ

8

va_copy не требуется (это ответственность вызываемым, которая использует va_list)

Не совсем верно. Я не нашел такого требования для vsnprintf в стандарте C11. Он сказал это в примечании:

как функции vfprintf, vfscanf, vprintf, vscanf, vsnprintf, vsprintf и vsscanf вызвать va_arg макрос, значение арг после возврата неопределенна.

При вызове vsnprintf, то va_list может передаваться по значению или по ссылке (это непрозрачный тип для всех мы знаем). Поэтому первый vsnprintf может фактически изменить va_list и разорить вещи для второго. Рекомендуемый подход заключается в создании копии с использованием va_copy.

И действительно, в соответствии с this article этого не происходит на x86, но это происходит на x64.

+0

Позитивный. Linux man pages 3.32 говорит что-то о том, что значение ap равно _undefined_ после вызова. – hellork

+0

Хороший момент, спасибо. На самом деле я не вижу обоснования передачи «va_list» по ссылке »(очевидно, нет ссылок на C) или каким-либо косвенным способом. MSVC использует ту же стратегию на AMD64, что и на x86 (теперь проверяется). Но, говоря исключительно в терминах стандарта, это действительно ошибка. Я знал о существовании 'va_copy', но я был уверен, что ответственность за сохранение состояния' va_list' лежит на контролере. – valdo

-1

Без прямого ответа: проверьте свои входы.

1

Первый аргумент vsnprintf не должен быть пустым в соответствии с:

http://msdn.microsoft.com/en-us/library/1kt27hek(v=vs.80).aspx

Edit 1: Вы не должны бесплатно * ПСЗ, если он является недействительным!

+4

И Microsoft снова борется со стандартами ... –

+0

да, C99 явно разрешает эту комбинацию –

+0

@David Grayson: (1) См. В конце руководства по микрософт, начиная с VS2005 SP1 это изменяется таким образом, что один из них ** допускается **, чтобы положить 'NULL', если размер буфера = 0. (2) В соответствии со стандартным 'free' ** может ** вызываться с указателем' NULL' – valdo

0

Кроме того, * psz может указывать на уже выделенную строку (в куче), которая потенциально может использоваться во время форматирования.

Для того, чтобы быть потенциально многоразовым, необходимо указать, является ли это мусором или действительным указателем кучи. Если нет аргумента функции, указывающего на это, вы можете принять единственное разумное соглашение для значения NULL-дозорного ... т. Е. Если *psz не является NULL, то вы можете повторно использовать его при условии, что данные, которые вы хотите отформатировать, могут помещаться в одно и то же пространство. Поскольку функции не указывается какая-либо информация о количестве ранее выделенной памяти, вы можете: - использовать realloc и доверять ей, чтобы избежать ненужного перемещения буфера - вывести минимальный размер существующего буфера от strlen() - это будет означать что если вы говорите, что записываете длинную строку, тогда короткую строку, а затем - длинную строку в буфер, последняя операция будет бесполезно заменять буфер.

Очевидно, что realloc лучше.

int StrPrintF(char **psz, const char *szFmt, ...) 
{ 
    va_list args; 
    int nLen; 
    va_start(args, szFmt); 
    if ((nLen = vsnprintf(NULL, 0, szFmt, args)) >= 0) 
    { 
     char *szRes = (char*) realloc(psz, nLen + 1); 
          //^realloc does a fresh allocation is *psz == NULL 
     if (szRes) 
      vsnprintf(*psz = szRes, nLen + 1, szFmt, args); // can't fail 
         //^note the assignment.... 
     else 
      nLen = -1; 
    } 
    va_end(args); 
    return nLen; 
} 

Примечание тоже - от Linux для страницы руководства printf() - если ваш sprintf() не возвращает полезную длину вы должны получить/написать реализацию, которая делает ....

статьи применима возвращаемое значение snprintf(), SUSv2 и C99 противоречит друг другу: когда вызывается snprintf() с размером = 0, тогда SUSv2 оговаривает неопределенное возвращаемое значение меньше 1, тогда как C99 позволяет str быть NULL в этом случае и дает возвращаемое значение (как всегда) как количество символов, которые были бы записаны в случае, если выходная строка была достаточно большой.

+0

Спасибо за ваш ответ. Да, разумное соглашение состоит в том, что '* psz' является' NULL', если он не содержит уже выделенную строку.Ваша идея с 'realloc' не попадает в типичный gotcha, который можно было бы ожидать в таком вопросе. Обратите внимание, что вы перераспределяете '* psz' ** до ** фактическую итоговую строку форматируют, то есть перед 2-м вызовом' vsnprintf'. Но что, если одним из параметров функции является текущее значение '* psz'? Я имею в виду, что можно написать 'StrPrintF (& s," hello% s, s), 'Doing' realloc' может освободить '* psz' предварительный! – valdo

+0

@valdo: вы не можете ожидать' sprintf (buffer, "hello% s ", buffer)' to work - я бы сказал, что для реализации целесообразно иметь аналогичные предпосылки/ограничения использования как 'sprintf()' .... (Объяснение: say buffer имеет «x \ 0», sprintf() запускает копии «hello» над «x \ 0» и следующие символы, в которых буфер точки не гарантируется. NUL завершен, поэтому копия '% s' может работать неограниченное время. –

+0

Но наша функция имеет другую семантику и ваши предположения о том, что ожидать от него не имеет значения. Я не представил полный вопрос интервью (он состоял из нескольких частей), но из этого можно сделать вывод, что эта функция должна работать корректно в таком случае, т. е. делать 'StrPrintF (& s, "hello% s, s);' должен поддерживаться. – valdo

1

Первый вызов vsnprintf() - это действительно попытка получить длину финальной строки. Однако это имеет побочный эффект! Он также перемещает переменную-аргумент в следующую в списке. Итак, следующий вызов vsnprintf() не содержит первого аргумента в списке. Легкий взлом - это сброс списка аргументов переменных, чтобы начать снова, как только вы получите длину от первого vsnprintf(). Возможно, есть еще один способ сделать это лучше, но, да, это проблема.