Урок 9. Форматированный ввод данных

Особенности языка С. Учебное пособие

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

scanf(строка_формата, адреса_переменных);

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

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

Ввод чисел, символов и строк

Пример ввода-вывода целого и вещественного чисел, символа и строки:

  int a;
  float b;
  char ch, str[30];
 
  scanf("%d%f%c%s", &a, &b, &ch, str);
  printf("%d %.3f %c %s\n", a, b, ch, str);

Результат:

45 34.3456y hello 
45 34.346 y hello 

Здесь при выполнении программы все данные были введены в одну строку. Разделителем между числами и строками является пробел, а также любой другой символ пустого пространства (например, '\n'). Однако при считывании символа, пробел учитывается как символ; чтобы этого не произошло, в примере букву записали сразу после числа. Данные можно было бы ввести, разделяя их переходом на новую строку (опять же при этом надо иметь ввиду, как считывается символ).

В строке формата функции scanf() между спецификациями вполне допустимо поставить пробелы: %d  %f %c %s. Они никакой роли не сыграют. Понятно, что данные можно было получить и так:

  scanf("%d", &a);
  scanf("%f", &b);
  scanf("%c", &ch);
  scanf("%s", str);

Обратите внимание, перед переменной str отсутствует знак амперсанда. Это не опечатка. В последующих уроках вы узнаете, что имя массива уже само по себе является ссылкой на массив (другими словами, str содержит адрес начала массива).

В функции scanf() в спецификации формата вещественных чисел не указывается точность представления числа. Запись типа %.3f или %.10lf приведет к невозможности получить вещественное число. Чтобы получить число типа double используют формат %lf, для long double - %Lf.

Для целых чисел: длинное целое - %ld, короткое целое - %hd. Кроме того, существуют спецификации для ввода восьмеричных и шестнадцатеричных чисел.

Функция scanf() возвращает количество удачно считанных данных; т.е. значение, возвращаемое функцией, можно проанализировать и таким образом узнать, корректно ли были введены данные. Например:

  int a;
  double b;
  char ch, str[30]; 
 
  ch = scanf("%d %lf %s", &a, &b, str);  
 
  if (ch == 3)
    printf("%d %.3lf %s\n", a, b, str);
  else
    printf("Error input\n");

Использование обычных символов

В строке формата scanf() допустимо использование обычных символов. В этом случае при вводе данных также должны вводится и эти символы:

  int a, b, c;
 
  scanf("%d + %d = %d", &a, &b, &c);
  printf("Your answer is %d\nThe correct answer is %d\n", c, a+b);

В данном случае, когда программа выполняется, ввод должен выглядеть примерно так: 342+1024 = 1366. Знаки "+" и "=" обязательно должны присутствовать между числами, наличие пробелов или их отсутствие абсолютно никакой роли не играет:

45 + 839=875 
Your answer is 875 
The correct answer is 884 

Запрет присваивания

Если какие-либо данные, вводимые пользователем, следует проигнорировать, то используют запрет присваивания, ставя после знака %, но перед буквой формата звездочку *. В таком случае данные считываются, но никакой переменной не присваиваются. Это можно использовать, например, когда нет определенной уверенности в том, что поступит на ввод, с одной стороны, и нужды сохранять эти данные, с другой:

  float arr[3];
  int i;
 
  for(i = 0; i < 3; i++)
    scanf("%*s %f", &arr[i]);
 
  printf("Sum: %.2f\n", arr[0]+arr[1]+arr[2]);

Здесь предполагается, что перед каждым числом будет вводится строка, которую следует проигнорировать, например:

First: 23.356 
Second: 17.285 
Third: 32.457 
Sum: 73.098 

Использование "шаблонов"

Для функции scanf() есть пара спецификаций формата, отдаленно напоминающих шаблоны командной оболочки и др. Формат […] позволяет получить строку, содержащую любые символы, указанные в квадратных скобках. Как только на ввод поступает символ, не входящий в указанный набор, считывание данных прекращается. Формат [^…], наоборот, помещает в строку символы, не входящие в указанный набор, до тех пор пока не встретит любой из указанных.

В примере ниже как только поступает не цифра, считывание ввода завершается. При этом если первый символ — не цифра, то в str вообще ничего не записывается:

  char str[30]="";
 
  scanf("%[0-9]", str);
  printf("%s\n", str);

А в этом случае строке будет присвоена последовательность символов до любого из указанных знаков препинания:

  scanf("%[^;:,!?]", str);
  printf("%s\n", str);

Результат:

Hello, World! 
Hello

Некоторые особенности и ограничения функции scanf()

Как только поступают некорректные данные, функция scanf() завершает свою работу. В примере:
scanf("%d%f", &a, &b);

если переменной a попытаться присвоить символ или строку, что невозможно, то переменная b уже обрабатываться не будет. Можно предположить, что так будет надежнее:

  scanf("%d", &a);
  scanf("%f", &b);

Вроде бы неудачное считывание a не должно оказывать никакого влияния на b, т.к. это уже иной вызов scanf(). Но не все так просто: при некорректном вводе данные остаются в буфере и пытаются "навязать" себя последующим вызовам scanf(). Поэтому при использовании scanf() надо думать о том, как в случае некорректного ввода очистить буфер. Например, это можно сделать так, как показано ниже, или путем использования специальных функций (здесь не рассматриваются):

  if (scanf("%d", &a) != 1) // если данные не удалось присвоить переменной,
    scanf("%*s"); // то выбросить их в виде строки.
  scanf("%f", &b);

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

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

Решение задач

  1. На прошлом занятии вы написали программу, содержащую функции, вычисляющие факториал числа и заданный элемент ряда Фибоначчи. Измените эту программу таким образом, чтобы она запрашивала у пользователя, что он хочет вычислить: факториал или число Фибоначчи. Затем программа запрашивала бы у пользователя либо число для вычисления факториала, либо номер элемента ряда Фибоначчи.
  2. Напишите программу, которая запрашивает у пользователя две даты в формате дд.мм.гггг. Дни, месяцы и года следует присвоить целочисленным переменным. Программа должна выводить на экран информацию о том, какая дата более ранняя, а какая более поздняя.
  3. Используя цикл, напишите код, в котором пользователю предлагается вводить данные до тех пор, пока он не сделает это корректно, т.е. пока все указанные в scanf() переменные не получат свои значения. Протестируйте программу. Что вы наблюдаете и почему? Как можно решить проблему?