Функция scanf() для форматированного ввода

Практический курс по C/C++: https://stepik.org/course/193691

На этом занятии речь пойдет о функции scanf(), которая выполняет форматированное чтение данных из стандартного входного потока stdin. Эта функция имеет следующее определение (прототип):

int scanf(const char* format, …);

Здесь первый параметр format – это указатель на форматную строку, на подобие той, что мы рассматривали в функции printf(). Последующее троеточие указывает на произвольное число дополнительных параметров, как правило, переменных. Функция возвращает целое значение типа int, равное числу прочитанных элементов из входного потока stdin.

Чтобы воспользоваться функцией scanf() нужно знать, как правильно задавать формат считываемых данных. Для этого, также как и в функции printf(), предусмотрены спецификаторы преобразований. Но они несколько иные.

Спецификатор

Описание

%d

Целое число со знаком в десятичной форме. (Приводится к типу int).

%i

Целое число в десятичной, шестнадцатеричной или восьмеричной системах. (Приводится к типу int).

%u

Целое беззнаковое (unsigned) число в десятичной форме. (Приводится к типу unsigned int).

%o

Целое число в восьмеричной форме. (Приводится к типу int).

%x, %X

Целое число в шестнадцатеричной форме. (Приводится к типу int).

%f, %e, %g

%F, %E, %G

Вещественное число. (Приводится к типу float).

%c

Символ в соответствии с текущей кодовой таблицей. (Приводится к типу char).

%s

Строка (последовательность символов). Читается до первого пробела, перевода строки или символа табуляции.

Существуют и другие спецификаторы, но мы остановимся только на этих. Также спецификатор %s для ввода строк рассмотрим позже, когда будем проходить строки.

Самый простой вариант использования функции scanf() соответствует чтению отдельных символов из входного потока. Для этого, очевидно, следует использовать спецификатор «%c» следующим образом:

#include <stdio.h>
 
int main(void)
{
    char byte;
 
    int count = scanf("%c", &byte);
    printf("count = %d, byte = %c\n", count, byte);
 
    return 0;
}

Давайте подробно разберемся, как это работает. Так как в форматной строке записан спецификатор «%c», то функция scanf() читает один байт из буфера входного потока stdin. Предположим, там находятся числа 100 и 53. Значит, функция читает первое значение 100. Далее, необходимо этот байт данных скопировать в переменную byte. И здесь возникает вопрос, как это сделать? Вначале, я напомню, что любая переменная – это непрерывная последовательность байт. В нашем примере – это одна ячейка, т.к. переменная byte имеет тип char. А значение переменной определяется тем, что записано в этих ячейках. То есть, для записи прочитанных данных из входного потока stdin в переменную byte достаточно в соответствующую ячейку памяти скопировать эти прочитанные данные. Именно поэтому функции scanf() передается не значение переменной (как это было в функции printf()), а адрес переменной. Забегая вперед отмечу, что оператор & перед именем переменной, как раз и возвращает адрес этой переменной. Зная этот адрес, функция scanf() имеет возможность менять значение переменной byte, записывая определенные данные напрямую в указанную ячейку памяти. Так происходит передача данных из входного потока в указанные переменные с помощью функции scanf().

Если данные были успешно прочитаны и занесены в переменную byte, то функция scanf() возвратит значение 1. Это говорит нам, что в одну переменную были успешно занесены данные из потока stdin.

Давайте для примера запишем два подряд вызова функции scanf() следующим образом:

#include <stdio.h>
 
int main(void)
{
    char byte1 = '0', byte2 = '0';
 
    int res1 = scanf("%c", &byte1);
    int res2 = scanf("%c", &byte2);
 
    printf("byte1 = %c, byte2 = %c\n", byte1, byte2);
    
    return 0;
}

Если при запуске программы введем сразу два символа, например cd, то во входном буфере окажутся эти символы и второй вызов функции scanf() автоматически прочитает букву d. После запуска программы увидим результат:

byte1 = c, byte2 = d

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

Конечно, эти два вызова можно объединить в один, например, так:

int res = scanf("%c%c", &byte1, &byte2);
printf("res = %d: byte1 = %c, byte2 = %c\n", res, byte1, byte2);

После ввода тех же символов cd, увидим строку:

res = 2: byte1 = c, byte2 = d

Обратите внимание, переменная res принимает значение 2, т.к. данные были успешно записаны в две переменные byte1 и byte2. Перед каждой переменной базового типа не забываем прописывать оператор амперсанд.

Пока, я думаю, все понятно. Давайте теперь поставим символ пробела между спецификаторами в форматной строке:

int res = scanf("%c %c", &byte1, &byte2);

Этот пробел означает любые пробельные символы, которые могут присутствовать между двумя порциями данных. Сразу отмечу, что к пробельным относят символы: пробела, перевода строки, табуляции (и реже некоторые другие). Кроме того, этот символ может и вовсе отсутствовать. Например, мы можем ввести с клавиатуры два символа следующими способами:

cd; c d; c    d; c\nd; c\td

Во всех вариантах будут прочитаны два символа c и d и занесены в переменные byte1 и byte2. То есть, форматная строка "%c %c" указывает сделать следующее: прочитать первый символ из входного буфера (любой символ), затем, пропустить все пробельные символы и прочитать следующий не пробельный.

А теперь давайте вместо пробела поставим, например, запятую:

int res = scanf("%c,%c", &byte1, &byte2);

При таком формате ввода будет ожидаться первый символ (любой), затем обязательно должна идти запятая, а затем еще один любой символ. Например, так:

c,d

А вот если входные данные не соответствуют формату, например:

cd

то функция scanf() успешно прочитает только первый символ, а следующий (второй) оставит во входном потоке, т.к. вместо запятой записана буква d. Переменная res в этом случае будет равна уже 1, а в переменной byte2 останется прежнее значение.

Конечно, мы можем комбинировать разные символы разделители в форматной строке, например, так:

int res = scanf("%c, %c", &byte1, &byte2);

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

c,d; c, d; c,\nd, c,\td и т.п.

Чтение числовых значений из входного потока stdin

Я думаю, с чтением отдельных символов (байт) с помощью функции scanf() в целом все понятно. Теперь можно сделать следующий шаг и посмотреть, как выполняется чтение числовой информации из входного потока.

Если данные представлены в виде целых десятичных чисел со знаком, то для этого часто используют спецификатор %d. Причем, этот спецификатор приводит целые числа к типу int. И это очень важный момент. Сейчас я покажу почему. Запишем нашу программу следующим образом:

#include <stdio.h>
 
int main(void)
{
    long long var_lli = 0;
 
    int res = scanf("%d", &var_lli);
    printf("res = %d: var_lli = %lld\n", res, var_lli);
 
    return 0;
}

Здесь на входе функция scanf() ожидает целое число со знаком, умещающееся в тип int. Если ввести с клавиатуры значение:

1234567890

то на выходе увидим строку:

res = 1: var_lli = 1234567890

То есть, все было прочитано успешно. Но, если значение увеличить, например, до:

12345678901234

то это значение сначала будет приведено к типу int и только затем записано в переменную var_lli типа long long. Поэтому на выходе увидим некорректное значение:

res = 1: var_lli = 1942892530

Вот почему важно правильно сочетать спецификаторы преобразований с типами переменных.

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

Модификатор

Описание

h

%hd, %hi – для short int;

%hx, %ho, %hu – для unsigned short

hh

%hhd – для signed char;

%hhu – для unsigned char

l

%ld, %li – для long int;

%lx, %lo, %lu – для unsigned long;

%lf, %lg, %le – для double

L

%Lf, %Lg, %Le – для long double

ll (в стандарте C99)

%lld – для long long int;

%llu – для unsigned long long

цифры

Максимальная ширина ввода (либо достигается максимальная ширина, либо служебный символ).

*

Пропуск данных.

Например, если нам нужно прочитать очень длинное целое число и сохранить его в типе long long, то следует в функции scanf() использовать модификатор %lld:

int res = scanf("%lld", &var_lli);

Теперь, при вводе длинного числа 12345678901234 мы его и увидим в выходной строке:

res = 1: var_lli = 12345678901234

И так со всеми остальными типами данных. Причем, обратите внимание, спецификаторы %f, %e, %g выполняют преобразование данных к типу float, а не double, как это делает функция printf(). Если в функции scanf() используется переменная типа double, то для нее следует применять модификаторы %lf, %le, %lg. Например:

#include <stdio.h>
 
int main(void)
{
    long long var_lli = 0;
    double var_d = 0;
 
    int res = scanf("%lld %lf", &var_lli, &var_d);
    printf("res = %d: var_lli = %lld, var_d = %.2f\n", res, var_lli, var_d);
 
    return 0;
}

В этом случае ожидается ввод сначала целого числа, а затем, вещественного, которое будет приведено к типу double. В частности, при вводе значений:

123 56.54

получим на выходе строку:

res = 2: var_lli = 123, var_d = 56.54

А вот если в функции scanf() будет указан модификатор для типа float:

int res = scanf("%lld %f", &var_lli, &var_d);

то после ввода тех же значений получим строку:

res = 2: var_lli = 123, var_d = 0.00

Обратите внимание, что переменная res по-прежнему принимает значение 2, т.к. формально входные данные соответствовали форматной строке, но копирование данных типа float в переменную типа double не дало ожидаемого результата. И все из-за неверного спецификатора %f.

Другие модификаторы подобные работают аналогичным образом. Интересной особенностью обладает модификатор *, который позволяет пропускать вводимые данные. Например, мы знаем, что на входе ожидаются данные в формате:

id

price

weight

целое число без знака

целое число

вещественное число

Нас интересуют только поля price и weight. Причем, будем полагать, что числа записаны через разделитель точка с запятой «;». Это частый формат csv-файла. Так вот, для считывания только двух последних значений форматную строку в функции scanf() можно записать следующим образом:

#include <stdio.h>
 
int main(void)
{
    unsigned int price = 0;
    double weight = 0.0;
 
    int res = scanf("%*llu; %u; %lf", &price, &weight);
    printf("res = %d: price = %d, weight = %.2f\n", res, price, weight);
 
    return 0;
}

Введем с клавиатуры данные:

10; 1000; 54.65

На выходе получим строку:

res = 2: price = 1000, weight = 54.65

Как видите, функция scanf() возвратила значение 2 и переменные price, weight принимают правильные значения (последние два). Первое значение 10 было прочитано, но проигнорировано. Конечно, оно при это должно соответствовать спецификатору %llu, то есть, быть десятичным. Если указать, скажем, вещественное значение:

10.34; 1000; 54.65

то это будет ошибка формата и последующие два числа прочитаны не будут:

res = 0: price = 0, weight = 0.00

Вот так, в целом, работает функция scanf(), которая позволяет читать данные из буфера стандартного входного потока stdin данные в указанном формате. При этом считывание останавливается либо после получения всех необходимых данных, либо при ошибке формата ввода. И следует помнить, что ошибочные данные остаются во входном буфере.

Этой информации будет вполне достаточно для большинства практических задач (кроме считывания строк, о которых мы будем говорить позже, когда дойдем до этой темы). Все остальное вам должно быть понятно.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме