Опубликован: 05.11.2013 | Доступ: свободный | Студентов: 414 / 22 | Длительность: 11:51:00
Лекция 5:

Препроцессор, оформление программы и средства ввода/вывода

< Лекция 4 || Лекция 5: 12345 || Лекция 6 >

4.3. Ввод/вывод

Как и большинство языков компактного типа, Си не содержит собственных средств ввода/вывода. Для этой цели используется богатая библиотека программных модулей, частично стандартизованная. Для простых целей чаще всего программист пользуется услугами программного модуля stdio, соответственно включая текст его макроподстановкой:

#include <stdio.h>  
    

Так модулю пользователя (включившему описания стандартного ввода/вывода) становятся доступными определение типа FILE, процедуры потокового ввода и вывода данных fopen, putc, getc, putchar, getchar, fclose и ряд констант: NULL, EOF.

Кроме того, импортирующий модуль может воспользоваться процедурами форматного преобразования printf, fprintf и sprintf, описания которых тоже включены в stdio. Процедуры printf и fprintf обеспечивают выполнение форматных преобразований при выводе в файл, а sprintf производит преобразование в форму выходной строки - своего первого параметра. Процедура printf не требует указывать имя файла вывода, так как осуществляет его стандартный выходной поток stdout.

По умолчанию в программе связываются с терминалом пользователя три файла: входной - stdin, выходной - stdout и файл сообщений об ошибках - stderr. Все они относятся к потоковому вводу/выводу. При этом данные, переносимые из входного буфера (a) в зону обработки программы (b) и из зоны (b) в выходной буфер обмена (с), рассматриваются как непрерывная последовательность символов (рис. 4.1).

Схема форматированного ввода/вывода

Рис. 4.1. Схема форматированного ввода/вывода

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

Так последовательность символов "3" и "5" может быть перенесена в зону (b) без изменений в виде двух байт с кодом:

0 0 1 1 0 0 1 1 0 0 1 1 0 1 0 1

или может быть преобразована по числовому формату %2d к двоичному числу:

0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1

Т.е. в первом случае они остаются символами в кодировке ASCII, а во втором преобразуются в двоичное значение числа 35, записанного в прямом коде.

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

Как при вводе, так и при выводе обычно надо указывать длину символьной последовательности, обрабатываемой или формируемой в потоке при преобразовании. При вводе стоит еще подумать об обработке возможных ошибок. Что если вместо цифровых символов в потоке окажутся буквы или какие-либо другие символы? Стандартное преобразование станет невозможным, и программа "сломается". Скорее всего, ее выполнение будет просто прервано.

Хороший стиль программирования предполагает, что программа введет любые данные как символы и лишь потом будет сама разбираться, что с ними делать. А если пользователь ошибочно ввел неправильные значения - сообщит ему об этом и попросит исправить ошибку.

Предположим, нам надо ввести три целых числа A, B и C. Очевидно, можно для этого использовать фрагмент программы:

а)

int A, B, C, Count;
Count = scanf("%d%d%d", &A, &B, &C) ;
    

Значение переменной Count при успешном вводе будет равно 3 - количеству введенных переменных. Следовательно, проверяя далее значение Count, можно судить об удачности попытки ввода всех переменных (Count равно 3) или только части из них (Count равно 2 или 1).

В ряде случаев удобнее решать вопрос по принципу "все или ничего". Тогда проще прибегнуть к чисто логической интерпретации результата ввода:

б)

Count = scanf("%d", &A) &&scanf("%d", &B)&&scanf("%d", &C);  
    

Значение Count, отличное от 0, будет говорить об успешности ввода всех значений. Заметим, что выражение

в)

Count = scanf("%d", &A) + scanf("%d", &B) +
+ scanf("%d", &C);  
    

по результату будет практически эквивалентно варианту (а) совместного ввода. Так, при вводе строки вида "12 13a 14bС" в случаях (а) и (в) значение переменной Count будет равно 2, параметры ввода A и B приобретут значение 12 и 13 соответственно, а переменная C останется без изменений.

В случае (б) значение Count будет равно 0, хотя переменные A, B и C изменятся таким же образом, как в (а) и (в). В буфере обмена во всех случаях останутся символы "a 14b". Любая попытка последующего ввода данных по числовому формату будет натыкаться на нецифровой символ и терпеть неудачу.

На самом деле похожие проблемы возникают в тех случаях, когда при вводе очередного значения, скажем k, возникла ошибка. Вместо входной строки "1 22 333" пользователь набрал "1 22 33w". В результате будут определены все три переменные - объекты ввода (A - 1, B - 22, C - 33), но во входном буфере останется символ "w". И любой последующий перенос данных из системного буфера в память программы будет начинаться именно с этого символа. Поэтому перед следующей операцией ввода системный буфер надо очистить.

Для этой цели можно воспользоваться операцией чтения строки символов:

Char Str[ 100] ;
  scanf("%s", Str);  
    

которая введет оставшуюся в буфере последовательность символов в область переменной Str. А если их там нет? Если пользователь просто ввел строку "1 22 33"? Тогда буфер пуст и система ввода будет ожидать от недоумевающего пользователя ввода символьной строки.

Более корректное решение - использование функций очистки буфера flush (); или fflush(stdin); которые очищают буфер обмена ото всех оставшихся в нем символов.

Но определенные проблемы могут возникнуть при вводе произвольной последовательности символов. Например, если необходимо считать строку со стандартного потока ввода, то первое и самое простое - это использование функции scanf:

#include <stdio.h>
int main()
{
  char Instr[100]; int Flag;
  printf("Input string:\n");
  Flag = scanf("%s", Instr);
  printf("Input=%s\n", Instr);
  return Flag;
}  
    

Но что произойдет, если будет введено больше 300 символов? В функции scanf контроля длины вводимой строки не происходит, она не знает, какого размера в программе переменная Instr. Получается, что все символы, не поместившиеся в массив Instr, будут записываться дальше в память, которая может использоваться другими переменными, в частности Flag. Эта ситуация называется нарушением границ массива.

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

Как же поступать? У функции scanf в спецификаторе ввода можно задать ограничение на длину считываемой строки:

scanf("%99s", Instr);  
    

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

int i = 0;
while ((*(Instr+i) = getc(stdin)) != '\n' && i++ < 99);
* (Instr+i) = '\0';  
    

Считывание происходит посимвольно, пока не встретится символ конца строки (символ '\n' ) или пока количество считанных символов не достигнет 99.

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

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

fgets(Instr,99,stdin);  
    

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

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

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

char c, ch[10]; int i;
printf("Enter passsword, please\n");
c = getch(); i = 0; /* get first character */
while( c != '\015' && i < 9 ) /* end of input string */
{
  ch[i++] = c; putchar('*');
  c = getch(); /* get next character */
}
ch[i] = '\0'; /* end string marker */
printf("\nYour password is:%s\n",ch);  
    

Цикл ввода продолжается, пока не достигнут символ конца строки (восьмеричный код 015) и есть место в массиве символов ch. При этом на экран выводится символ "*" в каждую введенную позицию. В конце оформленная символьная строка для наглядности выводится на экран.

Еще более интересный прием можно рассмотреть на примере ввода восьмеричного числа.

char c, ch[8]; int i, j;
printf("Enter less then 7 octal digits, please\n");
c = getch(); i = 0;
while(c != '\015')/* end of input string */
{
  if(c <= '7'&& c >= '0' && i < 7)
  {
    ch[i++] = c; putchar(c);
  }
  if(c == '\010'&& i > 0)/* back space */
  {
    putchar(c); putchar(' ');putchar(c);i--;
  }
  c = getch();
}
ch[i] = '\0';
printf("\nYour number is:%s\n",ch);
sscanf(ch, "%o", &j);
printf("Your integer value is:%d\n", j);  
    

Ввод в цикле ограничен только признаком конца строки. Зато при анализе введенного символа он сохраняется и печатается, только если ему соответствует восьмеричная цифра и не превышено ограничение на длину строки ввода.

Дополнительно в программу введена проверка на символ возврата на одну позицию назад (BS - back space, восьмеричный код 010). При его вводе программа возвращается на одну позицию (i--) в массиве введенных символов ch и выводит сам символ BS, пробел на место ранее введенного символа и еще раз символ BS.

Таким образом, у пользователя появляется возможность скорректировать ранее введенные данные. При попытке ввода всех остальных символов пользователь не увидит на экране реакции на нажатие клавиш. Остальная часть клавиатуры для него заблокирована.

Для наглядности введенная строка преобразуется во внутреннюю форму целого числа по формату %o, и затем распечатывается в десятичной форме.

< Лекция 4 || Лекция 5: 12345 || Лекция 6 >
Ильдус Кучкаров
Ильдус Кучкаров
Россия, Санкт-Петербург, СПбГУ
Антон Конычев
Антон Конычев
Россия, Москва, МГУПИ, 2013