Разница между scanf () и strtol () / strtod () в разборе чисел - PullRequest
12 голосов
/ 15 сентября 2009

Примечание: Я полностью переработал вопрос, чтобы более правильно отразить, для чего я назначаю вознаграждение. Пожалуйста, извините за любые несоответствия с уже предоставленными ответами, которые это могло создать. Я не хотел создавать новый вопрос, так как предыдущие ответы на него могут быть полезны.


Я работаю над реализацией стандартной библиотеки C, и меня смущает один конкретный угол стандарта.

Стандарт определяет числовые форматы, принятые семейством функций scanf (% d,% i,% u,% o,% x) в терминах определений для strtol, strtoul и * 1011. *.

Стандарт также гласит, что fscanf() вернет во входной поток максимум один символ, и поэтому некоторые последовательности, принятые strtol, strtoul и strtod, неприемлемы для fscanf ( ИСО / МЭК 9899: 1999, сноска 251).

Я пытался найти некоторые значения, которые проявили бы такие различия. Оказывается, что шестнадцатеричный префикс "0x", за которым следует символ, который не является шестнадцатеричной цифрой, является одним из таких случаев, когда два семейства функций различаются.

Достаточно забавно, стало очевидно, что никакие две доступные библиотеки C, похоже, не согласны с выводом. (См. Тестовую программу и пример вывода в конце этого вопроса.)

Что мне хотелось бы услышать, так это , что будет считаться стандартом поведения при разборе "0xz"? . В идеале приводятся ссылки на соответствующие части из стандарта.

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
    int i, count, rc;
    unsigned u;
    char * endptr = NULL;
    char culprit[] = "0xz";

    /* File I/O to assert fscanf == sscanf */
    FILE * fh = fopen( "testfile", "w+" );
    fprintf( fh, "%s", culprit );
    rewind( fh );

    /* fscanf base 16 */
    u = -1; count = -1;
    rc = fscanf( fh, "%x%n", &u, &count );
    printf( "fscanf:  Returned %d, result %2d, consumed %d\n", rc, u, count );
    rewind( fh );

    /* strtoul base 16 */
    u = strtoul( culprit, &endptr, 16 );
    printf( "strtoul:             result %2d, consumed %d\n", u, endptr - culprit );

    puts( "" );

    /* fscanf base 0 */
    i = -1; count = -1;
    rc = fscanf( fh, "%i%n", &i, &count );
    printf( "fscanf:  Returned %d, result %2d, consumed %d\n", rc, i, count );
    rewind( fh );

    /* strtol base 0 */
    i = strtol( culprit, &endptr, 0 );
    printf( "strtoul:             result %2d, consumed %d\n", i, endptr - culprit );

    fclose( fh );
    return 0;
}

/* newlib 1.14

fscanf:  Returned 1, result  0, consumed 1
strtoul:             result  0, consumed 0

fscanf:  Returned 1, result  0, consumed 1
strtoul:             result  0, consumed 0
*/

/* glibc-2.8

fscanf:  Returned 1, result  0, consumed 2
strtoul:             result  0, consumed 1

fscanf:  Returned 1, result  0, consumed 2
strtoul:             result  0, consumed 1
*/

/* Microsoft MSVC

fscanf:  Returned 0, result -1, consumed -1
strtoul:             result  0, consumed 0

fscanf:  Returned 0, result  0, consumed -1
strtoul:             result  0, consumed 0
*/

/* IBM AIX

fscanf:  Returned 0, result -1, consumed -1
strtoul:             result  0, consumed 1

fscanf:  Returned 0, result  0, consumed -1
strtoul:             result  0, consumed 1
*/

Ответы [ 8 ]

6 голосов
/ 19 сентября 2009

Общение с Фредом Дж. Тидеманом, заместителем руководителя PL22.11 (ANSI "C"), на comp.std.c пролило некоторый свет на это:

fscanf

Элемент ввода определяется как самая длинная последовательность вводимых символов [...] который является или является префиксом совпадающая входная последовательность. (7.19.6.2 P9)

Это делает "0x" самой длинной последовательностью, которая является префиксом совпадающей входной последовательности. (Даже с преобразованием %i, поскольку шестнадцатеричный "0x" является более длинной последовательностью, чем десятичный "0".)

Первый символ, если есть, после элемент ввода остается непрочитанным. (7.19.6.2 P9)

Это заставляет fscanf прочитать «z» и вернуть его как несоответствующее (соблюдая односимвольный предел отката в сноске 251)).

Если элемент ввода не совпадает последовательность, выполнение директива не выполняется: это условие ошибка соответствия. (7.19.6.2 P10)

Это делает "0x" не соответствующим, то есть fscanf не должно присваивать никакого значения, возвращать ноль (если %x или %i был первым спецификатором конв.) И оставить "z" в качестве первого непрочитанного символ во входном потоке.

strtol

Определение strtolstrtoul) отличается в одном критическом пункте:

Предметная последовательность определяется как самая длинная начальная подпоследовательность входная строка, начинающаяся с первого символ не-пробел, , который имеет ожидаемая форма . (7.20.1.4 P4, акцент мой)

Это означает, что strtol должен искать самую длинную действительную последовательность, в данном случае «0». Он должен указывать endptr на «x» и возвращать ноль в качестве результата.

3 голосов
/ 15 сентября 2009

Согласно спецификации C99, семейство функций scanf() анализирует целые числа так же, как и семейство функций strto*(). Например, для спецификатора преобразования x это выглядит так:

Соответствует опционально подписанному шестнадцатеричное целое число, формат которого так же, как и ожидалось для предмета последовательность функции strtoul с значение 16 для аргумента base.

Так что, если sscanf() и strtoul() дают разные результаты, реализация libc не соответствует.

Каковы ожидаемые от вас примеры кода , немного неясно, хотя:

strtoul() принимает необязательный префикс 0x или 0X, если base равен 16, а в спецификации указано

Предметная последовательность определяется как самая длинная начальная подпоследовательность входная строка, начинающаяся с первого не пробел, то есть ожидаемая форма.

Для строки "0xz", на мой взгляд, самая длинная начальная подпоследовательность ожидаемой формы равна "0", поэтому значение должно быть 0, а аргумент endptr должен быть установлен на x.

mingw-gcc 4.4.0 не согласен и не может проанализировать строку как с strtoul(), так и с sscanf(). Причина может заключаться в том, что самая длинная начальная подпоследовательность ожидаемой формы равна "0x" - что не является допустимым целочисленным литералом, поэтому анализ не выполняется.

Я думаю, что эта интерпретация стандарта неверна: подпоследовательность ожидаемой формы всегда должна приводить к действительному целочисленному значению (если вне диапазона возвращаются значения MIN / MAX и errno устанавливается в ERANGE).

cygwin-gcc 3.4.4 (который использует newlib, насколько я знаю) также не будет анализировать литерал, если используется strtoul(), но анализирует строку в соответствии с моей интерпретацией стандарта с помощью sscanf().

Остерегайтесь того, что моя интерпретация стандарта склонна к вашей первоначальной проблеме, то есть, что стандарт гарантирует возможность ungetc() только один раз. Чтобы решить, является ли 0x частью литерала, вам нужно прочитать впереди два символа: x и следующий символ. Если это не шестнадцатеричный символ, их нужно отбросить назад. Если есть больше токенов для анализа, вы можете их буферизовать и обойти эту проблему, но если это последний токен, вам нужно ungetc() оба символа.

Я не совсем уверен, что fscanf() должен делать в случае сбоя ungetc(). Может быть, просто установить индикатор ошибки потока?

3 голосов
/ 15 сентября 2009

Я не верю, что синтаксический анализ может давать разные результаты. Ссылка Plaugher просто указывает на то, что реализация strtol() может быть другой, более эффективной версией, поскольку она имеет полный доступ ко всей строке.

1 голос
/ 19 сентября 2009

Подводя итог, что должно происходить согласно стандарту при разборе чисел:

  • если fscanf() успешно, результат должен быть идентичен результату, полученному через strto*()
  • в отличие от strto*(), fscanf() завершится неудачей, если

    самая длинная последовательность входных символов [...], которая является или является префиксом совпадающей входной последовательности

    согласно определению fscanf() не является

    самая длинная начальная подпоследовательность [...] ожидаемой формы

    в соответствии с определением strto*()

Это несколько уродливо, но является необходимым следствием требования, что fscanf() должен быть жадным, но не может отталкивать более одного символа.

Некоторые разработчики библиотек выбрали другое поведение. На мой взгляд

  • позволять strto*() не обеспечивать согласованность результатов - глупо ( bad mingw )
  • отталкивание более чем одного символа, поэтому fscanf() принимает все значения, принятые strto*(), нарушает стандарт, но оправдано ( ура для newlib, если они не испортили strto*(): ()
  • не отбрасывать назад несоответствующие символы, но все еще только анализировать символы «ожидаемой формы», кажется сомнительным, поскольку символы исчезают в воздухе ( bad glibc )
0 голосов
/ 19 сентября 2009

Для входа в функции scanf () , а также для функций strtol () , в Sec. 7.20.1.4 P7 указывает: Если предметная последовательность пуста или не имеет ожидаемой формы, преобразование не выполняется; значение nptr хранится в объекте, на который указывает endptr, при условии, что endptr не является нулевым указателем . Также вы должны учитывать, что правила парсинга тех токенов, которые определены по правилам сек. 6.4.4 Константы , правило, указанное в Sec. 7.20.1.4 P5 .

Остальная часть поведения, такая как значение errno , должна зависеть от реализации. Например, в моем FreeBSD я получил значения EINVAL и ERANGE , а в Linux то же самое происходит, когда стандартные ссылки ссылаются только на значение ERANGE errno.

0 голосов
/ 15 сентября 2009

Я не уверен, как реализация scanf () может быть связана с ungetc (). scanf () может использовать все байты в буфере потока. ungetc () просто помещает байт в конец буфера, и смещение также изменяется.

scanf("%d", &x);
ungetc('9', stdin);
scanf("%d", &y);
printf("%d, %d\n", x, y);

Если вход «100», выход «100, 9». Я не вижу, как scanf () и ungetc () могут мешать друг другу. Извините, если я добавил наивный комментарий.

0 голосов
/ 15 сентября 2009

Ответ устарел после переписывания вопроса. Некоторые интересные ссылки в комментариях.


Если сомневаетесь, напишите тест. - пословица

После тестирования всех комбинаций спецификаторов преобразования и входных вариантов, о которых я мог подумать, могу сказать, что правильно, что два семейства функций не дают одинаковых результатов . (По крайней мере, в glibc, который у меня есть для тестирования.)

Разница появляется, когда встречаются три обстоятельства:

  1. Вы используете "%i" или "%x" (допускает шестнадцатеричный ввод).
  2. Ввод содержит (необязательный) "0x" шестнадцатеричный префикс.
  3. После шестнадцатеричного префикса нет действительной шестнадцатеричной цифры.

Пример кода:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char * string = "0xz";
    unsigned u;
    int count;
    char c;
    char * endptr;

    sscanf( string, "%x%n%c", &i, &count, &c );
    printf( "Value: %d - Consumed: %d - Next char: %c - (sscanf())\n", u, count, c );
    i = strtoul( string, &endptr, 16 );
    printf( "Value: %d - Consumed: %td - Next char: %c - (strtoul())\n", u, ( endptr - string ), *endptr );
    return 0;
}

Выход:

Value: 0 - Consumed: 1 - Next char: x - (sscanf())
Value: 0 - Consumed: 0 - Next char: 0 - (strtoul())

Это смущает меня. Очевидно, sscanf() не выдается на 'x', иначе он не сможет проанализировать любой "0x" с префиксом шестнадцатеричных чисел. Поэтому он прочитал 'z' и обнаружил, что он не совпадает. Но он решает использовать только ведущий "0" в качестве значения. Это будет означать отталкивание 'z' и 'x' назад. (Да, я знаю, что sscanf(), который я использовал здесь для простого тестирования, не работает с потоком, но я настоятельно полагаю, что они заставили все ...scanf() функции вести себя одинаково для согласованности.)

Итак ... один символ ungetc() на самом деле не причина, здесь ...?: - /

Да, результаты отличаются . Я все еще не могу объяснить это должным образом, хотя ...: - (

0 голосов
/ 15 сентября 2009

Я не уверен, что понимаю вопрос, но, с одной стороны, scanf () должен обрабатывать EOF. scanf () и strtol () - разные виды зверей. Может быть, вам лучше сравнить strtol () и sscanf ()?

...