Разница между scanf() и strtol()/strtod() в парсинг-номерах

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


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

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

В стандарте также говорится, что fscanf() возвращает только один символ во входной поток, и поэтому некоторые последовательности, принятые strtol, strtoul и strtod, неприемлемы для fscanf ( ISO/IEC 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
*/

Ответ 1

Общение с Фредом Й. Тидеманом, вице-президентом 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 был первым спецификатором conv), а оставить "z" первым непрочитанным символ во входном потоке.

strtol

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

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

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

Ответ 2

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

Ответ 3

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

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

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

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

strtoul() принимает необязательный префикс 0x или 0x, если base - 16, а spec читает

Тематическая последовательность определяется как самая длинная начальная подпоследовательность вводная строка, начиная с первого небелый пробел, то есть ожидаемая форма.

Для строки "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() не работает. Может быть, просто установите индикатор ошибки потока?

Ответ 4

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

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

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

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

    самая длинная начальная подпоследовательность [...], которая имеет ожидаемую форму

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

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

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

  • позволяя strto*() не давать согласованные результаты, является глупым (плохое mingw)
  • нажатие более одного символа, поэтому fscanf() принимает все значения, принятые strto*(), нарушает стандарт, но является оправданным (ура для newlib, если они не botch strto*():()
  • не отбрасывать несоответствующие символы, но все же только синтаксический анализ "ожидаемой формы" кажется сомнительным, поскольку символы исчезают в воздухе (плохой glibc).

Ответ 5

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

Ответ 6

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


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

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

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

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

Пример кода:

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

int main()
{
    char * string = "0xz";
    int i = -1;
    int count = -1;
    char c;
    char * endptr = NULL;

    sscanf( string, "%x%n%c", &i, &count, &c );
    printf( "Value: %d - Consumed: %d - Next char: %c - (sscanf())\n", i, count, c );
    i = strtoul( string, &endptr, 16 );
    printf( "Value: %d - Consumed: %td - Next char: %c - (strtoul())\n", i, ( 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() ведут себя одинаково для согласованности.)

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

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

Ответ 7

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

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

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

Ответ 8

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

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