Неправильная работа sscanf для спецификатора %n?

Столкнулся с проблемой(или это норма?) при написании кода для парсинга строки с числами. Функция sscanf, если исходить из мануалов, должна записывать количество уже обработанных ею символов в аргумент, который соответствует спецификатору %n. Но, оказывается, что если строкой форматирования задан некоторый символ, а вместо него идет другой, то счетчик в аргумент не записывается. Хотя предыдущие спецификаторы были обработаны и счетчик набирал значение. Например

    unsigned char c = 0;
    int n = 0;
    char str[10] = "30\0";
    res = sscanf(str+pos,"%d% %n", &c, &n);

Для такой строки прочитается число, но количество прочитанных символов 'n' окажется равным нулю. Потому, что следующим символом ожидается пробел, а идет '\0'. А здесь

char str2[100];
int n = 0;
int nn =0;
int res = 0;
unsigned int c = 0;

char str[1000] = "30 10.5 1.123e-50 45 46.555 200 300 500 500.100 233.100";
res = sscanf(str+pos, "%d%n%[.e0-9+-]%n"  , &c,&n, str2, &nn);

После выполнения n примет значение 2, а nn останется равным нулю. Так как для первого числа нет дробной части, то в 'nn' ничего не записывается. Для дробных чисел(если выполнить) всё выполняется нормально: n принимает значение исходя из суммы "предшествующий пробел+целая часть", nn становится равным " n + точка+символы после неё". Встретился с таким поведением в gcc компиляторе. Это нормальная работа? Почему такое происходит?


Ответы (2 шт):

Автор решения: Fat-Zer

Да, всё нормально, так и должно быть: строка форматирования scanf представляет собой последовательность директив, которые обрабатываются последовательно. Если ввод не соответствует тому, что требует очередная директива, то scanf сразу завершается, возвращая количество элементов, обработанных успешно.

Так строка форматирования "%d %n"¹ представляет из себя 3 директивы: %d, и %n. scanf читает число и записывает его в значение типа int, переданному по указателю, затем согласно директиве считывается ноль или более пробельных символов², а затем %n заносится количество уже прочитанных символов, по очередному указателю.

С %d%n%[.e0-9+-]%n ситуация аналогична: читается %d, затем согласно %n записывается количество обработанных символов. А затем %[.e0-9+-] ожидает один из заданных символов, хотя во вводе оказывается пробел, что приводит к завершению scanf. Стоит помнить, что спецификаторы %c, %[ и %n в отличии от других не вычитывают начальные пробелы при вводе строки.


¹ я так понимаю, то что в вопросе "%d% %n" — это опечатка т.к. "% " формально является нестандартным спецификатором ввода, но т.к. ни одна платформа AFAIK не поддерживает что-либо подобное, то это UB.`

² В ответе была ошибка. правильная картина изложена в ответе @wololo

→ Ссылка
Автор решения: wololo

Давайте разбираться.

unsigned char c = 0;
int n = 0;
char str[] = "30\0";
int res = sscanf(str,"%d% %n", &c, &n);

Поведение данного кода неопределено по следующим причинам:

  1. Спецификатор преобразования %d означает, что функция sscanf ожидает получить указатель на int, а вы передаёте ей указатель на unsigned char.

  2. Используется неправильный спецификатор преобразования "% ". Пробел не может следовать непосредственно за символом %.

Изменим тип переменной c на int, и уберём символ % перед пробелом:

int c = 0;
int n = 0;
char str[] = "30\0";
int res = sscanf(str,"%d %n", &c, &n);

Функция sscanf будет обрабатывать строку str следующим образом:

  1. Спецификатор преобразования %d говорит функции sscanf считать целое знаковое десятичное число. Что она и делает, и сохраняет результат в переменную c, в которой теперь хранится значение 30. Количество считанных символов из входной строки равно 2. Количество присвоенных значений равно 1.

  2. Директива " " говорит, что нужно считать все пробельные символы до тех пор пока не появится непробельный символ, либо пока не доберёмся до конца входного потока. Концом входного потока для функции sscanf служит нулевой символ \0. В данном случае между символами "30" и символом \0 никаких пробельных символов нет, поэтому директива " " не считывает из входного потока ни одного символа. Если какая-либо директива не считывает ни единого символа, то считается, что произошла ошибка при выполнении директивы, и функция завершает свою работу. Однако, директива " " особенная: она никогда не завершается ошибкой. Функция продолжает свою работу.

  3. Спецификатор преобразования %n говорит, что нужно записать в целочисленную знаковую переменную типа int количество считанных из входного потока символов. На данный момент считано всего два символа. Это значение и записывается в переменную n.

  4. Функция возвращает количество присвоенных значений. Так как присваивания, задаваемые спецификатором %n не учитываются, то функция вернёт 1.

Итого (gcc, clang, vc):

c   == 30
n   == 2
res == 1

Теперь рассмотрим код:

char str2[100] = {5, 6, 7};
int n = 0;
int nn = 0;
unsigned int c = 0;

char str[] = "30 10.5 1.123e-50 45 46.555 200 300 500 500.100 233.100";
int res = sscanf(str, "%d%n%[.e0-9+-]%n"  , &c,&n, str2, &nn);

Здесь также имеется несоответствие спецификатора преобразования и фактически передаваемого аргумента. Функция sscanf ожидает, что переменная c имеет тип int, но она имеет тип unsigned int. Дабы избежать неопределённого поведения, изменим тип переменной c на int:

int c = 0;

Обработка строки str будет происходить следующим образом:

  1. Выполняется спецификатор преобразования %d. В переменную c записывается значение 30. Количество присвоенных значений равно 1. Количество считанных символов равно 2.

  2. Выполняется спецификатор преобразования %n. В переменную n записывается количество считанных символов, т.е. 2. Количество присвоенных значений остаётся неизменным.

  3. Выполняется спецификатор преобразования %[.e0-9+-]. Данный спецификатор не игнорирует ведущие пробельные символы. А за считанными символами "30" как раз следует пробел. Функция sscanf сверяет пробел с маской и не находит соответствия. Таким образом, в процессе выполнения спецификатора преобразования %[.e0-9+-] ни один символ входного потока не считывается. Следовательно, функция sscanf считает, что произошла ошибка, и поэтому прекращает свою работу.

Итого (gcc, clang, vc):

c   == 30
n   == 2
str == {5, 6, 7, ...}
nn  == 0
res == 1
→ Ссылка