netlib.narod.ru | < Назад | Оглавление | Далее > |
Теперь мы намерены рассмотреть несколько программ обработки текстов. Вы обнаружите, что многие существующие программы являются просто расширенными версиями обсуждаемых здесь прототипов.
Стандартная библиотека поддерживает очень простую модель ввода-вывода. Текстовый ввод или вывод вне зависимости от того, откуда он исходит или куда направляется, имеет дело с потоком символов. Поток символов — это последовательность символов, разбитая на строки, каждая из которых содержит произвольное количество символов и завершается символом новой строки. Библиотека ввода-вывода следит, чтобы любой входной или выходной поток соответствовал этой модели; программист, пользуясь библиотекой, не должен заботиться о том, в каком виде строки представляются вне программы.
Стандартная библиотека предоставляет несколько функций для чтения и записи одного символа. Простейшие из них — getchar и putchar. При каждом вызове функции getchar читается один символ из входного потока, и этот символ возвращается в качестве результата. Так, после выполнения
c = getchar();
переменная c будет содержать очередной введенный символ. Обычно символы поступают с клавиатуры. Ввод из файлов рассматривается в главе 7.
Вызов функции putchar приводит к печати одного символа. Например,
putchar(c);
напечатает символ, код которого задан целой переменной c (обычно на экране). Вызовы putchar и printf могут произвольным образом чередоваться. Вывод будет формироваться в том порядке, в каком расположены обращения к этим функциям.
При наличии функций getchar и putchar, можно написать удивительно много полезных программ, ничего больше не зная о вводе и выводе. Простейший пример — программа, копирующая по одному символу из входного потока в выходной:
Чтение символа
while(символ не является признаком конца файла)
вывод только что прочитанного символа
чтение символа
Оформив алгоритм в виде программы на Си, получим
#include <stdio.h> /* копирование ввода на вывод, версия 1 */ main() { int c; c = getchar(); while (c != EOF) { putchar(c); c = getchar(); } }
Оператор сравнения != означает «не равно».
Каждый символ, вводимый с клавиатуры или появляющийся на экране, как и любой другой символ внутри машины, кодируется комбинацией битов. Тип char специально предназначен для хранения символьных данных, однако для этого годится и любой целый тип. Мы пользуемся типом int и делаем это по одной важной причине, которая требует разъяснений.
Существует проблема: как отличить конец потока ввода от обычных читаемых данных. Рещение заключается в том, чтобы функция getchar по исчерпании входного потока возвращала в качестве результата такое значение, которое нельзя было бы спутать ни с одним реальным символом. Это значение называется EOF (аббревиатура от end of file — конец файла). Мы должны объявить для переменной c тип, который может использоваться для представления всех возможных результатов, возвращаемых функцией getchar. Нам не подходит тип char, так как диапазон значений переменной должен быть достаточно велик для представления как любого значения типа char так и и EOF. Вот почему мы используем int, а не char.
EOF — целая константа, определенная в файле stdio.h. Какое значение имеет эта константа — неважно, лишь бы оно отличалось от любого из возможных значений типа char. Использование именованной константы гарантирует, что программа не будет зависеть от конкретного числового значения.
Программисты, имеющие опыт работы с Си, написали бы программу более сжато. В Си любое присваивание, например
c = getchar()
является выражением со значением, равным значению выражения, рнаходящегося слева от знака присваивания. Это значит, что присваивание может встречаться внутри более сложного выражения. Если присваивание значения переменной c расположить в проверке условия цикла while, то программу копирования можно будет записать в следующем виде:
#include <stdio.h> /* копирование ввода на вывод, версия 2 */ main() { int c; while ((c = getchar()) != EOF) putchar(c); }
Цикл while, пересылая в c полученное от getchar значение, сразу же проверяет не является ли оно признаком конца файла. Если это не так, выполняется тело цикла while, печатается символ и повторяется выполнение цила. При получении признака конца файла завершается работа цикла while и выполнение функции main.
В данной версии ввод централизован — в программе имеется только одно обращение к getchar. В результате она более компактна и легче воспринимается при чтении. Вам часто придется сталкиваться с такой формой записи, где присваивание делается вместе с проверкой. (Чрезмерное увлечение ею, может запутать программу, поэтому мы постараемся сдерживаться.)
Скобки внутри условия, расположенные вокруг присваивания, необходимы для правильной работы. Приоритет операции != выше, чем приоритет =, следовательно при отсутствии скобок проверка будет выполняться до операции присваивания. Таким образом, запись
c = getchar() != EOF
эквивалентна записи
c = (getchar() != EOF)
А это совсем не то, что нужно: переменной c будет присваиваться 0 или -1 в зависимости от того, встретит или не встретит getchar признак конца файла. Более подробно эта тема обсуждается в главе 2.
Упражнение 1-6 |
Провеьте, что значение выражения getchar != EOF равно 0 или -1. |
Упражнение 1-7 |
Напишите программу, печатающую значение константы EOF. |
Следующая программа похожа на программу копирования, но занимается подсчетом символов.
#include <stdio.h> /* Подсчет вводимых символов. Версия 1 */ main() { long nc; nc = 0; while (getchar() != EOF) ++nc; printf("%ld\n", nc); }
Инструкция
++nc;
представляет новый оператор ++, который означает увеличить на единицу. Вместо этого можно было бы написать nc = nc + 1, но ++nc намного короче, а часто и эффективнее. Существует аналогичный оператор --, означающий уменьшить на единицу. Операторы ++ и -- могут быть как префиксными (++nc), так и постфиксными (nc++). Как будет показано в главе 2, эти две формы в выражениях имеют разные значения, но и ++nc, и nc++ добавляют к nc единицу. В данном случае мы остановились на префиксной записи.
Для счетчика символов в программе используется переменная типа long. Целые числа типа long занимают не менее 32 битов. Хотя на некоторых машинах типы int и long имеют одинаковый размер, на других компьютерах числа типа int занимают 16 битов, и максимально возможное для них значение равно 32767. В результате счетчик типа int будет переполняться даже при сравнительно небольшом объеме входных данных. Спецификация %ld указывает printf, что соответствующий аргумент имеет тип long.
Возможно охватить еще больший диапазон значений, если использовать тип double (float с двойной точностью). Применим также инструкцию for вместо while, чтобы продемонстрировать другой способ написания цикла.
#include <stdio.h> /* Подсчет вводимых символов. Версия 2 */ main() { double nc; for (nc = 0; getchar() != EOF; ++nc) ; printf("%.0f\n", nc); }
Спецификация %f применяется в printf как для типа float, так и для double; спецификатор %.0f означает печать без десятичной точки и дробной части (последняя в нашем случае отсутствует).
Тело данного цикла for пусто, поскольку кроме проверок и приращения счетчика делать ничего не нужно. Но правила грамматики языка Си требуют, чтобы цикл for имел тело. Выполнение этого требования обеспечивает изолированная точка с запятой, называемая пустой инструкцией. Мы поместили точку с запятой на отдельной строке для большей наглядности.
Наконец, заметим, что если поток ввода не содержит ни одного символа, то при первом же обращении к getchar условие в цикле while или for не будет выполнено и программа выдаст нуль, что является правильным результатом. Это важно. Одно из привлекательных свойств циклов while и for состоит в том, что условие проверяется до того, как выполняется тело цикла. Если ничего делать не надо, то ничего делаться и не будет, пусть даже тело цикла не выполнится ни разу. Программа должна вести себя корректно и при нулевом количестве вводимых символов. Само устройство циклов while и for дает дополнительную уверенность в правильном поведении программы для граничных условий.
Следующая программа подсчитывает строки. Как упоминалось выше, стандартная библиотека обеспечивает модель ввода-вывода, в которой входной текстовый поток состоит из последовательности строк, каждая из которых заканчивается символом новой строки. Следовательно, подсчет строк сводится к подсчету числа символов новой строки.
#include <stdio.h> /* Подсчет строк во входном потоке */ main() { int c,nl; nl = 0; while ((c = getchar()) != EOF) if ( c == '\n') ++nl; printf("%d\n", nl); }
Тело цикла теперь содержит инструкцию if, управляющую инструкцией увеличения счетчика строк ++nl. Инструкция if проверяет условие в скобках и, если оно истинно, выполняет следующую за ней инструкцию (или группу ниструкций, заключенных в фигурные скобки). Мы опять делаем отступы в тексте программы, чтобы показать, что чем управляется.
Двойной знак равенства в языке Си обозначает оператор «равно» (он аналогичен оператору = в Паскале и .EQ. в Фортране). Двойной символ используется, чтобы отличить проверку на равенство от единичного =, применяемого в Си для обозначения присваивания. Предупреждаем: начинающие программировать на Си иногда пишут =, а имеют в виду ==. Как мы увидим в главе 2, в результате обычно получается допустимое выражение и никакого предупреждения не выдается.
Символ, заключенный в апострофы, представляет собой целое значение, равное коду этого символа в таблице символов данной машины. Это так называемая символьная константа, хотя по сути это всего лишь другой способ записи небольших целых чисел. Например, 'A' это символьная константа; в наборе символов ASCII ее значение равняется 65 — внутреннему представлению символа A. Конечно, 'A' в роли константы предпочтительнее, чем 65, поскольку смысл первой записи более очевиден и она не зависит от конкретного способа кодировки символов.
Используемые в строковых константах управляющие последовательности можно применять и в символьных константах. Так, '\n' обозначает код символа новой строки, который в ASCII равен 10. Следует обратить особое внимание на то, что '\n' обозначает один символ, рассматриваемый в выражении как целое значение, в то время как "\n" — строковая константа, которая содержит только один символ. Более подробно различие между символьными и строковыми константами разбирается в главе 2.
Упражнение 1-8 |
Напишите программу для подсчета пробелов, табуляций и строк. |
Упражнение 1-9 |
Напишите программу, копирующую вводимые символы в выходной поток и заменяющую несколько стоящих подрад пробелов на один пробел. |
Упражнение 1-10 |
Напишите программу, копирующую вводимые символы в выходной поток с заменой символа табуляции на \t, символа забоя на \b и каждой обратной наклонной черты на \\. Это сделает видимыми все символы табуляции и забоя. |
Четвертая из нашей серии полезных программ подсчитывает строки, слова и символы, причем под словом здесь имеется в виду любая последовательность символов, не содержащая пробелов, табуляций и символов новой строки. Эта программа является упрощенной версией программы wc системы UNIX.
#include <stdio.h> #define IN 1 /* внутри слова */ #define OUT 0 /* вне слова */ /* Подсчет строк, слов и символов */ main() { int c, nl, nw, nc, state; state = OUT; nl = nw = nc = 0; while((c = getchar()) != EOF) { ++nc; if (c == '\n') ++nl; if (c == ' ' || c == '\n' || c == '\t') state = OUT; else if (state == OUT) { state = IN; ++nw; } } printf("%d %d %d\n", nl, nw, nc); }
Каждый раз, встречая первый символ слова, программа увеличивает счетчик слов на 1. Переменная state фиксирует текущее состояние — находимся ли мы внутри слова или нет. Вначале ей присваивается значение OUT, что соответствует состоянию «вне слова». Мы предпочитаем использовать именованные константы IN и OUT, а не буквальные значения 1 и 0, поскольку это делает программу более понятной. В такой маленькой программе данный прием мало что дает, но в большой программе увеличение ее ясности окупает незначительные дополнительные усилия, потраченные на то, чтобы писать программу в таком стиле с самого начала. Вы обнаружите, что большие изменения гораздо легче вносить в те программы, где магические числа встречаются только в виде именованных констант.
Строка
nl = nw = nc = 0;
устанавливает все три переменные равными нулю. Такая запись является следствием того, что присваивание является выражением со своим собственным значением, а операции присваивания выполняются справа налево. Указанная строка эквивалентна строке
nl = (nw = (nc = 0));
Оператор || означает логическое ИЛИ, так что строка
if(c == ' ' || c == '\n' || c =='\t')
читается как «если с пробел, или c — символ новой строки, или с — символ табуляции». (Напомним, что управляющая последовательность \t обозначает символ табуляции.) Существует также оператор &&, означающий логическое И. Его приоритет выше, чем приоритет оператора ||. Выражения, связанные операторами && или ||, вычисляются слева направо; при этом гарантируется, что вычисления прервутся сразу, как только будет установлена истинность или ложность условия. Если c — пробел, не надо проверять является ли c символом новой строки или табуляции, и эти проверки выполнены не будут. В данном случае эта особенность вычислений не столь важна, но она имеет значение в более сложных ситуациях, которые мы вскоре рассмотрим.
В примере также встречается инструкция else, которая задает альтернативные действия, выполняемые если условие в инструкции if является ложным. В общем виде условный оператор записывается так:
if(выражние) инструкция1 else инструкция2
В конструкции if–else выполняется только одна из двух инструкций. Если выражение истинно, то выполняется инструкция1, если нет, то — инструкция2. Каждая из этих инструкций может быть либо единственной инструкцией, либо несколькими инструкциями, помещенными в фигурные скобки. В нашей программе после else стоит инструкция if, управляющая двумя операторамми в фигурных скобках.
Упражнение 1-11 |
Как протестировать программу подсчета слов? Какой набор входных данных вероятнее всего обнаружит ошибки, если они были допущены? |
Упражнение 1-12 |
Напишите программу, которая печатает вводимые данные, помещая по одному слову на строке. |
netlib.narod.ru | < Назад | Оглавление | Далее > |