Символы и их коды; текстовые данные - Язык Паскаль и начала программирования

Программирование: введение в профессию. 1: Азы программирования - 2016 год

Символы и их коды; текстовые данные - Язык Паскаль и начала программирования

Как мы уже знаем из § 1.6.5, тексты в компьютере представляются в виде последовательностей чисел, каждое из которых соответствует одному символу текста, причём один и тот же символ всегда представляется одним и тем же числом; это число называется кодом символа.

В этой главе мы будем рассматривать несколько упрощённую ситуацию: считать, что для хранения кода одного символа достаточно одного байта. В такую картину вполне укладывается кодировка ASCII и её многочисленные восьмибитные расширения (см. § 1.6.5); более того, многие программы, написанные в расчёте на однобайтовые коды символов, будут вполне успешно работать с UTF-8. Что касается “совсем корректной” работы с многобайтовыми символами, то это тема для отдельного разговора, причём тема достаточно сложная; подробное исследование этого вопроса отвлекло бы нас от более насущных задач, поэтому мы оставим эти проблемы за рамками нашей книги.

2.7.1. Средства работы с символами в Паскале

Как мы уже знаем, для хранения символов в Паскале предусмотрен специальный тип, который называется char. Значениями выражений этого типа являются одиночные символы, такие как ’a’, ’7’, ’G’, ’!’, ’ ’ (пробел), а также специальные символы, такие, например, как перевод строки, табуляция и т. п. Обсуждая в первой части нашей книги компьютерное представление текстовой информации, мы уже отмечали, что конкретный набор доступных символов зависит от используемой кодировки, но можно точно сказать, что символы таблицы ASCII присутствуют всегда и везде. Саму таблицу ASCII (с указанием кодов символов в десятичной системе) мы приводили на рис. 1.13. Для удобства на рис. 2.3 мы привели ту же таблицу с шестнадцатеричными кодами; сочетаниями из двух и трёх заглавных латинских букв обозначены специальные (управляющие) символы; пробел для наглядности обозначен “SPC”. Из всего многообразия этих “хитрых” символов, которые на самом деле не совсем символы, нас могут заинтересовать разве что NUL (“символ” с кодом 0, при выдаче на печать на экране ничего не меняется; часто используется в качестве ограничителя строкового буфера); BEL (bell, код 7, при выдаче на печать на экране ничего не меняется, но по идее должен прозвучать короткий звуковой сигнал; на телетайпах звенел звонок); BS (backspace, код 8, при выдаче на печать перемещает курсор на одну позицию влево), НТ (horizontal tabulation, табуляция, код 9; перемещает курсор вправо в ближайшую позицию, кратную восьми или другому числу, установленному в настройках терминала), LF (linefeed, уже знакомый нам перевод строки с кодом 10), CR (carriage return, возврат каретки, код 13, курсор перемещается в начало текущей строки) и ESC ( escape, при выводе на печать обычно обозначает начало управляющей кодовой последовательности, например, для перемещения курсора в заданную позицию).

Рис. 2.3. Шестнадцатеричные коды ASCII

Некоторые из управляющих символов могут быть получены программой при вводе текста с клавиатуры: это LF (при нажатии клавиши Enter), НТ (клавиша Tab), BS и DEL (соответственно клавиши Backspace и Delete), ESC (клавиша Escape); кроме того, “символы” с управляющими кодами при работе с терминалом (как настоящим, так и эмулируемым программно) можно ввести, нажав комбинацию клавиш с Ctrl: Ctrl-@ (0), Ctrl-A (1), Ctrl-B (2), ..., Ctrl-Z (26), Ctrl-[ (27), Ctrl-\ (28), Ctrl-] (29), Ctrl-ˆ (30), Ctrl-_ (31), но многие из этих кодов драйвер терминала обрабатывает сам, так что работающая в терминале программа их не видит. Например, если вы нажмёте Ctrl-C, драйвер терминала не отдаст вашей программе символ с кодом 3; вместо этого он отправит программе специальный сигнал, который её попросту убьёт (мы обсуждали этот эффект во вводной части). При необходимости драйвер терминала может быть перенастроен, и это может, в том числе, сделать ваша программа; в частности, чуть позже мы столкнёмся с программами, которые отказываются “убиваться” при нажатии Ctrl-C.

В программе на Паскале символы, как мы уже знаем, обозначаются с помощью апострофов: любой26 символ, заключённый в апострофы, обозначает сам себя. Кроме этого, можно задать символ с помощью его кода (в десятичной системе): например, #10 означает символ перевода строки, а #55 — это абсолютно то же самое, что ’7’ (как видно из таблицы, код символа семёрки составляет 3716, то есть 5510). Для задания специальных символов с кодами от 1 до 26 можно также воспользоваться так называемой “кареточной” нотацией: ˆA означает символ с кодом 1 (т. е. то же самое, что и #1), ˆB — то же, что и #2, и так далее; вместо #26 можно написать ˆZ.

К примеру, как это нам уже хорошо известно, оператор writeln, напечатав все свои аргументы, в конце выдаёт символ перевода строки, что как раз и приводит к перемещению курсора на следующую строку. При этом никто не мешает нам выдать символ перевода строки без посредства writeln; в частности, вместо хорошо знакомого нам

мы могли бы написать

или

(в обоих этих случаях сначала выдаётся строка, а потом отдельно символ перевода строки); забегая вперёд, отметим, что можно сделать ещё хитрее, “загнав” символ перевода строки прямо в саму строку одним из следующих способов:

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

Символ апострофа используется как ограничивающий для литералов, представляющих как одиночные символы, так и строки; если же возникает необходимость указать сам символ “’”, его удваивают, тем самым сообщая компилятору, что в данном случае имеется в виду символ апострофа как таковой, а не окончание литерала. Например, фразу “That’s fine!” в программе на Паскале задают так: “’That’’s fine!’”. Если нам потребуется не строка, а одиночный символ апострофа, то выглядеть соответствующий литерал будет так: “’’’’”; первый апостроф обозначает начало литерала, два следующих — собственно символ апострофа, последний — конец литерала.

Над символами определены операции сравнения — точно так же, как и над числами; на самом деле сравниваются попросту коды символов: например, выражение ’a’ < ’z’ будет истинным, а ’7’ > ’Q’ —ложным. Это, а также непрерывное расположение в ASCII-таблице символов некоторых категорий, позволяет вычислением одного логического выражения определить, относится ли символ к такой категории; так, выражение (с >= ’a’) and (с <= ’z’) есть запись на Паскале вопроса “является ли символ, находящийся в переменной с, строчной латинской буквой”; аналогичные выражения для заглавных букв и для цифр выглядят как (с >= ’A’) and (с <= ’Z’) и (с >= ’0’) and (с <= ’9’).

Во время работы программы можно по имеющемуся символу, то есть выражению типа char, получить численное значение его кода; для этого используется встроенная функция ord27. Например, если у нас есть переменные

то будет корректным присваивание n := ord(c), при этом в переменную n будет занесён код символа, хранившегося в переменной с. Обратная операция — получение символа по заданному коду — производится встроенной функцией сhr; присваивание с := сhr(n) занесёт в переменную с символ, код которого (в виде обычного числа) находится в n — при условии, что это число находилось в диапазоне от 0 до 255; результат сhr для других значений аргумента не определён. Конечно, сhr(55) — это то же самое, что и #55, и ’7’; но, в отличие от литералов, функция сhrпозволяет нам сконструировать символ, код которого мы не знали во время написания программы или который во время работы программы меняется. Например, табличку, похожую на ту, что показана на рис. 2.3 (только без управляющих символов) можно напечатать с помощью следующей программы:

Как видим, здесь chr берётся от выражения i*16 + j: в каждой строке у нас 16 символов, i содержит номер строки, j — номер столбца, так что это выражение как раз будет равно коду нужного символа, остаётся только превратить этот код в символ, что мы и делаем с помощью chr. Результат работы программы выглядит так:

2.7.2. Посимвольный ввод информации

Рассмотрим противоположную задачу, когда нам придётся получать код символа, неизвестного на момент написания программы. Мы уже знаем, что с помощью read можно прочитать с клавиатуры, например, целое число; но что будет, если наша программа попытается это сделать, а пользователь введёт какую-нибудь белиберду? Обычно при вводе чисел оператор readпроверяет корректность пользовательского ввода, и если пользователь ошибся и ввёл нечто, не соответствующее ожиданиям программы, выдаёт сообщение об ошибке и завершает программу. Выглядит это примерно так:

Зная, что программа написана на Free Pascal, мы можем найти (например, в Интернете) информацию о том, что такое 106, но и только; если же программа предназначена для пользователя, который сам программировать не умеет, то такая диагностика для него бесполезна и может разве что испортить настроение; к тому же, как уже говорилось ранее, завершать программу из-за любой ошибки — идея неудачная.

Мы можем “перехватить инициативу” к заявить компилятору, что сами будем обрабатывать ошибки. Это делается путём вставления в текст программы довольно странно выглядящей директивы {$I-} (“I” от слова input, “-” означает, что мы отключаем встроенные диагностические сообщения для ошибок пользовательского ввода). После этого мы всегда сможем узнать, успешно прошла очередная операция ввода или нет; для этого используется встроенная функция IOResult. Если эта функция возвращает 0 — операция прошла успешно; если же не ноль, то это свидетельствует о происшедшей ошибке. В нашем случае, если пользователь введёт белиберду вместо числа, IOResult вернёт вышеупомянутое 106. Например, программа, перемножающая два целых числа, могла бы (с учётом использования IOResult) выглядеть так:

Конечно, фраза “I couldn’t parse your input”, в переводе с английского означающая “я не смогла разобрать ваш ввод”, выглядит дружественней, чем пугающее “Runtime error 106”, но проблемы в полной мере это не решает. Мы не знаем, ошибся ли пользователь при вводе первого числа или второго, какой конкретно символ стал причиной ошибки, в какой позиции ввода это произошло — собственно говоря, мы не знаем вообще ничего, кроме того, что введено было вместо числа нечто неудобоваримое. Это лишает нас возможности выдать пользователю диагностическое сообщение, информационная ценность которого была бы хоть немного выше сакраментального “пользователь, ты не прав”.

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

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

В простейшем случае можно, например, вынести чтение целого числа в подпрограмму, которую мы оформим как функцию, возвращающую логическое значение: true, если всё в порядке, и false, если произошла ошибка. Само число функция будет отдавать “наружу” через параметр-переменную28; если пользователь допустил ошибку, функция напечатает сообщение об этом и вернёт false, при этом вызывающий может при желании вызвать её снова.

При формировании числа из получаемых символов мы воспользуемся двумя соображениями. Во-первых, как мы видели, коды символов- цифр в таблице ASCII идут подряд, начиная от 48 (код нуля) и заканчивая числом 57 (код девятки); это позволяет получить численное значение символа-цифры путём вычитания кода нуля из кода рассматриваемого символа. Так, ord(’5’) - ord(’0’) равно 5 (53 — 48), ord(’8’) - ord(’0’) таким же точно образом равно 8, и так далее.

Во-вторых, составить численное значение десятичной записи числа из отдельных значений его цифр, просматривая эти цифры слева направо, можно, действуя по достаточно простому алгоритму. Для начала нужно завести переменную, в которой будет формироваться искомое число, и занести туда ноль. Затем, прочитав очередную цифру, мы увеличиваем уже накопленное число в десять раз, а к тому, что получилось, прибавляем численное значение свежепрочитанной цифры. Например, при чтении числа 257 у нас перед началом чтения в переменной будет находиться ноль; после прочтения цифры “2” новое значение числа будет 0 ∙ 10 + 2 = 2, после прочтения цифры “5” получится 2 ∙ 10 + 5 = 25, после прочтения последней цифры мы получим 25 ∙ 10 + 7 = 257, что и требовалось.

Осталось отметить, что для посимвольного чтения можно воспользоваться уже знакомым нам оператором read, указав параметром переменную типа char. Для большей универсальности пусть наша функция работает с числами типа longint. Итоговый текст будет выглядеть так:

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

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

Вообще в операционных системах семейства Unix традиции организации диалога с пользователем несколько иные; считается, что программа не должна задавать пользователю вопросы к вообще не должна ничего говорить, если только всё идёт как должно идти; что-то говорить следует только в случае ошибок. Интересно, что это позволит переписать нашу главную часть программы гораздо короче:

Эта программа примечательна тем, что тела обоих циклов пустые; роль пустого оператора выполняет символ точки с запятой, точнее, он завершает оператор цикла while, так и оставив его с пустым телом. Как можно догадаться, это стало возможным благодаря побочному эффекту функции ReadLongint; этот побочный эффект, состоящий в операциях ввода-вывода и в занесении значения в параметр-переменную, как раз и образует настоящее тело цикла.

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

Итог этого параграфа можно выразить одной фразой: посимвольный ввод и анализ текстовой информации — это наиболее универсальный подход к её обработке.

2.7.3. Чтение до конца файла и программы-фильтры

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

До сих пор мы всегда точно знали, какую информацию должен ввести пользователь и сколько её будет, так что у нас не возникало проблемы, в какой момент прекратить ввод. Но дела могут обстоять иначе: нам может потребоваться читать данные, “пока они не закончатся”. Вопрос тут состоит в том, как программа узнает, что данные в стандартном потоке ввода закончились. Формально в таком случае говорят, что в потоке возникла ситуация “конец файла”; Паскаль позволяет проверить это с помощью встроенной функции eof, название которой образовано от слов end of file, то есть “конец файла”. При работе с потоком стандартного ввода функция eof используется без параметров; она возвращает значение типа boolean: false, если конец файла пока не достигнут и можно продолжать чтение, и true, если читать уже больше нечего.

Важно отметить, что “конец файла” — это именно ситуация, а не что-то иное. В литературе для начинающих, качество которой оставляет желать лучшего, встречаются такие странные понятия, как “символ конца файла”, “маркер конца файла”, “признак конца файла” и тому подобное; не верьте! Существование какого-то там “символа” конца файла — не более чем миф, который своими истоками восходит к тем далёким временам, когда “файл” представлял собой запись на магнитной ленте; в те времена на ленте действительно формировалась особая последовательность намагниченных участков, обозначавшая конец записи (впрочем, даже тогда это не имело ничего общего с “символами”). Файлы, расположенные на диске, совершенно не нуждаются ни в каких “признаках конца”, поскольку операционная система хранит на диске длину каждого файла (в виде обыкновенного целого числа) и точно знает, сколько данных можно из этого файла прочитать.

Итак, запомните раз и навсегда: в конце файла нет ни символа, ни признака, ни маркера, ни чего-либо ещё, что можно было бы “прочитать” вместо очередной порции данных, и каждый, кто вас пытается убедить в противоположном, попросту врёт. Когда мы читаем данные из файла и файл заканчивается, мы при этом ничего не прочитываем; вместо этого возникает ситуация “конец файла”, означающая, что читать из этого файла больше нечего. Именно это — возникла ли уже ситуация “конца файла” или нет — проверяет встроенная функция eof.

В системах семейства Unix традиционно выделяют целый класс программ, которые читают текст из своего стандартного потока ввода и выдают в поток стандартного вывода некий новый текст, который может быть модификацией прочитанного текста или совершенно новым текстом, содержащим какие-то результаты анализа прочитанного текста. Такие программы называются фильтрами. Например, программа grep выделяет из введённого текста строки, удовлетворяющие определённым условиям (чаще всего — просто содержащим определённую подстроку) и только эти строки выводит, а остальные игнорирует; программа sort формирует выдаваемый текст из строк введённого текста, отсортированных в определённом порядке; программа cut позволяет выделить из каждой строки некую подстроку; программа wc (от слов word count) подсчитывает во вводимом тексте количество символов, слов и строк, а выдаёт одну строку с результатами подсчёта. Все эти программы и многие другие как раз представляют собой фильтры.

Умея выполнять посимвольное чтение и обнаруживать ситуацию “конец файла”, мы можем сами написать на Паскале программу-фильтр. Начнём с совсем простого фильтра, который читает текст из стандартного потока ввода и в ответ на каждую введённую пользователем строку выдаёт строку “Ок”, а когда текст кончится — фразу “Good bye”. Для создания этого фильтра нам совершенно не нужно хранить в памяти всю введённую строку, достаточно вводить символы один за другим, и когда введён символ перевода строки, выдавать “Ок”; делать это следует до тех пор, пока не возникнет ситуация “конец файла”. После выхода из цикла чтения останется только выдать “Good bye”. Пишем:

Проверить корректную работу этой программы можно не только с помощью файла (это как раз не очень интересно — она выдаст “Ок” столько раз, сколько в файле было строк, но не считать же их, в самом деле), но и с использованием обычного ввода с клавиатуры. При этом нужно помнить (см. § 1.4.7), что драйверы терминалов в ОС Unix умеют имитировать конец файла; если терминал не перенастраивать, то он делает это при нажатии комбинации клавиш Ctrl-D. Если мы запустим программу FilterOk без перенаправления ввода и начнём вводить произвольные строки, в ответ на каждую введённую строку программа скажет “Ok”; когда нам надоест, мы можем нажать Ctrl-D, и программа корректно завершится, выдав на прощанье “Good bye”. Конечно, мы могли бы просто “убить” программу, нажав Ctrl-C, а не Ctrl-D, но тогда никакого “Good bye” она бы нам не выдала.

Напишем теперь более интересный фильтр, который подсчитывает длину каждой введённой строки и выдаёт результат подсчёта, когда строка заканчивается. Как и в предыдущем случае, нам совершенно не нужно при этом хранить всю строку целиком; читать текст мы будем посимвольно, а для хранения текущего значения длины строки заведём переменную count; при прочтении любого символа, кроме символа конца строки, мы эту переменную будем увеличивать на единицу, а при прочтении конца строки — выдавать накопленное значение и обнулять переменную для подсчёта длины следующей строки. Ещё важно не забыть обнулить нашу переменную в самом начале программы, чтобы длина самой первой строки тоже посчиталась корректно. Всё вместе будет выглядеть так:

Рассмотрим более сложный пример. Пусть нам нужна программа- фильтр, которая выбирает из вводимого текста строки, начинающиеся с непробельных символов, и только их печатает, а строки, которые начинаются с пробела или табуляции, как и пустые строки — игнорирует. Может возникнуть ощущение, что здесь необходимо чтение строки целиком, но и на этот раз это не так. На самом деле нам достаточно помнить, печатаем ли мы текущую строку или нет; бывает и так, что мы пока не знаем, печатать ли текущую строку — так случается, если мы ещё не прочитали ни одного символа строки. Для хранения обоих условии мы воспользуемся логическими29 переменными: одна, которую мы назовём know, будет помнить, знаем ли мы, печатать текущую строку или нет, а вторая — print — будет использоваться лишь в том случае, если know “истинна”, и в этом случае она будет показывать, печатаем ли мы текущую строку.

Прочитав символ, мы прежде всего проверим, не является ли он символом перевода строки. Если да, то мы для начала проверим, не выполняется ли печать текущей строки, и если да, то напечатаем символ перевода строки; после этого мы занесём в переменную know значение “лжи”, чтобы показать, что у нас начинается следующая строка, про которую мы пока не знаем, нужно ли её печатать.

Если прочитанный символ не является символом перевода строки, то у нас есть два варианта. Если мы ещё не знаем, печатается ли текущая строка, то самое время это узнать: в зависимости от того, прочитан ли пробельный символ (или табуляция) или нет, заносим в переменную print значение “лжи” или “истины”, после чего в переменную know заносим “истину”, ведь теперь мы уже точно знаем, печатается текущая строка или не печатается.

Дальше, каков бы прочитанный символ ни был, мы знаем, печатать его или не печатать: в самом деле, даже если мы только что этого не знали, то уже узнали. Если нужно, печатаем символ, и на этом тело цикла заканчивается.

В начале программы нужно не забыть указать, что мы пока что не знаем, будет ли печататься следующая строка; это делается занесением “лжи” в переменную know. На всякий случаи занесём “ложь” также и в переменную print, иначе компилятор выдаст предупреждение о том, что эта переменная может быть использована без инициализации (здесь это не так, но для компилятора ситуация оказалась слишком сложной). Полностью текст программы будет выглядеть так:

Интересно будет проверить эту программу, дав ей на вход её собственный исходный текст:

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

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

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

2.7.4. Чтение чисел до конца файла; функция SeekEof

В предыдущем параграфе мы обсудили ситуацию “конец файла”, её возникновение в потоке стандартного ввода и показали, как организовать чтение данных до тех пор, пока они не кончатся. Когда чтение выполняется посимвольно, никаких проблем с этим не возникает; начинающие часто попадаются здесь в довольно простую и стандартную ловушку, пытаясь применить тот же самый подход к чтению последовательностей чисел.

Рассмотрим простейшую задачу: пользователь с клавиатуры вводит целые числа, а нам нужно прочитать их все и посчитать их количество и общую сумму. Количество чисел заранее неизвестно, а о том, что числа кончились, пользователь извещает нас самым правильным способом — устроив нам “конец файла”, то есть нажав Ctrl-D, если ввод идёт с клавиатуры; если числа пользователь уже записал в файл или их вообще генерирует другая программа, то для возникновения “конца файла” даже нажимать ничего не придётся, всё произойдёт само собой. Будем считать, что нам достаточно разрядности типа longint, то есть пользователь не станет вводить числа, превышающие два миллиарда даже суммарно.

Конечно, текстовое представление числа можно прочитать по одному символу, как мы это делали в § 2.7.2, но очень уж это громоздко, так что возникает естественное желание воспользоваться возможностями, уже имеющимися в операторе read — он ведь умеет читать текстовое представление чисел, при этом перевод последовательности цифр в машинное представление числа он выполняет для нас сам. Начинающие программисты в такой ситуации часто пишут примерно такую программу:

image14

и с удивлением обнаруживают, что она работает “как-то не так”. Нажимать Ctrl-D приходится несколько раз, чтобы программа, наконец, успокоилась; при этом выдаваемое программой количество чисел оказывается больше, чем мы ввели.

Понять, что тут не так, может оказаться довольно сложно, но мы попробуем; для этого нам понадобится в деталях разобраться, что же, собственно говоря, делает read, когда мы требуем от него прочитать число, но с этим-то как раз проблем быть не должно, ведь мы уже делали это сами в § 2.7.2. Итак, первое, что делает read — это пропускает пробельные символы, то есть читает из потока ввода символы по одному до тех пор, пока не найдёт первую цифру числа. Найдя эту цифру, read начинает читать цифры, составляющие представление числа, одну за другой, пока снова не наткнётся на пробельный символ, и в процессе этого чтения накапливает значение числа подобно тому, как это делали мы, путём последовательности умножений на десять и прибавлений значения очередной цифры.

Обратим внимание на то, что ситуация “конец файла” не обязана возникнуть немедленно после прочтения последнего числа. На самом деле она практически никогда так не возникает; вспомним, что текстовые данные представляют собой последовательность строк, а в конце каждой строки находится символ перевода строки. Если данные, поданные на вход нашей программе, представляют собой корректный текст, то после самого последнего числа в этом тексте должен стоять ещё и перевод строки (это если пользователь не оставил после числа ещё и дюжину-другую незначащих пробелов, а ему этого никто не запрещает). Получается, что в тот момент, когда read завершает чтение последнего числа из потока ввода, числа уже кончились, но символы в потоке — ещё нет. Как следствие, eof пока что считает, что ничего не случилось, и выдаёт “ложь”; в итоге наша программа делает ещё один read, который при попытке прочитать очередной символ благополучно упирается в конец файла. Его поведение в такой ситуации несколько неожиданно — не выдавая никаких ошибок, он попросту делает вид, что прочитал число 0 (почему так сделано, непонятно, но реальность именно такова). Отсюда расхождение между количеством введённых чисел, подсчитанных программой, и количеством чисел, которые реально ввёл пользователь (хотя сумма выдаётся правильная, ведь прибавление лишнего нуля её не меняет).

И это ещё не конец истории. Для случая, когда ввод идёт не из настоящего файла (который действительно может кончиться), а с клавиатуры, где ситуацию конца файла приходится имитировать нажатием Ctrl-D, вполне возможно продолжение чтения после наступления ситуации конца файла; в применении к нашей ситуации это значит, что read, “уперевшись” в ситуацию “конец файла”, использовал её, так что eof этого конца файла уже не увидит. Именно поэтому приходится нажимать Ctrl-D дважды, чтобы программа завершилась. При работе с настоящими файлами такого эффекта не наблюдается, потому что, коль скоро файл кончился, связанный с ним поток ввода остаётся в ситуации конца файла “навсегда”.

Так или иначе, у нас имеется проблема, и нужно средство для её решения; Free Pascal нам такое средство предоставляет — это функция SeekEof. В отличие от обычного eof, эта функция сначала прочитывает и игнорирует все пробельные символы, и если она в итоге “упёрлась” в конец файла, то возвращает “истину”, а если нашла непробельный символ — возвращает “ложь”. При этом найденный непробельный символ “возвращается” в поток ввода, так что последующий read начнёт работу именно с него.

Приведённая выше “неправильная” программа превращается в правильную одним-единственным исправлением — нужно eof в заголовке цикла заменить на SeekEof, и всё будет работать именно так, как мы хотим.

На всякий случай упомянем также функцию SeekEoln, которая возвращает “истину”, когда достигнут символ конца строки. При этом, как и SeekEof, она читает и отбрасывает пробельные символы. Эта функция может потребоваться, например, если формат входных данных предполагает группы чисел переменного размера, сгруппированные по разным строкам.






Для любых предложений по сайту: [email protected]