Неправильная работа 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 шт):
Да, всё нормально, так и должно быть: строка форматирования 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
Давайте разбираться.
unsigned char c = 0;
int n = 0;
char str[] = "30\0";
int res = sscanf(str,"%d% %n", &c, &n);
Поведение данного кода неопределено по следующим причинам:
Спецификатор преобразования
%dозначает, что функцияsscanfожидает получить указатель наint, а вы передаёте ей указатель наunsigned char.Используется неправильный спецификатор преобразования
"% ". Пробел не может следовать непосредственно за символом%.
Изменим тип переменной c на int, и уберём символ % перед пробелом:
int c = 0;
int n = 0;
char str[] = "30\0";
int res = sscanf(str,"%d %n", &c, &n);
Функция sscanf будет обрабатывать строку str следующим образом:
Спецификатор преобразования
%dговорит функцииsscanfсчитать целое знаковое десятичное число. Что она и делает, и сохраняет результат в переменнуюc, в которой теперь хранится значение30. Количество считанных символов из входной строки равно2. Количество присвоенных значений равно1.Директива
" "говорит, что нужно считать все пробельные символы до тех пор пока не появится непробельный символ, либо пока не доберёмся до конца входного потока. Концом входного потока для функцииsscanfслужит нулевой символ\0. В данном случае между символами"30"и символом\0никаких пробельных символов нет, поэтому директива" "не считывает из входного потока ни одного символа. Если какая-либо директива не считывает ни единого символа, то считается, что произошла ошибка при выполнении директивы, и функция завершает свою работу. Однако, директива" "особенная: она никогда не завершается ошибкой. Функция продолжает свою работу.Спецификатор преобразования
%nговорит, что нужно записать в целочисленную знаковую переменную типаintколичество считанных из входного потока символов. На данный момент считано всего два символа. Это значение и записывается в переменнуюn.Функция возвращает количество присвоенных значений. Так как присваивания, задаваемые спецификатором
%nне учитываются, то функция вернёт1.
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 будет происходить следующим образом:
Выполняется спецификатор преобразования
%d. В переменнуюcзаписывается значение30. Количество присвоенных значений равно1. Количество считанных символов равно2.Выполняется спецификатор преобразования
%n. В переменнуюnзаписывается количество считанных символов, т.е.2. Количество присвоенных значений остаётся неизменным.Выполняется спецификатор преобразования
%[.e0-9+-]. Данный спецификатор не игнорирует ведущие пробельные символы. А за считанными символами"30"как раз следует пробел. Функцияsscanfсверяет пробел с маской и не находит соответствия. Таким образом, в процессе выполнения спецификатора преобразования%[.e0-9+-]ни один символ входного потока не считывается. Следовательно, функцияsscanfсчитает, что произошла ошибка, и поэтому прекращает свою работу.
c == 30
n == 2
str == {5, 6, 7, ...}
nn == 0
res == 1