/
Author: Грогоно П.
Tags: компьютерные технологии программирование языки программирования язык программирования паскаль
Year: 1982
Text
МАТЕМАТИЧЕСКОЕ
ОБЕСПЕЧЕНИЕ
ЭВМ
П.Грогоно
ПРОГРАММИРОВАНИЕ
НА ЯЗЫКЕ
ПАСКАЛЬ
***ф**<
******<
**>***◄
******◄
ш jAmb ^1мв Лшш. Ашшл
PROGRAMMING IN PASCAL
REVISED EDITION
Peter Grogono
Computer Center
Concordia University
Montreal, Quebec
ADDISON-WESLEY
PUBLISHING COMPANY. INC.
READING, MASSACHUSETTS
MENLO PARK, CALIFORNIA
LONDON • AMSTERDAM
DON MILLS, ONTARIO
SYDNEY
1980
МАТЕМАТИЧЕСКОЕ
ОБЕСПЕЧЕНИЕ
ЭВМ
П.Грогоно
ПРОГРАММИРОВАНИЕ
НА ЯЗЫКЕ
ПАСКАЛЬ
Перевод с английского
Л. Е. Карпова
под редакцией
Д. Б. Подшивалова
МОСКВА «МИР» 1982
ББК 32.973
Г 86
УДК 681.3
Грогоно П.
Г 86 Программирование на языке Паскаль: Пер. с англ.—
М.: Мир, 1982.— 384 с., ил.
В книге канадского автора содержится исчерпывающее описание (от подго-
товки программы до отладки) языка программирования Паскаль, который полу-
чил широкое распространение Он используется как язык для обучения програм-
мированию, для записи алгоритмов в монографиях и журналах. Паскаль послужил
основой для создания ряда новых эффективных языков, в частности языка Ада.
Паскаль реализован на отечественных машинах (БЭСМ-6, ЕС ЭВМ).
Книга необходима всем, кто занимается программированием.
Редакция литературы по математическим наукам
Г 041(01)-82 31~82, ч’ L 2405000000
ББК 32.973
6Ф7.3
© 1980 by Addison-Wesley Publishing
Company, Inc.
© Перевод на русский язык, «Мир», 1982
ОТ РЕДАКТОРА ПЕРЕВОДА
Предлагаемая читателю книга является одной из первых пуб-
ликаций в СССР, полностью посвященных языку программиро-
вания Паскаль. Этот язык был создан более десяти лет назад и
получил широкое распространение, особенно как язык публи-
каций: часто на его основе обсуждаются проблемы программи-
рования, создаются экспериментальные расширения языка и
проводится обучение программированию.
Обучение программированию, как и любой другой творческой
деятельности,— дело весьма трудное, так как знакомство с тео-
ретическими основами дисциплины, может быть даже и глубо-
кое, еще не есть умение решать соответствующие задачи. В пред-
лагаемой книге известный канадский специалист Грогоно пред-
принял довольно удачную попытку создать учебник программи-
рования. В основу процесса обучения автор кладет ознакомление
с многочисленными примерами программ, расположенными в по-
рядке возрастания трудности. При условии, что обучающийся
подойдет к разбору примеров с должным вниманием, такой прием,
очевидно, наиболее продуктивен, хотя процесс чтения книги при
этом затрудняется. В целом подход автора основан на так назы-
ваемом принципе «структурного» (или структурированного) про-
граммирования.
О принципах структурного программирования стали гово-
рить приблизительно в то же время, когда началась работа по
созданию языка Паскаль, что по мнению автора позволяет свя-
зывать одно с другим. Однако создание языка Паскаль явилось
завершением определенного этапа довольно сложного и противо-
речивого процесса разработки универсальных языков, которым
занимался как сам создатель Паскаля — Н. Вирт, так и другие
ученые. Причем сейчас уже совершенно ясно, что Паскаль не
был продуктом какого-либо регулярного подхода к созданию
языка, и в частности принципов структурного программирова-
ния, а появился в результате «творческого» акта. Создав Паскаль,
Н. Вирт попытался объяснить, как он это сделал, но это уже было
объяснение после выигранного сражения. Более того, другой
его язык — Модула — оказался далеко не таким удачным, как
Паскаль.
6
От редактора перевода
Подобные вопросы можно было бы здесь и не затрагивать,
если бы не одно обстоятельство. Желая исходить из принципов
структурного программирования, автор довольно часто выска-
зывает с нашей точки зрения спорные положения. Везде, где было
возможно, мы старались это отметить в примечаниях.
В последнее время, и это особенно характерно для структур-
ного программирования, много говорят о программировании как
о разделе некоторой науки — информатики. Однако это скорее
инструмент науки. Программирование подвержено влиянию
самой обычной моды, хотя эту моду очень часто пытаются объяс-
нить результатами «научных» исследований. Из работы в работу
кочуют тезисы, что структурное программирование удобно (для
кого?), надежно (кто это измерял?), и другие заявления, ссылаю-
щиеся на науку (какую?). Как, например, можно говорить о
научном подходе к вопросу о расположении программы на
бланке?
Часто на основе таких положений автор критикует даже сам
язык Паскаль и, что хуже, неявно его модифицирует. Изменяется
терминология, изменяются существующие синтаксические диа-
граммы, добавляются новые и т. д. При переводе книги мы ста-
рались придерживаться терминологии оригинального языка
Паскаль или опять же давали примечания.
Теперь о терминологии языка Паскаль. Уже в самых первых
примерах автор говорит «о естественном понимании» смысла
программ. К сожалению, это естественно лишь для англоязычно-
го читателя. Везде, где это было возможно, мы переводили про-
граммы на русский язык, но там, где начинается формализация
построения фраз и образование из них «операторов» языка Пас-
каль, приходится переходить к английским служебным словам.
Это происходит из-за того, что не существует трансляторов с
Паскаля, воспринимающих русскую версию служебных слов.
Более того, такой версии нет. На стр. 8 мы приводим список
возможных русских эквивалентов английским ключевым словам.
«Засилие» английских ключевых слов приводит к тому, что
даже в обсуждении языка при обозначении его компонент мы на-
чинаем уже не только пользоваться русскими словами англий-
ского происхождения, например «идентификатор», но и просто
коверкать русский язык, употребляя такие понятия, как «цикл
while» в Паскале или «цикл DO WHILE» в ПЛ1. Очень многие
из таких «коверканий» мы попытались убрать, предложив неко-
торую версию для терминов языка Паскаль. Удачно это сделано
или нет, не нам судить.
Нам кажется, что при обсуждении языков программирования
существуют две версии терминологии. Первая — версия, пред-
назначенная для профессионалов, создающих или реализующих
язык («лингвистов»). В ней определяющую роль играет точность
От редактора перевода
7
высказываний, не допускающих контекстных толкований, хотя
бы из-за обширности самих контекстов. Здесь, например, может
быть необходимо проводить тонкие различия между «обозначе-
нием значения», «именем значения» и «изображением значения» и
вводить для этих понятий короткие термины.
Вторая — версия, предназначенная для пользователей, ее
терминами пользуются программисты, составляющие более или
менее простые программы. Одна из особенностей такой версии —
ее разговорный характер. Кроме того, следует учитывать, что
этой версией должны пользоваться и, насколько возможно,
сразу ее понимать многие десятки тысяч программистов — не-
профессионалов, с образованием самого разного уровня, вплоть
до людей, не знающих английского языка. Мы исходили из того,
что перевод должен быть выдержан в рамках этой разговорной
версии. В качестве примера по поводу образования такой разго-
ворной версии можно привести эволюцию понятия «логический
тип». Трудно понять, по каким причинам, но со времен Алгола 60
для типа boolean стали пользоваться названием «логический».
Тем не менее очень многие программисты говорят «булевский»
(даже не «булев»). Очевидно, трудно говорить «логический тип»
и тут же писать boolean. Поэтому мы решили пользоваться терми-
ном «булевский».
И наконец, последнее. Автор говорит о том, что эту книгу
можно браться читать, ничего не зная о программировании и о
вычислительных машинах. Автору предисловия трудно предста-
вить себя на месте начинающего программиста, но тем не менее
ему кажется, что книга во многом ориентирована на более высо-
кий уровень и предполагает иногда довольно хорошее знание
языков программирования. Особенно это заметно в отступлениях
теоретического характера. Однако значительная часть матери-
ала, конечно, предназначена для начинающих программистов.
Таким образом, книгу с пользой для себя прочтет как опытный
профессионал, так и начинающий программист, берущийся за
составление своей первой большой программы.
Д. Б. Подшивалов
8
От редактора перевода
Список ключевых слов языка Паскаль и их перевод
and и nil НИЛ
array массив not не
begin начало of из
case вариант or или
const константа packed упакованная, упакованный
div дел procedure процедура
do выполнять program программа
downto уменьшая до record запись
else иначе repeat повторять
end конец set множество
file файл then то
for Для to увеличивая до
function функция type тип
goto переход на until до
if если var переменная
in в while пока
label метка with с
mod мод
Перевод некоторых ключевых слов состоит из двух русских
слов, а прилагательные могут изменять род. Это означает, что
использовать такой перевод в качестве ключевых слов в тран-
сляторе не так просто.
ПРЕДИСЛОВИЕ К ПЕРЕСМОТРЕННОМУ ИЗДАНИЮ
К моменту написания этой книги (август 1979) универсального
стандарта языка программирования Паскаль еще не существо-
вало. Британская рабочая группа по стандартам (British Stan-
dards Working Group — BSWG) DPS/13/14 опубликовала доку-
мент под названием «Третий рабочий проект» (Working Draft/3),
который был представлен на рассмотрение в Британский инсти-
тут стандартов (British Standards Institute — BSI) и в Междуна-
родную организацию по стандартам (International Standards
Organization — ISO). Возможно, этот документ будет принят
в качестве стандарта без существенной переделки. Новое, пере-
смотренное издание настоящей книги согласуется, насколько мне
известно, с «Третьим рабочим проектом». Он упоминается в книге
под названием «Предлагаемый стандарт языка Паскаль». В США
Комитет Американского национального института стандартов
(American National Standards Institute — ANSI) X3J9 и Комитет
по Паскалю Института инженеров по электротехнике и радио-
электронике (Institute of Electrical and Electronic Engineers —
IEEE) образовали Объединенный комитет по языку Паскаль.
Этот комитет предложил внести некоторые изменения в предла-
гаемый стандарт Паскаля.
Предлагаемый стандарт Паскаля есть, по-существу, уточнен-
ная версия Сообщения о Паскале, написанного профессором Ни-
клаусом Виртом. Фактически язык изменился только в одном
отношении: формальные параметры процедур и функций, в свою
очередь выступающих в роли формальных параметров, должны
быть описаны полностью. Это изменение было рекомендовано
самим Виртом. Естественный консерватизм стандартизации разо-
чарует тех, кто ожидал, что язык будет расширен. Однако, кон-
серватизм в данном случае оправдан. Паскаль является скорее
произведением одного человека, чем комитета. В языке найден
удачный компромисс между простотой и выразительной мощ-
ностью, эффективностью и переносимостью, лаконичностью и
многословием; его успех свидетельствует об умении, с которым
были достигнуты противоречивые цели, преследуемые при раз-
работке языка. К настоящему времени уже затрачено много
усилий и средств для построения компиляторов, транслирующих
Предисловие к пересмотренному изданию
программы с языка, определенного Сообщением о Паскале. На-
конец, существование стандарта, определяющего ядро языка,
не запрещает последующего определения совместимых расшире-
ний, •
Я изучил множество предложений и поправок, присланных I
читателями первого издания этой книги. Особую признатель-
ность хотелось бы выразить Барри Корнелиусу, Джону Мак-
Кею, Энди Микелю, Дэвиду Пресбергу, Дэвиду Пробсту и Сэ-
мюэлю Е. Роадсу.
ПРЕДИСЛОВИЕ К ПЕРВОМУ ИЗДАНИЮ
Язык программирования Паскаль был первым языком, в ко-
тором нашли отражение концепции структурного программиро-
вания, определенные Дейкстрой и Хоором. Как таковой он
представляет собой значительный шаг в развитии языков про*
граммирования. Паскаль был разработан Никлаусом Виртом
в Швейцарском техническом институте в Цюрихе (Eidgenossische
Technische Hochschule) на основе языка Алгол 60, но он мощнее 1
и проще в использовании. В настоящее время Паскаль широко
применяется как язык, который может быть эффективно реализо-
ван, а также как превосходное средство обучения программиро-
ванию.
Эта книга предназначена для тех, кто хочет писать программы
на языке Паскаль. Знания других языков программирования не
предполагается, и, таким образом, книга представляет собой
вводный курс. В совокупности с курсом по дискретным структу-
рам она может служить основой более подробного курса «Прин-
ципы программирования».
Главы 1 и 10 посвящены обсуждению общих принципов про-
граммирования для вычислительных машин. Глава 10, естест-
венно, сложнее главы 1, но читать ее нужно не обязательно в
последнюю очередь. Чтобы в ней разобраться, глубоких позна-
ний в Паскале не требуется, ее можно изучать параллельно с
остальными главами книги. В главах 2, 5—8 рассмотрены струк-
туры данных Паскаля, а главы 3 и 4 посвящены операторам язы-
ка. Такое построение, несмотря на его систематичность, не охва-
тывает некоторых наиболее тонких свойств языка, которые осве-
щаются в главе 9. Если Вы уже знакомы с каким-либо языком
программирования, первые главы покажутся Вам легкими, но,
так как в Паскале основные принципы определяются точнее, чем
в некоторых других языках, не следует пропускать эти главы.
Есть две причины для того, чтобы составлять программы
на языке высокого уровня, таком, как Паскаль, а не на языках
низкого уровня типа языков ассемблера. Во-первых, составлять
программу на языке высокого уровня легче, во-вторых, что еще
важнее, читать и понимать программы, составленные на языке
высокого уровня, также намного легче. Профессиональные про-
граммисты значительную часть своего времени тратят на исправ-
ление и модификацию программ, написанных ими самими или
другими программистами. Если они не до конца понимают эти
программы, при модификации могут быть внесены серьезные
1 К такой характеристике надо подходить осторожно, ибо на Паскале, на-
пример, трудновато написать процедуру умножения матриц.— Прим, ред.
12 Предисловие к первому изданию
ошибки. В связи с этим учиться читать и модифицировать про-
граммы так же необходимо, как и учиться их писать.
Хотя программы, включенные в эту книгу, большей частью
просты и невелики, это не фрагменты, а полные, готовые к работе
программы. Просто ознакомившись с ними, Вы достигнете лишь
поверхностного понимания, для подлинного же успеха необхо-
димо попытаться их улучшить. Многочисленные упражнения
укажут пути улучшения приведенных примеров программ, и,
выполнив эти упражнения, Вы быстро и безболезненно выучите
язык Паскаль. В других упражнениях в основном требуется со-
ставить собственные программы. Эти программы не столь триви-
альны, и на их составление может уйти довольно много времени.
Однако время, ушедшее на составление одной правильной про-
граммы разумного размера, будет затрачено более продуктивно,
чем то же время, ушедшее на составление полудюжины нереали-
стично маленьких программ или программ, которые не будут пра-
вильно работать. Научиться работать в коллективе — задача
более важная, чем приобрести навыки индивидуального про-
граммирования, поэтому некоторые упражнения глав 6—8 и 10
имеют размеры, оправдывающие коллективный подход к их ре-
шению.
Стало уже традицией выделять зарезервированные слова в
программах, написанных на алголоподобных языках. В типогра-
фиях зарезервированные слова набирают полужирным шрифтом
и строчными буквами, а идентификаторы выделяют курсивом С
Хотелось бы поблагодарить всех тех, кто помогал мне и под-
держивал в ходе работы над книгой. Особенно я благодарен за
советы, предложения и исправления Генри Ледгарду и Дереку
Оппену, а также сотрудникам факультета вычислительной тех-
ники Конкордийского университета. Я также благодарен Шарон
Нельсон, которая внимательно прочла и отредактировала все
черновые варианты и Ритве Сеппанен, отпечатавшей окончатель-
ный вариант. Всю ответственность за оставшиеся ошибки я при-
нимаю на себя. Наконец, я выражаю признательность вычисли-
тельной машине Cyber 172 Конкордийского университета за
усилия, затраченные ею, чтобы с помощью компилятора Урса
Аммана проверить примеры программ.
1 При переводе на русский язык идентификаторы были выделены курси-
вом только в тексте, в программах же они набраны прямым шрифтом.— Прим,
ред.
Глава 1.
ПРИНЦИПЫ ПРОГРАММИРОВАНИЯ
1.1. Программы
Программа — это последовательность команд. Программами
являются кулинарный рецепт, музыкальная партитура, узор
для вязания. Программы этого типа существовали задолго до
того, как были изобретены вычислительные машины. Однако
программы для вычислительных машин больше по размерам и
сложнее, чем программы любых других типов, и для их создания
требуется много внимания и аккуратности. Прежде чем начать
более подробное изучение программ для вычислительных машин,
определим некоторые широкоупотребительные термины и рас-
смотрим общие свойства систем программирования.
Для программы требуется автор, который ее составляет, и
процессор, который обработает команды. Обработка команд на-
зывается выполнением программы, а выполняемая программа
называется процессом. Выполнение рецепта — это приготовление
блюд, причем в качестве процессора выступает повар. Музыкаль-
ная партитура — это набор команд для исполнителя, также вы-
ступающего в роли процессора. Все программы имеют некоторые
общие свойства: *
1. Команды выполняются последовательно. Если не оговоре-
но иначе, мы начинаем с первой команды и выполняем их все до
тех пор, пока они не кончатся. Такая общая картина может на-
рушаться в некоторых явно определенных случаях, например
при необходимости повторить какую-то часть музыкального про-
изведения.
2. Процесс имеет результат. Этим результатом может быть
блюдо или звуки музыки. Если программа составлена для вы-
числительной машины, результат часто выражается в последова-
тельности отпечатанных или выведенных на экран дисплея сим-
волов.
3. Программа оперирует объектами. Команда «растереть мус-
катный орех» предполагает, что имеется некоторый орех, который
можно растереть. Объекты, которыми оперирует программа для
вычислительной машины, называются данными.
14
Гл. 1. Принципы программирования
4. Иногда командам предшествует описание объектов, кото-
рыми она оперирует. Например, рецептам часто предшествует
список необходимых ингредиентов. Во многих языках програм-
мирования программист обязан описать свои данные перед тем,
как писать команды.
5. Иногда команды строятся так, что решение нужно прини-
мать процессору. «Если у вас свежие томаты, очистите их и до-
бавьте перед тем, как класть лук, но если томаты консервиро-
ванные, кладите их в последнюю очередь». В этом случае автор
команд не знает, как поступит процессор в данный момент, но
он указывает критерий, которым следует воспользоваться для
принятия решения.
6. Иногда бывает необходимо выполнить команду или группу
команд более чем один раз. Часто это случается при вязании
или вышивании, так как это существенно повторяющиеся про-
цессы. Если выполнение команды нужно повторять, то следует
указать число повторений. Это можно делать либо непосредствен-
но указывая число требуемых повторений («вывязать десять ря-
дов»), либо указывая критерий, который зависит от состояния
процесса («довязать до конца ряда»). Обе формы повторений часто
встречаются в программах для вычислительных машин. Так
как современные машины могут выполнять более миллиона
команд за секунду, программа без повторений будет исполняться
не более чем за долю секунды.
7. Сама программа есть нечто статическое, но процесс выпол-
нения команд динамичен. Мы не путаем повара с рецептом, пи-
аниста с нотами и, что также важно, нельзя путать процессор
с программой.
Вышеуказанное свойственно всем программам, включая про-
граммы, написанные для вычислительных машин. Мы видим,
что программа — это по существу средство связи между автором
программы и процессором. Для такой связи требуется язык, и,
хотя естественные языки, такие, как английский, часто приме-
няются для описания неформальных команд, большинство задач
программирования требует специального языка. Даже рецепты
пишутся на специальном диалекте естественного языка, а музы-
канты, хореографы и текстильщики создали совершенно ориги-
нальные языки, на которых и записывают свои команды.
1.2. Структура программы
Самые первые программы для вычислительных машин пред-
ставляли собой не более чем последовательности простых команд,
1.2. Структура программы
15
которые машина выполняла непосредственно. Со временем про-
граммы стали все больше усложняться, и с ними человеку стало
уже трудно работать. Причина этих трудностей заключалась в
отсутствии структуры. Для машины выполнение последователь-
ности из нескольких тысяч команд не представляет проблемы, по-
тому что машина механически выполняет любые команды, неза-
висимо от их значения и местоположения. Однако для програм-
миста, которого волнует смысловое значение программы, пробле-
ма понимания последовательности из тысяч однообразных команд
становится непреодолимой. История языков программирования
есть в основном история введения некоторых структур в эти при-
митивные последовательности команд.
Впервые структура появилась при вычислении выражений.
Предположим, нам надо написать программу для вычисления
площади, например:
площадь=3.1415926535 X 52
Когда-то в таких случаях программист составлял последователь-
ность примерно таких команд:
считать 3.1415926535
умножить на 5
умножить на 5
записать в площадь
Программист должен был внимательно следить за правильной
интерпретацией выражения. Например, выражению 2x3 + 4
соответствует такая программа:
считать 2
умножить на 3
прибавить 4
а выражению 2 X (3 + 4) — программа:
считать 3
прибавить 4
умножить на 2
Со временем программисты поняли, что перевод символьных
выражений в список машинных команд является механической
операцией, которая может быть выполнена машиной, и вычисли-
тельная машина как раз более всего подходит для этого. Поэтому
выражения стали записывать в традиционной алгебраической
форме.
Следующим элементом, требующим структуризации, были
данные. ЭВМ имеет память размером в несколько тысяч слов, а
слово может содержать число, группу символов или команду.
При этом возникало ^акое неудобство: программист мог помес-
16
Гл. 1. Принципы программирования
тить множество из сотни чисел в слова памяти с 300 по 399, а
затем забыть об этом и поместить туда что-нибудь еще. В языке,
который допускает структуризацию данных, мы можем записать:
числа : массив [1 . . 100] целых чисел;
и знать, что у нас зарезервирована некоторая область памяти,
которая не может быть использована ни для чего другого.
После такого частичного решения проблемы структуризации
данных внимание вновь было обращено на улучшение структуры
самих -команд. Выяснилось, что все программы для вычислитель-
ных машин могут быть выражены в терминах четырех основных
конструкций. Этими конструкциями являются последователь-
ность, принятие решения, повторение, или цикл, и процедура.
Последовательность есть группа команд, исполняемых одна за
другой.
Принятие решения позволяет данным влиять на ход выпол-
нения программы. Во многих языках эта конструкция начинает-
ся со слова если, а команды записываются, например, так:
если х 0
то у : = х
иначе у := —х
Цикл используется для многократного повторения команды
или последовательности команд. При этом, хотя сами команды
не меняются от одного повторения цикла к другому, данные, с ко-
торыми они работают, могут меняться. Например, если повторять
команду
прибавить 1 к х
сто раз, результат выразится в прибавлении 100 к х. Нужно
соблюдать осторожность при определении количества повторений
команд в цикле. Если мы предположим, что первоначально х=0,
а у>0, то после выполнения программы
повторять
прибавить 1 к х
пока не станет справедливым х2>у
х будет иметь значение наименьшего целого числа, квадрат кото-
рого превышает у. Эта программа правильна, поскольку такое
число всегда будет найдено. Программа
повторять
прибавить 1 к х
пока не выполнится х2=у
не всегда приведет к должному результату. Если, например,
у=5, то условие х2=у никогда не будет выполнено. Теоретически
1.3. Неформальное введение в Паскаль
17
в этом случае цикл будет продолжаться до бесконечности. На
реальных вычислительных машинах программа будет выпол-
няться до тех пор, пока значение х2 не выйдет за пределы предста-
вимых в машине чисел, а затем остановится.
Процедура дает возможность заменить группу команд одной
командой. Процедуры часто используются в кулинарных книгах.
Хорошая кулинарная книга содержит процедуру приготовления,
например, крема и ссылается на эту процедуру в каждом рецепте,
требующем крема. Использование процедур в программировании
для вычислительных машин не только уменьшает размер програм-
мы и облегчает ее чтение, но также, что еще важнее, придает про-
граммам иерархическую структуру. Понятие процедуры на-
столько важно, что без него ни одна сколько-нибудь полезная
программа не могла бы быть создана.
Язык программирования Паскаль использует все эти методы
структурирования. Представлены они в простой и элегантной
форме, что делает Паскаль языком не только мощным, но и про-
стым в изучении и практическом применении.
1.3. Неформальное введение в Паскаль
В этом разделе нам предстоит изучить несколько очень прос-
тых программ, написанных на языке Паскаль, для того чтобы
понять, каким образом описанные в предыдущем разделе кон-
струкции реально используются в языке. Все эти программы «ра-
ботают», т. е. могут быть выполнены на вычислительной машине.
program квадратныйкореньиздвух (output);
begin
write(sqrt(2))
end.
Если выполнить эту программу на машине, то она напечатает
1.4142135624
что приблизительно равно значению квадратного корня из чис-
ла 2.
Третья строчка
write(sqrt(2))
составляет ядро программы квадратныйкореньиздвух. Write это
процедура, результатом которой является печать значения
sqrt(2), называемого аргументом. Sqrt — стандартная функция
языка Паскаль. Значением sqrt(2) будет \ 2 с точностью, харак-
терной для конкретной вычислительной машины
18
Гл. 1. Принципы программирования
Первая строчка программы содержит слово program. Оно
должно быть первым во всякой программе на Паскале. Следом
стоит квадратныйкореньиздвух, это имя программы. Имя про-
граммы выбирается программистом. Обычно стараются выбирать
имя, отражающее основную функцию, выполняемую программой.
После указания имени программы мы определяем связи между
нею и ее окружением. Слово output говорит о том, что программа
должна выдать некоторые результаты. Окружением программы,
работающей на современной вычислительной машине, обычно
является операционная система, и функцией операционной сис-
темы в нашем примере будет прием результатов, выдаваемых
программой, и передача их на печатающее устройство или на
экран дисплея.
Наконец, обратим внимание на слова begin (начать) и end
(закончить), эти слова, естественно, отмечают начало и конец
программы.
Программа квадратныйкореньиздвух не особенно представи-
тельна как пример программы для вычислительной машины. Мы
могли бы посмотреть значение | 2 в таблице, если бы оно нам
действительно понадобилось. Следующая программа представ-
ляет собой улучшенную версию программы квадратныйкоренъ-
издвух:
program квадратныйкорень (input,output);
var
х : real;
begin
read(x);
write(sqrt(x))
end.
Этот пример иллюстрирует последовательную организацию
программы. Программа содержит две команды read(x) и write-
(sqrt(x)). Они будут исполняться одна за другой в том порядке,
в котором мы их записали. Слова begin и end играют роль скобок,
обрамляющих последовательность команд. Программа содержит
только одну такую последовательность и, следовательно, только
одну пару скобок begin — end. Более сложные программы содер-
жат много разных последовательностей команд, каждая из кото-
рых заключена в такие скобки.
Программа квадратныйкорень получает данные из своего ок-
ружения. Первая строчка содержит слово input, указывающее
на то, что программе потребуются данные. Процедура read вы-
полняет подобный запрос. Результат команды read, т. е. чтения,
выражается в получении значения из внешнего источника и за-
дании этого значения переменной х. Подлинная природа внешне-
го источника неизвестна программе. Можно считать, что это зна-
1.3. Неформальное введение в Паскаль
19
чение было отперфорировано на картах или набрано на клавиа-
туре терминального устройства.
Третьим новым понятием, представленным программой квад-
ратныйкорень, является понятие данных. Объект, который мы
называли именем х, есть вещественная переменная, что указывает-
ся в описании
var
х : real;
Это описание придает переменной х вещественный тип.
Программа квадратныйкоренъ все же остается недостаточно
представительной как пример программы для вычислительной
машины. Ведь sqrt{x) не может быть вычислено в том случае,
если х отрицательно, а запретить пользователю ввести отрица-
тельное число мы не можем. Кроме того, вычислив один квадрат-
ный корень, программа останавливается, и, чтобы вычислить
другой, ее нужно запустить снова. Эти дефекты отсутствуют в
программе квадратныекорни.
program квадратныекорни (input,output);
var
х : real;
begin
repeat
read (x);
if x^O
then write (sqrt (x))
else write ('ошибка в параметре')
until x=0
end.
Эта программа является иллюстрацией использования кон-
струкции принятия решения и цикла. Прочитав значение х,
процессор должен выбрать одно из двух действий. Если значение
х неотрицательно, печатается значение }^х. Если х меньше нуля,
то должно быть отпечатано сообщение
ошибка в параметре
Кроме того, оператор цикла с пост-условием, записываемый
с помощью пары repeat — until, гарантирует, что выполнение
программы будет продолжаться до тех пор, пока не будет счита-
но нулевое значение.
Хотя процессор выполняет команды последовательно, поря-
док выполнения здесь не совпадает с тем, в котором команды за-
писаны. После печати значения квадратного корня процессор
проверяет значение х. Если х не нуль, то снова выполняется
20 Гл. 1. Принципы программирования
команда чтения. Процесс закончится только тогда, когда выпол-
нится условие х=0.
Мы должны отличать статическую программу и динамический
процесс ее выполнения. В обычной речи мы часто затемняем это
различие, говоря: «Программа делает то-то и то-то». Надо пом-
нить, что это лишь сокращение от фразы «Процессор, выполняя
программу, делает то-то и то-то». Программа — это текст, а
процессор — это машина, выполняющая команды, которые со-
держатся в тексте.
Программа квадратныекорни вполне приемлема, хотя диапа-
зон ее возможностей невелик. Условие ее окончания (число со
значением нуль) не особенно удобное, и дальше в книге мы еще
познакомимся с более красивыми способами организации оконча-
ния программ.
1.4. Компиляция и выполнение
По мере усложнения структуры языков программирования
создавать и анализировать программы становилось проще и
удобнее. В то же время задача перевода программ на язык про-
стых команд, выполняемых машиной, становилась все труднее.
Поэтому потребовалась специальная программа, которая пере-
водила бы программы с языка, на котором они написаны, на язык
машинных команд. Такая программа называется компилятором
с языка. Программа, используемая для перевода программ с язы-
ка Паскаль на язык машинных команд, называется компилятором
с языка Паскаль, и мы часто будем говорить об этой программе
в нашей книге.
Если мы всегда пишем правильные программы и имеем воз-
можность работать с хорошим компилятором, то для практиче-
ских целей можно условно считать компилятор и вычислитель-
ную машину единой машиной, которая может непосредственно
выполнять программы. Тем самым, мы можем совершенно не
задумываться о процессе компиляции. Комбинация компилятора
и вычислительной машины иногда называется виртуальной ма-
шиной. Для языка Паскаль созданы хорошие компиляторы,
и мы можем рассматривать комбинацию как Паскаль-машину.
Паскаль-машина изображена на рис. 1.1. На этой и следую-
щей диаграммах окружности соответствуют статическим объек-
там, таким, как программы, а прямоугольники обозначают про-
цессоры. Линии обозначают передачу данных к процессору или
от него, а направление передачи указывается стрелкой. Рис. 1.1
показывает, что для Паскаль-машины требуется исходная про-
грамма, под которой подразумевается всякая программа на язы-
ке Паскаль, и некоторые входные данные. При работе Паскаль-
1 4. Компиляция и выполнение
21
машины создаются выходные данные. Если мы предположим, что
исходной программой является программа квадратныекорни,
а входные данные представляют собой числа
2 3 4 0
то Паскаль-машина после непродолжительной работы напечатает
в качестве результата:
1.41421356
1.73205088
2.00000000
0
Итак, виртуальная машина — это вычислительная машина
и компилятор. Ее структура показана на рис. 1.2. Процессором
является сама вычислительная машина, причем она используется
Рис. 1.1. Паскаль-машина
дважды. На первом этапе программа — это компилятор с языка
Паскаль, а входные данные — исходная программа. Результатом
будет перевод исходной программы в машинные команды, т. е.
в рабочую программу. Вычислительная машина будет затем ис-
пользована еще раз при выполнении рабочей программы, вход-
ными данными для которой послужат вводимые данные. Резуль-
татом будут ожидаемые выходные данные.
Нам не нужно было бы вникать в подробности структуры Пас-
каль-машины, если бы не возможность возникновения ошибок.
Существуют три типа ошибок в программах:
1. Ошибки, которые можно обнаружить при компиляции. На-
пример, пропуск слова end, соответствующего слову begin.
22
Гл. 1 Принципы программирования
Такие ошибки называются ошибками времени компиляции.
2. Ошибки, обнаруживаемые при выполнении рабочей програм-
мы. Если выполнялась программа квадратныйкорень и было
прочитано значение —1, то программа не будет работать, так
Рис. 1.2. Строение Паскаль-машины
как функция sqrt не может быть вычислена для отрицательного
аргумента. Такой тип ошибок называется ошибками времени
выполнения.
3. Ошибки, которые не обнаруживаются вычислительной маши-
ной ни при компиляции, ни при выполнении программы. '
Например, если вы напишите в программе квадратныекорни
sqr (х) вместо sqrt (х)
1.4. Компиляция и выполнение
23
программа будет успешно выполняться, но будет печатать
значение х2, а не Ух п.
На рис. 1.3 представлена схема обнаружения ошибок в Пас-
каль-машине. Заметим, что мы теперь имеем выходные данные
трех различных типов. Первыми появляются ошибки, обнаружен-
ные во время компиляции. Если эти ошибки достаточно серьез-
ны, выполнение рабочей программы может быть запрещено.
Рис. 1.3. Обнаружение ошибок в Паскаль-машине
Если программа была успешно откомпилирована, она может
сбиться при выполнении, при этом мы получим различные диаг-
ностические сообщения. Если программа успешно откомпилиро-
Функция sqr(t) возводит аргумент в квадрат.— Прим. ред.
24
Гл. 1. Принципы программирования
вана и выполнена, мы получим ожидаемые нами выходные дан-
ные при условии отсутствия в программе ошибок третьего типа.
Однако ошибки могут быть не только в программе, они могут
появляться и в данных. Если вы пользуетесь программой квад-
ратныекорни для вычисления У 5, но по невнимательности ввели
число 6, то ответ будет неверным. Такая ошибка — это пример
ошибки во входных данных, и у нас нет методов, при помощи ко-
торых программа могла бы обнаружить ошибки этого типа.
Разработчик языка программирования может до некоторой
степени выбирать время обнаружения ошибки: при компиляции
или при выполнении программы. Вообще говоря, по мере роста
избыточности в языках программирования растет и потенциаль-
ная возможность обнаружения ошибок при просмотре текста
программы. Язык Паскаль задуман так, чтобы при компиляции
обнаруживалось максимально возможное число ошибок и, следо-
вательно, в этом языке достаточное количество избыточных эле-
ментов.
Компилятор может оказать помощь и путем введения в рабо-
чую программу проверок, которые будут проведены во время
выполнения. Когда в исходной программе вы пишете
write (sqrt (х))
то команды, реально формируемые компилятором, будут равно-
сильны следующим:
if х^О
then write (sqrt (х))
else halt
где halt — это процедура, которая заканчивает выполнение
программы и выдает соответствующее сообщение об ошибке. (За-
метим, что проверки времени выполнения, вставляемые компиля-
тором, ни в коей мере не запрещают нам вставлять свои собствен-
ные проверки, как мы это сделали в программе квадратныекорни.)
Подобные проверки неизбежно замедляют работу как компи-
лятора, так и рабочей программы. Однако в конечном итоге время
работы машины будет сэкономлено, потому что правильные про-
граммы быстрее получаются тогда, когда у нас есть развитая сис-
тема диагностики ошибок.
В дальнейшем, говоря «этот оператор не верен» или «этот
оператор неприемлем», мы будем иметь в виду, что оператор вы-
зовет выдачу сообщения об ошибке при компиляции. Если же мы
говорим «этот оператор приведет к ошибке во время выполнения»,
то подразумеваем, что он правилен «с точки зрения компилятора»,
но выполнен правильно не будет.
1.5. Представление программы и примеры
25
1.5. Представление программы и примеры
Для компилятора программа представляет собой не более чем
последовательность символов. Компилятор обработает програм-
му квадратныекорни даже в том случае, если она будет иметь
вид
program квадратныекорни (input,output); var х : real;
begin repeat read(x); if x^O then write (sqrt(x)) else
write ('ошибка в параметре') until x O end.
Однако для удобства чтения мы пользуемся более разверну-
той записью. Запись текста программы должна отвечать ее струк-
туре. Программа на языке Паскаль структурирована по уровням,
и уровень оператора указывается величиной отступа от левого
края при записи оператора. Программа квадратныекорни имеет
четыре уровня. Самый внешний уровень отмечается словами begin
и end, следующий — словом repeat и соответствующим ему until.
Обратите внимание на то, что такой подход помогает нам сразу
увидеть, какие операторы входят в цикл и насколько труднее
определить то же самое по записи, приведенной выше. Условный
оператор (принятие решения) имеет два уровня, причем к вну-
треннему уровню относятся два альтернативных оператора.
Имеется и другая возможность, с помощью которой можно
облегчить чтение программы. В программе на языке Паскаль есть
два типа слов: зарезервированные слова, такие, как begin, end,
repeat и until, имеющие фиксированные значения, и идентифика-
торы, например х, sqrt, read и write, значение которых может быть
переопределено. В этой книге зарезервированные слова записы-
ваются полужирным шрифтом, а идентификаторы строчным
шрифтом. Это сделано только для удобства чтения, так как ком-
пилятор не отличает этих шрифтов 1 . В журналах зарезервиро-
ванные слова иногда печатаются прописным курсивным шриф-
том.
Текст «ошибка в параметре» в программе квадратныекорни
называется строкой. Строки печатаются на выходе точно в той
же форме, в какой они появляются в программе. Оператор
write ('Ошибка в параметре')
вызовет печать ^екста
Ошибка в параметре
Мы закончим эту главу несколькими примерами программ.
Пока мы должны удовольствоваться лишь внимательным чтением
Компилятор «знает» все зарезервированные слова и поэтому легко отли-
чает их от идентификаторов.— Прим. ред>
26
Гл. 1 Принципы программирования
программ, пытаясь разобраться в них, насколько это вам удастся,
на основе краткого обзора. После прочтения гл. 2 и 3 вы сумеете
написать такие же или похожие программы самостоятельно.
Первая программа, таблицастепеней, печатает таблицу сте-
пеней целых чисел. Строчка, начинающаяся со слова for, задает
цикл, который исполняется один раз для каждого целого значе-
ния в диапазоне от 1 до размертаблицы. Выполнение оператора
присваивания заключается в вычислении значения выраже-
ния в правой части оператора и присваивании этого значения
переменной, имя которой стоит слева. После выполнения
квадрат := sqr (основание)
будем иметь
квадрат = основание2
Таблица дает значения величин основание, основание2, основа-
ние*, основание*, основание'\ основание'2, основание'*, основа-
ние'*. Слова и фразы, заключенные в фигурные скобки
‘являются примечанием (или комментарием), служащим для удоб-
ства чтения; все эти слова игнорируются компилятором. Проце-
дура writein похожа на процедуру write, но после печати значе-
ний параметров производится перевод строчки на печатающем
устройстве. Знак V означает умножение: на большинстве вы-
числительных машин нет традиционного символа 'х', а неявное
умножение ab, в котором подразумевается axb, запрещено в
языках программирования.
program таблицастепеней (input,output);
var
размертаблицы,основание, квадрат, куб,
четвертаястепень : integer;
begin
read (размертаблицы);
for основание := 1 to размертаблицы do
begin
квадрат :== sqr (основание);
куб := основание * квадрат;
четвертаястепень := sqr (квадрат);
writein (основание,квадрат,куб,
четвертаястепень,
1/основание, 1/квадрат, 1 /куб,
1/четвертаястепень)
end {for}
end. {таблицастепеней}
Ввод:
5
1.5. Представление программы и примеры
27
Вывод:
1 1 1 1 1.000000 1.000000 1.000000 1.000000
2 4 8 16 0.500000 0.250000 0.125000 0.062500
3 9 27 81 0.333333 0.111111 0.037037 0.012345
4 16 64 256 0.250000 0.062500 0.015625 0.003906
5 25 125 625 0.200000 0.040000 0.008000 0.001600
Программа делители читает числа и вычисляет их делители.
Работа заканчивается, если прочитано неположительное число.
Значение выражения
число mod делитель
представляет собой остаток от деления числа на делитель. Значе-
ние делителя печатается всякий раз, когда остаток равен нулю,
поэтому программа печатает все делители числа, а не только про-
стые делители.
program делители (input,output);
var
число, делитель : integer;
begin
repeat
read (число);
if число > О
then
begin
writein ('делителями числа',
число, 'являются:');
for делитель : = 2 to число do
if число mod делитель О
then writein (делитель)
end
until число 0
end. {делители}
Ввод:
20 17 0
Вывод:
делителями числа 20 являются:
2
4
5
10
20
делителями числа 17 являются:
17
28
Гл. 1. Принципы программирования
Программа минимакс читает ряд чисел, подсчитывает их ко-
личество и выдает минимальное и максимальное из них. Maxint —
это стандартная константа, равная максимальному целому числу,
которое может быть представлено в машине. Переменная чтение
может иметь только два значения: true (истина) и false (ложь).
Операторы, стоящие между словами begin и end следом за словом
while, повторяются, пока переменная чтение имеет значение
true (истина). Вы должны разобраться в сложных условных
операторах этой программы на основе предположений о том, что
делает эта программа.
program минимакс (input,output);
var
чтение : boolean;
число, минимум, максимум, счетчик
: integer;
begin
чтение := true;
минимум:=maxint;
максимум := —maxint;
счетчик := 0;
while чтение do
begin
read (число);
if число = 0
then чтение : = false
else
begin
счетчик := счетчик + 1;
if число < минимум
then минимум : = число;
if число > максимум
then максимум :== число
end
end; {while}
writein (счетчик, 'чисел прочитано');
writein ('наименьшее было', минимум);
writein ('наибольшее было', максимум)
end. {минимакс}
Ввод:
—6 3 —15 27 —2 64 1 0
Вывод:
7 чисел прочитано
наименьшее было —15
наибольшее было 64
1.5. Представление программы и примеры
29
Следующие две программы используют переменные символь-
ного п типа (char). Значением таких переменных являются сим-
волы. Обе программы читают данные из символьного файла. Бу-
левская (логическая) функция eof, которая может принимать зна-
чения true или false, имеет значение false, пока не будет достигнут
конец файла, при этом значение переменной станет равным true,
и, следовательно, обе программы читают файл от начала до конца.
Программа сдвоенныесимволы обнаруживает и печатает сдвоенные
символы, как, например, «нн» в слове «ванна». Программа под-
счетсимволов подсчитывает все символы, содержащиеся во вход-
ном файле, и отдельно пробелы, запятые и точки.
program сдвоенныесимволы (input,output);
const
пробел = '
var
старыйсимвол, новыйсимвол : char;
begin
старыйсимвол := пробел;
while not eof do
begin
read (новыйсимвол);
if (новыйсимвол =/= пробел) and
(старыйсимвол = новыйсимвол)
then writein (старыйсимвол,
новыйсимвол);
старыйсимвол : = новыйсимвол
end {while}
end. {сдвоенныесимволы}
Ввод:
Класс, касса, веер, странник,
Кто идет по аллее?
Вывод:
сс
сс
ее
нн
лл
ее
Символ — character — означает букву, цифру или какой-то другой типо-
графский знак. Иногда это слово переводится как литера, в таком случае гово-
рят о данных литерного типа.— Прим ред.
30
Гл. 1. Принципы программирования
program подсчетсимволов (input,output);
const
пробел = ' ';
запятая ',';
точка =
var
числосимволов, числопробелов, числозапятых,
числоточек : integer;
символ : char;
begin
числосимволов 0;
числопробелов : — 0;
числозапятых 0;
числоточек : = 0;
while not eof do
begin
read (символ);
числосимволов числосимволов + 1;
if символ - пробел
then числопробелов : = числопробелов + 1;
else if символ = запятая
then числозапятых : = числозапятых + 1;
else if символ = точка
then числоточек числоточек + 1;
end; {while}
writein (числосимволов, 'символов');
writein (числопробелов, 'пробелов');
writein (числозапятых, 'запятых');
writein (числоточек, 'точек')
end. {подсчетсимволов}
Ввод:
Класс, касса, веер, странник,
Кто идет по аллее?
Вывод:
49 символов
8 пробелов
4 запятых
0 точек
Глава 2.
ДАННЫЕ, ВЫРАЖЕНИЯ
И ПРИСВАИВАНИЯ
Эта глава делится на две части. Первая, большая по размеру
часть включает разд. 2.1 —2.7, она посвящена данным. Вторая
часть, т. е. разд. 2.8, описывает структуру программы. Это
большая и подробная глава, поскольку в ней описаны свойства
всех стандартных типов Паскаля.
Для понимания нескольких следующих глав книги нет необ-
ходимости вникать во все детали при первом чтении, поэтому пер-
воначально главу можно прочитать достаточно бегло. Однако
позднее она может оказаться полезным справочным пособием, так
как содержит полное описание.
2.1. Идентификаторы
Программы строятся из элементов двух типов. Элементы одно-
го типа присущи самому языку. В языке Паскаль элементы этого
типа могут состоять из одного или из пары символов, например
или быть зарезервированным словом, например
begin end if then repeat until
В этой книге зарезервированные слова языка всегда печата-
ются полужирным шрифтом.
Элементы другого типа, используемые в программах, это
идентификаторы. Среди них есть стандартные, например
integer real write sqrt
другие идентификаторы выбираем мы сами.
Идентификаторы состоят из букв и цифр, но первым симво-
лом идентификатора может быть только буква. В этой книге все
идентификаторы печатаются строчными буквами (в программах —
прямыми, в тексте — курсивными). Следующие идентификаторы
составлены без ошибок.
ДЖОН
генрихв
маркерконцавходныхданных
32
Гл. 2. Данные, выражения и присваивания
А вот такие идентификаторы, напротив, ошибочны:
первый раз
следующее.слово
16май77
поскольку первые два содержат символ, не являющийся ни бук-
вой, ни цифрой (пробел в первом случае, точку во втором), а тре-
тий начинается с цифры.
Правила построения идентификаторов удобно описывать с
помощью синтаксических диаграмм. Рис. 2.1 представляет син-
таксическую диаграмму для идентификаторов. Прямоугольники
на синтаксических диаграммах содержат ссылки на другие син-
таксические диаграммы. Например, рис. 2.1 ссылается на син-
таксические диаграммы для букв (рис. 2.2) и цифр (рис. 2.3).
Кружок или овал содержат элемент, который должен встречаться
в тексте непосредственно. Следуя схеме на рис. 2.1, мы можем по-
строить т) такие элементы, как
генрих8 но не $3.00
Синтаксическими диаграммами можно также пользоваться,
когда нужно определить, принадлежит или не принадлежит дан-
ный элемент некоторому синтаксическому классу. Схема на
рис. 2.1, например, дает возможность определить, принадлежит
данный элемент к классу идентификаторов или нет.
К сожалению, несмотря на краткость и точность описания
синтаксиса идентификаторов, рис. 2.1 ничего не говорит нам о
длине идентификаторов. Реально идентификатор не может rfepe-
ходить с одной строки программы на другую, и это ограничивает
длину идентификатора до 80 символов или около этого. В пред-
лагаемом стандарте языка Паскаль говорится, что значение про-
граммы не должно меняться при укорачивании идентификаторов
до восьми символов. В результате этого идентификаторы
длинаглавнойбалки и длинаглавнойстойки
рассматриваются компилятором с предлагаемого стандарта язы-
ка Паскаль как одинаковые. Один из возможных выходов из
этого положения заключается в том, чтобы выносить различаю-
щиеся зоны идентификаторов (балки, стойки) вперед, оставляя
позади сходные зоны (длина)
балкиглавнойдлина и стойкиглавнойдлина
Синтаксические диаграммы автором языка Паскаль Н. Виртом были
введены как средство, облегчающее анализ программы, а не их синтез. Для по-
строения (порождения) отдельных компонент, если таковое действительно не-
обходимо, используется запись в форме Бэкуса — Наура (БНФ). Синтаксис
языка Паскаль также определен в БНФ.— Прим. ред.
2 1. Идентификаторы
33
Рис. 2.2. Синтаксис буквы
2 № 3388
31
Гл 2. Данные, выражения и присваивания
Помните, что использование только строчных букв не более
чем традиция; эти же идентификаторы можно записать как
БалкиГ лавной Длина и Стойки Г лавнойДлина
не изменив их значения, при условии если ваша машина допуска-
ет работу с прописными буквами.
Первое появление идентификатора в программе всегда про-
исходит в описании, в котором идентификатор определяется.
Идентификатор затем используется до тех пор, пока где-нибудь в
программе он не станет неопределенным. Идентификатор, остаю-
щийся определенным до конца программы, называется глобаль-
ным идентификатором. Если идентификатор становится неопре-
деленным раньше, чем кончается программа, он называется ло-
кальным идентификатором. Пока, до гл. 4, с локальными иден-
тификаторами мы не будем иметь дело. Поэтому в примерах каж-
дый идентификатор будет определен от точки, где он описан, до
конца программы.
Выбор идентификаторов является важным аспектом хорошего
программирования. Правильно выбранные идентификаторы не
только облегчают чтение и понимание программы, но также умень-
шают число мест, в которых могут возникнуть ошибки, связан-
ные с невнимательностью при написании, вводе и исправлении
программы. Намного легче модифицировать программу, напи-
санную другим программистом, если идентификаторы были вы-
браны удачно. Длинный идентификатор необязательно будет
лучше короткого.
Если идентификатор встречается только несколько раз па ко-
ротком отрезке текста программы, можно воспользоваться одно-
буквенными именами, но такой выбор весьма нежелателен, если
идентификатор часто встречается в различных местах большой
программы.
Имеется несколько основных правил, которых следует при-
держиваться, выбирая идентификаторы, безотносительно к зна-
чению или контексту идентификатора. Избегайте тех букв и
цифр, которые можно спутать. Некоторые печатающие устрой-
ства не делают различия между буквой 'О' и цифрой 'О', а иногда
плохо видна черта у буквы 'Q'. Нет ничего плохого в слове 'коо-
ператив', так как ясно, что здесь стоят б^квы 'о', но 'до20' —
это неудачный идентификатор.
2.2. Константы
При написании программ нам часто бывают нужны значения,
которые известны до того, как программа начинает выполнять-
ся. Мы можем, например, знать, что страница содержит 60 стро-
2.2. Константы
35
чек, некоторые вычисления нужно повторить ровно 100 раз, зна-
чение числа л равно 3.1415926535, или что файл кончается симво-
лом Разрешается, хотя это не очень хорошо, писать эти
значения в программе именно в таком виде, в каком они записаны
здесь, что и сделано в следующем фрагменте программы:
while строка < 60 do
writein;
for счетчик 1 to 100 do
вычисление;
длинаокружности := 3.1415926535 * диаметр;
repeat
read (сим)
until сим '8';
Значение, записанное в программе в таком виде, называется
константой. В приведенном выше примере такими константами
являются 60, 100, 3.1415926535 и 'S'. С константой может быть
связано некоторое имя. В языке Паскаль имена даются констан-
там в разделе определения констант. Дадим пример раздела опре-
деления констант:
const
размерстраницы 60;
пределцикла 100;
числопи = 3.1415926535;
граничныйсимвол
С помощью таких констант предыдущий фрагмент можно пере-
писать:
while строка размерстраницы do
writein;
for счетчик :1 to пределцикла do
вычисление;
длинаокружности := числопи * диаметр;
repeat
read (сим)
until сим = граничныйсимвол;
Точный смысл первой строчки приведенного выше раздела
определения констант таков:
Определим объект, имеющий два атрибута — имя и значение.
Имя объекта есть идентификатор размерстраницы, а значение
объекта есть целое число 60.
Впредь мы не будем столь педантично подходить к определе-
нию простейших принципов. Этот пример мы привели для ил-
люстрации различия между именем и значением объекта. Для
краткости мы можем делать такие замечания, как 'размерстрани-
2*
36
Гл. 2. Данные, выражения и присваивания
цы есть 60', и во многих случаях эти замечания будут достаточно
точными. Позднее мы встретимся с объектами, которые могут
иметь имя и несколько значений, а также с объектами, которые
вовсе не имеют имени, и в этих случаях педантизм будет вполне
оправдан.
Есть несколько веских причин, чтобы пользоваться поимено-
ванными константами вместо их значений, и хорошие программы
редко содержат значения констант вне раздела определения кон-
стант. Предположим, идентификатор размерстраницы представ-
ляет размер страниц, на которых программа печатает результаты
своей работы, и предположим, что эта программа разрабатыва-
лась в условиях работы с устройством, страницы которого со-
держали по 60 строк, а затем программу надо было модифициро-
вать для работы с меньшими страницами, на которых не уме-
щается больше 40 строк. Единственное изменение, которое нам
надо сделать в программе, это внести исправления в раздел
определения констант, где мы меняем
размерстраницы = 60; на размерстраницы = 40;
Если по всей программе использовалась сама константа, а
не ее имя, то внести нужные исправления значительно труднее,
причем мы можем ошибиться дважды: число 60, которое нужно за-
менить, может остаться незамеченным и, что еще страшнее, чис-
ло 60, которое не имеет отношения к размеру страниц, может
быть по невнимательности заменено на число 40. Кроме того,
идентификатор размерстраницы содержит больше информации
для читателя, чем значение 60 (или 40).
Синтаксис раздела определения констант программы на язы-
ке Паскаль показан на рис. 2.4, а синтаксис самих констант
приведен на рис. 2.5, 2.6 и 2.7 1}. Идентификатор константы
есть идентификатор, которому приписано значение константы.
Например, можно писать
const
наибольшее = 1000;
наименьшее = —наибольшее;
После первого определения слово наибольшее становится
идентификатором константы и потому может быть использовано
во втором описании для определения константы наименьшее.
Раздел определения констант может быть использован для того,
чтобы дать имена символам и строкам символов, как, например:
const
пробел = '
Ни в описании языка Паскаль, ни в описании предлагаемого стандарта
таких конструкций нет.— Прим. ред.
2.2. Константы
37
Рис. 2.5. Синтаксис константы
"Целое
{ цифра].
Рис. 2.6. Синтаксис целого числа
38
Гл. 2. Данные, выражения и присваивания
дата = 'пятница, ноябрь 27';
ошибка = 'у семи нянек';
Синтаксическая диаграмма для вещественных чисел, приве-
денная на рис. 2.7, позволяет писать
1 1.5 1Е5 1.5Е—5
но запись
.5 ЕЗ 1.Е—2 З.ОЕ
будет неправильной. Заметим, в частности, что константы для
целых и вещественных чисел всегда начинаются с цифры.
Рис. 2.7. Синтаксис вещественного числа
2.3. Данные
Данные в программе играют роль ингредиентов в рецептах.
Каждый элемент данных в программе является либо константой,
либо переменной, отличие заключается в том, что значение пере-
менной может меняться во время выполнения программы.
С каждой переменной программы связан некоторый тип, кото-
рый определяет как возможные значения переменной, так и опе-
рации, которые могут с ней выполняться.
В языке Паскаль имеются четыре стандартных типа целый
(integer), вещественный (real), булевский (boolean) и символьный
(char).
Значения
— 100 0 9999
являются значениями целого типа. Значения
—99.9 0.1 9999.0
являются значениями вещественного типа. Значения
false true :
являются значениями булевского (или логического) типа, причем
у переменных этого типа других значений быть не может. Нако!
нец, |
'Г 'а' '=' I
I
i
2.3. Данные
39
являются значениями символьного типа.
Константы также относятся к какому-нибудь типу, но так
как этот тип может быть определен компилятором, нам нет необ-
ходимости описывать его. Однако при описании переменной ее
тип должен быть указан. Синтаксис описания переменных приве-
ден на рис. 2.8 и 2.9, а ниже приведен пример использования
раздела описания переменных:
var
счетчик, индекс, номерсимвола : integer;
первоезпачепие, последнеезначение, среднеезначение :
real;
конеиданных : boolean;
символ, последнийсимвол : char;
Из констант и переменных можно строить выражения по пра-
вилам, очень похожим на обычные алгебраические.
Рис. 2.8. Синтаксис типа
После описания
var
начало, шаг, количество : integer;
40
Гл. 2. Данные, выражения и присваивания
мы можем записать, например, такие выражения;
начало + количество * шаг
Здесь символы '+' и '*' обозначают соответственно операции
сложения и умножения. Выражение имеет тип и значение, при-
чем для языка Паскаль характерно то, что тип выражения всегда
может быть определен из текста программы. Приведенное выше
выражение имеет операнды целого типа и такие же операции,
поэтому его тип тоже целый, независимо от конкретных значений
переменных начало, количество и шаг. Цель разд. 2.4—2.7 —
описать допустимые выражения, а также показать, как опреде-
ляется тип выражения в случае каждого стандартного типа.
Значение переменной изменяется с помощью оператора при-
сваивания. Оператор присваивания имеет такой общий вид
переменная : = выражение
Знак ': = ' читается 'присвоить'. После описания
var
начало, шаг, количество, конец : integer;
мы можем писать
начало := 1;
шаг : = 2;
количество := 100;
конец := начало + количество * шаг;
После выполнения всех этих операторов значение переменной
конец будет равно 201.
Необходимо отметить асимметрию оператора присваивания.
С правой стороны располагается выражение, которое дает значе-
ние, а с левой стороны указывается имя переменной, с которой
это значение сопоставляется (присваивается).
Все типы, описываемые в этой главе, являются упорядочен-
ными типами. Это значит, что, если бим и бом — переменные од-
ного типа, то выражение
бим < бом
(читается 'бим меньше бом') определено и имеет одно из двух
значений: true или false. Для целого типа, например, выражения
3<4 —1<1
имеют значения true, а выражения
4<3 3<3
имеют значения false. Так как эти значения характерны для пере-
менных булевского типа, то выражения наподобие бим < бом
2.4. Целый тип
41
называются выражениями булевского типа или булевскими выра-
жениями. Более подробно такие выражения обсуждаются в
разд. 2.6.
С каждым стандартным типом связаны некоторые стандарт-
ные функции. Мы уже сталкивались со стандартной функцией
sqrt, но можно напомнить, что значение sqrt(x) при условии х О
есть Vх. Некоторые функции имеют тот же тип, что их аргумен-
ты. Другие позволяют преобразовывать один тип к другому.
Например, функция round с вещественным аргументом выдает в
качестве результата целое значение. Множество значений, для
которых функция определена, называется областью определения,
а множество значений, которые может принимать функция, на-
зывается областью или диапазоном значений.
2.4. Целый тип
Термин целый употребляется в обычном смысле. Целые числа
могут быть положительными и отрицательными. Число 0 есть
целое так же, как 963 и 25. Числа 1.5 и 2.718281828 не являются
целыми.
В ЭВМ может быть представлено только конечное подмноже-
ство целых чисел. Для каждой конкретной машины имеется целое
число maxint такое, что целое число N может быть представлено
в машине 1}, лишь если
—maxint <1 N <1 maxint
Попытка вычислить выражение, значение которого выходит
за пределы этого диапазона, приведет к возникновению ошибки
при выполнении. Целые переменные обычно используются для
организации счетчиков, индексации и т. д., а для этих целей ог-
раниченный диапазон их значений вполне достаточен. Нужно со-
блюдать осторожность, чтобы при вычислениях комбинаторных
коэффициентов или разложения в ряд значения целочисленных
переменных не превысили предельного значения.
Знаки операций
+ — * div mod
связаны с целым типом. Они обозначают всем известные операции
сложения, вычитания, умножения, деления и взятия остатка от
деления. Эти операции являются инфиксными (двуместными):
это значит, что они записываются между операндами по тради-
Это не всегда верно Существуют системы представления чисел с maxint
и minint, когда minint^— maxint Если используется такая система, то ошиб-
ка может возникнуть даже при вычислении выражения типа — а.— Прим,
ред.
42
Гл. 2 Данные, выражения и присваивания
ционным алгебраическим правилам. Знак минус '—' может быть
знаком унарной (одноместной) операции, например —50.
Целочисленные переменные и константы могут в совокупно-
сти с этими знаками объединяться в выражения, результатом вы-
числения которых будхт целые числа. После описаний
const
размерстроки 100;
var
номер, счегчик, строка : integer;
можно писать такие целочисленные выражения:
номер — счетчик
номер + строка div размерстроки
номер — 100
счетчик mod размерстроки + 1
Приведем несколько примеров, которые покажут, каким об-
разом вычисляются значения этих выражений. В примерах ис-
пользуются константы, но, разумеется, те же результаты полу-
чаются и с целочисленными поименованными константами и пере-
менными, имеющими те же значения. Операции умножения, де-
ления и взятия остатка от деления выполняются перед операция-
ми сложения и вычитания, так что
2 + 3# 4=14
Как и в обычной алгебраической нотации порядком вычисле-
ний можно управлять с помощью скобок:
(2 + 3) *4--20
В результате деления всегда получается целое число, остаток
игнорируется
5 div 2-2
3 div 4-0
Многих программистов постигла нехдача потому, что они упу-
стили из виду тот факт, что результат целочисленного деления
равен нулю, когда делитель больше делимого. Если нужно вы-
числить остаток, это можно сделать, пользуясь операцией взятия
остатка mod:
14 mod 3 2
Вообще выражение
число mod основание
в котором и число, и основание есть целые числа, представляет
собой остаток ог деления значения числа на значение основания.
Знак '/' применяется для обозначения операции нецелочислен-
2.4. Целый тип
43
ного (вещественного) деления. Его можно использовать и с целы-
ми операндами, но результат будет все равно вещественным:
2 div 3=0
2 / 3-0.66666667
5 div 1=5
5 / 1=5.0
Значение целочисленного выражения может быть присвоено
переменной целого и вещественного типов. Описав переменные
var
скорость, сечение : integer;
скоростьпотока : real;
мы получим возможность писать в дальнейшем выражения такого
вида:
скоростьпотока := скорость * сечение
При выполнении этого оператора вычисляется целое значе-
ние выражения, стоящего справа, затем это значение преобра-
зуется к вещественному типу, так что его можно теперь присво-
ить переменной скоростьпотока. Такое преобразование называет-
ся неявным преобразованием типа, поскольку знак операции или
функции преобразования в явном виде отсутствует. Такое пре-
образование возможно, так как на большинстве вычислительных
машин диапазон представляемых вещественных значений шире
диапазона целых значений, и присваивание не вызовет непред-
сказуемого результата.
Если число это целая переменная, то оператор read (число)
приведет к чтению числа из внешнего источника, затем преобра-
зованию его к целому значению и присваиванию этого значения
переменной число. Число при вводе имеет вид последовательности
цифр, например 13564. При этом необходимо понимать, что вы-
полнение оператора т) чтения вызовет неявное преобразование
из этого вида к форме внутреннего представления чисел в кон-
кретной вычислительной машине. Синтаксис чисел, которые мо-
гут быть считаны таким образом, тождествен синтаксису целых
констант, которые допустимы в программе на языке Паскаль;
этот синтаксис представлен на рис. 2.6. Числам, вводимым с
внешнего источника, могут предшествовать пробелы (включая
целые строчки из одних пробелов), и процедура read пропустит
их. Она не будет пропускать символы, отличающиеся от пробе-
лов, и если такие символы присутствуют, они приведут к ошибке
при выполнении программы.
И В языке Паскаль специальных операторов записи и чтения нет, это про-
сто обращение к стандартным процедурам read и write. Однако автор, а следом
за ним и мы, иногда такие обращения будем называть операторами.— Прим,
ред.
44
Гл. 2. Данные, выражения и присваивания
Оператор записи
write (число : ширинаполя)
где число — это целая переменная или выражение целого типа,
производит вывод значения число на какое-либо внешнее устрой-
ство. В этом случае также производится неявное преобразование,
на этот раз из внутреннего машинного представления в последо-
вательность десятичных цифр для печати. При этом число распо-
лагается в правой части поля, ширина которого в символах равна
значению выражения целого типа ширинаполя. Например, если
значения переменных число и ширинаполя есть 173 и 6 соответст-
венно, то будет напечатано
ГГП 173
(Каждый значок '□ ' означает один символ «пробел».) Если
число состоит из большего количества символов, чем указано в
переменной ширинаполя, оно будет отпечатано в правильном виде
в поле большей ширины. Программа форматы иллюстрирует ре-
зультат выполнения оператора записи в том случае, если ширина-
поля имеет минимальное значение, т. е. 1.
program форматы (output);
const
множитель = 10;
конечноезначение = 1000000;
var
степень : integer;
begin
степень : — множитель;
repeat
writelnf*', степень :1,
степень := степень * множитель
until степень > конечноезначение
end.
Результат работы программы форматы'.
*10*
*100*
*1000*
*10000*
*100000*
Параметр ширинаполя можно не указывать, в этом случае
принимается некоторое стандартное значение. Если (по умолча-
нию) подставляется стандартное значение 10, оператор
write (число)
эквивалентен оператору
write (число : 10)
2.4. Целый тип
45
В таблице 2.1 перечислены функции, которые могут иметь
целочисленные аргументы и функции, которые выдают целочис-
ленный результат. Функции pred (предшествующий элемент) и
succ (следующий элемент) имеют целочисленные аргументы и да-
ют целочисленный результат. Этот результат есть соответственно
либо предшествующее, либо последующее число по отношению
к аргументу функции, следовательно, имеем
pred (5)=4 succ(—3) = —2
Функции abs и sqr выдают целое значение, если их аргументы
целые, и вещественное значение, если аргументы вещественные.
Результат функции abs (число) есть абсолютное значение числа т)
(т. е. 1число1), а функция sqr (число) выдает величину, равную зна-
чению числа, возведенному во вторую степень. Имеем
Таблица 2.1
Стандартные функции
integer real boolean char file
integer pred succ abs sqr triinc round ord ord
real sin cos arctan. In. exp sqrt abs sqr sin cos arctan In exp sqrt
boolean. odd pred succ eof eoln
char chr pred succ
Хотя здесь и идет речь об идентификаторах, тем не менее мы будем, если
это необходимо, их склонять.— Прим. ред.
46
Гл, 2. Данные, выражения и присваивания
abs (10)= 10 sqr (10)--= 100
ab s (— 10) = 10 s q г (— 10) = 100
abs(O)=O sqr(O)=O
Функции sin, cos, arctan, In, exp и sqrt могут работать с цело-
численными аргументами, но значения, которые при этом полу-
чаются, всегда вещественные, поэтому такие функции рассмотре-
ны в следующем разделе.
2.5. Вещественный тип
В программах переменные вещественного типа используются
таким же образом, как в прикладной математике используются
вещественные числа. В памяти ЭВМ могут быть представлены
числа только из некоторого конечного подмножества веществен-
ных чисел, и, хотя .обычно это не оказывает особенного влияния
на результат вычислений, необходимо учитывать, что в некото-
рых случаях могут возникнуть серьезные ошибки.
В программах числа представляются либо целыми, либо ве-
щественными константами. Вещественные константы записы-
ваются, например, так
12.7 1.0 0.00005
Научные вычисления часто проводятся с очень большими или
очень маленькими величинами. Их нелегко представить обычной
десятичной записью. Например, масса покоя электрона прибли-
зительно равна
0.000000000000000000000000000910956 грамм
Такие числа чаще записывают в более удобном виде:
9.10956Х 10“28
можно их так записывать и в языке Паскаль. Часть числа, кото-
рая читается как «умножить на десять в степени», сокращается
до «Е» и такое число выглядит в программе как
9.10956Е—28
Синтаксис вещественных констант приведен на рис. 2.7. Как
мы уже видели, при описании константы ее значению может
предшествовать знак. Буква Е в вещественном числе не может
быть спутана с идентификатором, поскольку перед ней всегда
находится по крайней мере одна цифра.
Вещественные переменные обладают двумя важными харак-
теристиками — диапазоном и точностью. Например, о вычисли-
тельной машине могут говорить, что она обрабатывает числа,
находящиеся в диапазоне 10±75 и имеющие точность 10 десятич-
ных цифр. Это означает, что одиночная операция (сложение, ум-
2.5. Вещественный тип
47
ножение и т. д.) будет выполняться в большинстве случаев с точ-
ностью до 10 десятичных цифр. В некоторых частных случаях
точность может быть меньшей, а конечный результат вычисления,
для достижения которого нужно произвести тысячи или даже мил-
лионы простых операций, будет почти всегда получен с меньшей
точностью. Большинство вычислительных машин позволяет с
достаточной точностью и в достаточном диапазоне проводить
простые вычисления, необходимые для инженерных и физических
расчетов, для решения задач прикладной математики. В более
сложных приложениях, например при решении систем уравнений
или решении дифференциальных уравнений численными метода-
ми, для проведения точных длительных вычислений необходимо
принимать специальные меры.
Вещественные переменные естественным образом упорядоче-
ны. Тем не менее если и i] — вещественные числа (в математи-
ческом смысле) и представлены в машине вещественными перемен-
ными х и у, то мы можем с уверенностью говорить лишь о том,
что из
т] следует х=у,
но не о том, что из
£<т| следует, что х<у,
так как, если значения £ и т| слишком близки, машина не сможет
их различить, и, следовательно, в общем случае мы можем толь-
ко утверждать, что из
£<Л следует х^у.
В целом можно считать, что в вычислительной машине с точ-
ностью представления чисел до 10 десятичных цифр два числа,
отличающиеся только в одиннадцатой значающей цифре, будут
иметь одинаковые представления.
Знаки операций
—* * /
при использовании с вещественными операндами обозначают со-
ответственно операции сложения, вычитания, умножения и де-
ления. Знаки +, — и * могут использоваться как для целочис-
ленных вычислений, так и для вычислений с вещественными чис-
лами.
Не надо думать, что результат вычислений всегда будет точ-
ным. Арифметические операции выполняются в вычислительной
машине так же, как и в карманном калькуляторе.
Например, значение выражения
1000000 + 0.0000001
будет равно
1000000
48
Гл. 2. Данные, выражения и присваивания
если только в вашей машине числа не представляются с точностью
до 14 десятичных цифр или больше. В случае большой ЭВМ про-
блема даже серьезней, чем в случае карманного калькулятора,
так как на ЭВМ производится гораздо больше вычислений и на
ней обычно нет индикации накопления большой ошибки. Вся
забота о получении точных результатов ложится на программи-
ста. Следует применять только хорошие алгоритмы и, если не-
обходимо, контролировать промежуточные результаты, ^тобы
убедиться в правильности вычислений.
Несмотря на то что договоренность о выполнении умножения
и деления перед выполнением сложения и вычитания соответст-
вует общепринятому правилу, вычисление выражений слева на-
право не всегда дает ожидаемый результат. В частности, выра-
жение
груши / сливы * персики
вычисляется слева направо как
(груши / сливы) * персики
В вещественных выражениях можно использовать целые зна-
чения. Если один из операндов операций '+', '—' или ве-
щественный, то другой автоматически преобразуется к вещест-
венному типу перед выполнением операции. Рассмотрим выра-
жение
(6 + 4)*(1 +0.1)
Сначала вычисляются подвыражения в скобках
6 + 4=10 (целое)
Во втором подвыражении величина 0.1 является веществен-
ной, поэтому перед сложением другой операнд преобразуется к
вещественному типу:
1 + 0.1 = 1.1 (вещественное)
В операции умножения теперь участвуют целый операнд
(10) и вещественный операнд (1.1). Целый операнд преобразуется
в вещественный, и мы имеем
10.0 * 1.1 = 11.0 (вещественное)
Заметим, что результат имеет вещественный тип, хотя в дан-
ном случае дробная часть равна нулю.
Операция деления выполняется иначе. Здесь оба операнда
всегда приводятся к вещественному типу и результат, как мы
уже видели в разд. 2.4, всегда вещественный.
Запрещается присваивать результат вещественного выраже-
ния целой переменной.
2.5. Вещественный тип
49
Функции, у которых могут быть вещественные аргументы, а
также функции, выдающие вещественные результаты, указаны
в табл. 2.1. Функции abs (абсолютное значение) и sqr (вторая сте-
пень), если их используют с вещественными аргументами, вы-
дают вещественные значения.
Функции sin (синус), cos (косинус), arctan (арктангенс), In
(натуральный логарифм), ехр (экспонента) и sqrt (квадратный ко-
рень) могут иметь как целые, так и вещественные аргументы, но
всегда выдают вещественный результат.
Две оставшиеся функции, trunc и round, применяются для
преобразования вещественных значений в целые. Значением
trunc (величина) является целое число, получающееся при от-
брасывании дробной части вещественной величины. Например:
trunc (3.14159)=3 trunc (—4.8)=—4
значением round (величина) является целое число, ближайшее к
заданной величине, поэтому
round (3.14159)=3
round (2.71828)=3
round (—4.8)=—5
Если при преобразовании получается слишком большое целое
число, не представимое в машине, возникает ошибка во время
выполнения.
Если данное — это вещественная переменная, то оператор
read (данное)
вводит число из внешнего источника, преобразует его в соот-
ветствующее внутреннее представление и присваивает его значе-
ние переменной данное. Синтаксис вещественных чисел, которые
можно читать этим оператором, не отличается от синтаксиса ве-
щественных констант, показанного на рис. 2.7. Ошибка в записи
вещественного числа приведет к возникновению ошибки во время
выполнения. Оператор
write (данное : ширинаполя : точность)
производит вывод строки такого вида
I I I I I I ± dddddd.dddd
Здесь общее число выводимых символов равно величине шири-
наполя, а число цифр после точки определяется величиной точ-
ность. Каждая буква 'd' означает цифру, а означает пробел.
Параметр х) точность можно опускать, в этом случае будет при-
Как и автор, мы используем два термина: и аргумент, и параметр там, где
надо было бы пользоваться термином «фактический параметр».— Прим. ред.
50 Гл. 2. Данные, выражения и присваивайия
нято представление числа с плавающей точкой:
I I I Г| ± O.ddddddE ± dd
Точность этого представления определяется только компиля-
тором. Параметр ширинаполя также может быть опущен, и в этом
случае будет принято представление с плавающей точкой, а для
ширины будет выбрано некоторое стандартное значение (часто
20 символов).
Если вы пишете программу, в которой приходится печатать
вещественные значения, то их точность необходимо выбрать
так, чтобы она согласовывалась и с задачей, и с машиной, на ко-
торой решается задача. Не следует печатать лишние цифры, не
имеющие значения для результата, или те, которые уже будут
вычислены не точно. В частности, не пытайтесь печатать больше
значащих цифр, чем машина в состоянии вычислить.
По всей программе нужно использовать согласованные фор-
маты выдаваемой информации, если только нет каких-либо при-
чин, вынуждающих отказаться от этого. Величины, задающие
формат, следует определять в разделе определения констант, как
в этом примере:
const
точность = 6;
ширинаполя - 16;
var
результат : real;
writein (результат : ширинаполя : точность)
Если все сделано именно так, переделка программы для дру-
гой машины, имеющей большую (или меньшую) точность, стано-
вится простой задачей.
Параметры, задающие точность и ширину, в большинстве
примеров в этой книге не указаны. Это не означает, что ими не
надо пользоваться. Они опускаются, потому что их фактические
значения обычно не имеют отношения к рассматриваемым вопро-
сам, а сами они занимают слишком много места в маленьких при-
мерах. Значения, наиболее подходящие для этих параметров, в
какой-то степени зависят от машины, которой Вы пользуетесь для
проверки программ, и Вы можете подставить их самостоятельно.
2.6. Булевский тип
51
2.6. Булевский тип1*
Булевские переменные могут иметь одно из двух значений,
true (истина) и false (ложь). Большей частью такие переменные
используются при управлении порядком выполнения операторов
программы.
Имеются три булевские операции: and, or и not. Эти операции
можно обозначать, кроме того, и так: /\ (and), V (or), "I (not),
но в этой книге мы всегда будехМ использовать «содержательные»
обозначения.
Если мы опишем
var
окончен, пуст, слишкомвелик : boolean;
то следующие выражения будут иметь булевские значения
окончен and пуст or слишкомвелик
not пуст or слишкомвелик
слишкомвелик and (пуст or окончен)
Операция not выполняется всегда в первую очередь. Второе
из этих выражений эквивалентно следующему
(not пуст) or слишкомвелик
Операция and выполняется всегда перед операцией or, по-
этому первое выражение можно записать так
(окончен and пуст) or слишкомвелик
С помощью скобок порядок вычислений может быть изменен,
как, например, и сделано в третьем выражении.
Значения простых булевских выражений приведены в табли-
це 2.2, где предполагается, что операнды левый и правый — это
булевские переменные с указанными значениями.
Кроме булевских операций, существуют еще операции отно-
шения или сравнения'.
< меньше
меньше или равно
= равно
Ф не равно
больше или равно
> больше
Так как эти переменные получили свое название в честь английского
математика-самоучки Дж. Буля (1815—1864), то попытки назвать такие пере-
менные в русских переводах «логическими» выглядят несколько непочтительно.
Кроме того, многие программисты все равно называют их «булевскими», поэто-
му мы решили восстановить справедливость.— Прим. ред.
52
Гл. 2. Данные, выражения и присваивания
которые выдают булевские значения для выражений любого упо-
рядоченного типа. Например,
2 < 3=Zrr/e
3 —5=false
Таблица 2.2
Значения булевских операций
левый правый not левый левый and правый левый or правый
true true false true true
true false false false true
false true true false true
false false true false false
При описаниях
var
количество, итог : integer;
длина,высота : real;
готов : boolean;
выражения, приведенные ниже, дают булевские значения:
количество < итог
(количество = итог) and (длина высота) and готов
(количество mod итог = 0) or (количество 100)
Если операции сравнения перемежаются с булевскими опера-
циями, как в двух последних примерах, то используются скобки.
Математики привыкли писать такие выражения
минимум <1 значение максимум
В языке Паскаль этого делать нельзя. Выражение такого
типа должно быть записано в полной форме:
(минимум значение) and (значение максимум)
На равенство два вещественных значения лучше не сравни-
вать, так как числа, которые теоретически равны, после серии
вычислений будут равны друг другу лишь приблизительно.
Вместо сравнения а = Ь, где а и b — вещественные значения,
следует использовать такое
abs (а — Ь) < эпсилон
Для эпсилон нужно выбрать подходящее значение. Если по-
рядок чисел а и b неизвестен, то эпсилон должна быть функцией
одного из них. К примеру выражение
abs (а — b) < abs (а * 1Е—6)
будет иметь значение true, если а не отличается от b более чем на
одну миллионную долю.
Значение булевского выражения может быть присвоено бу-
2.6. Булевский тип
53
левской переменной. Мы можем дать описание:
const
максимум = 1000;
var
окончен, ошибка : boolean;
счетчик : integer;
а затем писать
окончен := счетчик > максимум;
ошибка := eof and not окончен;
Функции coin и eof дают булевские значения. В качестве па-
раметра обеим функциям передается имя файла, причем, если
оно опущено, то подразумевается файл input. (Рассмотрение фай-
лов ввода, отличающихся от стандартного файла input, мы откла-
дываем до гл. 7.)
Функция eoln дает значение true, если из вводного файла по-
ступил конец строки, а значение false во всех остальных случаях.
Если значение функции eoln есть true, то текущим символом во
входном потоке является пробел. Можно читать файл, не поль-
зуясь вовсе функцией eoln, в этом случае файл рассматривается
как одна длинная строка, внутри которой размещены дополни-
тельные пробелы. Если важно знать, где заканчиваются строки
файла, то для выяснения места расположения концов строк при-
меняется функция eoln. Эта функция будет более детально
описана позже.
Функция eof дает значение true, если процесс «дошел» до конца
файла. Попытка прочитать что-нибудь из файла после того, как
значение функции стало равно true, приведет к возникновению
ошибки при выполнении, поэтому перед чтением всегда нужно
проверять конец файла:
if eof
then write ('конец файла')
else read (сим)
Функция odd имеет целочисленный аргумент. Она выдает
значение true, если этот аргумент четный, а иначе выдается
значение false.
Если ключ это булевская переменная, то при выполнении
оператора
write (ключ)
будет напечатано 'TRUE' или 'FALSE'. Оператор
write (ключ : ширинаполя)
напечатает слово 'TRUE', перед которым будут пробелы в коли-
54
Гл. 2. Данные, выражения и присваивания
честве ширинаполя — 4 или слово 'FALSE', перед которым бу-
дет ширинаполя — 5 пробелов, при условии, что ширина поля
достаточно велика.
Процедура read не может иметь аргументов булевского типа.
Нетрудно, однако, написать программу, которая будет интер-
претировать символы 'И' и 'Л' как значения true и false соответ-
ственно.
const
символистика = 'И';
символложь = 'Л';
var
сим : char;
ключ : boolean;
read (сим);
if сим = символложь
then ключ := false
else if сим = символистика
then ключ : = true
else write ('во входном тексте ошибка')
2.7. Символьный тип
Значением переменной символьного типа является символ.
Символьный тип обозначается как char (сокращение от английско-
го слова character — символ). В языке Паскаль не определено
конкретное множество символов, поскольку всегда используется
то множество, которое имеется на вычислительной машине.
Предлагаемый Стандарт языка Паскаль, однако, требует, чтобы
множество символов обладало некоторыми свойствами:
(1) каждый символ должен иметь порядковый номер;
(2) порядковые номера цифр 0, 1, 2, . . . , 9 должны быть
упорядочены по возрастанию и следовать друг за другом.
(3) порядковые номера прописных букв А, В, . . . , / должны
быть упорядочены по возрастанию, но их номера не
обязаны идти подряд;
(4) порядковые номера строчных букв a, ft,..., z (если та-
кие буквы допускаются) также должны быть упорядоче-
ны по возрастанию, но не обязаны идти подряд.
Два наиболее широко используемых множества символов
ASCII (American Standard Code for Information Interchange —
американский стандартный код обмена информацией) и
EBCDIC (Extended Binary Coded Decimal Information Code —
2.7. Символьный тип
55
расширенный двоично-десятичный информационный код) удов-
летворяют всем необходимым требованиям х).
В программах константы символьного типа, определенные
своим значением, заключаются в кавычки или апострофы.
'А' представляет букву А;
'а' представляет букву а;
' ' представляет пробел;
"" представляет один символ
Функция ord выдает порядковый номер символа. Свойство (1)
говорит о том, что если сим1 и сим2 имеют символьный тип, и
сим1 сим2, то ord(ctiMl) ord(cuM2).
Символы упорядочены и упорядоченность эта построена на
основе порядковых номеров. Над переменными символьного типа
можно производить только операции сравнения
<< = ¥=>>
причем
сим1 < сим2
эквивалентно
ord (сим 1) < ord(cuM2)
Согласно свойству (2), последовательность
ord ('Г), ord('2'), ...» ord('9')
есть возрастающая последовательность целых чисел, идущих под-
ряд. (В коде ASCII эта последовательность есть 48, 49, . . . , 57.)
В большинстве множеств символов
ord ('0') #= О
и поэтому функция ord не преобразует цифру в соответствующее
значение. Для преобразования цифры сим символьного типа
в соответствующее численное значение чис надо воспользоваться
таким оператором:
чис \= ord (сим) — ord ('О')
Свойства (3) и (4) гарантируют, что буквы располагаются в
алфавитном порядке, но их порядковые номера могут идти не
подряд. Например, в коде EBCDIC
ord (7')=201, но ord (V')=209.
Функция dir — обратная по отношению к функции ord. Она
имеет целый аргумент, выдает символьное значение и определена
Х) В СССР наибольшее распространение получили два множества симво-
лов: система ГОСТ и система ISO. В системе ISO буквы упорядочены в соот-
ветствии с латинским алфавитом, а цифры имеют последующие номера; в систе-
ме ГОСТ дело обстоит наоборот, что, однако, не противоречит указанным выше
требованиям.— Прим, перев.
56
Гл. 2. Данные, выражения и присваивания
только в диапазоне значений функции ord. Если дать описание
var
номерсимвола : integer;
то значение chr (номерсимвола) будет определено, только если су-
ществует символ сим такой, что ord (сим) = номерсимвола. В этом
случае будет, как и следовало ожидать,
chr (номерсимвола) = сим
В частности, если цифра это целое и
О цифра 9
то соответствующим символом будет
chr (цифра + ord ('О'))
Например
chr(3 + ord ('О'))=3
Символы можно считать из входного файла при помощи стан-
дартной процедуры read:
var
сим : char;
read (сим)
Этот оператор будет читать очередной символ из входного
файла. Процедура read при чтении символов не пропускает про-
белы, как это делается при чтении целых и вещественных чисел.
Если нужно пропускать пробелы, мы должны для этой цели
включить в программу явные операторы:
const
пробел = '
var
сим : char;
repeat
read (сим)
until сим =# пробел
Этот оператор будет продолжать чтение до тех пор, пока либо
не будет прочитан символ, не являющийся пробелом, либо не
встретится признак конца файла. Если оператор окончит работу
правильно, переменная сим содержать очередной символ,
не являющийся пробелом.
Оператор
write (сим)
2.8. Построение программы
57
в котором сим есть символьная переменная или символьное вы-
ражение, запишет символ в выходной файл. Оператор
write (сим : ширинаполя}
в котором ширинаполя есть целое выражение, выведет сначала
пробелы в количестве ширинаполя—1, а затем символ сим.
Частным случаем является оператор
write (' ' : ширинаполя}
который выдает пробелы в количестве, равном значению пере-
менной ширинаполя.
2.8. Построение программы
Правила построения полной программы на языке Паскаль
столь же точны, как и правила построения отдельных элементов
программы, например разделов определения констант и описания
переменных. Рассмотрим синтаксис подмножества языка Паскаль,
состоящего из операторов присваивания, чтения и записи х).
Это подмножество описывается в манере «сверху-вниз». Понятие
«сверху-вниз» имеет большое значение в информатике и означает
развитие от абстрактного ('сверху') к конкретному ('вниз').
Архитектор, начинающий работу с эскизов проектируемого зда-
ния, пользуется методом «сверху-вниз», в то время как архитек-
тор, начинающий с просмотра каталога деталей, следует методу
«снизу-вверх». Оба подхода имеют свои достоинства, однако срав-
нительно недавно было осознано, что для разработки программ
больше подходит метод «сверху-вниз».
Программы и блоки
Программа состоит из заголовка и блока и заканчивается точ-
кой, как показано на рис. 2.10—2.12. Типичный заголовок вы-
глядит так:
program началомарша (input,output);
Слово program в языке Паскаль зарезервировано и всегда яв-
ляется первым словом программы. Следом за ним стоит имя
программы и список файлов, с которыми работает программа.
До гл. 7 мы не будем рассматривать программы, которые исполь-
зуют какие-нибудь файлы, кроме файлов input и output.
За заголовком следует блок (рис. 2.12). Описания и определе-
ния в блоке могут отсутствовать, но еслй оба раздела присутст-
вуют, то определение констант должно предшествовать описанию
переменных. Синтаксис разделов определения констант и опи-
сания переменных уже был нами рассмотрен (рис. 2.4 и 2.9).
х> См. прим, на стр. 43.— Прим. ред.
58
Гл. 2. Данные, выражения и присваивания
Операторы
Составной оператор (рис. 2.13) есть последовательность опе-
раторов, перед которой стоит слово begin, а в конце — слово end.
Между любыми двумя операторами должна стоять точка с запя-
той. Точка с запятой не является частью оператора, это раздели-
тель операторов. Поэтому между последним оператором состав-
ного оператора и словом end точка с запятой отсутствует. Рис. 2.14
программа-----> заголовок
Ьлок
Рис. 2. 10. Синтаксис программы
Рис. 2.11. Синтаксис заголовка
Рис. 2.12. Синтаксис блока
составной
оператор "
BEGIN J
Рис 2.13. Синтаксис составного оператора
показывает, что оператор может, в частности, оказаться состав-
ным оператором. Эта конструкция очень важна по следующей
причине: в любом месте программы, где можно написать опера-
2.8. Построение программы
59
тор, можно написать также и составной оператор. В разд. 3.3
описан оператор цикла с предусловием, имеющим такой общий
вид:
while булевское выражение do
оператор
оператор .
-------------5---оператор присваивания [---.
---->»| оператор чтения |_____>
4---оператор записи |------——>
---->»[ составной оператор [--/
Рис. 2.14. Синтаксис оператора
Этот оператор не был бы так полезен, если бы не тот факт,
что внутренний оператор может быть составным, и мы можем
писать:
while булевское выражение do
begin
оператору
оператор2;
операторк
end
Хотя операторы и составные операторы определяются частич-
но с помощью друг друга, их определения не образуют «пороч-
ного круга», в чем легко убедиться, глядя на диаграммы. Эти
определения рекурсивны. Тем, что язык Паскаль достаточно
выразителен и богат, несмотря на относительно простой синтак-
сис, он в значительной степени обязан аккуратному использова-
нию рекурсии при его разработке.
Оператор присваивания имеет вид
переменная : = выражение
что показано на рис. 2.15. Оператор присваивания асимметри-
чен: правая часть операнда отвечает на вопрос: «Какое значе-
ние?», а левая часть отвечает на вопрос: «Чему это значение
должно быть присвоено?» Можно, следовательно, писать такие
операторы присваивания, как '
первоечисло := 1
60 Гл. 2. Данные, выражения и присваивания
длинаокружности := 2 * пи * радиус
и даже
следующеечисло := следующеечисло + 1
в результате последнего оператора значение переменной следую-
щеечисло увеличивается на единицу. Однако лишены смысла опе-
раторы, такие, как
1 := первоечисло
длина * ширина := площадь
поскольку левые части этих операторов не могут быть интерпре-
тированы как величины, которым можно присвоить значение.
оператор
прис0аибания‘
идентификатор
переменной
Выражение
Рис. 2.15. Синтаксис оператора присваивания
Мы будем продолжать пользоваться операторами чтения и за-
писи, не определяя их формально, вплоть до гл. 7, где они под-
робно рассмотрены. Они необходимы для примеров, и их резуль-
тат обычно очевиден. Мы уже видели в предыдущих разделах этой
главы, как процедуры read и write работают с одиночным аргу-
ментом. Они могут иметь и несколько аргументов, так что после-
довательность
read (первый);
read (средний);
read (последний)
можно сократить до одного оператора
read (первый, средний, последний)
Процедура write может иметь в качестве аргументов еще и
константы, и можно писать
const
приветствие = 'эй, кто там, за дверью?';
write (приветствие)
или просто
write ('эй, кто там, за дверью?')
Процедура writein производит перевод строки в выходном
файле, поэтому
write (верх);
write (низ);
writela
2.8. Построение программы
61
можно сокращенно записать как
writein (верх, низ);
Выражения
Синтаксис выражений показан на рис. 2.16а — 2.16д. Этот
синтаксис отражает обычные отношения предшествования алгеб-
раических операций. Например, простое выражение
а + b * с
есть сумма двух слагаемых, причем второе слагаемое состоит из
двух множителей. Перемножение выполняется перед сложением.
Заметьте также, что знак операции логического умножения (and)
как знак умножения входит в число мультипликативных опера-
ций, знак операции логического сложения (or) — в число аддитив-
ных операций, а знак операции отрицания (not), являясь частью
множителя, имеет приоритет и перед теми и перед другими. Из
этих диаграмм становится ясно, для чего нужны скобки в выраже-
ниях вида
(минимум значение) and (значение максимум)
Пробелы и комментарии
На синтаксических диаграммах пробелы никак не отражены.
Это объясняется тем, что они могут встречаться практически в
любом месте программы, и включение их в синтаксические диа-
граммы приведет к большой путанице. Пробелы не должны встре-
чаться внутри зарезервированных слов, идентификаторов или со-
ставных символов. Составными символами являются
В некоторых реализациях языка Паскаль могут быть добав-
лены другие составные символы. Например, знаки
{}
могут быть заменены на
< - о >= (* *)
и тогда внутри этих составных символов пробелов быть не
должно.
Несколько пробелов, стоящих подряд, эквивалентны одному
пробелу. Между двумя строчками программ всегда подразуме-
вается наличие одного пробела, поэтому зарезервированные сло-
ва, идентификаторы или составные символы нельзя разрывать
при переходе от одной строчки к другой.
Комментарий (или примечание) имеет вид
{последовательность символов}
62
Гл. 2. Данные, выражения и присваивания
Выражение
-^4 простое Выражение
простое Выражение
Рис. 2.16a. Синтаксис выражения
Рис. 2.166. Синтаксис простого выражения
Рис. 2.16в. Синтаксис слагаемого
2.8. Построение программы
63
константа
5ез знака
Рис. 2.16д. Синтаксис константы без знака
причем последовательность символов может содержать любой
символ, кроме '}'. Комментарии эквивалентны пробелам, поэто-
му их можно помещать в программу всюду, где разрешены про-
белы. Комментарии вставляются в программу, чтобы помочь
читателю. Программы, включенные в эту книгу, не содержат
подробных примечаний, поскольку предварительного описания
достаточно для понимания работы программы.
Пример
Этот пример завершит рассмотрение простого подмножества
языка Паскаль. Вам следует убедиться, что приведенная здесь
программа может быть проанализирована с помощью синтакси-
ческих диаграмм, рассмотренных в этой главе.
program сфера (input,output);
const
пи - 3.1415926535;
ширина = 10;
точность = 4;
64
Гл. 2. Данные, выражения и присваивания
var
радиус, площадьповерхности, объем : real;
begin
read (радиус);
площадьповерхности := 4 * пи * sqr (радиус);
объем := радиус-v площадьповерхности / 3;
writein ('Параметры сферы');
writein ('Радиус = ', радиус : ширина
: точность);
writein ('Площадь поверхности=',
площадьповерхности : ширина : точность);
writein ('Объем = ', объем : ширина
: точность)
end. {сфера}
Ввод:
10
Вывод:
Параметры сферы
Радиус = 10.0000
Площадь поверхности = 1256.6372
Объем = 4188.7906
Упражнения
2.1 . Данные переписи кодируются в форме, показанной ниже. Нарисуйте
диаграммы, описывающие синтаксическую структуру этих данных
С514 1-СМИТ, 2—ДЖОН, 3—БОСТОН, 4—КУЗНЕЦ/
С515 1— +, 2—МЭРИ, 3—+, 6—24/
Д516 1—ДЖОНС, 2—ТОМ, 3—КОНКОРД, 4—ФЕРМЕР; 6—58/
Д517 1—+, 2—МАРГАРЕТ, 3—+, 4—КАССИР В БАНКЕ, 6—+/
2.2 Напишите раздел определений констант для программ, о которых го-
ворится ниже. Идентификаторы старайтесь выбирать по смыслу.
(а) Редактор текста, печатающий строки по 70 символов на страницах
размером 66 строк с отступлениями на абзацах в 5 позиций. Слово можно пе-
реносить только в том случае, если оно длиннее 10 символов. Символ «&» за-
меняется союзом «и», а символ % означает переход на новую страницу.
(б) Программа вычисления упругости по известным значениям модуля
Юнга Л и модуля сдвига |Л.
сталь медь алюминий
Л 11.2 9.5 2.6 (всеХЮ11)
и 8.1 4.5 2.6 (всеХЮ11)
2.3. Пусть сделаны описания
const
интервал ='
var
Упражнения
65
m, n : integer;
a,b : real;
p, q : boolean;
cl, c2 : char;
Определите, правильно ли записаны эти операторы, обоснуйте ответ
(a) m := trunc(b) — а
(б) р m -г п
(в) read(cl, с2, ' ')
(г) cl :=-интервал
(д) Р := q and (ord(cl) #= 'а')
(е) m := n mod a
(ж) 'сГ := 'c2'
(з) c2 := chr('a')
(и) m := m — ord('O')
(к) writeln(a,p,m,n,q,q,b)
(л) n a — trunc(a)
(m) b := 2.99 * 109
(h) a m I n
(o) b := ord(cl) + ord(c2)
2.4. Определите тип и, если это возможно, значение следующих выраже-
ний. Переменные р, q, г и s булевского типа, а переменная k целого типа
(a) sqr(2)
(б) sqr(2.0)
(в) огб('я') — ord('a')
(г) trunc(—99.9)
(д) —round(99.9)
(е) — round(—99.9)
(ж) not (р and q)- not (not p and not q)
(з) 10 div 3
(и) 10/3
(к) 126 div 3 mod 5
(л) (p and (q and not q)) or not (r or (s or not s))
(m) (round (—65.3)<trunc(—65.3)) and p
(h) odd(k) or odd(k-pl)
2.5. Напишите по правилам языка Паскаль операторы присваивания,
соответствующие данным формулам. Выберите подходящие идентификаторы,
все переменные считайте вещественными, дайте, где надо, описания констант.
(а) Период колебаний маятника длины / вычисляется по формуле
где g— ускорение свободного падения (981 см/сек2).
(б) Сила притяжения F между телами массы тх и т2, находящимися
на расстоянии г друг от друга, есть
Г— утгт21г~,
где гравитационная постоянная р—6.673Х 10~ч см2/г-сек2.
(в) Давление р газа связано с объемом v соотношением
риу — с,
где у и с — постоянные. (Замечание: =evln(t,)).
(г) Площадь треугольника со сторонами а, b и с выражается формулой
A = Vp{P—a)<P — b)(p—c),
где р= с)/2.
3 № 3388
66
Гл. 2 Данные, выражения и присваивания
(д) Периметр р правильного /t-угольника, описанного около окружности ’
радиуса г, равен
p=2nr tg(nln).
(е) Расстояние s от точки с координатами (£, т|) до прямой, описываемой
уравнением
Ах— Z31/С2== 0
вычисляется по формуле
Л^Вр-LC
S------------«
]ЕА2-гВ2
(ж) Энергия Е, излучаемая черным телом на волне длиной X при темпера-
туре Т, есть
г___ 2nc/iX“5
ech/B\T _ j '
где с=2.997924Х 108— скорость света, Л=6.6252Х 10“34 — постоянная План- |
ка, а В=5.6687Х 10~8—постоянная Больцмана. j
2.6. Для каких значений х можно ожидать нарушения правильности или I
точности этих выражений: >
(а) ехр (х) — ехр (—%), |
(б) (х- 1)/(х+1), ;
(в) 1-х+х2/2!-х3/3!-Ь. . .+х”/п!, |
где п достаточно велико, так что выполняется хп!п\<^\.
2.7. Выясните, каковы значения следующих величин для вашей вычисли-j
тельной машины: 1
(а) Наибольшее возможное целое (maxint). !
(б) Наибольшее по абсолютной величине отрицательное целое (которое может‘|
не равняться —maxint). 1
(в) Наибольшее представимое вещественное число. |
(г) Точность представления вещественных чисел. |
(д) Наименьшее значение |х|, отличающееся от нуля, где х — вещественное!
число. . J
(е) Наименьшее значение £ такое, что машина различит числа 1 и 1 +е.|
(ж) Символы, которые могут быть представлены в машине, и их порядковые-^
номера. «
Глава 3.
УСЛОВИЯ И ЦИКЛЫ
При создании программ часто требуется указать несколько
вариантов возможных действий, так чтобы выбор одного из них
происходил уже при выполнении программы.
Мы уже встречались с этой ситуацией в гл. 1 на примере про-
граммы квадратныекорни, которая печатала одно сообщение в
случае и другое в случае х<0. Условный оператор дает
возможность выбирать одно из двух действий, причем выбор де-
лается на основании результата вычисления булевского выраже-
ния. В контексте условного оператора булевское выражение
можно называть условием или предикатом.
Часто также требуется выполнить оператор повторно. Хотя
сам оператор при этом не меняется, данные, с которыми он рабо-
тает, изменяются. Группа операторов, которая исполняется пов-
торно, называется циклом. Каждый цикл должен завершиться
после конечного числа повторений и, следовательно, после
каждого повторения нужно принимать решение, продолжать
повторение или закончить цикл. Критерий такого решения носит
название условия окончания цикла.
3.1. Условный оператор
Условный оператор в языке Паскаль начинается с ключевого
слова if, смысл которого полностью соответствует английскому
слову 'if', т. е. 'если'. Предложение «если бар открыт, то прине-
сите мне пива, а иначе принесите кофе» имеет такую же структуру,
что и следующий оператор в языке Паскаль (слово then перево-
дится как то, а слово else — как иначе)'.
if
бароткрыт
then питье := пиво
else питье := кофе
Этот оператор есть частный случай условного оператора,
который в общем виде выглядит так:
if булевское выражение
then оператор
else оператор
3*
68
Гл. 3. Условия и циклы
Как мы уже говорили, в этом контексте термин условие час-
то выступает как синоним термина булевское выражение. Если зна-
чение условия есть true, выполняется оператор, стоящий за сло-
вом then, а если значение условия равно false, выполняется опе-
ратор, стоящий за словом else. Для английского языка естествен-
нее здесь было бы слово 'otherwise', что соответствует русскому
«в противном случае», но слово else стало традиционным для
многих языков программирования, возможно потому, что в этом
слове меньше шансов ошибиться. Ниже приведены примеры
условных операторов:
var
число, основание : integer;
сторона, площадь : real;
begin
if число < основание — 1
then число := число + 1
else число := 0;
if площадь 0
then сторона sqrt (площадь)
else
begin
сторона := 0;
write ('отрицательная площадь')
end;
end
Синтаксис
На рис. 3.1 приведена синтаксическая диаграмма для услов- }
ного оператора. По ней видно, что часть оператора, начинаю- I
щаяся со слова else, может быть опущена, и в этом случае, если |
условие имеет значение false, никаких действий не предприни- :• I
мается. Внутри условного оператора точки с запятой не нужны, I
поэтому будет ошибкой ставить этот знак перед словами then I
или else. Каждый оператор может быть составным оператором, J
как это указано на рис. 2.13, поэтому можно писать: II
var 1
большой, маленький : real; I
1
if большой > маленький |
then 1|
begin Я
большой : = маленький; 11
3.1. Условный оператор
69
маленький := О
end
Этот оператор имеет совершенно другой эффект по сравнению с
такими:
if большой > маленький
then большой := маленький;
маленький := О
поскольку во втором случае оператор присваивания маленький
:= 0 выполняется всегда, независимо от того, справедливо ли
условие большой > маленький или нет.
условный
оператор
Рис. 3.1. Синтаксис условного оператора
Внешний вид записи условных операторов очень важен. Ос-
новным принципом, которым необходимо руководствоваться,
является сдвиг альтернативных частей по отношению к условию.
Обычно условный оператор записывается так:
if условие
then оператору
else оператор2
Если операторы составные, их расположение несколько ме-
няется:
if условие
then
begin
операторы
end
else
begin
операторы
end
70
Гл. 3. Условия и циклы
Составные условные операторы г)
Операторы, стоящие после слов then и else, сами могут быть ус-
ловными операторами, в этом случае условный оператор назы-
вается составным. Примером составного условного оператора мо-
жет служить следующий:
if типсимвола = цифра
then читатьчисло
else
if типсимвола =- буква
then читатьимя
else выдатьошибку
В зависимости от значения типсимвола будет предпринято од-
но из трех действий: читатьчисло, читатьимя или выдатьошибку.
В этом случае второй альтернативной частью условного операто-
ра является другой условный оператор. Такая ситуация доволь-
но часто встречается в программах, и, если выполняется целая
последовательность проверок, текст программы придется сдви-
гать вправо, пока он не выйдет за пределы страницы. Поэтому мы
предлагаем располагать оператор так, как это показано ниже.
Заметьте, что и в этом случае запись оператора достаточно хорошо
отражает действия, выполняемые программой:
if типсимвола = цифра
then читатьчисло
else if типсимвола = буква
then читатьимя
else выдатьошибку
Другой вид составного условного оператора, в котором один
условный оператор является первой альтернативной частью
второго, выглядит так:
if форма = окружность
then
if радиус > расстояние
then вписан := true
else вписан := false
else write ('неверная форма')
При исполнении этого оператора будет либо отпечатано со-
общение 'неверная форма' (если форма Ф окружность) и значение
переменной вписан не изменится, либо переменной вписан будет
Такие операторы в языке Паскаль не выделяются в специальный класс
и название для них «составные» употребляет лишь автор. Надо отметить, что
с этим названием он связывает лишь рекомендуемое весьма специфическое
размещение этих операторов на бланке.— Прим. ред.
3.1. Условный оператор
71
дано значение, зависящее от значений радиус и расстояние. Этот
вид составного условного оператора может ввести в заблуждение
и потенциально опасен. Его лучше записывать таким образом:
if форма #= окружность
then write ('неверная форма')
else if радиус > расстояние
then вписан := true
else вписан := false
К этому времени вы уже должны понимать, что существует
еще лучший способ записи этого оператора:
if форма = окружность
then вписан := радиус > расстояние
else write ('неверная форма')
А вот такой формой условного оператора лучше не пользо-
ваться, но если делать это необходимо, записывайте его так:
if условие!
then
if условие2
then
if условие3
then оператор!
else оператор2
else операторз
else оператор^
Очевидная неоднозначность оператора
if условие!
then
if условие2
then оператор!
else оператор2
(К какому if относится else?) может быть разрешена, например,
с помощью синтаксической диаграммы. Этот оператор else отно-
сится к ближайшему условному оператору if, не имеющему аль-
тернативы else. Именно поэтому вторая форма составных услов-
ных операторов может ввести в заблуждение. Исключение из
приведенного выше примера строчки 'else оператор^ изменит
смысл следующей строчки 'else оператор * .
Пример: Решение квадратного уравнения
В качестве менее тривиального примера использования услов-
ного оператора составим программу, вычисляющую корни квад-
ратного уравнения:
ах2+Ьх+с==0.
72
Гл. 3. Условия и циклы
Математик имеет счастливую возможность заметить, что
квадратным это уравнение является только в случае а #= О,
именно так он определяет само понятие квадратного уравнения.
Кроме того, он может указать, что все квадратные уравнения
имеют два корня, которые могут быть в некоторых случаях рав-
ными. ЛАы, как программисты, лишены такой возможности. Мы
решаем уравнения по заказу пользователей программы, и они
могут и не быть столь любезными, чтобы обеспечить а =# О.Если
наша программа универсальна, она должна правильно работать
во всех следующих случаях.
Если а = 0 и b = 0, уравнение либо тавтологично (с = 0),
либо противоречиво (с у= 0). В этом случае мы должны печатать
сообщение о том, что уравнение вырождено.
Если а = 0 и b 0, имеется один корень со значением —
с/Ь.
Если а =# 0 и с= 0, имеются два корня — Ыа и с. Во всех
других случаях уравнение выглядит как
ах2 + Ьх + с = 0 (все коэффициенты ненулевые), либо как
ах2 + Ьс = 0 (а #= 0 и с 0).
В этих двух случаях можно использовать формулу
— b ± Y(Ь2— 4ас)
корни ------------------ •
Величина Ь2 — Аас называется дискриминантом уравнения.
Если дискриминант 0, то имеются два вещественных корня
(возможно равных).
Если дискриминант < 0, то имеются два комплексных корня.
Эти рассуждения приводят к следующей программе:
program квадратное (input, output);
var
а, b, с, дискриминант, re, im : real; »
begin
read (a, b, c);
if (a=0) and (b=0)
then writein ('Уравнение вырождено')
else if a = 0
then writein ('Единственный корень равен', —c/b)
else if c = 0
then writein ('Корни равны', —b/a, 'и', 0)
else
begin
re := —b/ (2*a);
дискриминант := sqr(b) — 4»a*c;
im : = 5дгЦаЬз(дискриминант)) / (2*a);
3.2. Оператор цикла с пост-условием
73
if дискриминант О
then writein ('Корни равны', re + im,
'и', re — im)
else writein ('Комплексные корни равны',
ге, ' + !*', im, 'и',
re, '—I*', im)
end
end. . {квадратное}
Ввод'.
0 0 7
0 10 2
2 3 0
1 5 6
1 1 1
Вывод:
Уравнение вырождено
Единственный корень равен —0.200000
Корни равны —1.500000 и 0
Корни равны —2.000000 и —3.000000
Комплексные корни равны — 5.000000 + 1*0.866025
и _ 5.000000 — 1*0.866025
Однако в одном случае эта программа может дать неточные
результаты. Если Ь'2 намного больше, чем 4ас, то
дискриминант ~ Ь2
и один из корней будет очень маленьким. Вычисление меньшего
корня производится при вычитании двух чисел, оказывающихся
почти равными, а это может привести к исчезновению значения.
Лучше будет сначала вычислить значение большего корня, а
уже затем определить меньший из соотношения
меньшийкорень = с! (ахбольшийкорень)
3.2. Оператор цикла с пост-условием
Мы уже пользовались операторами цикла с пост-условием
(начинающимися со слова repeat) в программе квадратныекорни
в гл. 1 и в некоторых других примерах в тексте. Оператор цикла
с пост-условием имеет две части: собственно цикл и условие
окончания. Общий вид этого оператора таков:
repeat
операторы
until условие
74
Гл. 3. Условия и циклы
Оператор цикла с пост-условием, например, применяется,
если во время написания программы мы не знаем, сколько именно
повторений потребуется. Скажем, если мы хотим узнать, сколько
членов гармонического ряда потребуется,ч чтобы выполнилось
неравенство
1 । 1 । 1 । । 1
1 + у + у+... + -> предел,
можно воспользоваться оператором с пост-условием:
program ряды (input, output);
var
числочленов : integer;
сумма, предел : real;
begin
числочленов := 0;
сумма : = 0;
read (предел);
repeat
числочленов : = числочленов + 1;
сумма := сумма + 1/ числочленов
until сумма > предел;
write (числочленов)
end. {ряды}
Ввод:
5
10
Вывод:
83
12367
При составлении цикла с пост-условием необходимо принимать
во внимание три момента:
(1) Начальное условие должно быть правильным.
(2) Операторы внутри цикла должны иметь правильную после-
довательность, причем должен присутствовать хотя бы один опе-
ратор, влияющий на условие окончания (иначе цикл будет про-
должаться бесконечно).
(3) Условие окончания должно в конце концов быть удовлетво-
рено.
Предположим, требуется распечатать квадратные корни чи-
сел, являющихся степенью 10, от 1 до 1000000. Представляется
разумным начать программу так:
var
степеньдесяти : real;
3.2. Оператор цикла с пост-условием
75
begin
степеньдесяти := 1;
repeat
write(sqrt(cTeneHbAecHTH));
степеньдесяти := 10 # степеньдесяти
until?
end.
Заметим, что условие окончания
степеньдесяти = 1000000
будет неправильным. Правильное условие окончания:
степеньдесяти = 10000000
Программа будет несколько легче читаться, если записать ее
таким образом:
var
степеньдесяти : real;
begin
степеньдесяти := 0.1;
repeat
степеньдесяти : = 10 * степеньдесяти;
^угИе1п(5дг1(степеньдесяти))
until степеньдесяти = 1000000
end.
В любом случае число различных значений, принимаемых
переменной степеньдесяти во время выполнения программы, на
единицу превышает число отпечатанных значений: нам нужно
одно лишнее значение, которое может равняться либо 0.1, либо
10000000.
Синтаксис
На рис. 3.2 изображена синтаксическая диаграмма оператора
цикла с пост-условием. Зарезервированные слова repeat и until
по действию похожи на операторные скобки, какими являются
оператор цикла,
с пост-условием
'Рис. 3.2. Синтаксис оператора цикла с пост-условием
76
Гл. 3. Условия и циклы
begin и end. Вследствие этого между словами repeat и until можно
поместить несколько операторов, отделяя их друг от друга
точкой с запятой. При записи оператора слово until обычно вы-
равнивается на слово repeat, к которому оно относится, а внут-
ренние операторы цикла несколько сдвигаются вправо.
Пример: Вычисление квадратного корня
Мы воспользуемся оператором с пост-условием для создания
более сложной версии программы квадратныекорни, где значе-
ние квадратного корня из числа вычисляется непосредственно в
программе, а не с помощью стандартной функции sqrt. Как и
в программе квадратныекорни, здесь вводятся числа и отбрако-
вываются отрицательные. Если введено нулевое число, резуль-
тат можно печатать сразу. В противном случае для вычисления
квадратного корня применяется метод Ньютона, основанный на
том, что
если прибл есть приближенное значение Vчисло
то (число/прибл + прибл) / 2 есть лучшее приближение
В программе в качестве первого приближения принимается
число 1, а затем проводится цикл вычислений до тех пор, пока
не будет получено достаточно точное значение Y число. Проб-
лема, возникающая при написании этой программы, заключается
в выборе подходящего критерия окончания итеративного процес-
са. Не очень хороши критерии вида
I число — прибл2 | < 10‘6,
поскольку если число > Ю15, то вычисления будут проводиться
с избыточной точностью, а когда число ни о какой
точности вообще говорить не приходится. Следовательно, здесь
нужен относительный критерий. Если принять
I число / прибл2 — 1 I < 10“6
то это будет гарантировать, что наш результат будет правильным
с точностью до миллионных долей при любом значении числа.
Эти рассуждения приводят к программе найтиквадратные*
корни:
program найтиквадратныекорни (input, output);
const
эпсилон = IE—6;
var
число, корень : real;
begin
repeat
геаб(число);
if число < О
3.3. Оператор цикла с пред-условием
77
then writeln('0iiJH6Ka в параметре')
else if число = О
then writeln(O)
else {число >0}
begin
корень := 1;
repeat
корень := (число/корень +
корень)/2
until abs (число/здг(корень)
— 1) < эпсилон;
writeln(KopeHb)
end
until число = 0
end. {найтиквадратныекорни}
Ввод:
1 2 3 4 5 —1 0
Вывод:
1.000000
1.414214
1.732051
2.000000
2.236067
Ошибка в параметре
0
3.3. Оператор цикла с пред-условием
Другой способ организации повторного выполнения опера-
тора связан с использованием цикла с пред-условием (начинаю-
щегося со слова while). Этот оператор похож на оператор цикла
с пост-условием (repeat), однако условие вычисляется и прове-
ряется в начале цикла повторения, а не в конце. Вид оператора
цикла с пред-условием таков
while условие do
оператор
Например, если нам надо убрать множители, равные 2, из значе-
ния переменной произведение, мы можем написать п:
while not осИ(произведение) do
произведение : = произведение div 2
Функция odd—предикат, проверяющий нечетность аргумента.—
Прим. ред.
78
Гл. 3. Условия и циклы
Если для некоторой величины нечетноезначение мы имеем
произведение = 2" нечетноезначение
то оператор присваивания будет выполнен п раз. Если значение
величины произведение нечетно, то /z=^0, и оператор присваива-
ния не будет выполнен ни разу.
Как мы уже видели, в случае оператора цикла с пост-услови-
ем существенным является тот факт, что один из операторов, на-
ходящихся внутри цикла, должен в конечном счете влиять на зна-
чение условия, поскольку иначе цикл будет повторяться беско-
нечно. В случае оператора цикла с пред-условием (while) необ-
ходимо наложить дополнительное требование на условие, кото-
рое заключается в том, что это условие должно иметь определен-
ное значение перед началом выполнения оператора.
В приведенном выше примере условие четко определено при
входе в цикл, если определено значение величины произведение.
Чтобы выполнение оператора цикла закапчивалось, присваива-
ние должно изменять значение величины произведение, что оно
и делает, если только произведение ф 0.
Такой цикл, следовательно, потенциально опасен. Его можно
несколько улучшить, соединив с условным оператором if:
if произведение ф 0
then
while not обсЦпроизведение) do
произведение : = произведение div 2
Оператор, входящий в цикл с пред-условием, может быть сос-
тавным. Чтобы знать, сколько раз выполнится оператор присваи-
вания, можно вставить счетчик:
счетчик := 0;
if произведение =^= 0
then
while not обс!(произведение) do
begin
произведение := произведение div 2;
счетчик : = счетчик + 1
end
Следующий пример позволяет устранить путаницу, часто воз-
никающую при использовании оператора цикла с пред-условием.
Что напечатает такая программа?
program головоломка (output);
var
число : integer;
begin
3.3. Оператор цикла с пред-условием
79
число := 0;
while число 10 do
begin
число := число + 1;
\\гп1е(число)
end
end. {головоломка}
В результате будут отпечатаны числа от 1 до 11. В начале
последнего такта число имеет значение 10, а условие истинно
(имеет значение true). Во время последнего такта оператор
число := число + 1
запишет в число значение 11, которое и будет напечатано. Иногда
полагают, что оператор цикла с пред-условием каким-то мисти-
ческим образом следит за изменением значения переменной число
и завершает цикл, как только число = 10, но это не так. Помните,
что условие вычисляется только в начале каждого такта (отсюда
и термин «пред-условие»).
При практическом программировании оператор цикла с пред-
условием оказывается гораздо полезнее оператора цикла с пост-
условием. Это связано с тем фактом, что во многих случаях необ-
ходимо вначале установить, не следует ли пропустить цикл цели-
ком. Если у вас возникают сомнения при выборе операторов цикла
с пред-условием и пост-условием, попробуйте сначала использо-
вать оператор цикла с пред-условием.
Синтаксис
Синтаксис оператора цикла с пред-условием показан на
рис. 3.3. Так как в этом операторе нет зарезервированного слова,
соответствующего слову until в операторе цикла с пост-условием,
оператор цикла
с преЗ-условием
WHILE
условие
оператор
Рис. 3.3. Синтаксис оператора цикла с пред-условием
тело цикла может состоять только из одного оператора. В боль-
шинстве случаев тело цикла с пред-условием представляет
собой составной оператор.
Пример: Преобразование чисел
В качестве примера использования оператора цикла с пред-
условием рассмотрим программу преобразования чисел. Стан-
дартная процедура read выполняет чтение из входного файла и
80
Гл. 3. Условия и циклы
преобразование из внешнего представления числа, т. е. последо-
вательности цифр, во внутреннее машинное представление числа
в двоичном виде. Процедурой read нельзя пользоваться, когда во
входном файле есть нецифровые символы. Она не будет работать,
если во входном файле содержатся, например, такие сообщения:
значения равны 100 и 504.75
10 * (14.75—8.60)
Программа преобразовать читает символы из входного потока
до тех пор, пока там не будет найдена цифра, затем она читает
цифры до конца числа и выполняет преобразование во внутрен-
нюю форму. На рис. 3.4 показана синтаксическая диаграмма для
чисел, которые могут обрабатываться этой программой.
Рис. 3.4. Синтаксис числа
Число может содержать десятичную точку, и, если она есть,
ьсе цифры, стоящие за ней, будут прочитаны и правильно проин-
терпретированы.
Программа прочитает числа
5 7 19.5 0.732
правильно, но проигнорирует десятичную точку в числе
.732
и прочитает это число как 732. Числа с дробной частью читаются
и преобразуются, как если бы они были целыми, но цифры, стоя-
щие после десятичной точки, подсчитываются, и их количество
учитывается при формировании масштабного множителя после
прочтения числа. Например, в результате чтения числа 1234.5678
значение будет равно 12345678, а масилтб 4.
Преобразование завершается делением значения на
так что
12345678 / 104-1234.5678
как и требовалось. Не вычисляя масштаб непосредственно, про-
грамма многократно производит деление па 10. В данном случае
это простительно, так как эта программа не рассчитана на чтение
очень маленьких чисел (меньше, чем 10~10, например). Для
чтения чисел, записанных с плавающей точкой, следует приме-
нять более эффективный масштабирующий алгоритм.
3.3. Оператор цикла с пред-условием
81
По окончании работы программы преобразовать из входного
файла будет прочитан один символ после последней цифры. Ясно,
что это совершенно неизбежно, так как единственное средство
определить конец числа — это прочесть символ, не являющийся
цифрой.
program преобразовать (input, output);
const
нуль ='0';
девять — '9';
точка =
основание = 10;
var
результат : real;
масштаб : integer;
символ : char;
begin
результат := 0;
repeat
геаб(символ)
until (нуль < символ) and (символ < девять);
while (нуль < символ) and (символ < девять) do
begin
результат := основание * результат +
ord (символ) — ord (нуль);
read(cимвoл)
end; {whilejj
if символ = точка
then
begin
масштаб := 0;
геаб(символ);
while (нуль < символ) and
(символ < девять) do
begin
результат := основание * результат +
огб(символ) — огб(нуль);
read (символ);
масштаб := масштаб + 1
end; {while}
while масштаб > 0 do
begin
результат := результат I основание;
масштаб масштаб — 1
end {while}
end;
writeln(peзyльтaт)
end. {преобразовать}
Ввод'.
1234.5678
.124
0.999999999999999
82
Гл. 3. Условия и циклы
Вывод'.
1234.567800
124.000000
1.000000
Заметим, что при входе в первый оператор цикла с пред-
условием известно, что переменная символ содержит цифру, поэ-
тому цикл обязательно выполняется хотя бы один раз. Можно
заменить этот оператор на оператор цикла с пост-условием.
repeat
результат := основание * результат +
огб(символ) — ord (нуль);
геаб(символ)
until not ((нуль <1 символ) and (символ девять))
Однако со вторым оператором цикла с пред-условием так по-
ступать нельзя, поскольку, если после десятичной точки цифр
нет, операторы, стоящие внутри тела цикла, не должны совсем вы-
полняться. Так как оба оператора выполняют одну и ту же
функцию, программа будет выглядеть понятнее, если мы запишем
их в одной и той же форме.
Чтение чисел
Процедура read может конечно работать как с целым, так и с
вещественным аргументом, что было показано в разд. 2.4 и 2.5.
Возникают, однако, некоторые проблемы при чтении из входного
файла последовательности чисел. Рассмотрим к примеру задачу
нахождения среднего значения множества вещественных чисел п.
Решение заключается в подсчете чисел и нахождении их суммы.
Следующая программа внешне выглядит подходящей:
program среднее (input, output);
var
значение, сумма : real;
счетчик : integer;
begin
сумма := 0;
счетчик 0;
while not eof do
begin
геаб(значение);
сумма := сумма + значение;
счетчик := счетчик + 1
end; {while}
Проблема, о которой пойдет речь, скорее объясняется нашей неполной
осведомленностью о том, что такое файл. Чаще всего она решается путем чет-
кой фиксации понятия «конца файлам, вводимого на уровне конкретной про-
граммы, а не системы.— Прим. ред.
3.3. Оператор цикла с пред-услсвием
83
writeln ('Среднее = сумма / счетчик) .
end. {среднее}
К сожалению эта программа не работает. Предположим, вход-
ной файл содержит одно число 4.7. Это число будет прочитано во
время первого прохода по телу цикла, и в конце этого прохода
будем иметь:
значение = 4.7
сумма = 4.7
счетчик = 1
Казус заключается в том, что за числом 4.7 в файле находятся
пробелы. Если входной файл организован на перфокартах, на
этих картах обязательно есть пробелы. Если вводить число
4.7 с терминала, то в качестве пробела будет выступать символ
перевода строки. Следовательно, значением функции eof пос-
ле первого прохода по телу цикла не может быть значение true,
а отсюда следует, что цикл будет исполняться повторно. Введен-
ным значением будет нуль, поэтому в конце второго исполнения
тела цикла мы получим:
eof = true
значение = О
сумма = 4.7
счетчик = 2
Среднее значение, выдаваемое этой программой, будет 4.7/2 =
=2.35, что не соответствует действительности.
Следовательно, нужно реорганизовать программу таким об-
разом, чтобы последнее значение, прочитанное процедурой read,
не принималось во внимание и не влияло на значение счетчика.
Это можно сделать, поместив оператор чтения в конец цикла:
while not eof do
begin
сумма := сумма + значение;
счетчик := счетчик 4- 1;
геас!(значение)
end
Теперь нам будет необходим еще один оператор чтения, чтобы
прочесть первое число. Его надо расположить вне цикла, что и
придаст программе среднее окончательный, правильный вид.
program среднее (input, output);
var
значение, сумма, среднее : real;
счетчик : integer;
begin
84
Гл. 3. Условия и циклы
сумма : = 0;
счетчик := 0;
геас1(значение);
while not eof do
begin
сумма : = сумма + значение;
счетчик := счетчик + 1;
геас!(значение)
end; {while}
if счетчик > 0
then
begin
среднее := сумма / счетчик;
writein (счетчик, 'значений',
прочитано,', 'среднее = ', среднее)
end
else writein ('не прочитано ни одного',
'значения')
end. {среднее}
Ввод:
2.5 6.36 7.81 9.98
Вывод:
4 значений прочитано, среднее = 6.662500
3.4. Оператор цикла с параметром
Если нужно выполнить некоторый оператор несколько раз,
причем число повторений не зависит от результата работы опе-
ратора, лучше всего пользоваться оператором цикла с парамет-
ром. Этот оператор начинается со слова for. В начале разд. 3.2
мы приводили пример использования оператора цикла с пост-
условием для определения количества членов гармонического
ряда, требуемого для того, чтобы сумма ряда превысила заданный
предел. Обратная задача нахождения суммы по заданному числу
членов ряда лучше всего решается с помощью оператора цикла с
параметром.
program гармоническийряд (input, output);
var
членряда, числочленов : integer;
сумма : real;
begin
геаб(числочленов);
сумма := 0;
for членряда := 1 to числочленов do
сумма := сумма + 1 / членряда;
3.4. Оператор цикла с параметром
85
writeln(cyMMa)
end. {гармоническийряд}
Результатом работы оператора цикла с параметром будет
выполнение оператора присваивания
сумма := сумма + 1 / членряда
для каждого целочисленного значения переменной членряда от 1
до значения числочленов. Если числочленов = 1, то оператор при-
сваивания выполнится только один раз, и будет напечатано число
1. Если числочленов < 1, оператор присваивания вообще не будет
выполняться, и будет напечатано число 0.
Общий вид оператора цикла с параметром таков:
for уп : = выражение! to выражение2 do
оператор
Он эквивалентен следующему составному оператору:
begin
раб1 := выражение!;
раб2 := выражение2;
if раб1 раб2
then
begin
уп := раб1;
оператор;
while уп =# раб2 do
begin
уп : = succ(yn);
оператор
end
end
end
Переменная уп называется управляющей переменной, или
параметром цикла. Рабочие переменные раб! и раб2 имеют
тот же тип, что и уп. Они создаются компилятором, и к ним
нет доступа из программы. Они нужны только для того, чтобы
показать, что выражение! и-. выражение2 вычисляются лишь
один раз, перед входом в цикл, и поэтому границы цикла не могут
изменяться операторами, стоящими внутри цикла.
Типы управляющей переменной и граничных выражений
должны совпадать в любом случае. Их типом может быть только
такой тип, для которого определена функция succ. Тем самым ис-
пользование управляющей переменной вещественного типа в
операторе цикла с параметром запрещается. Оператор цикла с
параметром не дает каких-либо последствий, если
выражение! > выражение2
86
Гл. 3. Условия и циклы
Когда выполнение оператора цикла с параметром завершается,
значение управляющей переменной не определено.
Тот факт, что оператор цикла с параметром может быть за-
писан в форме оператора цикла с пред-условием, означает, что
этот оператор избыточен. Тем не менее имеется ряд важных при-
чин, чтобы применять оператор цикла с параметром там, где это
представляется возможным. Оператор цикла с параметром дает
больше информации человеку, изучающему программу. Четко
определены значение, которое будет присвоено управляющей
переменной, и число повторений цикла. Эта же информация
нужна и компилятору, который часто может создать более эффек-
тивную программу для оператора цикла с параметром, чем для
оператора цикла с пред-условием.
Ключевое слово to оператора цикла с параметром можно за-
менить словом downto. Оператор будет иметь вид:
for уп := выражение! downto выражение2 do
оператор
В этом случае значение управляющей переменной уп умень-?
шается при каждом повторении тела цикла, а не увеличиваетсяJ
Этот вид оператора цикла с параметром не дает результата, если|
выражение! < выражение2
На рис. 3.5 приведена синтаксическая диаграмма для опера1
тора цикла с параметром.
Рис. 3 5. Синтаксис оператора цикла с параметром
Пример: Высота музыкальных тонов
Отношение высот двух музыкальных нот, отстающих друг от-
друга на один полутон по эквимодулированной шкале, есть
(2)1/12с^1.05946, а стандартный концертный основной тон обычно,
получается при настройке среднего «ля» на 440 гц. Самая нижняя
нота фортепьяно на четыре октавы ниже этого «ля» и, следова-
тельно, должна быть настроена на частоту 440z24 = 27.5 гц.*
Следующей программой можно пользоваться для вычисления
теоретических частот звука других нот фортепьяно:
program частоты (output);
3.4. Оператор цикла с параметром
87
const
нижняянота = 27.5;
ширинаклавиатуры = 88;
полутон = 1.05946;
var
частота : real;
нота : integer;
begin
частота := нижняянота;
for нота : = 1 to ширинаклавиатуры do
begin
writeln(4acTOTa);
частота : = частота * полутон
end {for}
end. {частоты}
Для верхних нот эта программа точных результатов не даст,
так как при умножении будет накапливаться ошибка. Лучше
всегда вычислять каждое значение в цикле независимо, а не
получать последующее значение из предыдущего, даже если
программа при этом будет работать медленнее. Можно восполь-
зоваться тем фактом, что частота связана с номером ноты соот-
ношением:
частота = нижняянота X 2<нота/12).
Показательную функцию можно вычислить по формуле
ах ех In а,
а в записи на языке Паскаль будем иметь:
частота := нижняянота * ехр (нота * In (2) / 12)
Теперь мы можем составить новую версию программы частоты:
program частоты (output);
const
нижняянота = 27.5;
ширинаклавиатуры = 88;
var
частота : real;
нота : integer;
begin
for нота := 0 to ширинаклавиатуры— 1 do
begin
частота := нижняянота * ехр(нота *
1п(2) / 12);
writeln(4acTOTa)
end {for}
end. {частоты}
88
Гл. 3. Условия и циклы
Эта программа будет давать точный результат, но при этом
значение In (2)/12 будет вычисляться на каждом проходе по цик-
лу, т. е. 88 раз. Можно ликвидировать этот недостаток введе-
нием вещественной переменной отношение, значение которой
инициируется оператором присваивания:
отношение := In (2) / 12
Итак, окончательная версия программы частоты такова:
program частоты (output);
const
нижняянота = 27.5;
ширинаклавиатуры = 88;
var
частота, отношение : real;
нота : integer;
begin
отношение := 1п(2) / 12;
for нота := 0 to ширинаклавиатуры — 1 do
begin
частота := нижняянота * ехр (отношение
* нота);
writeln(4acTOTa)
end {for}
end. {частоты}
Вывод:
27.500000
29.135235
30.867706
32.703196
34.647829
36.708096
и т. д.
1 О.
Упражнения
3.1. Напишите программу, вычисляющую кубические корни, используя
тот факт, что если а есть приближенное значение х, то величина
Р=(2а+л7а2)/3
дает более точное приближение.
3.2. Модифицируйте программу частоты таким образом, чтобы она пе-
чатала после каждой октавы две пустые строки (октава состоит из 12 полутонов).
3.3. Покажите, что
(а) любой оператор цикла с пост-условием можно записать с помощью
условного оператора и оператора цикла с пред-условием;
(б) любой оператор цикла с пред-условием можно записать с помощью
условного оператора и оператора цикла с пост-условием.
Упражнения
89
3.4. Следующий оператор языка Паскаль записан несколько коряво:
if a<b then if c<d then x := 1
else if a<c then if b<d then x := 2
else x := 3
else if a<d then if b<c then x := 4
else x 5
else x := 6
else x 7
(а) Перепишите этот оператор, используя более понятный способ располо-
жения операторов.
(б) Нет ли в этом операторе избыточных или противоречивых условий?
(в) Напишите оператор, имеющий тот же результат, но более простой,
чем данный.
3.5. Для решения уравнения
х sin х— 1
методом последовательного приближения можно применить формулу Ньюто-
на и Рафсона. Рекуррентное соотношение таково:
х _ l+4cos(xn)
” + 1 sin (%„)+%„ cos (x„) *
Напишите программу, вычисляющую положительные корни этого урав-
нения.
3.6. Напишите программу, которая определяет все способы размена лю-
бой суммы денег до 99 копеек с помощью монет достоинством 1,5, 10 и 20 копеек.
3.7. Дополните программу среднее так, чтобы она вычислила среднеква-
дратичное отклонение по формуле
у —|i)2/(n —1) .
а =
3.8. Напишите программу, которая проверяла бы справедливость нера-
венства
для целых положительных п.
3.9. Модифицируйте программу преобразовать таким образом, чтобы она
могла читать и правильно интерпретировать
(а) отрицательные числа,
(б) восьмеричные (по основанию восемь) числа.
3.10. Напишите программу, которая бы читала и вычисляла выражения
вида
+20 — 4 — 3 + 169;
Все числа целые, перед каждым стоит знак, а выражение заканчивается
точкой с запятой.
3.11. Улучшите программу квадратное так, чтобы при
4 ас < эпсилонХ Ь2
корень, больший по абсолютной величине, вычислялся первым, а затем по
формуле
маленькийкоренъ — с / (а X большойкорень)
вычислялся бы второй корень квадратного уравнения,
90 Гл. 3. Условия и циклы
3.12. Напишите программу, которая вычисляла бы сумму ряда
1Н-х-;-х2/2!Н-х3/3!-г. . .-\-хп!п\
в прямом и обратном направлениях, а затем сравнивала бы результаты. По-
чему при некоторых х этот ряд нельзя использовать для вычисления значения
ех? (Для вычисления факториалов пользуйтесь вещественными переменными,
иначе вы рискуете выйти за пределы представления целых чисел.)
3.13. Опыт состоит в бросании монеты до тех пор, пока не выпадет орел,
Среднее число проб в опыте есть
lim Ё •
П -> оо i^\
Найдите приблизительное значение этого предела.
Глава 4.
ПРОЦЕДУРЫ И ФУНКЦИИ
Наиболее сложные системы, будь то биологические, бюрокра-
тические или военные, организованы иерархическим способом.
Иерархия — это система уровней, в которой верхние уровни
работают с более общей информацией, а нижние — имеют дело с
техническими подробностями. Для придания иерархической
структуры программам используются процедуры.
4.1. Процедуры
Процедурой в языке Паскаль является оператор, с которым
связано некоторое имя. Оператор
repeat
геаб(символ)
until символ #= пробел
будет читать символы из входного файла до тех пор, пока не
встретится символ, не являющийся пробелом. Для этого опера-
топа подходящим именем будет пропуститыгробелы. Определим
процедуру пропуститьпробелы следующим образом:
procedure пропуститьпробелы;
begin
repeat
read(cимвoл)
until символ пробел
end;
После введения такого определения оператор пропуститьпро-
белы будет приводить к такому же результату, как и оператор
цикла с пост-условием. Оператор пропуститьпробелы есть
вызов процедуры, и говорят, что он вызывает процедуру про-
пуститьпробелы (обращается к ней).
При использовании процедуры пропуститьпробелы вместо
составляющего ее оператора, мы достигаем некоторого уровня аб-
стракции. Вызов пропуститьпробелы, при условии что мы вы-
брали подходящее имя, отвечает на вопрос, «что должен сделать
этот оператор?» Оператор, который мы видим в описании проце-
92 Гл. 4. Процедуры и функции д
дуры пропуститыгробелы, т. е. Ж
repeat ч •?
геас1(символ) \ * 1 ' \ ;
until символ =/= пробел *
отвечает на вопрос, «как это надо сделать?» ‘
Воспользуемся процедурами в программе, вычисляющей час- i
тичные суммы гармонического ряда. Программа будет вычислять
Н (п) для различных значений п, где '/
Н (п) = 1 + у + у + • • • + у Z
Результат вычисления будет выражен в форме рационального
числа, т. е. числа вида числитель / знаменатель, где числитель и
знаменатель есть целые числа. В языке Паскаль мы представ-
ляем рациональное число двумя числами целого типа. ;
Нам потребуется процедура исключения общих множителей ,
из числителя и знаменателя. Для этого будем делить числитель и
знаменатель на их наибольший общий делитель. Чтобы вычис-
лить наибольший общий делитель в процедуре сократитьдробь, \
применим несколько улучшенную версию алгоритма Евклида: |
procedure сократитьдробь;
begin
копиячислителя := числитель; |
копиязнаменателя := знаменатель; J I
while копиязнаменателя 0 do \
begin д
остаток := копиячислителя mod
копиязнаменателя;
' копиячислителя := копиязнаменателя;
копиязнаменателя := остаток
end; {while}
if копиячислителя > 1
then л
begin
числитель := числитель div
копиячислителя;
знаменатель := знаменатель div
копиячислителя;
end
end; {сократитьдробь}
Определяться и использоваться процедура сократитьдробь
будет в полной программе убратьмножители. Обратите внимание
на организацию этой программы:
заголовок программы
4.1. Процедуры
93
описание переменных
описание процедуры
тело программы
При выполнении программы убратьмножители первой ис-
полняемой командой будет
геас!(числитель, знаменатель)
Операторы, содержащиеся в процедуре сократитьдробь. не
выполняются до тех пор, пока не встретится команда сократить-
дробь.
Этот порядок не может быть изменен, так как любой объект
должен быть определен, прежде чем им можно пользоваться.
Переменные используются внутри процедуры и, следовательно,
должны быть описаны перед ней, а главная программа исполь-
зует процедуру и поэтому должна следовать за ее описанием.
Первая строка описания процедуры в нашем случае
procedure сократитьдробь;
называется заголовком процедуры. Составной оператор, непосред-
ственно следующий за заголовком, называется телом процедуры.
он должен заканчиваться точкой с запятой. Чтобы сделать текст
нагляднее, перед и после описания процедуры вставляются по
одной пустой строчке. На обычных машинных «распечатках» ис-
пользуется даже более просторное расположение текста х). Лучше
всего будет вставлять пустые строчки между разделами опреде-
ления констант и описания переменных и две или три строчки
после описания последней процедуры.
program убратьмножители (input, output);
var
числитель, знаменатель, копиячислителя,
копиязнаменателя, остаток : integer;
procedure сократитьдробь;
begin
копиячислителя := числитель;
копиязнаменателя знаменатель;
while копиязнаменателя 0 do
begin
остаток := копиячислителя mod
копиязнаменателя;
копиячислителя := копиязнаменателя;
копиязнаменателя :== остаток
end; {while}
П Правда, при этом следует помнить, что бумага, как и время программи-
ста, стоит денег.— Прим. ред.
94
Гл. 4. Процедуры и функции
if копиячислителя > 1
then
begin
числитель := числитель div
копиячислителя;
знаменатель := знаменатель div
копиячислителя
end
end; {сократитьдробь}
begin {убратьмножители}
геас!(числитель, знаменатель);
сократитьдробь;
У¥п(е1п(числитель, знаменатель)
end. {убратьмножители}
Мы определили переменные копиячислителя, копиязнамена-
теля и остаток наравне с переменными числитель и знаменатель
как глобальные переменные, и поэтому они могут использовать-
ся всюду, как в процедуре сократитьдробь, так и в главной про-
грамме. Однако пользоваться ими в главной программе нежела-
тельно, поскольку любой вызов процедуры сократитьдробь
изменит их значения. Ясно, что они принадлежат только проце-
дуре, а не всей программе в целом. Поэтому мы можем поместить
их описания внутрь процедуры:
program убратьмножители (input, output);
var
числитель, знаменатель : integer;
procedure сократитьдробь;
var
копиячислителя, копиязнаменателя, остаток
: integer;
Переменные копиячислителя, копиязнаменателя и остаток
теперь являются локальными переменными. На рис. 4.1 (а) пока-
зана ситуация, имеющая место перед вызовом процедуры сокра-
титьдробь', переменные копиячислителя, копиязнаменателя и
остаток еще не определены. Рис. 4.1(6) показывает ситуацию,
возникающую во время выполнения процедуры сократить-
дробь: определены (и ими разрешено пользоваться) переменные
копиячислителя, копиязнаменателя и остаток. Процедура
имеет также возможность обращаться к глобальным переменным
числитель и знаменатель. На рис. 4.1 (в) закончилось выполнение
процедуры, и теперь переменных копиячислителя, копиязнаме-
нателя и остаток больше не существует. Если к процедуре про-
исходит еще одно обращение, локальные переменные создаются
4.1. Процедуры
95
вновь. Не следует никогда предполагать, что при повторном
вызове процедуры ее локальные переменные будут иметь те зна-
чения, какие у них были в конце предыдущего обращения. Мы
говорим, что областью определения переменных копиячислителя,
копиязнаменателя и остаток является процедура сократить-
дробь. Еще раз напомним о необходимости различать статические
(текст программы) и динамические (выполнение программы)
. копия
числителя
копия
Знаменателя
остаток
числитель
числитель
числитель
Знаменатель
Знаменатель
знаменатель
(5)
(6)
Рис. 4.1. Локальные переменные
понятия. В тексте программы обращаться к переменной копиячис-
лителя из главной программы нельзя, так как в главной програм-
ме эта переменная не определена. Однако во время выполнения
при вызове процедуры сократитьдробь все переменные могут
быть в действии одновременно.
Процедура сократитьдробь определена все же не очень хо-
рошо. Для того чтобы ею пользоваться, рациональное число, ко-
торое нужно привести к несократимой дроби, должно быть запи-
сано в переменных числитель и знаменатель. Если, к примеру, оно
записано в переменных верх и низ, то следует произвести копи-
рование:
числитель := верх;
знаменатель := низ;
сократитьдробь
С процедурой было бы легче работать, если бы ей можно было
задавать аргументы (параметры). В этом случае мы могли бы со-
кратить дробь числитель / знаменатель, написав
сократитьдробь(числитель, знаменатель)
а дробь верх / низ, написав
сократитьдробь(верх, низ)
Это легко сделать, введя в описание процедуры сократить-
дробь параметры следующим образом:
procedure сократитьдробь (var числ, знам : integer);
96
Гл. 4. Процедуры и функции
var
копиячислителя, копиязнаменателя,
остаток : integer;
begin
копиячислителя : числ;
копиязнаменателя : знам;
while копиязнаменателя =/= 0 do
begin
остаток := копиячислителя mod
копиязнаменателя;
копиячислителя :== копиязнаменателя;
копиязнаменателя := остаток
end; {while}
if копиячислителя > 1
then
begin
числ := числ div копиячислителя;
знам := знам div копиячислителя
end
end; {сократитьдробь}
Переменные числ и знам теперь являются формальными пара-
метрами процедуры сократитьдробь. К заголовку процедуры
добавляется список параметров, в котором описаны формальные
параметры. Слово var (variable — переменная), стоящее перед
списком параметров, указывает, что значения параметров могут
изменяться внутри тела процедуры. Теперь вызов новой версии
нашей процедуры происходит с помощью обращений вида
сократитьдробь(числитель, знаменатель)
сократитьдробь(верх, низ)
В этих обращениях переменные числитель, знаменатель, верх
и низ являются фактическими параметрами. Новая версия про-
граммы убратьмножители делает то же, что и предыдущая,
но для связи между главной программой и процедурой сократить-
дробь используются параметры. И переменные, и формальные
параметры процедуры сократитьдробь являются локальными
данными этой процедуры, причем процедура имеет как свое
собственное рабочее пространство (копиячислителя, копиязна-
менателя и остаток), так и связи с остальной частью програм-
мы (числ и знам).
program убратьмножители (input, output);
var
числитель, знаменатель : integer;
procedure сократитьдробь (var числ, знам : integer);
var
4.1. Процедуры
97
копиячислителя, копиязнаменателя,
остаток: integer;
begin
копиячислителя : = числ;
копиязнаменателя : = знам;
while копиязнаменателя =/=0 do
begin
остаток : = копиячислителя mod
коп иязнаменателя;
копиячислителя : = копиязнаменателя;
копиязнаменателя := остаток
end; {while}
if копиячислителя > 1
then
begin
числ : = числ div копиячислителя;
знам :=знам div копиячислителя
end
end; {сократитьдробь}
begin {убратьмножители}
геаб(числитель, знаменатель):
сократитьдробь(числитель, знаменатель);
writein (числитель, знаменатель)
end. {убратьмножители}
Ввод:
9 24
1024 128
Вывод:
3 8
8 1
Различие между формальными и фактическими параметрами
можно разъяснить по аналогии с доказательством теоремы из гео-
метрии. В доказательстве фигурируют углы и длины. Эти углы и
длины соответствуют формальным параметрам процедуры. Если
подставить фактические углы и длины, например 30° и 4 см,
вместо углов и длин, фигурирующих в доказательстве, каждое
высказывание теоремы останется верным. Сделав конкретную
подстановку, мы докажем частный случай теоремы. Это аналогич-
но передаче фактических параметров процедуре. Точно так же,
как геометрическое доказательство является верным для целого
класса фигур, процедура представляет класс вычислений. При
вызове процедуры выполняется какое-то вычисление из этого
класса. Отсюда видно, что любая программа для вычислительной
А
•» ООО О
98
Гл. 4. Процедуры и функции
машины представляет собой класс вычислений, а выполнение
программы с конкретным набором входных данных есть выполне-
ние вычисления, входящего в этот класс.
Программе, вычисляющей частичные суммы гармоничного
ряда, требуется еще одна процедура. Мы должны иметь возмож-
ность складывать два рациональных числа по формуле:
числ __ числ1 . числ2 _ числ1 * знам2 + числ2 * знам1
знам знам! ‘ знам2 знам1*знам2
В соответствии с этой формулой определим процедуру сло-
житьдроби
procedure сложитьдроби (var числ, знам : integer;
числ1, знам1,
числ2, знам2 : integer);
begin
числ := числ1 * знам2 + числ2 * знам1;
знам := знам! * знам2
end; {сложитьдроби}
Переменные числ1, знам1, числ2 и знам2 не изменяются в
процедуре, поэтому в заголовке процедуры их именам не пред-
шествует слово var. Мы можем модифицировать эту процедуру
так, чтобы у нее были только четыре параметра и выполнялось
«присваивание».
числ1 е _числ1 * знам2+ числ2 * знам1
знам! * знам1*знам2
Новое определение выглядит так:
procedure сложитьдроби (var числ1, знам1 : integer;
числ2, знам2 : integer);
begin
числ1 : = числ1 * знам2 + числ2 # знам1;
знам1 := знам1 * знам2
end; {сложитьдроби}
Заметьте, что если в первой версии порядок операторов при-
сваивания был произволен, то во второй он существен.
Программа суммагармоник является полной программой, ис-
пользующей две определенные нами процедуры. Для указанного
числа членов ряда вычисляются частичные суммы Н (п), каждая
такая сумма печатается.
program суммагармоник (input, output);
const
первыйчлен = 2;
var
числитель, знаменатель,
4.1. Процедуры
99
последнийчлен, числочленов : integer;
procedure сократитьдробь(уаг числ, знам : integer);
var
копиячислителя, копиязнаменателя,
остаток : integer;
begin
копиячислителя := числ;
копиязнаменателя := знам;
while копиязнаменателя =7^ 0 do
begin
остаток : = копиячислителя mod
копиязнаменателя;
копиячислителя := копиязнаменателя;
копиязнаменателя := остаток
end; {while}
if копиячислителя > 1
then
begin
числ := числ div копиячислителя;
знам := знам div копиячислителя
end
end; {сократитьдробь}
procedure сложитьдроби (var числ1,знам1 : integer;
числ2,знам2 : integer);
begin
числ1 := числ1 * знам2 + числ2 * знам1;
знам1 := знам1 * знам2
end; {сложитьдроби}
begin {суммагармоник}
числитель := 1;
знаменатель := 1;
геаб(последнийчлен);
for числочленов := первыйчлен to последнийчлен do
begin
сложитьдроби(числитель, знаменатель,
1, числочленов);
сократитьдробь(числитель, знаменатель);
шп(е!п(числитель : 1, 7', знаменатель : 1)
end {for}
end. {суммагармоник}
Ввод:
10
100
Гл. 4. Процедуры и функции
Вывод:
3/2
11/6
25/12
137/60
49/20
363/140
761/280
7129/2520
7381/2520
Переменные и параметры
Очень важно ясно понимать разницу между локальными и гло- ?
бальными переменными, а также разницу между параметрами-
значениями и параметрами-переменными. Рассмотрим различия I
между ними на простом примере с одной процедурой.
program простая (output); i
var £
х : integer;
t
procedure изменить; I
begin I
x 1 |
end; {изменить} |
begin |
x := 0; |
изменить; |
write(x) |
end. {простая} I
Эта программа имеет только одну переменную х, которая яв-
ляется глобальной. Значение х первоначально, в главной про-
грамме, устанавливается равным нулю. Программа обращается к
процедуре изменить, которая изменяет значение х на 1, и это
значение печатается последним оператором программы. Теперь
рассмотрим полностью аналогичную программу, но имеющую
описание локальной переменной:
program простая (output);
var
х : integer;
procedure изменить;
var Л’
х : integer;
begin
4.1. Процедуры
101
х := 1
end; {изменить}
begin
х := 0;
изменить;
write(x)
end. {простая}
У этой версии программы две переменные, обе имеющие имя
л\ Одна из них — глобальная, а другая (описанная в процедуре)
локальна в процедуре. Присваивание х := 1 устанавливает
значение локальной переменной равным 1. Заметьте, что описание
локальной переменной х отменяет внутри тела процедуры описа-
ние глобальной переменной х. На самом деле описание локаль-
ной переменной х, имеющей то же имя, что и глобальная пере-
менная х, вообще лишает процедуру возможности доступа к
глобальной переменной х. Поэтому оператор, х :== 1 не имеет
никакого отношения к глобальной переменной х, значение кото-
рой остается нулевым. Именно это значение и печатается про-
граммой.
В следующей версии программы простая процедура изменить
имеет параметр:
program простая (output);
var
х : integer;
procedure изменить (var у : integer);
begin
У := 1
end; {изменить}
begin
x := 0;
изменить (x);
write(x)
end. {простая}
Программа имеет глобальную переменную х и процедуру
изменить с формальным параметром. Мы назвали этот параметр у
во избежание недоразумений, но результат не изменился бы,
если бы мы у заменили на х. Описанию у в заголовке процедуры
предшествует указатель var. Тем самым у описывается как пара-
метр-переменная, что в свою очередь означает, что у можно рас-
сматривать как синоним фактического параметра х: то, что де-
лается с у, одновременно происходит и с х. В соответствии с
этим, присваивание у :=1 изменит значение фактического пара-
102
Гл. 4. Процедуры и функции
метра хна 1, что в конечном итоге и будет напечатано програм-
мой. Если формальный параметр является параметром-перемен-
ной, соответствующий фактический параметр тоже должен
быть переменной, но никак не выражением. Обращения
изменить (2 * х) и изменить (2)
будут неправильными при
нить, поскольку
нашем определении процедуры изме-
2 * х и 2
являются выражениями, а не переменными.
Рассмотрим теперь, как работает параметр-значение:
program простая (output):
var
х : integer;
procedure изменить (у : integer);
begin
У := 1
end; {изменить}
изменить(х); |
write(x) I
end. {простая} |
Как и раньше, у нас есть глобальная переменная х и формаль-1
ный параметр у. Однако в данном случае перед описанием r/i
нет указания на то, что он параметр-переменная. Такой пара-1
метр называется параметром-значением. При вызове процедурыJ
изменить (х) неявно выполняется оператор присваивания у := х
перед выполнением первого оператора, стоящего в теле процеду-
ры. После входа в тело процедуры никаких связей между х и у
не остается, поэтому присваивание у := 1 не оказывает никакого
влияния на значение х. Программа, следовательно, напечатает то
значение х, которое было у этой переменной перед входом в про-
цедуру, т. е. нуль. Если формальный параметр есть параметр-;
значение, соответствующий фактический параметр может быть
выражением. При нашем последнем определении процедуры изме-
нить можно писать такие обращения, как i
изменить (2 * х) или изменить^)
В первом случае перед выполнением процедуры будет выполнен!
неявный оператор присваивания у 2 * х, а во втором у : — 2.!
Чтобы лучше запомнить, в чем состоит разница между пара-*
4.1. Процедуры
103
метрами-значениями и параметрами-переменными, обратите вни-
мание на следующий факт: фактическим параметром, соответст-
вующим формальному параметру-значению, может быть любой
объект в правой части оператора присваивания, в то время как
фактическим параметром, соответствующим формальному пара-
метру-переменной, может быть только объект, встречающийся
в левой части оператора присваивания.
Выражение, соответствующее параметру-значению, должно
иметь тот же тип, что и сам параметр. Однако, как мы видели в
гл. 2, в языке Паскаль можно использовать выражения целого
типа вместо вещественного, поэтому в качестве фактического
параметра, соответствующего вещественному формальному пара-
метру, можно использовать выражение целого типа.
Синтаксис
На рис. 4.2 приведена синтаксическая диаграмма списка па-
раметров. Список параметров может отсутствовать. В этом слу-
чае, для того чтобы процедура могла оказать какое-нибудь влия-
ние на выполнение программы, она должна пользоваться нело-
кальными переменными. На рис. 4.3 приведена синтаксическая
диаграмма описания процедуры. Заголовок процедуры состоит
из слова procedure, идентификатора процедуры и списка парамет-
ров. Телом процедуры является блок, синтаксическая структура
которого представлена на рис. 2.12. Теперь мы можем дать рас-
ширенное определение блока, включив в него описания процедур
и функций, как показано на рис. 4.4.
Синтаксическая диаграмма для полной программы на языке
Паскаль приведена на- рис. 4.5, который просто воспроизводит
рис. 2.10. Сравнивая рис. 4.3 и 4.5, мы видим, что на самом
деле программа есть та же процедура, но с необычным заголовком
и специальным знаком окончания «.». Стандартные типы, кон-
станты и функции для этой «процедуры» являются нелокальными
идентификаторами. Как следствие, вовсе не запрещается опреде-
лить новое «локальное» значение для любого стандартного иден-
тификатора, хотя лучше этого не делать. Можно рассматривать
программу как процедуру операционной системы и в рамках этой
интерпретации можно считать файлы input и output формальными
параметрами: соответствующими фактическими параметрами бу-
дут тогда файлы, передаваемые программе операционной систе-
мой во время выполнения.
Синтаксические диаграммы описания процедуры (рис. 4.3) и
блока (рис. 4.4) взаимно рекурсивны, вследствие чего процедуры
Могут быть вложенными. Ниже показана структура программы
с вложенными процедурами. Тела процедур обозначены лишь схе-
матически, списки параметров опущены.
104
Гл. 4. Процедуры и функции
Рис. 4.3. Синтаксис описания процедуры
Блок
Рис. 4.4. Синтаксис блока
составной оператор
Рис. 4.5. Синтаксис программы
4.1. Процедуры
105
program вложенность (input, output);
var
a, 6 : integer;
procedure внешняя;
var
в, г : integer;
procedure внутренняя;
var
д, e : integer;
begin {внутренняя}
{операторы внутренней процедуры}
end; {внутренняя}
begin {внешняя}
{операторы внешней процедуры}
end; {внешняя}
begi п {вложенность }
{операторы процедуры вложенность}
end. {вложенность}
Переменные д и е являются локальными по отношению к про-
цедуре внутренняя, они могут использоваться только в этой про-
цедуре. Переменные виг локальны в процедуре внешняя, их
можно использовать в теле процедуры внешняя. Так как проце-
дура внутренняя вложена в процедуру внешняя, эти переменные
могут присутствовать и в теле внутренней процедуры. Переменные
а и б — глобальные, и с ними можно оперировать в любом
месте программы. Процедура внутренняя локальна по отноше-
нию к процедуре внешняя и поэтому может быть вызвана только
из тела этой процедуры, но не из главной программы.
На рис. 4.5 приведен синтаксис вызова процедуры. Факти-
ческие параметры перечислены в списке, следующим за именем
процедуры. Для каждого формального параметра, упомянутого в
описании процедуры, должен найтись параметр в списке опера-
ВызоВ
процедуры
иЗенгпификатпор процедуры
Выражение
Рис. 4.6. Синтаксис вызова процедуры
106
Гл. 4. Процедуры и функции
тора вызова процедуры. Если формальный параметр есть пара-
метр-переменная, соответствующий фактический параметр также
должен быть переменной, а не выражением. Тип фактического
параметра должен совпадать с типом формального параметра, с
одним только исключением: если формальным параметром явля-
ется параметр-значение вещественного типа, фактическим пара-
метром может быть выражение целого типа соответственно обще-
му правилу, по которому целые выражения можно записывать
там, где по контексту должно находиться выражение веществен-
ное.
4.2. Функции
Мы уже встречались с некоторыми стандартными функциями
языка Паскаль. Например, sqrt — это стандартная функция с
одним параметром вещественного или целого типа, которая выдает
значение вещественного типа. В то время как процедура имеет
результат или эффект, функция имеет значение. Если синтакси-
чески вызов процедуры представляет собой разновидность опе-
ратора, то обращение к функции — разновидность множителя.
Все формальные параметры стандартных функций языка —
параметры-значения, а это значит, что обращения к функциям
могут в выражении вкладываться одно в другое. При работе со
стандартными функциями sqrt и sqr можно, например, написать
такую программу:
program треугольник (input, output);
var
перваясторона, втораясторона,
гипотенуза : real;
begin
геас!(перваясторона, втораясторона);
гипотенуза := sqrt(sqr(nepBancTopona) +
sqr(BTopancTOpona));
\угке(гипотенуза)
end.
Эта программа содержит три обращения к функциям. Функция
sqr вызывается дважды, и один раз вызывается функция sqrt
с параметром
sqr (перваясторона) + sqr (втораясторона)
Создание функций
Описание функции похоже на описание процедуры. Ниже
приведено описание функции квадратныйкорень, которая вычис-
ляет квадратный корень своего параметра с помощью того же
алгоритма, что и программа найтиквадратныекорни из гл. 3.
4.2. Функции
107
function квадратныйкорень (значение : real) : real;
const
эпсилон = IE—6;
var
корень : real;
begin
if значение = 0
then квадратныйкорень :=0
else
begin
корень := 1;
repeat
корень : = (значение I корень 4*
корень)I 2
until аЬ5(значение/5дг(корень) — 1)
< эпсилон;
квадратныйкорень := корень
end
end. {квадратныйкорень}
Тип функции указывается в ее заголовке после списка пара-
метров. У функции квадратныйкорень вещественный тип, ее па-
раметр значение тоже вещественного типа. Функция не проверяет
знак параметра, и, если при вызове ей будет передан отрицатель-
ный параметр, возникнет ошибка. Значение, возвращаемое функ-
цией, определяется оператором присваивания, в левой части ко-
торого находится идентификатор квадратныйкорень. В нашем
примере два таких оператора: квадратныйкорень : = 0, выпол-
няющийся, если значение = 0, и квадратныйкорень := корень,
выполняющийся, если значение > 0. С функцией квадратный-
корень можно работать точно так же, как и со стандартной функ-
цией sqrt, поэтому мы можем написать, к примеру:
гипотенуза := квадратный корень
(здг(перваясторона) +
sqr(BTopancTOpoHa))
Между определенной нами функцией квадратныйкорень и
стандартной функцией sqrt имеются два существенных различия.
Во-первых, если функции sqrt передать отрицательный параметр,
выполнение программы будет прервано, а на печать будет вы-
дано сообщение об ошибке. При попытке выполнить с отрицатель-
ным параметром функцию квадратныйкорень, будет выдано
дезориентирующее сообщение типа «деление на нуль». Этот де-
фект легко устранить, проверяя значение параметра внутри тела
функции, но «объяснить» причину ошибки вызывающей програм-
ме не так просто. Во-вторых, основные математические функции.
108
Гл. 4. Процедуры и функции
такие, как sqrt, обычно пишутся разработчиками математичес-
кого обеспечения вычислительной машины с особой тщательно-
стью, чтобы добиться максимально возможной скорости и точ-
ности. Такая функция, как квадратныйкорень, обычно бывает
менее быстрой и менее точной, чем стандартная функция.
Алгоритм процедуры сократитьдробь в разд. 4.1 для нахож-
дения наибольшего общего делителя двух целых чисел может
быть переписан в виде функции:
function нод(числ, знам : integer) : integer;
var
остаток : integer;
begin
while знам =Д 0 do
begin
остаток : = числ mod знам;
числ : = знам;
знам := остаток
end; {while}
нод : = числ
end; {нод}
Мы не производим копирования параметров внутри функции.
Поскольку и числ и знам являются параметрами-значениями,
их копирование выполняется неявно, причем последующая ра-
бота с ними не оказывает никакого влияния на значения факти-
ческих параметров, передаваемых при вызове. С помощью этой
функции мы легко можем переопределить процедуру сократить-
дробь:
procedure сократитьдробь (var числ, знам : integer);
var
делитель : integer;
begin
делитель := нод(числ, знам);
if делитель > 1
then
begin
числ := числ div делитель;
знам := знам div делитель
end
end; {сократитьдробь}
Теперь рассмотрим несколько иное описание этой процедуры
procedure сократитьдробь (var числ, знам : integer);
begin
числ := числ div нод(числ, знам);
4.2. Функции
109
знам := знам div нод(числ, знам)
end; {сократитьдробь}
Хотя это описание короче и у^бнее для чтения, оно хуже
по нескольким причинам. В этой процедуре дважды тратится
время на вычисление наибольшего общего делителя, кроме того,
она выполняет лишнее деление, если наибольший общий делитель
равен 1. Самым важным, однакД^вляется то, что эта процедура
не работает, поскольку первый оператор присваивания изменяет
значение переменной числ, делая второй оператор присваивания
неправильным. В ловушки такого сорта легко попасть, если про-
граммировать слишком поспешно, и в особенности, программируя
непосредственно из математических выкладок.
Синтаксис
Синтаксис описания функции показан на рис. 4.7. В отличие
от определения процедур здесь используется ключевое слово
function, а не procedure, а вслед за списком параметров указы-
описание_________
функции
список параметров
^FUNCTION
идентификатор
Рис. 4.7. Синтаксис описания функции
Рис. 4.8. Синтаксис множителя
по
Гл. 4. Процедуры и функции
вается тип функции. Вызов функции является одной из разно-
видностей понятия множитель; на рис. 4.8 приведена синтак-
сическая диаграмма для множителя, включающая возможность
обращения к функциям. Для каждого формального параметра
из описания функции в обращении к функции должен быть ука-
зан фактический параметр.
Предварительное описаниеп
Вызов процедуры (или функции) может предшествовать полно-
му описанию процедуры, если для этой процедуры дано предвари-
тельное описание, например
procedure раскопки (var клад : real;
var найден : boolean);
forward;
{вызовы процедуры раскопки}
procedure раскопки;
{тело процедуры раскопки}
Заметьте, что список параметров пишется только один раз, в
предварительном описании.
Слово forward называется директивой. Оно имеет специальное
значение только в этом контексте и может использоваться как
обыкновенный идентификатор в других частях программы.
4.3. Рекурсия
В конце девятнадцатого столетия в Европе появилась игра
под названием Ханойские башни. Популярности игры содейство-
вали рассказы о том, что этой игрой заняты служители храма
брахманов и что завершение игры будет означать конец света.
Предметы, которыми пользовались монахи, будто бы состояли
Список допустимых директив зависит от реализации, поэтому, прежде
чем пользоваться даже директивой forward, убедитесь, что версия, с которой
вы работаете, ее «понимает».— Прим. ред.
4.3. Рекурсия
111
из медной платформы с укрепленными на ней тремя алмазными
иглами с насаженными шестьюдесятью четырьмя золотыми дис-
ками. Более современная версия игры, продававшаяся в магази-
нах всем желающим, состояла из восьми картонных колец,
размещенных на трех деревянных стержнях. Цель игры — пере-
нести башню с левой иглы (см. рис. 4.9) на правую, причем за
один раз можно переносить только одно кольцо, кроме того,
запрещается помещать большее кольцо над меньшим.
Предположим, что иглы пронумерованы числами 1, 2 и 3 и
что монахам надо перенести 64 диска с иглы 1 на иглу 3. Обозна-
чим поставленную задачу таким образом:
перенестибашню(£А, 1,3)
Наша цель будет заключаться в разработке алгоритма, который
подскажет монахам последовательность корректных перемеще-
ний, в результате чего задача будет решена. К простому решению
приводит догадка о том, что основное внимание нужно обратить
на нижний диск иглы 1, а не на верхний. Задача перенестибаш-
ню (64,1,3) после этого оказывается эквивалентной следующей
последовательности подзадач:
перенестибашню (63,1,2);
перенести диск с иглы 1 на иглу 3;
перенестибашню (63,2,3)
Это маленький, но значительный шаг к решению. Маленький,
поскольку нам все еще надо дважды переносить по 63 диска.
Значительный, потому что мы можем повторить подобный анализ
столько раз, сколько будет необходимо. Например, задача
перенестибашню (63,1,2)
может быть выражена как
перенестибашню (62,1,3);
перенести диск с иглы 1 на иглу 2;
перенестибашню (62,3,2)
Для построения общего алгоритма нам нужно указывать, какой
иглой можно пользоваться как временным хранилищем. Это мож-
но сделать, расширив нашу запись так, чтобы
перенестибашню (/г, а, Ь, с)
значило бы
перенести п дисков с иглы а на иглу &,
используя иглу с для временной башни
Мы можем утверждать, что задача
перенестибашню (и, а, Ь, с)
112
Гл. 4. Процедуры и функции
может быть решена за три шага:
перенести башню (п— 1, а, с, Ь)\
перенести диск с а на Ь\
перенести башню (п— 1, с, &, а)
Этот алгоритм не имеет смысла для случая n < 1, поэтому добав-
ляем правило:
ничего не делать, если п 1
Теперь, наконец, можно на языке Паскаль написать процедуру,
выполняющую эти действия:
procedure перенестибашню (высота, сиглы,
наиглу, рабочаяигла:
integer);
begin
if высота > О
then
begin
перенестибашню(высота — 1, сиглы,
рабочаяигла, наиглу);
перенестидиск(сиглы, наиглу);
перенестибашню(высота — 1,
рабочаяигла, наиглу, сиглы)
end
end; {перенестибашню}
Возникает вопрос: может ли процедура обращаться сама к себе
таким образом? К счастью, ответом будет «да», причем такое об-
ращение называется рекурсивным вызовом процедуры. Программа
ханойскиебашни печатает последовательность перемещений,
нужных для переноса башни произвольной высоты с иглы 1 на
иглу 3.
program ханойскиебашни (input, output);
var
всегодисков : integer;
procedure перенестибашню (высота, сиглы,
наиглу, рабочаяигла :
integer);
procedure перенестидиск (взять, положить :
integer);
begin
writeln(B3HTb, положить)
end; (перенестидиск}
begin {перенестибашню}
4 3. Рекурсия
113
if высота > О
then
begin
перенестйбашню(высота — 1, сиглы,
рабочаяигла, наиглу);
перенестидиск(сиглы, наиглу);
перенестибашню(высота — 1, рабочаяигла,
наиглу, сиглы)
end
end; {перенестибашню}
begin {ханойскиебашни}
геаб(всегодисков);
перенестибашню (всегодисков, 1, 3, 2)
end. {ханойскиебашни}
Ввод:
3
Вывод:
1->3
1->2
3->2
1->3
2->1
2->3
1->3
Рекурсия в Паскале возможна благодаря тому, что при вызове
процедуры динамически создаются новые локальные перемен-
ные х). Предположим, программа ханойскиебашни выполняется
для случая с общим количеством дисков, равным 3. Первое об-
ращение к процедуре перенестибашню выглядит так:
перенестибашню (3, 1, 3, 2)
При выходе в процедуру будем иметь
высота = 3 наиглу = 3
сиглы — 1 рабочаяигла = 2
Так как высота > 0, первым действием процедуры будет вызов
перенестибашню (2, 1, 2, 3)
Наоборот, решение допустить в языке Паскаль рекурсивные вызовы
процедур или функций приводит к тому, что приходится динамически созда-
вать локальные переменные этих процедур. Как правило, это требует некото-
рых затрат, и задача выполняется медленнее, чем в случае ее решения без ре-
курсивных механизмов. Однако введение таких накладных расходов обычно
оправдывается полезностью самого принципа рекурсии.— Прим, ред,
114 Гл 4. Процедуры и функции
На этот раз при входе в процедуру перенестибашню имеем
высота = 2 наиглу = 2
сиглы = 1 рабочаяигла = 3
Эти шаги иллюстрируются на рис. 4.10(а—в). При втором входе
в процедуру для параметров отводится новое место. На этих ди-
аграммах имена переменных представлены первыми буквами.
Рис. 4.10. Рекурсия в программе ханойские башни
Значения на первом уровне не уничтожаются, но становятся
недоступными программе вплоть до завершения второго обраще-
ния к процедуре перенестибашню.
Полная картина работы программы ханойскиебашни для
случая с общим количеством дисков, равным 3, приведена в
табл. 4.1. Здесь также имена представлены первыми буквами.
Перенос с иглы а на иглу b записывается как а-+Ь. Дейст-
вия на уровне 4, когда высота = 0, не расписаны. Время, нуж-
ное программе ханойскиебашни для переноса башни, состоящей
из п дисков, приблизительно пропорционально 2Л; можно ска-
зать, что этот алгоритм имеет экспоненциальную сложность в
смысле временных затрат. Алгоритмы этого типа применять
нежелательно, поскольку даже для вполне разумных значений
входных данных время выполнения может становиться слишком
большим. Самые мощные современные вычислительные машины,
решая задачу Ханойской башни, могут вычислить (но не напе-
чатать) одно перемещение за одну миллионную долю секунды.
Даже при такой скорости для расчета переноса башни из 64 дис-
ков им потребуется около миллиона лет.
4.3. Рекурсия
115
Таблица 4.1
Трассировка программы Ханойские башни
уровень 0 уровень 1 уровень 2 уровень 3 перенос
всегодисков =3 to П II и и и 4 СО О X сх "О СО СО to <м СО СО СО тЧ to " 11 11 11 f II II II к f •»-< со о х сх со tO СО г- СО ^ч СО тч СО тЧ тч ГН СО СО СО 11 " 11 11 1 " 11 11 11 t II п II II t II и и и * СООХСХгЧ СО OX cxto СО О X Q.CO О О X го со 7^ 77 10 4 4 If ft f v. т-1 ГО гч СО СО -гч
Рекурсия и итерация
Рекурсивное решение проблемы состоит из двух этапов. Пер-
вый этап решения заключается в сведении данной проблемы к
новой, похожей на исходную, но несколько проще. При решении
задачи Ханойской башни этот этап состоял в замене переноса
64 дисков переносом 63 дисков. Подобная замена может произво-
диться повторно до тех пор, пока решение проблемы не станет
тривиальным. Задача Ханойской башни будет решена, когда
для переноса не останется ни одного диска.
Многие математические функции можно выразить рекурсивно.
116 Гл. 4. Процедуры и функции
Например, для положительных значений п имеем:
( 1, если п = 0;
Xn = \ „ , | XXX""1, если п > 0;
(1, если п = 0;
n! = i
\nx(n— 1)!, если п > 0;
' 1, если п = 0,
Pn (x) = X, если п= 1,
I ((2„— l)xPn_t (х)—(п— 1) Рп_2 (х))/п,
если п > 1;
По таким определениям нетрудно составить описания рекур-
сивных функций на языке Паскаль. Для третьего примера,
представляющего собой рекурсивное определение полиномов
Лежандра, получим:
function р (n : integer; х : real) : real;
begin
if n = о
then p := 1
else if n = 1
then p := x
else p :== ((2*n—l)«x*p(n—l,x) —
(n—l)«p(n—2, x))/n
end;
Однако в большинстве случаев рекурсивное решение не является
лучшим, так как более простое решение может быть найдено с
помощью итерации. Полиномы Лежандра, например, могут быть
более эффективно вычислены следующим образом:
function р (n : integer; х : real) : real;
var
предыдущий, этот, следующий : real;
счетчик : integer;
begin
if n = О
then p := 1
else if n = 1
then p := x
else
begin
предыдущий := 1;
этот := x;
for счетчик := 2 to n do
begin
4.3. Рекурсия
117
следующий := ((2 * счетчик — 1)
* х * этот
— (счетчик — 1) *
предыдущий) / счетчик;
предыдущий := этот;
этот := следующий
end; {for}
р : = следующий
end
end; {р}
Выбор между рекурсией и итерацией обычно определяется
требованиями к объему рабочей памяти. При решении задачи
Ханойской башни информация о позиции дисков на каждом
этапе хранилась в локальных переменных, и если для этих пере-
менных места в памяти не хватает, проблема становится нераз-
решимой. Можно найти и нерекурсивное решение, но в данном
случае рекурсия выглядит просто и естественно. С другой сто-
роны, класс функций, имеющих определение вида
G (х), если и = 0;
Н если п>0;
может всегда быть выражен итеративно, так что рекурсивное ре-
шение не является обязательным.
В рассмотренных выше случаях процедуры или функции
вызывали себя сами, это называется прямой рекурсией. Кроме
того, процедура Р может вызывать процедуру Q, которая в
свою очередь вызывает процедуру Р: это называется косвенной
рекурсией. Ниже приведен пример, в котором используется кос-
венная рекурсия.
Пример: Моделирование карманного калькулятора
В этом разделе мы детально рассмотрим построение програм-
мы, моделирующей работу карманного калькулятора. Здесь
иллюстрируется принцип разработки программы «сверху вниз», а
конечная программа демонстрирует отношения между рекурсив-
ной процедурой и рекурсивной структурой данных.
Программа читает задачу, синтаксис которой приведен на
рис. 4.11. Задача может быть, например, такой:
180 /(2 * 3.14159), 16 * 62.5 * 27, 169 * (5+8);
Программа произведет последовательное вычисление этих выра-
жений и ответит
28.647913 27000 2197
Выражение состоит из слагаемых и множителей, а множитель
118
Гл. 4. Процедуры и функции
строка
ЦЦ(рр
(5)
(в)
(9)
Задача
(е)
Рис. 4.11. Синтаксис задачи для калькулятора
4.3. Рекурсия
119
может содержать выражение, поэтому выражения могут строить-
ся рекурсивно. Буква П может встречаться в позиции множителя,
но ее интерпретацию мы отложим до пятой главы.
Синтаксис разрабатывался так, чтобы можно было проводить
анализ без возвратов. Это значит, что программа может читать
за один раз один символ с входного носителя и сразу безошибоч-
но выбирать правильную последовательность действий: возмож-
ность неверного выбора и повторения каких-либо действий исклю-
чается. Например, если на входе ожидается появление множите-
ля, следующим символом может быть цифра, буква П или левая
скобка: любой другой символ будет ошибочным. В соответствии с
этим мы построили нашу программу так, чтобы очередной символ
всегда мог быть проанализирован.
Первый вариант нашей программы будет выглядеть так:
program калькулятор (input, output);
const
точкасзапятой =
var
следсимвол : char;
результат : real;
begin
читатьсммвол (следсимвол);
while следсимвол =# точкасзапятой do
begin
читатьвыражение(следсимвол, результат);
\угНе1п(результат)
end {while}
end. {калькулятор}
Процедура читатъсимвол читает следующий символ из вход-
ного источника. Процедура читатьвыражение продолжает чте-
ние и анализ до тех Пор, пока не встретится конец выражения, воз-
вращая значение выражения в переменной результат. Если
символ, следующий За выражением, есть точка с запятой, вы-
полнение программы завершается, в противном случае произво-
дится чтение следующего выражения. Недостатком этой програм-
мы является то, что в качестве ограничителя выражения она вос-
принимает любой символ, кроме точки с запятой, а не обязатель-
но запятую. Мы исправим это позже, при описании механизма
обработки этой программой ошибок.
Выражение должно содержать по меньшей мере одно слагае-
мое, но после первого слагаемого очередные слагаемые могут
последовать, а могут и нет. Оператор цикла с пред-условием
является наиболее подходящей конструкцией в процедуре чи-
татьвыражение.
procedure читатьвыражение (var символвыр : char;
120
Гл. 4. Процедуры и функции
var взначение : real);
const
плюс =
минус = '—
var
аддитивнаяоперация : char;
значениеследующегослагаемого : real;
begin
читатьслагаемое(символвыр, взначение);
while (символвыр = плюс) or
(символвыр = минус) do
begin
аддитивнаяоперация : символвыр;
читатьсимвол (символвыр);
читатьслагаемое(символвыр,
значениеследующегослагаемого);
if аддитивнаяоперация = плюс
then взначение := взначение +
значениеследующегослагаемого
else взначение := взначение —
значениеследующегослагаемого
end {while}
end {читатьвыражение}
Процедура читатьслагаемое может быть составлена аналогич-
ным образом с использованием знаков мультипликативных опе-
раций * и / вместо знаков аддитивных операций. Процедура
читатьмножшпель слегка отличается, так как в ней необходимо
рассматривать несколько вариантов.
procedure читатьмножитель (var символмнож : char;
var мзначение : real);
const
нуль — 'О';
девять = '9';
леваяскобка — '(';
праваяскобка = ')';
begin
if (нуль символмнож) and
(символмнож девять)
then читатьчисло(символмнож, мзначение)
else if символмнож = леваяскобка
then
begin
читатьсимвол (символмнож);
читатьвыражение(символмнож, мзначение);
if символмнож = праваяскобка
4.3. Рекурсия
121
then читатьсимвол(символмнож) ‘
else выдатьошибку {ожидается ')}
end
else выдатьошибку {запрещенный символ}
end {читатьмножитель}
Процедура читатьчисло похожа на программу преобразовать
из гл. 3. Рассмотрим теперь процедуру выдатьошибку.
Программа может распознавать три ошибочные ситуации:
отсутствие запятой в конце выражения, отсутствие правой скобки
после выражения, которое должно быть заключено в скобки, и
появление на входе при чтении множителя запрещенного символа.
Кроме того, процедура читатьслагаемое, выполняющая умноже-
ния и деления, должна выдавать ошибку, если делитель оказался
равным нулю. Простейшим решением будет печать одного из
четырех сообщений, но много пользы это не принесет, поскольку
при этом останется неизвестным место возникновения ошибки.
Значительно полезнее будет указывать не то, какая обнаружена
ошибка, а место, где она произошла. Это легко будет делать,
если процедура читатьсимвол будет выполнять дополнительную
работу: указывать позицию текущего символа во входной строчке.
Это означает, что у программы читатьсимвол будут два парамет-
ра, причем потребуются соответствующие изменения в уже на-
писанных процедурах.
При написании процедуры читатьсимвол мы введем три не-
больших усовершенствования, благодаря чему с конечной прог-
раммой будет проще работать:
1. Процедура читатьсимвол будет пропускать пробелы, так
что пользователь сможет вставлять пробелы в своих зада-
чах для наглядности.
2. Нам будет удобно кончать выражения вместе с концом
строчки, поэтому символ конец-строчки будет переводить-
ся в запятую.
3. Пользователь может забыть поставить точку с запятой в
конце задачи, поэтому символ конец-файла будем перево-
дить в точку с запятой.
Теперь можно написать процедуру читатьсимвол:
procedure читатьсимвол (var сим : char;
var позиция : integer);
const
запятая
точкасзапятой =
пробел . = '
begin
repeat
if eof
122
Гл. 4. Процедуры и функции
then сим := точкасзапятой
else if eoln
then
begin
позиция : = 0;
сим := запятая;
readln
end
else
begin
позиция := позиция + 1;
геас1(сим)
end
until сим^пробел
end ; {читатьсимвол}
Процедура выдатьошибку печатает маркер под ошибочным
символом. Если ошибка была обнаружена в выражении, его
дальнейший анализ становится бессмысленным, поэтому про-
цедура выдатьошибку пропускает символы во входном файле,
пока не будет найдена запятая или точка с запятой.
procedure выдатьошибку (var ошсимвол : char;
var ошпозиция : integer);
const
маркер = ' f ';
запятая = ',
точкасзапятой =
begin
writeln(MapKep : ошпозиция);
while not ((ошсимвол=запятая)
or (ошсимвол=точкасзапятой)) do
читатьсимвол(ошсимвол,ошпозиция)
end; {выдатьошибку}
•
Учтя все предыдущие замечания, мы теперь можем написать
полную программу, использующую описанные нами процедуры,
program калькулятор (input, output);
const
запятая=' ;
точкасзапятой
var
следсимвол : char;
следпозиция : integer;
результат : real;
procedure читатьсимвол (var сим : char;
var позиция : integer);
const
4.3. Рекурсия
123
запятая='
точкасзапятой=';';
пробел=' ';
begin
repeat
if eof
then сим := точкасзапятой
else if eoln
then
begin
позиция := 0;
сим := запятая;
readln
end
else
begin
позиция := позиция + 1;
геад(сим)
end
until сим #= пробел
end; {читатьсимвол}
procedure выдатьошибку (var ошсимвол : char;
var ошпозиция : integer);
const
маркер—7 f ';
запятая=',';
точкасзапятой
begin
writeln(MapKep : ошпозиция);
while not ((ошсимвол= запятая)
or (ошсимвол = точкасзапятой)) do
читатьсимвол(ошсимвол,ошпозиция)
end; {выдатьошибку}
procedure читатьчисло (var символчис : char;
var чиспозиция : integer;
var чзначение : real);
const
нуль = 707 ;
девять = '9';
точка =
основание = 10;
var
счетчик, масштаб : integer;
begin
чзначение := 0;
while (нуль < символчис) and
(символчис С девять) do
begin
чзначение := основание * чзначение
читатьсимвол(символчис,чиспозиция)
end; {while}
if символчис = точка
then
+ оМ(символчис)
— огЬ(нуль);
124
Гл. 4. Процедуры и функции
begin
читатьсимвол(символчис,чиспозиция);
масштаб := 0;
while (нуль с символчис) and
(символчис < девять) do
begin
чзначение := основание * чзначение
+ огб(символчис) — огб(нуль);
читатьсимвол (символчис,чиспозиция);
масштаб := масштаб + 1
end; {while}
for счетчик := 1 to масштаб do
чзначение := чзначение / основание
end
end; {читатьчисло}
procedure читатьвыражение (var символвыр : char;
var вырпозиция : integer;
var взначение : real);
const
плюс = '+';
минус = '—
var
аддитивнаяоперация : char;
значениеследующегослагаемого : real;
procedure читатьслагаемое (var символслаг : char;
var слагпозиция : integer;
var сзначение : real);
const
умножить ='*';
разделить =7';
var
мультипликоперация : char;
множследзначение : real;
procedure читатьмножитель (var символмнож : char;
var множпозиция : integer;
var мзначение : real);
const
нуль = 'O';
девять — '9';
леваяскобка = '(';
праваяскобка = ')';
begin {читатьмножитель}
if (нуль с символмнож) and
(символмнож С девять)
then читатьчисло(символмнож,
множпозиция, мзначение)
else if символмнож = леваяскобка
then
begin
читатьсимвол(символмнож,
множпозиция);
читатьвыражение(символмнож,-
множпозиция, мзначение);
if символмнож = праваяскобка
then читатьсимвол(символмнож^
множпозиция)
else выдатьошибку(символмнож,
4.3. Рекурсия
125
множпозиция)
end
else
begin
выдатьошибку(символмнож$
множпозиция);
мзначение := О
end
end; {читатьмножитель}
begin {читатьслагаемое}
читатьмножитель(символслаг,
слагпозиция, сзначение);
while (символслаг = умножить) ог
(символслаг = разделить) do
begin
мультипликоперация :=
символслаг;
читатьсимвол(символ слаг,слагпозиция);
читатьмножитель(символслаг,
слагпозиция, множследзначение);
if мультипликоперация = умножить
then сзначение := сзначение *
множследзначение
else if множследзначение #=0
then сзначение сзначение
множследзначение
else выдатьошибку (символслаг,
слагпозиция)
end {while}
end; {читатьслагаемое}
begin {читатьвыражение}
читатьслагаемое(символвыр,вырпозиция,взначение);
while (символвыр = плюс) ог
(символвыр = минус) do
begin
аддитивнаяоперация := символвыр;
читатьсимвол(символвыр, вырпозиция);
читатьслагаемое(символвыр, вырпозиция,
значследслагаемого);
if аддитивнаяоперация = плюс
then взначение := взначение +
значследслагаемого
else взначение := взначение —
значследслагаемого
end {while}
end; {читатьвыражение}
begjp {калькулятор}
следпозиция 0;
читатьсимвол(следсимвол, следпозиция);
while следсимвол У= точкасзапятой do
begin
читатьвыражение(следсимвол,следпозиция
результат);
126
Гл. 4. Процедуры и функции
if (следсимвол = запятая) ог
(следсимвол = точкасзапятой)
then write In (результат)
else выдатьошибку(следсимвол, следпозиция);
читатьсимвол(следсимвол, следпозиция)
end {while}
end.{калькулятор}
4.4. Нелокальные переменные и побочные эффекты
Мы уже видели, что процедуры и функции могут использо-
вать и изменять значения нелокальных переменных. Если про-
цедура или функция изменяет значения нелокальных перемен-
ных, говорят, что она имеет побочный эффект. Термин побоч-
ный эффект имеет то же значение, что и в фармакопии. Лекарство
призвано воздействовать на человека специфическим образом,
и все дополнительные влияния называются побочными эффектами.
При этом подразумевается, что побочные эффекты вредны или
по крайней мере нежелательны 1}. В программировании, в про-
тивоположность фармакопии, мы имеем полный контроль над
побочными эффектами. Более того, они могут не принести ника-
кого вреда программам: главным возражением против них яв-
ляется то, что они затемняют структуру программы, делая ее
трудной для понимания.
Процедуры из программы калькулятор не обращаются к нело-
кальным переменным и не имеют никаких побочных эффектов.
Благодаря этому такие процедуры можно читать как единое
целое, так как они работают только со своими собственными
константами, переменными и параметрами. С другой стороны,
структура программы в целом при этом несколько затемняется.
Каждый раз обрабатывается только один символ, но этот символ
может иметь имена сим, ошсимвол, символчис, символмнож,
символслаг, символвыр и следсимвол. В дальнейшем мы приведем
другую версию этой программы, в которой процедуры будут
иметь побочные эффекты, и сравним две программы.
При написании процедур все «за» и «против» использования
нелокальных переменных должны быть тщательно взвешены.
Процедура должна обращаться или изменять значения нело-
кальных переменных только в тех случаях, если для этого есть
веские причины. Пользуйтесь критериями понятности и безо-
пасности. При коллективной работе над программой нужно
строже придерживаться дисциплины, чем при индивидуальном
программировании.
И Это утверждение относится к фармакопии и, пожалуй, к функциям.
Если говорить о процедурах, то весь их смысл заключается в создании побоч-
ных эффектов. Другое дело, что эти эффекты лучше явно регулировать меха-
низмом параметров. Правда, это часто приводит к ухудшению эффективности
работы процедур.— Прим. ред.
4 5. Псевдослучайные числа
127
При написании функций вообще не пользуйтесь нелокальны-
ми переменными и не изменяйте их значений. Относитесь к функ-
циям, как к математическому понятию, и пишите такие функции,
значения которых зависят только от их параметров.
Берегитесь неожиданных побочных эффектов. Предположим,
что у вас есть переменная х в процедуре, но вы забыли описать ее.
Чаще всего компилятор расценит это как ошибку, но если вы
описали х как переменную в объемлющей процедуре или в глав-
ной программе, никаких сообщений об ошибке выдано не будет.
Это может привести к самому неожиданному поведению програм-
мы. Мораль здесь тройная: не используйте одинаковых имен на
разных уровнях; не забывайте включать в программу описания
при введении новых переменных, вставляйте описания для них
незамедлительно; не пользуйтесь именами, подобными х.
4.5. Псевдослучайные числа
Предположим, у нас есть возможность выбирать случайным
образом число г из множества целых чисел 1, 2, . . ., N. При
небольших значениях N для такого выбора существуют простые
механизмы. Для N=2 можно подбрасывать монету, для
кидать игральную кость, а для М=36 можно воспользоваться
колесом рулетки. Последовательность гх, г2, . . . таких целых
чисел называется случайной последовательностью. Алгоритм,
вырабатывающий случайную или кажущуюся случайной после-
довательность, называется генератором случайных чисел. Неко-
торые примеры программ из этой книги написаны с помощью ге-
нератора случайных чисел, и здесь мы ненадолго отвлечемся от
нашей темы и посмотрим, как можно построить генератор слу-
чайных чисел.
Чаще всего для генерации случайных чисел применяется ме-
тод, называемый линейным конгруэнтным методом. Каждое
число rk, входящее в последовательность, вычисляется при
известном значении своего предшественника rk_1 по формуле:
rk=(множитель * г^+добавка) mod модуль
Числа, получающиеся при повторных применениях этой фор-
мулы, по правде говоря, не являются случайными в том смысле,
в каком случайны подбрасывания монеты или выпадения иг-
ральной кости, так как мы всегда в состоянии определить значе-
ние rk по заданному значению г0. Последовательность, формируе-
мую по этой формуле, будет правильнее называть псевдослучайной
последовательностью, а числа, входящие в нее,— псевдослучай-
ными числами. В программировании случайные числа применя-
ются довольно широко, и в большинстве случаев подходят псев-
128
Гл. 4. Процедуры и функции
дослучайные числа при условии, что приняты некоторые эле-
ментарные меры предосторожности.
На многих вычислительных машинах имеется стандартный
генератор псевдослучайных чисел линейного конгруэнтного типа
с тщательно подобранными значениями коэффициентов множи-
тель, добавка и модуль. Если на вашей машине есть такой гене-
ратор и к нему можно обращаться из программ, написанных на
Паскале, пользуйтесь им. Если у вас нет доступа к такому ге-
нератору, можете воспользоваться следующим простым алго-
ритмом: он будет работать на большинстве вычислительных ма-
шин и сформирует 65 536 случайных чисел до того, как начнет
повторяться, кроме того, он подходит для всех примеров из
этой книги. Примем такие коэффициенты:
модуль=216=65536,
множитель=25173.
добавка = 13849,
и, следовательно, имеем
/*=(25173*^+13849) mod 65536.
Такие вычисления не вызовут переполнения на машинах, у ко-
торых
maxint^231—1.
Генератор псевдослучайных чисел оформляется на языке Па-
скаль в виде функции:
function случайноечисло (var заготовка : integer)
: integer;
const
множитель=25173;
добавка =13849;
модуль=65536;
begin
случайноечисло := заготовка;
заготовка := (множитель « заготовка + добавка)
mod модуль
end;
Заметьте, что программа этой функции противоречит правилу,
гласящему, что функция не должна менять значения своих пара-
метров. Так как нам нужно, чтобы функция случайноечисло при
каждом обращении возвращала разные значения, очевидно, что
данное правило не применимо. Для экономии места при исполь-
зовании функции в примерах вместо поименованных констант
будем пользоваться непосредственно значениями множителя.
4.5. Псевдослучайные числа
129
добавки и модуля. Функция выдает перестановки из чисел
О, 1, 2, . . 65535
а затем, исчерпав их, выдает ту же последовательность повторно.
Первым выдаваемым числом будет начальное значение перемен-
ной заготовка.
Для большинства применений такие числа должны быть спе-
циальным образом масштабированы. Например, если программа
имитирует выпадения игральной кости, мы можем написать
var
показание, заготовка : integer;
показание := случайноечисло(заготовка) mod 6 + 1
Часто нужны числа, находящиеся в диапазоне от 0 до 1. Для этих
целей можно применять модифицированную версию функции
случайноечисло:
function случайноечисло (var заготовка : integer)
: real;
begin
случайноечисло := заготовка/65535;
заготовка := (25173 * заготовка + 13849)
mod 65536
end;
Эта функция будет выдавать числа, для которых выполняются
неравенства
Ъ^случайноечисло(заготовка)^ 1
Функцию легко модифицировать так, чтобы исключить число О
или 1.
Пример: Вычисление объема
Одно из применений случайные числа нашли в вычислениях
площадей и объемов. Предположим, у нас есть некоторое не-
правильной формы тело S, заключенное в кубе С. Генерировать
случайные точки, входящие в С, легко, и можно показать, что
вероятность для случайной точки, находящейся в С, одновре-
менно принадлежать S равна Vs/VCl где Vc есть объем С,а есть
объем S. Если мы располагаем простым критерием, чтобы опре-
делить, принадлежит ли данная точка S, то можно найти объем S,
генерируя случайные точки и подсчитывая число тех, которые
попадают внутрь S. Программа объемсферы вычисляет объем
сферы именно таким образом. Куб С определяется так:
И<1.
5 № 3388
130
Гл. 4. Процедуры и функции
Одна восьмая часть сферы
x2+f/2+z2^l
находится внутри куба, поэтому если генерировать случайную
точку (%, £/, z) внутри куба, то вероятность, что она будет также
находиться внутри сферы, равна V$/8. Абсурдно применять этот
метод для вычисления объема сферы, но он оказывается весьма
полезным в тех случаях, когда может быть найден простой кри-
терий принадлежности заданному телу, а формулы для опреде-
ления объема нет.
program объемсферы (input, output);
var
попытка, числопопыток, попадание,
заготовкадляслучайногочисла : integer;
function случайноечисло (var заготовка : integer)
: real;
begin
случайноечисло := заготовка / 65535;
заготовка := (25173 * заготовка + 13849)
mod 65536
end; {случайноечисло}
begin
геаб(числопопыток);
попадание := 0;
for попытка : = 1 to числопопыток do
if здг(случайноечисло
(заготовкадляслучайногочисла))
+здг(случайноечисло
(заготовкадляслучайногочисла))
+здг(случайноечисло
(заготовкадляслучайногочисла))^!
then попадание := попадание +1;
writelnfВычисленный объем =',
8 * попадание/числопопыток)
end {объемсферы}
Ввод:
5000
Вывод:
Вычисленный объем=4.16800
Упражнения
131
Упражнения
4.1. Что случится, если программа убратьмножители прочитает отрицатель-
ное число? Подправьте программу так, чтобы она выдавала правильный ре-
зультат и в этих случаях.
4.6. Укажите, что напечатают приведенные ниже программы, и объясните
ваши ответы.
(a) program абсурд (output);
var
штука : integer;
procedure обман (var иа, ya ; integer);
begin
иа := —1;
ya := —иа
end;
begin
штука 1;
обман(штука, штука);
writeln(urryKa)
end.
(6) program вздор (output);
var
штука : integer;
procedure ложь (var иа : integer; ya : integer);
begin
иа := 10 * ya
end;
begin
штука := 10;
ложь(штука, штука);
writeln(urryKa)
end.
4.3. Составьте описания следующих функций и проверьте ваши решения:
(а) . Обратные тригонометрические функции:
sin”1 (х) =- tan ”1 (х/ ]/ (1 — х2)),
cos”1 (х) = tan”1 (]Л(1 — х2)/х) (х > 0),
(б) . Гиперболические функции:
s i n h (х) - (ех—е ” х)/2,
cosh (х) = (е*+г “ *)/2,
tanh (x)=sinh(x)/cosh(x).
(в) Обратные гиперболические функции
sinh-1(x)=ln (х+ ]Л(х2+1)),
cosh’1(x)=ln(x+ /(х3—1)).
4.4. Напишите функцию цифра (п, k), которая выдает значение &-той циф-
5*
132
Гл. 4. Процедуры и функции
ры справа от начала числа п. Например,
цифра (254693,2)=9 цифра (7622,6)=0
4.5. Функция 0=arctg (х) выдает значение такое, что —л72<0<л;/2.
Напишите функцию atan (х, у), которая с учетом знаков х и у вычисляет зна-
чение 0, такое, что tg ($)=у/х и —л<0<л;.
4.6. Вездеход может проехать 500 километров с полностью заправленным
баком. От начального склада горючего, содержащего топливо на N заправок^
устанавливая по пути склады с горючим, вездеход может проехать
L=500 (1+1/3+1/5+. . .+ 1/(2^1))
километров. Напишите функцию, вычисляющую значение N по данному зна-
чению L.
4.7. Модифицируйте программу ханойскиебашни так, чтобы она не вызы-
вала себя, если дисков для переноса нет. Дополните ее так, чтобы в результате
работы программы печаталась таблица, подобная табл. 4.1.
4.8. Напишите итеративную и рекурсивную функции для вычисления зна-
чений полиномов Эрмита Нп (х):
Но(х)=1,
Ht(x)=2x,
Нп(х)=2хЯ„_1(х)—2(п—1)ЯП_2М для л>1.
Сравните затраты времени при работе этих двух функций.
4.9. Напишите рекурсивную функцию для вычисления значений функции
Аккермана, Ack(/n, и), определенной для и п>0 с помощью формул
Аск (0, п)=/г+1,
Ack (т, 0)=Ack(m—1, 1),
Аск (т, n)=Ack(zn— 1, Ack(m, /г—1)) для /и>0 и п>0.
Внутрь тела функции вставьте оператор печати, чтобы каждое обращение
к функции было зафиксировано.
4.10. Программа калькулятор не допускает выражений, начинающихся
со знака операции, например: —16 *27.5, +3183—2475. Модифицируйте
первую синтаксическую диаграмму, а затем и программу, чтобы устранить
этот недостаток.
4.11. Нарисуйте синтаксическую диаграмму для выражений, которые
могут включать операцию возведения в степень ' f', где
a f b= аь.
Возведение всегда производится перед выполнением любой другой операции?
если только скобочная структура не указывает обратного. Выражение a f b f с
запрещено. Напишите программу, которая вычисляла бы выражения, содер-
жащие степени. Воспользуйтесь тем фактом, что
a f х=ехр (хХ1п(а)) для а>0.
4.12. Нарисуйте синтаксическую диаграмму, подходящую для комплек-
сных констант. Напишите процедуру для чтения комплексных констант. При-
мените эту процедуру в программе, вычисляющей комплексные выражения.
4.13. Насколько точна программа объемсферы? Не кажется ли вам, что
5000000 случайных точек дадут лучший результат, чем 50000 точек? Не будет
ли лучше генерировать вместо этого прямоугольную решетку из точек?
Упражнения
133
4.14. Воспользуйтесь методом случайных чисел для вычисления объема$
заключенного между поверхностями, которые определяются с помощью сле-
дующих уравнений в полярных координатах (г, 0, z):
|z|<e“r2, r^5.
4.15. Частица движется случайным образом согласно следующим усло-
виям: В момент /=0 частица находится в исходной точке, х=0, z/=0. В момен-
ты времени /=1, 2, 3, . . . она делает случайный шаг в одном из четырех на-
правлений
х := х—1 у := у—\
х:=х+1 у=у-\-\
Движение заканчивается, когда х2+*/2>Я2. Экспериментально определите
отношение между временем движения и значением 7?.
1
Глава 5.
ПЕРЕМЕННЫЕ ТИПЫ1’
Во второй главе мы обсуждали стандартные типы Паскаля:
целый, вещественный, булевский и символьный. Характеристики
этих типов полностью определяются используемой нами конкрет-
ной реализацией языка. В этой главе, так же как и в последую-
щих трех главах, мы представим дополнительные абстрактные
типы, характеристики которых мы можем определять самостоя-
тельно. Введение таких типов расширяет возможности языка
Паскаль сразу в двух отношениях. Во-первых, с определением
подходящих типов появляется возможность яснее и точнее ста-
вить задачу, чю в свою очередь облегчает составление и чтение
программы. Во-вторых, определение типов дает нам возможность
передавать компилятору больше информации о программе. При
помощи этой информации компилятор может проделать более
тщательные проверки синтаксических ошибок и сформировать
более эффективную рабочую программу.
Новые типы определяются в разделе определения типов, ра-
сполагающемся между разделами определения констант и описа-
ния переменных. Определения типов, появляющихся внутри
тела процедуры, являются локальными по отношению к этой
процедуре.
5.1. Скаляры
Определением скалярного типа 2) является просто список
значений, которые могут принимать переменные этого типа.
Определение
type
единица = (дюйм, фут, стадия, миля);
утверждает, что переменная типа единица может иметь одно из
четырех указанных значений и никаких других. Тип единица
1 Слово «переменные» следует понимать скорее как метафору, которая
указывает на возможность определения новых типов.— Прим. ред.
- В стандарте языка Паскаль (и в языке Ада) этот тип получил другое
название — перечислимый тип (enumerated type), что больше соответствует
смыслу определения.— Прим. ред.
5 1. Скаляры
135
используется в описании переменных таким же образом, как это
было и в случае стандартных типов.
var
масштаб : единица;
Оба описания могут быть объединены в одно:
var
масштаб : (дюйм, фут, стадия, миля);
но в большинстве случаев предпочтительнее отделять определение
типа от описания переменной. Приведем другие примеры опре-
деления скалярных типов:
type
день=(понедельник, вторник, среда, четверг,
пятница, суббота, воскресение);
отношение^ (предок,брат,потомок,кузен);
типзаписи=(кпол учению, коплате, накладная,
кредитсчет);
знакоперации^ (плюс,минус,умножить,разделить);
тригонометфункция = (синус, косинус,тангенс,
секанс, косеканс, котангенс);
В следующем разделе описания переменных мы описали не-
сколько переменных указанных типов:
var
выходной, рабочий : день;
родственник : отношение;
вводная запись, выводная запись : типзаписи;
аддитивнаяоперация,мультипликативнаяоперация : знак;
Значение не может относиться более чем к одному типу. Сле-
дующие определения несовместимы, так как слово помидор
встречается в обоих списках значений:
type
фрукты=(яблоко,апельсин, лимон,ананас, помидор);
овощи = (картофель, морковь, помидор, горох, капуста);
Имена значений, перечисленные в описании скалярного типа,
являются константами этого типа. Следовательно, мы можем пи-
сать:
выходной : = воскресение;
вводнаязапись := коплате;
масштаб := миля;
аддитивнаяоперация := минус;
Смешанные присваивания не разрешаются. Нельзя, например,
написать:
X 1
л
136 Гл. 5. Переменные типы
выходной := миля
Единственной операцией, которая может быть произведена
над скалярными переменными, является операция отношения, а
получающиеся при этом выражения имеют булевские значения.
Упорядоченность в скалярных типах определяется порядком, в
котором значения перечислены в определении типа. Следова-
тельно, следующие выражения имеют значения true:
понедельник < пятница
брат > предок
умножить < делить
Стандартный булевский тип есть скалярный тип, неявно опре-
деленный описанием
type
boolean^ (false, true)
Следовательно, имеем
false < true=true
Для скалярных аргументов определены функции pred и
succ. Возвращаемое значение имеет тот же тип, что и аргумент,
и является для этого аргумента предшествующим (или после-
дующим) элементом по списку определения. Например:
зисс(понедельник)=вторник
ргес!(тангенс)=косинус
Первый элемент списка не имеет предшествующего, а послед-
ний — последующего.
Функция ord имеет скалярный аргумент и выдает целое число,
являющееся порядковым номером скалярного значения по спис-
ку определения. Первое значение в списке имеет порядковый
номер ноль, таким образом:
ог<1(коплате)=1
ord(wnoc)=0
ог<1(котангенс)=5
К сожалению, в стандартной версии языка Паскаль не разре-
шается непосредственно читать и выводить на внешние устрой-
ства скалярные значения. Если вы напишете
родственник := кузен;
5УгИе(родственник)
то не увидите в выходном файле слова 'кузен'. На самом деле вы,
наверное, не увидите в выходном файле вообще ничего, так как
5.2. Ограниченные типы
137
компилятор укажет в этом месте на ошибку. Можно было бы
написать
write(ord(poACTBeHHHK))
что приведет к выдаче на печать числа 3.
Скалярные переменные часто используются совместно с опе-
ратором цикла с параметром, например, в таких конструкциях:
for масштаб : = дюйм to миля do
преобразовать
Заметим, что в этом контексте оператор цикла с параметром
не может быть заменен оператором цикла с пред-условием. Если
использовать программу
масштаб := дюйм;
while масштаб^миля do
begin
преобразовать;
масштаб :=зисс(масштаб)
end
то при попытке вычислить succ (миля) будет зафиксирована ошиб-
ка.
5.2. Ограниченные типы п
Ограниченный тип определяется двумя константами, задаю-
щими диапазон значений для этого типа. Например,
type
индекс=1 .. 20;
Переменная типа индекс есть целое число, ограниченное по
величине. Константы в определении должны быть различными
и относиться к одному и тому же типу. Таким типом может быть
целый, символьный или скалярный тип. Ограничения веществен-
ного типа не разрешены. Приведем еще примеры ограниченных
типов:
буква = 'а' .. 'я';
цифра= '0' .. '9';
буднийдень=понедельник., пятница;
Буднийдень — это ограничение скалярного типа день, опреде-
ленного в разд. 5.1. Базовым скалярным типом ограниченного
типа является тип констант, посредством которых определяется
Ограниченный тип (subrange-type), иногда о нем можно говорить просто
как о диапазоне, подразумевая диапазон значений, которые могут принимать
переменные такого типа.— Прим. ред.
138
Гл. 5. Переменные типы
последний. Например, базовым скалярным типом для типа
буднийдень будет тип день. Константы определения, задающие
диапазон значений, называются нижней границей и верхней гра-
ницей ограниченного типа, и определение является корректным
только в том случае,
если
нижняя граница верхняя граница.
Переменные ограниченных типов описываются обычным образом
в разделе описания переменных.
var
счетчик,вход : индекс;
первыйсимвол,последнийсимвол : буква;
Два раздела, необходимые для описания переменной ограни-
ченного типа, могут быть объединены в один. Мы можем написать:
var
счетчик, вход : 1 ..20;
первыйсимвол, последнийсимвол : 'а' .. 'я';
Однако в большинстве случаев предпочтительнее отделять оп-
ределение типа от описания переменной.
Любая операция, применимая к переменной некоторого типа,
может также применяться и к переменной, относящейся к соот-
ветствующему диапазону. Более того, в одном выражении могут
встречаться переменные, относящиеся к различным диапазонам
одного основного типа. Мы можем определить:
var
основание : 1 .. 10;
небольшоечисло : 0 .. 100; *
результат : integer;
и затем составить выражение
результат + небольшоечисло div основание
Аналогичным образом переменные ограниченных типов могут
фигурировать в обеих частях оператора присваивания. Имея в
виду вышеуказанное определение, можно написать:
основание небольшоечисло;
небольшоечисло результат;
Попытка присвоить переменной ограниченного типа значение,
не входящее в необходимый диапазон, приведет, однако, к воз-
никновению ошибки при счете.
Все функции, определенные для базового скалярного типа,
могут применяться и к ограниченному типу. Значение функции
5.3. Множества
139
не обязательно будет принадлежать диапазону, к которому при-
надлежит аргумент. Например, значение
sqr (небольшоечисло);
не укладывается в диапазон 0 .. 100;
Для параметров процедур ввода и вывода символьной инфор-
мации (read и write) можно пользоваться ограничениями целого
и символьного типов, результаты будут правильными.
Важным, но часто пренебрегаемым аспектом программиро-
вания для вычислительных машин является проверка диапазонов.
Введение ограничений перекладывает тяжесть этой проверки с
программиста на компилятор. Ограничения, в частности огра-
ничения целого типа, следует применять всюду, где это только
возможно. В самом деле, целый тип редко встречается в хорошо
написанной программе на языке Паскаль, поскольку очень ред-
ко бывает так, что диапазон значений переменной целого типа
совершенно непредсказуем. Использование определений огра-
ниченных типов улучшает наглядность программы, так как ука-
зывая диапазон значений, которые может принимать перемен-
ная, вы сообщаете тому, кто изучает вашу программу, неко-
торую информацию об этой переменной.
5.3. Множества
Множество — это набор различных объектов одинакового
типа. Если S — множество объектов типа Т, то объект типа Т
либо является элементом S, либо нет.
Мы можем определить множественный тип для каждого ска-
лярного (перечислимого) типа. Значениями множественного типа
будут множества значений соответствующего скалярного типа.
Определим скалярный тип
type
ингредиенты = (яблоки, земляника, бананы,
орехи, мороженое, шоколад,
сливки, печенье, сахар, лед);
и множественный тип
type
десерт=5е! of ингредиенты;
Переменные типа десерт описываются в разделе описания
переменных обычным образом:
var
сливочноемороженое, яблочныйджем, сладкое : десерт;
Тип десерт — это производный множественный тип для типа
140
Гл. 5. Переменные типы
ингредиенты. Наоборот, ингредиенты — это базовый тип для
типа десерт.
Значениями констант или переменных типа десерт будут
подмножества множества ингредиенты. Множества представля-
ются списком элементов, заключенным в квадратные скобки, и
эти конструкции будут константами типа десерт:
[мороженое, шоколад]
[мороженое, бананы, сливки]
[мороженое]
Если элементы множества являются последовательными зна-
чениями базового типа, можно указывать только первый и по-
следний из них, таким образом
[яблоки, земляника, бананы, орехи]
можно записать иначе:
[яблоки .. орехи]
Множество может не иметь элементов вовсе, в этом случае оно
называется пустым множеством и записывается как [ ].
Имеются 210= 1024 возможных значения типа десерт. В общем
случае, если базовый тип имеет п значений, то производный мно-
жественный тип имеет 2п значений.
Объединением двух множеств называется множество элемен-
тов, принадлежащих обоим множествам. Знаком операции объе-
динения служит знак '+' так, что имеем
[яблоки] + [печенье, сахар] = [яблоки, печенье, сахар]
Пересечением двух множеств называется множество тех эле-
ментов, которые принадлежат одновременно двум множествам.
Знаком операции пересечения служит знак '*', следовательно:
[бананы, мороженое, сливки] * [мороженое, орехи] =
= [мороженое]
Разностью двух множеств называется множество, содержащее
те элементы первого множества, которые не являются элементами
второго. Разность обозначается знаком '—'. Имеем
[яблоки, земляника, бананы] — [земляника, сливки] =
[яблоки, бананы]
Для сравнения множеств используются знаки операций от-
ношения.
= обозначает тождественность множеств;
=7^= обозначает нетождественность множеств;
обозначает 'содержится в';
обозначает 'содержит'.
5.3. Множества
141
Множество X содержит множество У, если каждый элемент
Y является также элементом X. Следующие выражения истинны
(имеют значение true)-.
[мороженое, шоколад] = [шоколад, мороженое]
[мороженое] =# [лед, сливки]
[земляника] [земляника, сливки]
[яблоки .. лед] [лед]
Для проверки присутствия элемента в множестве служит
отношение in (зарезервированное в языке Паскаль). Слева от in
пишется выражение, а правым операндом является выражение
множественного типа производного от типа левого операнда.
Выражение
яблоки in [яблоки, печенье, сахар]
истинно, а выражение
яблоки in [земляника, сливки]
ложно.
С помощью оператора присваивания переменной множествен-
ного типа можно присваивать значения выражений, построенных
из множеств.
яблочноепеченье := [яблоки, печенье, сахар];
сладкое := яблочноепеченье-f-[мороженое];
Если вы знакомы с алгеброй множеств, то наверное заметили,
что для обозначения операций над множествами в языке Паскаль
используются нетрадиционные символы. Это связано с тем, что
на периферийных устройствах вычислительных машин обычно
отсутствуют знаки операций над множествами. В следующей таб-
лице показано соответствие между традиционными символами
и символами языка Паскаль.
Понятие
Множество
Объединение
Пересечение
Содержит
Содержится в
Принадлежит
Пустое множество
Традиционное Обозначение
обозначение в Паскале
Пример: Гаммы
Если представить двенадцать тонов хроматической октавы
определением типа
type
142
Гл. 5. Переменные типы
звуки = 1 .. 12;
то звуки в гамме (которая состоит из нескольких звуков) могут
быть представлены с помощью производного множественного
типа.
type
ряд=$е1 of звуки;
При игре гамм, каждый звук гаммы должен быть произведен
в точности один раз до того, как гамма повторяется. Используя
переменные
var
звук : звуки;
последовательность : ряд;
мы можем дать последовательности начальное значение, соответ-
ствующее отсутствию звуков;
последовательность := [ ]
Каждый раз, добавляя новый звук, можно включать его в
последовательность посредством операции объединения:
псследовательность := последовательность + [звук]
Далее, звук можно вставлять в последовательность только в
том случае, если его там еще не было, т. е.
if not (звук in последовательность)
Наконец, последовательность полна, если она содержит
каждый из двенадцати звуков:
последовательность = [1 .. 12]
Эти соображения в сочетании с генератором случайных чисел,
рассмотренным в гл. 4, приводят к программе гаммы, которая
генерирует требуемое число случайных гамм. Второй параметр,
читаемый программой, используется как начальное значение
генератора случайных чисел.
program гаммы (input, output);
const
длинаряда = 12;
type
звуки=1 .. длинаряда;
ряд=5е! of звуки;
var
начало, счетчик, циклы : integer;
звук : звуки;
последовательность : ряд;
5.3. Множества 143
function случайныйзвук (var случай : integer) : звуки;
begin
случайныйзвук : = случай mod длинаряда + 1;
случай := (25173 * случай + 13849) mod 65536
end; {случайныйзвук}
begin
геа 6(циклы, начало);
for счетчик := 1 to циклы do
begin
последовательность := [ ];
repeat
звук : = случайныйзвук(начало);
if not (звук in последовательность)
then
begin
write(3ByK);
последовательность :=
последовательность + [звук]
end
until последовательность = [1 .. длинаряда];
writein
end {for}
end. {гаммы}
Ввод:
5 20000
Вывод:
4 5 2 10 8 11 9 3 1 6 12 7
8 2 12 10 7 11 6 1 4 5 9 3
9 3 2 7 1 12 5 10 4 11 6 8
10 3 12 7 11 4 9 1 6 2 8 5
9 6 1 3 5 12 7 8 2 11 10 4
Пример: Комбинаторная задача
В качестве другого примера работы с множествами рассмо-
трим простую комбинаторную задачу. Пусть в ящике содержится
г пронумерованных шаров, нас интересуют возможные варианты
вытаскивания k шаров из этого ящика. Ясно, что число возмож-
ных вариантов легко подсчитать с помощью соответствующих
биномиальных коэффициентов. Более интересной проблемой
является генерация выборок. Ящик может быть пустым, а также
содержать несколько или даже все шары в каждый момент вре-
мени. Его содержимое может быть представлено в виде множества.
144
Гл. 5. Переменные типы
const
емкость=20;
type
объект =1 .. емкость;
контейнер=set of объект;
var
ящик : контейнер;
шар : объект;
Теперь предположим, что нам нужно выбрать все шары из
ящика и что мы уже выбрали несколько из них. Рассмотрим зада-
чу выборки следующего шага: либо выбраны не все и мы должны
искать в ящике очередной шар, либо число выбранных равно
числу всех шаров и надо прекратить процесс. Это делается ре-
курсивной процедурой выбор в программе выборка. После вы-
таскивания шара номер его печатается. Например, число в
третьей колонке, выведенной на печать таблицы, указывает,
что шар с данным номером был выбран третьим, это также ука-
зывает на уровень глубины рекурсии.
program выборка (input, output);
const
емкость=20;
type
счетчик=0 .. емкость;
объект=1 .. емкость;
контейнер=set of объект;
var
количествошаров, числопроб : счетчик;
procedure выбор (ящик : контейнер;
проба, числовыбранныхшаров
: счетчик);
var
шар : объект;
begin
if числовыбранныхшаровСпроба
then
for шар := 1 to емкость do
if шар in ящик
then
begin
writeln(map : 3 *
числовыбранныхшаров);
выбор (ящик — [шар], проба,
числовыбранныхшаров +1)
end
5.3. Множества
145
end; {выбор}
begin
read (количествошаров, числопроб);
if количествошаров^числопроб
then выбор ([1 .. количествошаров],
числопроб, 0)
else writelnfHeeepHbie исходные данные')
end. {выборка}
Ввод:
4 3
Вывод:
1
2
3
4
3
2
4
4
2
3
2
1
3
4
3
1
4
1
3
Применения множеств
Широко используются множества символов. В простых слу-
чаях можно пользоваться постоянными множествами. Например,
условие
('0' сим) and (сим '9')
встречавшееся в программах преобразовать (гл. 3) и калькулятор
(гл. 4) может быть записано более компактно таким образом
сим in ['0' .. '9']
146
Гл. 5. Переменные типы
Если мы определим
type
множествосимволов=5е! of char;
то далее можно будет написать
procedure пропусксимволов (var сим : char;
пропускаемыесимволы :
множествосимволов);
begin
while сим in пропускаемыесимволы do
геас1(сим)
end;
Обращения
пропусксимволов (сим, ['а' .. 'я'] )
пропусксимволов (сим [', ', '; '])
приведут к пропуску букв и знаков препинания соответственно.
Часто оказывается легче работать с лексемами, чем с символа-
ми. В программе на языке Паскаль, каждая из следующих кон-
струкций рассматривается как единая лексическая единица.
begin
end
имяпеременной
1235.685
Для ввода символов из входного файла и сборки из них
лексем применяется процедура, часто называемая сканером
(scanner). Лексемы классифицируются в определениях типов,
и в простом случае мы можем видеть, например:
type
типлексемы=Дзарезервированноеслово, идентификатор,
число, присваивание);
множестволексем=8е! of типлексемы;
Теперь можно написать процедуры ввод лексемы, которая вво-
дит очередную лексему и определяет ее тип и значение, и про-
пусклексемы, которая пропускает ненужные лексемы тем же са-
мым образом, как процедура пропусксимволов пропускает сим-
волы.
Во время выполнения большой программы могут возникать
самые разные ошибки. При обнаружении ошибки печатается код
этой ошибки, а в конце счета печатаются пояснения к возник-
шим ошибкам. Чтобы реализовать такой механизм, опишем
5.3. Множества
147
const
максимальныйкодошибки=50,
type
кодошибки = 1 .. максимальныйкодошибки;
множествоошибок=8е! of кодошибки;
var
наборошибок : множествоошибок;
ошибка : кодошибки;
При задании начальных значений в программе установим
наборошибок : = [ ]
При возникновении ошибки будем обращаться к процедуре
выдатьошибку:
procedure выдатьошибку (ошибка : кодошибки);
begin
writein ('Ошибка', ошибка : 3);
наборошибок := наборошибок + [ошибка]
end;
В конце прогона программы будем выполнять следующий
цикл:
for ошибка : = 1 to максимальныйкодошибки do
if ошибка in наборошибок
then выдать полное описание ошибки
Синтаксис
Синтаксис простого типа показан на рис. 5.1. Идентификатор
типа это либо один из стандартных идентификаторов integer,
real, boolean, либо идентификатор, описанный в предыдущих
определениях типа. Тип это либо простой тип, либо производ-
ный множественный тип простого типа, как показано на рис. 5.2.
148
Гл. 5. Переменные типы
5.4. Отношения между типами
Типы, которые мы можем определять сами, представляют
собой весьма важную часть языка Паскаль. Однако они прине-
сли с собой несколько довольно сложных проблем 2). Рассмо-
трим следующие описания:
type
сторонакости=1 .. 6;
var
попытка : сторона кости;
бросок : 1 ..6;
сумма : integer
Эти описания ставят сразу несколько вопросов. Имеют ли пе-
ременные попытки и бросок один и тот же тип? Правильными ли
будут такие выражения:
сумма := сумма + попытка + бросок
в которых участвуются «разные типы»?
Мы уже пытались ответить на эти вопросы неформально,
в этом разделе мы изучим отношения между типами в том виде,
как они определены в предлагаемом стандарте языка Паскаль.
Между двумя типами возможны отношения трех видов: идентич-
ность, совместимость и совместимость для присваивания.
Типы, используемые в двух или более местах программы,
являются идентичными, если истинно по крайней мере одно из
следующих утверждений:
1. В каждом месте используется один и тот же идентификатор
типа.
2. Идентификаторы типов различаются (например, Т1 и Т2),
но типы определены как эквивалентные, т. е. определение
имеет вид Т1=Т2.
Два типа совместимы, если истинно одно из следующих ут-
верждений:
1. Типы идентичны.
2. Один является ограничением другого.
Эти проблемы относятся скорее к тематике разработки трансляторов.—
Прим, ред.
5.4. Отношения между типами
149
3. Оба они являются ограничениями одного и того же типа.
4. Они являются множественными типами с совместимыми базо-
выми типами.
Выражение В типа Т2 совместимо для присваивания с типом
Т1, если истинно по крайней мере одно из следующих утвержде-
ний:
1. Т1 и Т2 идентичны.
2. Т1 есть вещественный тип, а Т2 есть либо целый тип, либо
ограничение целого типа.
3. Т1 и Т2 есть совместимые упорядоченные (перечислимый,
ограниченный или целый) типы, и значение В есть разрешенное
значение для типа Т1.
4. Т1 и Т2 есть совместимые множественные типы, и каждый эле-
мент множества В является разрешенным значением для базо-
вого типа Т1.
Цель введения таких определений заключается в том, чтобы
достичь ситуации, при которой отношения между типами будут
четко определены при любых обстоятельствах, и в то же время
эти отношения будут достаточно разумными. Рассмотрим теперь
несколько примеров, иллюстрирующих введенные определения.
type
Т1=0.. 100;
Т2=Т1;
ТЗ=О..1ОО;
Т4=О..1О;
Т5=(А, Б, В, Г, Д);
Т6=(А, Б, В);
Т7=(Г, Д);
Ml=set of Т5;
M2=set of Тб;
var
U, V : Tl;
W : Т2; '
X : T5;
Y : Тб;
Z : Т7;
Переменные U и V имеют идентичные типы, поскольку для
описания каждой из них используется один и тот же идентифи-
катор типа Т1. Тип переменной W идентичен типам U и V, так
как Т2 определяется с помощью равенства Т2—Т1. Хотя тип
ТЗ допускает те же значения, что и тип Т1, но ТЗ не идентичен Т1.
Если процедура или функция имеет формальный параметр-
переменную, тип соответствующего фактического параметра
150
Гл. 5. Переменные типы
должен быть идентичен типу формального параметра. Важным
следствием из этого является то, что формальный параметр-пе-
ременная всегда должен быть специфицирован с помощью иден-
тификатора типа, ни при каких обстоятельствах нельзя пользо-
ваться подобными конструкциями:
procedure неверная (var пар : 1 .. 5);
Если два типа идентичны, они одновременно и совместимы,
следовательно, типы Т1 и Т2 совместимы. Т1 и ТЗ также совме-
стимы, так как являются ограничениями целого типа. Тб яв-
ляется ограничением Т5 и, следовательно, совместим и с Т5,
и с Т7, поскольку Т7 тоже представляет собой ограничение Т5.
Множественные типы Ml и М2 совместимы, поскольку совмести-
мы их базовые типы.
Идентичность и совместимость являются симметричными от-
ношениями: если тип X идентичен (или совместим) с типом У,
то Y идентичен (или совместим) с типом X. Совместимость для
присваивания не симметрична и определяется в терминах выра-
жения и типа. Правила определения совместимости для присваи-
вания дают нам возможность выяснить, является ли оператор
V := В
допустимым или нет. В противоположность идентичности и сов-
местимости, которые всегда могут быть проверены компилято-
ром, вопрос о том, имеет ли место совместимость для присваива-
ния, может оставаться невыясненной вплоть до фазы выполнения
программы, поскольку, вообще говоря, компилятор не имеет
возможности определить значение выражения В. Используя
приведенные выше описания, заметим, что оператор
X Y
всегда может быть успешно выполнен, но оператор
Y := X
может работать неправильно (например, если значением X бу-
дет D).
Правила определения совместимости для присваивания ис-
пользуются и при выяснении вопроса о соответствии фактиче-
ского параметра формальному параметру-значению: значение
фактического параметра должно быть совместимо в смысле при-
сваивания с соответствующим формальным параметром-значени-
ем. Если мы опишем
5.5. Оператор варианта
151
procedure игра (игрушка : объектигры);
и затем обратимся к процедуре
игра (мяч)
то выражение мяч должно быть совместимо в смысле присваива-
ния с переменными типа объектигры.
5.5. Оператор варианта
Условный оператор позволяет в процессе выполнения про-
граммы выбирать одно из двух возможных действий в зависимо-
сти от значения логического выражения. Оператор варианта,
начинающийся со слова case, есть обобщение условного операто-
ра: он дает возможность выполнить одно из нескольких действий
в зависимости от значения скалярного выражения или выраже-
ния ограниченного типа.
Для иллюстрации предположим, что имеется мост с платным
проездом, причем такса такова:
велосипеды и мотоциклы : бесплатно
легковые автомобили : 25 центов
легковые автомобили с прицепами : 50 центов
грузовые автомобили : 25 центов за тонну веса
Нам нужно написать программу вычисления стоимости про-
езда. Во-первых, составим несколько описаний:
const
максимальныйвес=50;
максимальнаяплата = 1250;
type
видтранспорта = (двухколесный, легковой,
легковойсприцепом, грузовик);
var
вес : 1 .. максимальныйвес; •
плата : 0 .. максимальнаяплата;
транспорт : видтранспорта;
Часть программы, в которой вычисляется плата за проезд,
с использованием оператора варианта выглядит так:
case транспорт of
двухколесный :
плата : = 0;
легковой :
плата := 25;
легковойсприцепом:
плата := 50;
грузовик :
152
Гл. 5. Переменные типы
плата := 25 * вес
end
Переменная .транспорт в этом операторе служит в качестве
селектора варианта. Различные значения, которые может иметь
селектор варианта, называются в операторе варианта метками
варианта. После каждой метки располагается оператор. Опе-
ратор варианта позволяет записать эту программу аккуратнее,
чем если бы мы использовали условный оператор:
if транспорт = двухколесный
then плата : = О
else if транспорт = легковой
then плата : = 25
else if транспорт = легковойсприцепом
then плата : = 50
else плата := 25 * вес
Когда одно и то же действие необходимо выполнить для не-
скольких различных значений селектора варианта, эти значения
можно записать в виде списка:
type
месяц=(январь,февраль,март,апрель,май,июнь,июль,
август,сентябрь,октябрь,ноябрь,декабрь);
год =1900 .. 2000;
продолжительностьмесяца=28 ..31;
var
гг : год;
мм : месяц;
продолжительность : продолжительностьмесяца;
case мм of
январь,март,май, июль,август,октябрь,декабрь:
продолжительность : = 31;
апрель,июнь,сентябрь,ноябрь :
продолжительность := 30;
февраль :
if (гг mod 4=0) and (гг mod 100=^0)
then продолжительность := 29
else продолжительность := 28
end {case}
Часто случается так, что для некоторых значений селектора
варианта никаких действий производить не надо, как, например,
для значения воскресенье в следующем примере:
var
день : (воскресенье,понедельник,вторник,среда,
5.5. Оператор варианта
153
четверг, пятница,суббота);
case день of
воскресенье : ;
понедельник,вторник,среда,
четверг,пятница :
begin
уходнаработу;
работа;
возвращениедомой
end;
суббота :
мытьемашины
end {case}
В ситуациях, когда имеется действие, которому соответствует
длинный список меток, оператор проверки можно совмещать с
условным оператором, как в этом примере:
var
управление : 1 ..20;
if управление in [2,3,5,7,8,11,17,20]
then
case управление of
2,5 :
действие(1);
3,7,11 :
действие{2);
8,17 :
действие(З);
20 :
действие(4)
end {case}
else writein ('неверное управляющее значение')
Пример: Исключение примечаний
Рассмотрим задачу построения программы, убирающую при-
мечания из программы на Паскале. При работе эта программа
будет находиться в одном из двух состояний, мы будем называть
их копирование и примечания. Начальным является Состояние
копирование, в котором символы копируются из входного файла
в выходной. При прочтении символа '{' программа переходит
в состояние примечания, в котором символы читаются из вход-
ного файла, но вплоть до символа '}', на котором опять проис-
ходит переход в состояние копирование, в выходной файл ни
один из них не попадает.
154
Гл. 5. Переменные типы
В некоторых реализациях языка Паскаль символы '{' и
запрещены, вместо них применяются составные символы
'(#' и '*)'• Это делает проблему исключения примечаний более
интересной, потому что, если в состоянии копирование был встре-
чен символ '(', это может означать начало комментария, а может
и нет. Программа не должна пересылать символ '(' в выходной
файл до тех пор, пока не будет прочитан следующий символ, и не
будет проверено, является он символом или нет. Таким об-
разом, требуется дополнительное состояние началопримечания,
когда примечание ожидается. Четвертое состояние, конецприме-
чания, необходимо в тех случаях, когда внутри примечания
встречается символ
Программы этого типа, имитирующие конечные автоматы,
очень легко писать на языке Паскаль. Для представления
состояний используется перечислимый тип, а для указания
действий, которые выполняются автоматом в каждом из со-
стояний, используется оператор варианта.
program исключитьпримечания (input, output);
var
символ : char;
состояние : (копирование, началопримечания
примечание, конецпримечания);
begin
состояние := копирование;
while not eof do
if eoln
then
begin
readin;
writein
end
else
begin
геаб(символ);
case состояние of
копирование
if символ ='('
then состояние :=
началопримечания
else write(cимвoл);
началопримечания :
if символ ='*'
then состояние :=
примечание
else if символ ?= '('
then
begin
write ('(');
write(cимвoл);
состояние :=
копирование
end
5.6. Пересмотр программы калькулятор
155
else write ('(');
примечание :
if символ =V
then состояние : =
конеипримечания;
конецпримечания:
if символ =')'
then состояние : =
копирование
else if символ # ' *
then состояние : =
примечание
end {case}
end
end. {исключитьпримечания}
Синтаксис
На рис. 5.3 показана синтаксическая диаграмма оператора ва-
рианта. Зарезервированные слова case и end действуют как
скобки, обрамляющие оператор. За последним оператором в
списке вариантов точка с запятой не ставится. Полный синтак-
сис языка Паскаль, приведенный в приложении В, допускает
пустой оператор.
На рис. 5.3 не предусмотрен случай с меткой варианта без ка-
кого-либо действия п.
5.6. Пересмотр программы калькулятор
Мы закончим эту главу разработкой второй версии имитатора
карманного калькулятора, описанного в гл. 4. Это позволит
нам, во-первых, проиллюстрировать методы программирования,
представленные в этой главе, а во-вторых, показать, как можно
воспользоваться глобальными идентификаторами за счет неко-
Так как оператор может быть «пустым», то такой вариант автоматически
охватывается синтаксической диаграммой, специально его указывать нет не-
обходимости.— Прим, ред.
156
Гл, 5, Переменные типы
торого ухудшения наглядности программы. Изменения, внесен-
ные во вторую версию программы калькулятор, перечислены
ниже, далее следует обсуждение их относительных достоинств.
(1) Все константы определяются в разделе определения кон-
стант основной программы, поэтому все идентификаторы
констант глобальны.
(2) Переменные следсимвол и следпозиция изменяются непосредст-
венно процедурами, а не передаются как параметры.
(3) Процедура читатьмножитель распознает символ «П», значе-
нием «П» является предыдущий результат, это дает «кальку-
лятору»
возможность работать с простой «памятью». Для задачи
5,П*П,П#П
результаты будут такими:
5 25 625
Для реализации этой возможности процедура читатьмножи-
тель имеет доступ к глобальной переменной предыдущийре-
зулыпат.
(4) Глобальная переменная следпозиция, а также локальные
переменные счетчик и масштаб имеют соответствующие ог-
раниченные типы.
(5) Переменные цифры, аддитивнаяоперация, мультипликатив-
наяоперация и разделители используются как константы
множественного типа, что делает булевские выражения в
программе более наглядными.
(6) Некоторые условные операторы заменены операторами ва-
рианта.
Обсуждение
Так как две версии программы калькулятор функционально
тождественны, за исключением возможности использования сим-
вола «П», мы будем интересоваться в основном различиями в на-
глядности программ. Первая версия программы написана таким
образом, что работа любой процедуры могла быть прослежена
по тексту только этой процедуры, если нам известно, что делают
процедуры, вызываемые из данной процедуры. Это особенно
важное преимущество, проявляющееся в длинной программе.
Однако калькулятор — это небольшая программа, и вторая вер-
сия делает ее функциональную структуру яснее, так как здесь
меньше повторений и меньше имен переменных, которые необхо-
димо запоминать. Тот факт, что, например, процедура читатьсла-
гаемое возвращает значение введенного слагаемого как параметр,
но имеет побочный эффект, влияя на переменные следсимвол и
следпозиция, точно описывает ее функцию в программе. Анало-
гично, единственный идентификатор следсимвол заменяет семь
5.6. Пересмотр программы калькулятор
157
разных идентификаторов исходной программы, ссылавшихся
на одну переменную.
Заметим, что имеется общая проблема относительно пере-
менных, которые используются в программе как постоянные.
Мы описали, например, цифры как глобальную переменную с
постоянным значением
['О' .. '9']
и разрешили доступ к ней из процедур читатъчисло и читатьмно-
житель. В разделе определения констант нельзя определять
константы множественного типа, и, следовательно, если бы пере-
менная цифра была описана локально, то процедура должна была
бы восстанавливать ее значение при каждом вызове, что совер-
шенно очевидно связано с потерей эффективности. Значение может
быть написано как константа без имени там, где оно необходимо,
но это затрудняет изменение программы, требующееся, например,
для обработки восьмеричных чисел, когда соответствующее зна-
чение переменной цифры есть
['О' .. '71
Некоторые из этих рассуждений применимы также и к опреде-
лениям констант. Использование глобальных объектов часто
предпочтительнее для сопровождения программы. Вторую вер-
сию калькулятора проще подправить, чтобы использовать, на-
пример, для вложенных выражений не круглые, а квадратные
скобки.
Заметим, наконец, что типы не могут быть локальными,
если процедуре передаются параметры этих типов. Тип позиция,
например, не может быть локальным в процедуре читатьсимвол.
Можно заключить, что решение о том, пользоваться ли не-
локальными идентификаторами, должно приниматься в зависи-
мости от конкретной задачи. Выдвинутая здесь аргументация
не применима к программе длиной в сотни страниц. Многие пре-
подаватели программирования с гораздо большим усердием,
чем мы в этой книге, доказывают, что использование нелокаль-
ных идентификаторов всегда плохо. На самом деле, однако, ока-
зывается, что большинство нетривиальных программ имеют гло-
бальные идентификаторы, причем в результате они оказываются
более наглядными.
program калькулятор (input, output);
const
основание^ 10; максимальныймасштаб—20;
пробел =' '; максимальнаядлина=80;
запятая=' ,'; точкасзап ятой=';';
точка='.'; плюс='+';
минус='—'; умножить—'*';
делить=7'; маркер=' f ';
158
Гл. 5. Переменные типы
нуль='О'; девять='9';
пред='ГГ; леваяскобка='С;
праваяскобка^')';
type
позиция =0 .. максимальнаядлина;
множествосимволов^Бе! of char;
var
следсимвол : char;
следпозиция : позиция;
результат, предрезультат : real;
цифры, аддитивнаяоперация,
мультипликативнаяоперация, разделители :
мн ожествосимвол ов;
procedure читатьсимвол;
begin
repeat
if eof
then следсимвол := точкасзапятой
else if eoln
then
begin
следпозиция := 0;
следсимвол := запятая;
readln
end
else
begin
следпозиция := следпозиция +1;
геаб(следсимвол)
end
until следсимвол #= пробел
end; {читатьсимвол}
procedure выдатьошибку;
begin
writein (маркер : следпозиция);
while not (следсимвол in разделители) do
читатьсимвол
end; {выдатьошибку}
procedure читатьчисло (var значениечисла : real);
type
масштабныймножитель =0 .. максимальныймасштаб;
var
счетчик, масштаб : масштабныймножитель;
begin
значениечисла := 0;
while следсимвол in цифры do
begin
значениечисла := основание * значениечисла
+ огб(следсимвол) — огб(нуль);
читатьсимвол
end; {while}
if следсимвол = точка
then
5.6. Пересмотр программы калькулятор
159
begin
читатьсимвол;
масштаб : = 0;
while следсимвол in цифры do
begin
значениечисла := основание *
значениечисла + огд(следсимвол)
— огб(нуль);
читатьсимвол;
масштаб масштаб + 1
end; {while}
for счетчик := 1 to масштаб do
значениечисла := значениечисла /
основание
end
end; {читатьчисло}
procedure читатьвыраженис (var значениевыражения : real);
var
знакаддитивнойоперации : char;
значениеследующегослагаемого : real;
procedure читатьслагаемое (var значениеслагаемого : real);
var
знакмультипликативнойоперации : char;
значениеследующегомножителя : real;
procedure читатьмножитель (var значениемножителя : real);
begin
if следсимвол in цифры + [пред, леваяскобка]
then
case следсимвол of
читатьчисло(значениемножителя);
'IT :
begin
читатьсимвол;
значениемножителя :=
предрезультат
end;
'(' :
begin
читатьсимвол;
читатьвыражение
(значениемножителя);
if следсимвол — праваяскобка
then читатьсимвол
else выдатьошибку
end
end {case}
else
begin
выдатьошибку;
значениемножителя := 0
end
end; {читатьмножитель}
160
Гл. 5. Переменные типы
begin {читатьслагаемое}
читатьмножитель(значениеслагаемого);
while следсимвол in мультипликативнаяоперация do
begin
знакмультипликативнойоперации := следсимвол;
читатьсимвол;
читатьмножитель(значениеследующегомножителя);
case знакмультипликативнойоперации of
умножить :
значениеслагаемого :=
значениеслагаемого *
значениеследующегомножителя;
делить :
if значениеследующегомножителя Ф О
then значениеслагаемого :=
значениеслагаемого /
значениеследующегомножителя
else выдатьошибку
end {case}
end {while}
end; {читатьслагаемое}
begin {читатьвыражение}
читатьслагаемое(значениевыражения);
while следсимвол in аддитивнаяоперация do
begin
знакаддитивнойоперации := следсимвол;
читатьсимвол;
читатьслагаемое(значениеследующегослагаемого);
case знакаддитивнойоперации of
плюс :
значениевыражения := значениевыражения
+ значениеследующегослагаемого;
минус :
значениевыражения := значениевыражения
— значениеследующегослагаемого
end {case}
end {while}
end; {читатьвыражение}
begin {калькулятор}
цифры := [нуль .. девять];
аддитивнаяоперация := [плюс, минус];
мультипликативнаяоперация := [умножить, делить];
разделители := [запятая, точкасзапятой];
пред := 0;
следпозиция := 0;
читатьсимвол;
while следсимвол =# точкасзапятой do
begin
читатьвыражение(результат);
if следсимвол in разделители
then
begin
writein (результат);
пред := результат
end
5.6. Пересмотр программы калькулятор
161
else выдатьошибку;
читатьсимвол
end {while}
end. {калькулятор}
Упражнения
5.1. Покажите, как можно использовать оператор варианта для печати
значений скалярной переменной.
5.2. В следующих фрагментах программы укажите операторы, которые
(1) будут выполняться правильно, (2) при исполнении которых может возник-
нуть ошибка, (3) при трансляции которых будет зафиксирована ошибка. Имей-
те в виду, что переменные, не определенные в этих фрагментах, получили зна-
чения где-то в другой части программы.
const
минимум=—100;
нуль=0;
максимум= 100;
type
счетчик=1 .. maxint
нульминимум= минимум .. нуль;
нульмаксимум=нуль .. максимум;
монета= (копейка, пятак, гривенник, пятиалтынный,полтинник);
var
счет : счетчик;
маленький : нульминимум;
большой, стоимость : нульмаксимум;
мелочь : монета;
индикатор : boolean;
набор : set of монета;
begin
счет := 0;
маленький := большой;
мелочь := копейка;
repeat
write(мeлoчь : 10);
мелочь := зисс(мелочь)
until мелочь > полтинник;
гривенник := 2 * пятак;
счет := счет + ord (индикатор);
набор := набор + [полтинник];
case монета of
копейка:
значение := 1;
пятак :
значение := 5;
гривенник :
значение := 10;
пятиалтынный :
значение : = 25;
end;
end
6 Ха 3388
162
Упражнения
5.3. Дано определение
type
числовоемножество = set of минимум .. максимум;
где минимум и максимум — целые константы Напишите процедуру печать-
множества, которая печатает значение переменной типа числовоемножество.
Множество, элементами которого являются, например, числа 3, 7, И и 19,
должно быть отпечатано в таком виде
[3, 7, 11, 19]
5.4. Напишите программу, которая вводит текст, подсчитывая слова, в
которых не менее четырех гласных букв.
5.5. Определите экспериментальным путем вид функции р (k, Л')> опре-
деленной следующим образом:
var
х, у : set of 1. . N;
Вырабатываются случайные значения х и у, причем каждое из них имеет k
элементов. Тогда:
р (k, N) = вероятность того, что х и у не пересекаются =• вероятность
(х*у=[ ])
5.6. Напишите программу, которая играет в игру со следующими прави-
лами:
Вычислительная машина строит случайный набор цифр. Игрок вводит
набор цифр со своего терминала. Игра кончается, когда вводится набор, иден-
тичный набору, созданному машиной. Если набор игрока не совпадает с на-
бором, созданным машиной, то машина сообщает, сколько одинаковых цифр
имеют эти два набора.
5.7. Модель конечного автомата, примененная нами при написании про-
граммы исключитъпримечание, дает эффективную, но не очень компактную
программу. Напишите более короткую программу, которая бы убирала приме-
чания из входного текста.
Г лава 6.
СЛОЖНЫЕ ТИПЫ
Все типы переменных, с которыми мы сталкивались до сих
пор, были простыми типами. Типы, о которых будет рассказано
в этой и в следующих главах, являются сложными типами.
Сложные типы отличаются от простых тем, что переменные слож-
ных типов имеют более чем одну компоненту. Каждая компонента
сложного типа является переменной либо простого, либо слож-
ного типа. На самом нижнем уровне компоненты сложного типа
имеют простые типы, которым могут присваиваться значения и
которые могут присутствовать в выражениях так же, как и про-
стые переменные. Очень важным аспектом, связанным со слож-
ными переменными, является способ доступа к ее компонентам.
В этой главе мы рассмотрим переменные регулярного и комбини-
рованного типов 2), которые позволяют нам использовать память
машины более гибким образом, чем мы делали это до сих пор.
В следующей главе мы изучим переменные файлового типа, ко-
торые дают возможность запоминать информацию на носителях,
являющихся внешним по отношению к доступной памяти про-
граммы. Мы будем пользоваться терминами простая и сложная
переменные для обозначения переменных, имеющих простые и
сложные типы соответственно.
6.1. Массивы
Массив есть упорядоченный набор переменных одинакового
типа. Строчка текста может быть представлена в виде массива
символов; вектор представляет собой массив вещественных чисел,
и, так как матрица состоит из столбцов, каждый из которых —
вектор, матрица может быть представлена как массив векторов.
Регулярный тип определяется в терминах типа индексов и
типа компонент.
type
направление=(х,у,г);
вектора array [направление] of real;
Объекты регулярного типа называются массивами, а комбинирован-
ного— записями.— Прим. ред.
6*
164
Гл. б. Сложные типы
Типом индексов вектора является тип направление, типом компо-
нент — вещественный. Мы можем описать переменные типов
направление и вектор обычным образом:
var
s,t : направление;
u,v : вектор;
Переменная типа вектор имеет три компоненты, по одной на
каждое из трех значений типа направление. Три компоненты век-
тора v таковы:
v[x], vly], v[z]
В традиционной математической записи для обозначения ком-
понент вектора используются индексы. Компоненты вектора о
будут иметь вид vx, vy, vz. В связи с этим значения индексно-
го типа часто называются просто индексами.
Каждая из компонент v — это вещественная переменная
и она может встретиться в любом контексте, в котором может ис-
пользоваться вещественная переменная. Например, норма век-
тора есть сумма квадратов его компонент. Мы можем вычислить
норму v либо с помощью оператора
норма :== sqr (v[x])+sqr(v[y])-|-sqr(v[z])
либо так:
begin
норма := 0;
for s := х to z do
норма := норма + sqr(vls])
end
Значение одного массива может быть присвоено другому мас-
сиву того же типа в одном операторе присваивания. Оператор
v := и
в котором и v, и и — вектора, эквивалентен группе операторов
vlx] := ufx];
vty] := u[y);
viz] := u[z];
Массив может быть также параметром-значением или парамет-
ром-переменной процедуры или функции. Например, следующая
функция вычисляет скалярное произведение двух векторов:
function скалярноепроизведение (u, v : вектор) : real;
var
ip : real;
s: направление;
6.1. Массивы
165
begin
ip 0;
for s := x to z do
ip := ip + uls] * vis];
скалярноепроизведение : = ip
end;
Векторное произведение двух векторов есть вектор. Массив не
может быть значением функции, поэтому написать функцию для
вычисления векторного произведения нельзя. Вместо этого
можно вычислить векторное произведение с помощью процедуры,
где вектор w будет произведением векторов и и v:
procedure векторноепроизведение (u, v : вектор;
var w : вектор);
begin
w[x] := uly] * viz] — ulz] * v[yj;
wly] := ulz] * vlx] — ulx] * viz];
wlz] := ulx] * vly] — uly] * vlx]
end;
Синтаксис
Синтаксис определения регулярного типа показан на рис. 6.1а.
Типом индексов может быть скалярный тип или ограниченный
тип. Помните, что вещественный тип не является скалярным.
Компоненты могут быть любого типа, даже сложного.
Рис. 6.1а. Синтаксис определения регулярного типа
Компонента массива имеет те же свойства, что и переменная
базового типа и, следовательно, является одним из вариантов
синтаксической конструкции множитель. Синтаксис компонен-
ты массива показан на рис. 6.16.
166
Гл. 6. Сложные типы
Пример: Преобразование чисел в строки
Программа преобразовать в гл. 3 выполняла преобразование
строк цифр в числа во внутреннем представлении вычислитель-
ной машины. Рассмотрим теперь обратную задачу преобразова-
ния значения переменной в строку цифр. Значение каждой циф-
ры в строке легко может быть получено многократным повторе-
нием деления и вычислением остатка:
var
данное, цифра : integer;
repeat
цифра : = данное mod 10;
данное := данное div 10;
until данное =0
К сожалению, эта простая программа вырабатывает цифры числа в
порядке, обратном тому, в котором их надо печатать. Например,
если данное= 123, во время выполнения оператора цикла у пере-
менной цифра будут последовательно значения 3, 2 и 1. Мы можем
преодолеть это затруднение, запоминая последовательные зна-
чения переменной цифра в некотором массиве, а затем печатая
их в обратном порядке. Вместо того, однако, чтобы запоминать
значения переменной цифра, мы будем запоминать их символь-
ные эквиваленты, чтобы по окончании процесса число было гото-
во к печати. Опишем следующие константы и переменные
const
максимальнаядлина=32;
нуль^'О';
основание^ 10;
var
jp, kp : 0 .. максимальнаядлина;
буфер : array [1 .. максимальная длина] of char;
число : integer;
Цикл преобразования будет теперь выглядеть так:
kp := 0;
repeat
kp : = kp+1;
буфер [kp] := chr (число mod основание + огб(нуль));
число := число div основание
until число=0
После этого цифры могут быть напечатаны в правильной после-
довательности:
for jp := kp downto 1 do
wгite(бyфep[jp])
6.1. Массивы
167
В нашем примере мы выбрали число 10 в качестве делителя, по-
тому что мы привыкли пользоваться десятичной системой счисле-
ния. Вычислительная машина может одинаково легко пользо-
ваться любым другим делителем и печатать числа по разным осно-
ваниям. Программа системысчисления читает число, обращается
к стандартной процедуре чтения (которая, конечно, выполняет
десятичное преобразование) и затем производит обратное пре-
образование во все системы счисления от двоичной до десятичной,
program системысчисления (input, output);
const
наибольшееоснование= 10;
максимальнаядлина=32;
минус='—';
нуль = 'О';
type
основание^ .. наибольшееоснование;
var
данное : integer;
система : основание;
procedure записатьчисло (число : integer; осн. :
основание);
var
jp, kp : 0 .. максимальнаядлина;
буфер : array [1 .. максимальнаядлина]
of char;
begin
if число < 0
then
begin
write(MHHyc);
число := аЬз(число)
end;
kp := 0;
repeat
kp := kp 4- 1;
буфер[кр] := сИг(число mod основание +
оМ(нуль ));
число := число div осн
until число = 0;
for jp := kp downto 1 do
write(6y$)ep[jp])
end {записатьчисло}
begin {системысчисления}
read^aHHoe) ;
168
Гл. 6. Сложные типы
for система := 2 to наибольшееоснование do
begin
записатьчисло(данное, система);
writein
end {for}
end. {системысчисления}
Ввод:
100
Вывод:
1100100
10201
1210
400
244
202
144
121
100
Пример: Сортировка массива
Слово сортировка используется в вычислительной технике
для обозначения процесса перестановки объектов в порядке их
«размеров». Это возможно в тех случаях, если с данным типом
объектов связано некоторое отношение порядка. Набор целых
чисел, например
41 —7 23 —4 4902
можно отсортировать, так как с целым типом связано некоторое
отношение порядка. Числа могут быть упорядочены как по воз-
растанию величин, так и по убыванию:
—7 —4 01223449
94432210 —4 —7
Формально можно описать массив а и индекс i
var
а : array [I] of Т;
i, j : I;
утверждается, что если существует некоторое отношение порядка,
связанное с типом Т, то массив упорядочен по возрастанию вели-
чин, когда
из i>j следует, что alil^atj],
6.1. Массивы
169
и массив упорядочен по убыванию величин, когда
из i>j следует, что a[i]^a[j].
Проблема сортировки неупорядоченного массива относится
в вычислительной технике к классическим. Она кажется простой,
ведь всем приходилось выполнять какую-либо механическую
сортировку, была ли то раскладка игральных карт, гардеробных
номерков, карточек из библиотечного каталога или денежных
счетов. Простота эта иллюзорна. Хотя первые программы сорти-
ровки были написаны фон Нейманом в 1945 году, какого-либо зна-
чительного продвижения в теории сортировки не наблюдалось
в течение последующих двадцати лет.
Сортировка до сих пор остается важным аспектом вычисли-
тельной техники, однако постепенно значение ее уменьшается.
Все шире внедряются вычислительные системы, позволяющие
иметь непосредственный доступ к запоминаемой информации.
Система, которая один раз в день или один раз в неделю сортирует
информацию, не может эффективно обеспечивать непосредствен-
ный доступ. В связи с этим современные системы проектируются
так, чтобы не требовалась сортировка больших объемов инфор-
мации.
Алгоритм, которым мы будем пользоваться для сортировки
массива, был придуман в 1959 году Дональдом Л. Шеллом. Он
называется сортировкой Шелла или сортировкой с уменьшающим-
ся приращением. Программа, которую мы построим, будет чи-
тать числа из входного файла, запоминать их в массиве, сортиро-
вать этот массив, а затем печатать его. Сортировка Шелла явля-
ется перестановочной сортировкой. Это означает, что сортировка
производится перестановкой пар чисел в массиве до тех пор, пока
он не будет полностью отсортирован. Эффективность перестано-
вочной сортировки зависит только от выбора переставляемых
чисел.
При любой сортировке объект имеет начальное и конечное
положения, а в процессе сортировки он перемещается из одного
положения в другое. В простейших вариантах перестановочных
сортировок объекты перемещаются за один такт только на одну
позицию. Метод, предложенный Шеллом, позволяет ускорить
сортировку, перемещая объекты сначала на большие расстоя-
ния, а затем по мере приближения к пункту назначения все на
меньшие и меньшие. Несмотря на простоту идеи, математический
анализ проблемы весьма затруднителен. Очень нелегко опреде-
лить величину первого перемещения, еще труднее понять, как
вообще определять величину перемещения. Мы выбрали простой
вариант сортировки Шелла, при котором первичное перемещение
делается наполовину длины массива, а все повторные переме-
щения уменьшаются вдвое по сравнению с предыдущими.
170
Гл. 6 Сложные типы
Алгоритм сортировки состоит из трех вложенных циклов.
Самый внешний цикл управляет диапазонохм перемещений. Вну-
три этого цикла производится сканирование массива, продолжа-
ющееся до тех пор, пока есть возможность переставлять его эле-
менты, находящиеся на заданном расстоянии друг от друга.
Непосредственно сканирование осуществляется самым внутрен-
ним циклом. Эти циклы показаны на приведенной ниже схеме.
Значение переменной длина равно числу компонент сортируемого
массива ряд.
скачок := длина;
while скачок>1 do
begin
скачок := скачок div 2;
repeat
for т 1 to максимум do
begin
п := m + скачок;
if ряд [m] > ряд[п]
then
поменять местами ряд [т] и ряд [я]
end
until нет возможностей для перемещений
end
Значение величины максимум в операторе цикла с параметром
должно быть выбрано таким образом, чтобы в программе не про-
изводилось попыток обращения к несуществующим компонентам
массива. В соответствии с этим,
максимум + скачок длина
поэтому наиболее подходящим значением для величины максимум
будет
длина — скачок
Перестановка двух значений в программе всегда производится
с помощью рабочей переменной. Взяв в качестве таковой пере-
менную рабочая мы будем записывать перестановку компонент
ряд [т] и ряд [и] таким образом:
рабочая := ряд [гл];
ряд [т] ряд [nJ;
ряд [п] := рабочая;
Оператор цикла с пост-условием должен обеспечивать просмотр
массива до тех пор, пока не будет выполнен один полный про-
смотр массива без единого перемещения компонент. Булевской
переменной готово присваиваться значение true в начале
6.1. Массивы
171
каждого цикла просмотра, и если будет выполнена какая-либо
перестановка, это значение будет изменено на false. Цикл про- должается до тех пор, пока в конце очередного повторения зна- чение этой переменной не окажется равным true.
исходная последовательность 9 8 1 7 6 3 4 5 4 1
скачок «5 8 1 7 6 9. 4 5 4 1
3 4 1 7 6 9 £ 5 4 1
3 4 1 4 6 9 8 5 2 1
3 4 1 4 х 9 8 5 7 X
Скачок = Z х 4 х 4 1 9 8 5 7 6
1 4 1 4 3 9 8 5 7 6
1 4 1 4 3 _5_ 8 2 7 6
1 4 1 4 3 5 2 9 8. 6
1 4 1 4 3 5 7 X 8 9.
скачок = 1 1 1 4/ 4 3 5 7 6 8 9
1 1 4 3 4 5 7 6 8 9
1 1 4 3 4 5 X 2 8 9
1 1 2 4 4 5 6 7 8 9
окончательная последовательность 1 1 3 4 4 5 6 7 8 9
Рис. 6.2. Схема сортировки по методу Шелла
Работу этого алгоритма легче всего понять, изучив диаграм-
му. Верхний ряд чисел на рис. 6.2 соответствует несортированно-
му массиву. В нижнем ряду представлен тот же массив, но уже
после сортировки. Каждый из промежуточных рядов содержит
два подчеркнутых числа: эти числа были переставлены на пре-
дыдущем шаге работы алгоритма. Расстояние между подчеркну-
тыми числами равно значению переменной скачок, оно уменьши-
172
Гл. 6. Сложные типы
ется по мере работы, что и показано в самой левой колонке на
этом рисунке.
Сортировка Шелла демонстрирует способ вычисления индек-
сов и их использования для произвольного доступа к компонен-
там массива. Здесь же иллюстрируются два основных свойства
перестановочных сортировок. Во-первых, память, необходимая
для проведения сортировки, состоит из самого массива и одной
дополнительной переменной (рабочая), используемой при пере-
становке значений. Это очень важно, поскольку более быстрые
алгоритмы требуют больше памяти — приходится расплачи-
ваться за увеличение скорости. Во-вторых, направление сорти-
ровки определяется только выражением
ряд [гл] > ряд [п]
Чтобы произвести сортировку в порядке убывания величин,
достаточно заменить это выражение таким:
ряд [гл] < ряд [л]
program сортировкашелла (input, output);
const
максимальнаядлина= 1000;
type
индекс=1 .. максимальнаядлина;
типряда=аггау [индекс] of integer;
var
исходныйряд : типряда;
счетчик : 0 .. максимальнаядлина;
инд : индекс;
procedure сортировка (var ряд : типряда; длина :
индекс);
var
скачок, m, п : индекс;
раб : integer;
готово : boolean;
begin
скачок := длина;
while скачок > 1 do
begin
скачок := скачок div 2;
repeat
готово := true;
for m := 1 to длина — скачок do
begin
n := m + скачок;
if ряд [m] > ряд[п]
6.1. Массивы
173
then
begin
раб := ряд[гп];
ряд[т] := ряд[п];
ряд[п] := раб;
готово := false
end
end {for}
until готово
end {while}
end; {сортировка}
begin {сортировкашелла}
счетчик := 0;
геаб(исходныйряд[счетчик + 11);
while not eof do
begin
счетчик := счетчик + 1;
геаб(исходныйряд[счетчик + 11)
end; {while}
if счетчик >0
then
begin
сортировка(исходныйряд, счетчик);
for инд := 1 to счетчик do
write(ncxoflHbiflpafl[HHfl])
end
else write ('отсутствует информация на входе')
end. {сортировкашелла}
Ввод:
9817634541
Вывод:
1 134456789
Многомерные массивы
Базовым типом массива может в свою очередь быть массив.
В этих определениях базовым типом объекта матрица служит
тип столбец.
const
размер=10;
type
индекс=1 .. размер;
столбец=аггау [индекс] of real;
матрица=аггау [индекс] of столбец;
174
Гл 6 Сложные типы
Определение типа столбец может быть включено в определение
типа матрица:
type
матрица = array [индекс] of
array [индекс] of real;
а это довольно неуклюжее выражение можно в свою очередь
упростить до такой более удобной формы:
type
матрица = array [индекс, индекс] of real;
Теперь опишем некоторые переменные:
var
a,b,c : матрица;
r,s,t : индекс;
Столбец s матрицы а есть компонента a[s]. Компонента t столбца s
есть вещественная переменная, которую можно записать как
tzts] [Л. По аналогии с сокращенным определением, можно упро-
стить запись до ds, t\. Массив а называется двумерным, потому
что мы можем представлять себе, что он хранится в памяти в та-
ком виде:
all.l] at 1,2] at 1,3] . . all,101
a[2,l] a[2,21 al2,3] . . at2,10]
a[3,l] a[3,2] a[3,3] . . a[3,10]
at 10,11 al 10,2] at 10,3] . . . allO,101
Двумерный массив представляет собой чистую абстракцию,
поскольку память у машин одномерна. Компилятор должен уметь
ставить в соответствие абстрактному двумерному массиву кон-
кретный одномерный массив, которым является память вычис-
лительной машины.
Предположим, значения массива единичная матрица опре-
деляется соотношениями:
a[s,t] = 1,
a[s,t]=0,
если s=t;
если s#=t.
Мы можем присваивать массиву эти значения, выполняя такие
действия
for s : = 1 to размер do
for t := 1 to размер do
if s=t
then a[s,t] := 1
else a[s, t] :=0
6 1. Массивы
175
Произведение с двух матриц а и b получается при выполнении
оператора
for г := 1 to размер do
for s : = 1 to размер do
begin
c[r, s] := 0;
for t := 1 to размер do
c[r, s] : = c [r, s] + a [r, t] * b [t, s]
end
Доступ к компоненте с [г, s] внутри самого внутреннего цикла
осуществляется 2 * размер раз. Если ваш компилятор доста-
точно хорош, это не имеет значения, но если вы пользуетесь про-
стым компилятором, ему нужно помочь, обращаясь к с [г, s]
только один раз:
for г := 1 to размер do
for s : — 1 to размер do
begin
сумма := 0;
for t : = 1 to размер do
сумма := сумма + a[r, t] * b[t, s];
dr, s] сумма
end
Пример: Частоты буквенных пар
В качестве примера использования многомерного массива
представим программу, которая вычисляет частоты повторения
пар соседних букв, в словах. Она говорит нам, например, встре-
чается ли сочетание «ее» чаще, чем сочетание «ие». Программа
подсчитывает только внутрисловные пары, поэтому из слов
«тик так» будут выделены только такие сочетания: «та», «ик»,
«та» и «ак», но не «кт». Счетчики запоминаются в двумерном
массиве, индексами для которого служат буквы. Опишем
type
буква — 'а' .. 'я';
var
счетмат : array [буква, буква] of integer;
Нам нужно «окно» шириной в два символа, через которое можно
было бы просматривать текст. Окно движется по тексту на один
символ за такт, и если оба символа в окне являются буквами,
то к соответствующей компоненте матрицы счетмат прибавля-
ется единица. Центральный цикл программы имеет такой вид:
while not eof do
begin
176
Гл. 6. Сложные типы
геасЦтексим);
if [тексим, предсим] ['а' . . 'я']
then
счетмат[тексим, предсим] : =
счетмат[тексим, предсим] + 1;
предсим := тексим
end
Тексим и предсим это два символа, видимые через окно. Заметим,
к слову, что использование множеств значительно сокращает бу-
левское выражение, которое в противном случае должно было
иметь вид:
('а' тексим) and (тексим 'я')
and ('а' предсим) and (предсим 'я')
Программа поисксмежных имеет вид:
program поисксмежных (input, output);
const
числоцифр = 4;
пробел = '
сма = 'а';
смя = 'я';
type
буква = сма . . смя;
var
счетмат : array [буква, буква] of integer;
строка, столбец : буква;
тексим, предсим : char;
begin
for строка := сма to смя do
for столбец := сма to смя do
счетмат [строка, столбец] : = 0;
предсим := пробел;
while not eof do
begin
геас!(тексим);
if [тексим, предсим] [сма . . смя]
then
счетмат [тексим, предсим] : =
счетмат [тексим, предсим] + 1;
предсим := тексим
end; {while}
write(npo^ : 2);
for столбец : = сма to смя do
\\тНе(пробел : числоцифр — 1, столбец);
writein; writein;
6 1. Массивы
177
for строка := сма to смя do
begin
\уп!е(пробел, строка);
for столбец : = сма to смя do
write(c4eTMaT [строка, столбец] :
числоцифр);
writein
end {for}
end. {поисксмежных}
Упакованные массивы
Компоненты массива запоминаются в последовательно распо-
ложенных словах памяти вычислительной машины. Такой способ
расположения эффективен для запоминания целых и веществен-
ных компонент, так как на многих машинах для подобных объек-
тов требуется по одному слову памяти или больше. При работе с
другими типами компонент это не всегда, однако, удобно, по-
скольку память расходуется неэкономно. Размер теряемой памя-
ти может быть уменьшен за счет упаковки нескольких компонент
массива в одно слово. Компилятор сделает это, если при описании
массива вы укажете, что этот массив является упакованным.
Такое указание делается с помощью специального слова packed.
Например, массив длиннаяапрока, описанный как
var
длиннаястрока: array [1 . . 1000] of char;
занимает 1000 слов памяти. Массив с описанием
var
длиннаястрока : packed array [1 . . 1000] of char;
занимает 100 слов памяти на машине CDC 6000, в одно слово ко-
торой помещается 10 символов, и 250 слов на машинах серий
IBM 360/370, которые допускают размещение в одном слове па-
мяти 4 символов.
Упакованный массив может использоваться в программе так
же, как и любой неупакованный массив, за одним важным исклю-
чением: во многих реализациях языка Паскаль упакованный мас-
сив нельзя передавать процедуре или функции в качестве пара-
метра-переменной. Программа, написанная с использованием
упакованных массивов, будет работать несколько медленнее, чем
та же программа с неупакованными массивами. Это связано с тем,
что компонента неупакованного массива может быть выбрана из
памяти машины быстрее, чем компонента массива упакованного.
Принятие решения, упаковывать некоторый массив или нет,
зависит от многих факторов, в том числе от таких, как размер
доступной памяти, скорость работы процессора, заданное время
178
Гл 6 Сложные типы
решения задачи и объем данных. Выбор, подходящий в одной
ситуации, может оказаться совершенно неприемлемым в другой.
Во многих случаях дополнительное время, требуемое для ра-
боты с упакованным массивом, может быть уменьшено выполне-
нием упаковки и распаковки компонент массива в одной операции,
а не раз за разом при каждом обращении к массиву. Это можно
сделать с помощью стандартных процедур pack и unpack. Пред-
положим, нужно прочесть текст из файла и запомнить слова тек-
ста в переменной слово, которая является упакованным массивом
размером в 20 символов. Вместо того чтобы копировать символы из
файла непосредственно в данную переменную, мы будем запоми-
нать их во временном неупакованном буфере. Описания этих
переменных выглядят таким образом:
const
размерслова-=20;
var
буфер : array [1 . . размерслова] of char;
слово : packed array [1 . . размерслова] of char;
Когда буфер будет заполнен, мы сможем вызвать такую про-
цедуру:
раск(буфер, 1, слово)
При выполнении процедуры все символы массива буфер будут
переданы с упаковкой в массив слово. Обратная операция
ипраск(слово, буфер, 1)
передает символы из упакованного массива слово в неупакован-
ный массив буфер.
В общем случае, когда мы описываем массив А типа Г и соот-
ветствующий упакованный массив Р, также имеющий тип Т,
будем иметь
var
А : array [m . .n] of Т;
P : packed array [i. . .j] of T;
где i, j, m и n есть скалярные константы, причем п —
j—i. Оператор
for k : = i to j do
P[k] : = A[k — i + ml
может быть сокращенно записан как
pack (A, m, Р)
что в свою очередь читается так: «упаковать компоненты массива
6 1. Массивы
179
Л, начиная с Alm] и кончая А [/ — i + ml, и переслать их в ком-
поненты массива Р, начиная с PU] и кончая P[j].
Оператор
for k : = i to j do
A[k—i “pm]:— P[k]
сокращенно записываем так
unpack(P, A, m)
что читается: «распаковать компоненты массива Р, начиная с
P\i] до Plj], и переслать их в массив Л, начиная с компоненты
Л 1ml и кончая Л[/ — i + т]».
Булевские массивы
Массив, имеющий в качестве базового булевский тип, имеет те
же свойства, что и множество х). Каждая компонента массива соот-
ветствует некоторому элементу множества, который может отсут-
ствовать (false) или присутствовать (true). Если мы опишем
type
индекс = 1. . 20;
var
инд : индекс;
хмнож : set of индекс;
хмасс : array [индекс] of boolean;
то операции
хмасс[инд] := false
хмасс[инд] : = true
будут эквивалентны операциям
хмнож хмнож — [инд]
хмнож := хмнож + [инд]
а булевское выражение
хмасс [инд]
будет эквивалентно выражению
инд in хмнож
Операции и применяемые для проверки вхождения одного
множества в другое, конечно, нельзя применять к булевским мас-
сивам. Операции над множествами производятся быстрее, чем те
же операции над булевскими массивами, поэтому там, где это
1 Правда, при этом следует учитывать, что для хранения объектов булев-
ского типа вовсе не обязательно будет использован Один разряд памяти. По-
этому может оказаться, что массив в 100 000 компонент потребует 100 000
слов.— Прим. ред.
180
Гл. 6. Сложные типы
только возможно, множествам следует отдавать предпочтение
перед булевскими массивами. Однако в большинстве реализаций
языка Паскаль булевский массив может иметь большее число
компонент, чем множество. Когда нам требуется очень большое
множество, скажем на 10 000 или на 100 000 компонент, может
оказаться, что программу легче написать (и прочитать), если
используется один булевский массив, а не массив множеств. Зна-
чительное количество памяти машины может быть сэкономлено,
но за счет увеличения времени выполнения, требующегося для
упаковки массива.
Классический алгоритм нахождения простых чисел называет-
ся решетом Эратосфена. Предположим, нам надо найти простые
числа, меньшие 10. Начнем с того, что выпишем все числа от 2
до 10:
23456789 10
Затем уберем самое маленькое число, решив, что оно простое, и
уберем все числа, делящиеся на него. После первого шага будем
иметь простое число 2 и решето, содержащее только нечетные
3 5 7 9
После второго шага у нас будет второе простое число 3, а в ре-
шете останутся только числа 5 и 7. Процесс кончается при опу-
стошении решета.
Опишем
const
максимум = 100000;
var
решето : packed array [2 . . максимум] of boolean;
Первоначально каждая компонента массива решето получает
значение true. Это означает, что все числа присутствуют в решете.
По мере исключения чисел, соответствующие компоненты полу-
чают значение false. Программа состоит из двух вложенных цик-
лов, один из которых служит для нахождения наименьшего числа
из тех, что еще присутствуют в решете, а второй исключает числа,
делящиеся на данное. Условием окончания работы внешнего цик-
ла является отсутствие чисел в решете, а это легко проверить,
если все время подсчитывать количество чисел, находящихся в
решете.
program простыечисла (input, cutput):
const
первоепростое = 2;
максимум = 100000;
6.1. Массивы
181
var
решето : packed array [первоепростое . . максимум]
of boolean;
остаток, диапазон, множитель, кратное : 0 . .
максимум;
begin
геаё(диапазон);
for множитель : = первоепростое to диапазон do
решето [множитель] := true;
остаток :== диапазон — первоепростое + 1;
множитель := первоепростое — 1;
repeat
множитель := множитель + 1;
if решето [множитель]
then {множитель есть простое число}
begin
writeln(мнoжитeль);
кратное := 1;
while множитель * кратное
диапазон do
begin
if решето [множитель * кратное]
then {убрать кратное}
begin
х решето[множитель *
кратное] := false;
остаток := остаток — 1;
end;
кратное := кратное + 1
end {while}
end
until остаток = О
end. {простыечисла}
Ввод:
50
Вывод:
2
3
5
7
11
13
17
182
Гл. 6. Сложные типы
19
23
29
31
37
41
43
47
Строки символов
Мы уже знакомы с использованием символов или переменных
символьного типа в программах языка Паскаль. Строка — это
есть просто последовательность символов. Строками являются:
cogito, ergo sum
волшебник изумрудного города
под’езд был слабо освещен
При написании строковых констант в программах на Паскале их
нужно заключить в апострофы, играющие роль кавычек. Для
первых двух строк это сделать довольно легко:
'cogito, ergo sum'
'волшебник изумрудного города'
Запись третьей строки несколько затруднена, поскольку в ней
самой содержится апостроф. Мы указываем компилятору, что
этот апостроф входит внутрь строки, помещая непосредственно за
ним второй апостроф:
'подъезд был слабо освещен'
Строковые константы можно определить в разделе определения
констант, они могут также появляться в операторах записи:
const
сообщение = 'пора домой';
\уп1е(сообщение);
write('nopa домой')
Эти два оператора записи имеют один и тот же результат. Если
переменная строка представляет собой строку длиной п сим-
волов, то оператор
write (строка : т)
даст следующий результат: если т п, то перед строкой будут
выданы в файл (т — п) пробелов, если т < и, то в файл будут
выданы только т первых символов строки. Следующий оператор,
используя первый вариант оператора, рисует график затухаю-
6.1. Массивы
183
щего гармонического движения на печатающем устройстве:
for t := 0 to 60 do
writein ('*' : round (60 *(1 + exp(—t 30) * sin (t/3))))
Строковая константа не может превышать по длине строчку
печатающего устройства. Строка может содержать произвольные
символы. Символы в строках представляют сами себя, в противо-
положность символам, находящимся в других местах программы,
которые представляют числа, идентификаторы, зарезервирован-
ные слова и другие элементы языка. Следовательно,
'begin' не имеет ничего общего с begin;
'123' не является числом;
'+' не является знаком операции.
Переменная-строка (или строковая переменная) — это упако-
ванный массив символов. Приведем несколько определений «стро-
ковых» типов:
type
образкарты = packed array [ 1 . . 80] of char;
образстроки = packed array [1 . . 150] of char;
длиннаястрока = packed array [1 . . 1000] of char;
Длина строки определяется ее типом и не может меняться. Тем
самым язык Паскаль представляет меньше возможностей для ра-
боты со строками, чем некоторые другие языки программирова-
ния, допускающие изменение длины строки вовремя выполнения
программы.
Многие свойства строк вытекают из обычных свойств масси-
вов. Например, строки одного и того же типа могут фигуриро-
вать в операторах присваивания, их можно сравнивать при
помощи знаков ' = ' и '^='. Дополнительное свойство строк связа-
но с их упорядоченностью. Сравнение строк производится путем
последовательного сравнения входящих в них символов, начи-
ная с первых, и продолжается до первого несовпадения. Несов-
падающие символы и определяют упорядоченность строк. Это
обычный алфавитный порядок в тех случаях, когда строки состоят
только из букв. Если в строках присутствуют другие символы, их
порядок определяется внутренним машинным представлением
символов. Например,
'пароход' > 'паровоз'
поскольку 'х' > 'в'.
Стандартная процедура чтения read не предназначена для
автоматического чтения строк из входного файла. Если вам необ-
ходимо читать строки, то нужно проделывать это в цикле, символ
за символом.
184
Гл. 6. Сложные типы
В общем случае регулярные типы совместимы, если они иден-
тичны. Для строк это правило не является столь строгим, строки
совместимы, если их длины одинаковы.
6.2. Записи х)
Запись, как и массив,— это сложная переменная с нескольки-
ми компонентами. Однако компоненты записи могут иметь раз-
ные типы, кроме того, доступ к ним осуществляется не по индек-
су, а по имени. Массивы и записи являются абстрактными пред-
ставлениями областей для хранения данных.
Описание планеты должно включать следующую информацию:
имя,
видна ли невооруженным глазом (да/нет),
диаметр,
средний радиус орбиты.
Для имени лучше всего использовать строку, для указания види-
мости — булевскую переменную, а последние две величины —
это вещественные числа. Можно описать тип планета так:
type
планета =
record
имя : packed array [1 . . 10] of char:
видимость : boolean;
диаметр, орбрад : real
end;
Переменные этого типа (который называется комбинированным)
могут быть описаны обычным образом:
var
внутренняя, внешняя : планета;
Компонента записи выбирается с использованием имени пере-
менной-записи и имени ее компоненты, разделенных точкой:
внутренняя . имя есть имя внутренней планеты
внешняя . диаметр есть диаметр внешней планеты
Эти имена называются селекторами записи, в программах они
используются так же, как и переменные других типов. Возмож-
ны, например, такие операторы присваивания:
внутренняя . имя := 'венера ';
внутренняя . видимость : = true;
внутренняя . диаметр := 12104; {километров}
п Записи относятся к объектам комбинированного типа.— Прим. ред.
6.2. Записи
185
внутренняя . орбрад := 108.2; {гигаметров}
внешняя . имя : = 'нептун
внешняя . видимость : = false;
внешняя . диаметр := 49500;
внешняя . орбрад := 4496,6;
Сложные типы, массивы и записи могут комбинироваться. Ис-
пользуя приведенное ранее определение типа планета, мы можем
теперь описать:
const
дальняя = 10;
type
планета = (см. выше)
var
солнечнаясистема : array [1 . . дальняя] of планета;
номплан : 1 . . дальняя;
Теперь могут быть выполнены такие операторы:
for номплан := 1 to дальняя do
if солнечнаясистема[номплан] . орбрад < 4000
then солнечнаясистема[номплан] . видимость := true
else солнечнаясистема[номплан] . видимость := false
Последний оператор может быть сокращенно записан так:
for номплан := to дальняя do
солнечнаясистема[номплан] . видимость : =
солнечнаясистема[номплан] . орбрад < 4000
Имена компонент внутри записи не должны повторяться. Мы
не можем использовать повторно идентификатор имя в описании
записи планета, но можем пользоваться им для обозначения пере-
менной или компоненты другой записи. Нет ни одной операции,
которая бы воспринимала запись как нечто целое. Однако значе-
ние записи можно пересылать в другие переменные — записи с
помощью операторов присваивания. Возвращаясь к приведен-
ному выше примеру, можно написать
внутренняя := внешняя
что эквивалентно операторам присваивания
внутренняя . имя := внешняя . имя
внутренняя . видимость := внешняя . видимость
внутренняя . диаметр := внешняя . диаметр
внутренняя . орбрад : = внешняя . орбрад
Запись можно передавать в качестве параметра процедуре или
функции, но значением функции запись быть не может.
186
Гл. 6. Сложные типы
Оператор присоединения
Часто возникает необходимость обращаться к одной и той же
компоненте записи, или к нескольким компонентам одной и той
же записи, несколько раз на небольшом отрезке текста програм-
мы. Рассмотренный в предыдущем подразделе оператор цикла с
параметром представляет собой как раз образец повторения:
запись солнечнаясистема [номплан] встречается в двух последо-
вательно идущих строчках программы трижды. С помощью опе-
ратора присоединения мы можем записать этот оператор таким
обра зом:
for номплан := 1 to дальняя do
with солнечнаясистема[номплан] do
видимость : = орбрад < 4000
Общий вид оператора присоединения таков:
with идентификатор записи do
оператор
Внутри оператора к компонентам записи можно обращаться толь-
ко с помощью имени поля: компилятор сам подставит имя записи.
Кроме экономии места при написании программы, оператор при-
соединения оказывается еще полезным компилятору по той при-
чине, что ссылка на запись в этом случае подготавливается им
только один раз.
Записи с вариантами
Записи одного и того же типа не обязательно должны содер-
жать те же самые компоненты. Проиллюстрируем этот факт при-
мером из геометрии. Предположим, что нужно написать програм-
му, которая бы производила некоторые вычисления с использо-
ванием таких геометрических понятий, как точки, прямые и
окружности. Начнем с определений:
type
координаты =
record
абсцисса, ордината : real
end;
Точка очень легко представляется своими координатами:
type
точка =
record
положение : координаты
end;
Прямая лучше всего может быть представлена с помощью коэф-
6.2. Записи
187
фициентов ее уравнения
А.х :В.у + С-0
type
прямая =
record
хкоэффициент, укоэффициент, сдвиг : real
end;
Окружность также может быть представлена с помощью своего
уравнения
(х — р)г + (у — <?)2 = г2,
параметры р, q и г представляют окружность, центр которой
находится в точке (р, q) и радиус которой равен г.
type
окружность =
record
центр : координаты;
радиус : real
end;
Комбинированные типы точка, прямая и окружность могут быть
объединены в определении одного комбинированного типа, кото-
рый мы назовем фигура.
Такие записи, как записи типа фигура, имеющие варианты,
разделяются на две части. Первая часть называется общей частью,
а вторая часть носит название вариантной части. В нашем при-
мере общей частью записи фигура является лишь признак'—флаг,
указывающий, какой тип фигуры представляется записью, а ва-
риантная часть содержит описания типов точка, прямая и окруж-
ность, уже сделанные нами выше. Полное определение типа
фигура таково:
type
координаты =
record
абсцисса, ордината : real
end;
форма = (точка, прямая, окружность);
фигура =
record
флаг : форма;
case форма of
точка :
(положение : координаты);
прямая :
188
Гл. 6. Сложные типы
(хкоэффициент, укоэффициент,
сдвиг : real);
окружность :
(центр : координаты;
радиус : real)
end;
Это описание интересно по нескольким причинам. Роль слова
case с первого взгляда аналогична его роли в операторе вариан-
та, описанном в гл. 5, действительно, оператор варианта очень
удобен при работе с вариантами записей, что мы и увидим в даль-
нейшем. Однако между этими двумя ролями вариантов имеется
значительное различие. Во-первых, селектором варианта в
записи является непеременная, а тип. В нашем примере селекто-
ром варианта является тип форма. Во-вторых, после указания
вариантов не нужно ставить слово end, поскольку это слово и
так ставится в конце описания всей записи, что означает, тем
самым, и окончание вариантов. Так как вариантная часть записи
всегда следует за общей частью, после ее окончания в записи уже
не могут появиться никакие поля. Идентификаторы, используе-
мые в описаниях различных вариантов, не должны повторяться
в этой же записи: идентификатор поля нельзя применять для
обозначения компонент в двух вариантах одной записи, его
также нельзя применять для обозначения компонент в общей и в
вариантной частях записи. Однако в определении записи другого
типа можно использовать повторно тот же идентификатор поля.
Хотя в принципе поле признака можно опускать (в нашем
примере это поле задает форму фигуры), ясно, что в большинстве
случаев это поле для записи с вариантами нам просто необходи-
мо. В этих случаях приведенные выше определения можно со-
кращенно записывать так:
type
фигура =
record
case флаг : форма of
точка :
(положение : координаты);
прямая :
(хкоэффициент, укоэффициент,
сдвиг : real);
окружность :
(центр : координаты;
радиус : real)
end;
Флаг должен иметь скалярный тип. Нет необходимости опреде-
6.2. Записи
189
лять варианты записи для всех возможных значений признака,
хотя в интересах надежности работы программы это весьма же-
лательно х).
Вариантная часть определения записи с вариантами часто
сопровождается использованием в теле программы операторов
варианта. Ниже приводится процедура, имеющая формальный
параметр типа фигура, которая печатает значения, связанные с
этим типом.
procedure печатьфигуры (шаблон : фигура);
begin
with шаблон do
case флаг of
точка :
with положение do
write('To4Ka : (',абсцисса,',',
ордината, ')');
прямая :
write('HpHMan : ' хкоэффициент,' * X + ',
укоэффициент, ' * Y +', сдвиг,' = О');
окружность :
with центр do
^уп1е('Окружность : центр
(',абсцисса,',',
ордината,')
Радиус : ',радиус)
end; {case}
writein
end; {печатьфигуры}
Для обозначения «комбинированных типов с вариантами исполь-
зуется технический термин объединенный тип. Объединения бы-
вают свободными, если поле признака отсутствует, и с дискрими-
нантом, если это поле входит в состав записи. Используется
именно этот термин, потому что запись с вариантами есть по сути
объединение двух или более типов. Опишем:
type
видпредмета = (целый, вещественный, булевский);
предмет =
record
case видпредмета of
целый :
(целоезначение : integer);
1 Так как никакого контроля поля селектора при обращении к компонен-
те из вариантной части нет, то о надежности говорить здесь не стоит. В языке
можно, например, обратиться к компоненте фигура центр при значении фи-
гура. флаг-прямая. Это одно из «темных» мест языка.— Прим. ред.
190
Гл. 6. Сложные типы
вещественный :
(вещественноезначение : real);
булевский :
(булевскоезначение : boolean)
end;
var
нечто : предмет;
Предметно данному определению является свободным объеди-
ненным типом. Нечто есть объект с тремя различными именами:
нечто . целоезначение, нечто . вещественноезначение, и нечто .
булевскоезначение. В зависимости от имени, этот объект будет рас-
сматриваться как целая, вещественная или булевская перемен-
ная. Есть ситуации, в которых свободные объединенные типы
вполне допустимы, но все же эти ситуации встречаются редко.
В большинстве случаев для объединенных типов нужно создавать
дискриминанты, используя при их определении поле признака,
и принимая все меры предосторожности для того, чтобы значе-
ния поля признака всегда соответствовали содержимому записи.
Синтаксис записей
Синтаксис описания записи приведен на рис. 6.3. Записи, как
и массивы, могут упаковываться, что приводит к экономии места
за счет увеличения времени выполнения программы. Компонен-
ты упакованной записи нельзя передавать в качестве параметров-
переменных процедурам или функциям. Из диаграммы, показы-
вающей синтаксис списка полей, видно, что запись может содер-
жать общую часть, вариантную часть или обе эти части, но если
в записи используются обе части, то общая часть должна обяза-
тельно предшествовать вариантной части. Синтаксис вариантной
части снова содержит список полей, поэтому записи с вариантами
могут быть вложенными. Приведенное ниже определение типа
содержит три уровня вложенности.
type
видинструмента = (струнный, духовой);
типинструмента = (ударный, смычковый);
типматериала = (медь, дерево);
инструмент =
record
case вид : видинструмента of
струнный :
(
case тип : типинструмента of
ударный :
(
case клавиатура : boolean of
false :
6.2. Записи
191
(диапазон : real);
true :
(числоклавиш : 1 . . 100)
);
смычковый :
(размер : (контрабас, виолончель,
альт, скрипка))
);
духовой :
(
case материал : типматериала of
медь :
(метод : (клавишный,
подвижный));
дерево :
(язычок : одинарный, двойной))
)
end;
>| Вариантная часть
список | с
П0Лей--------тН оОьца-я чагть
Описание записи
Рис. 6.3. Синтаксис описания записи
----идентификатор
•(record)-
>] список полей
192
Гл. 6. Сложные типы
Переменная типа инструмент имеет либо три, либо четыре
компоненты:
var
пианино : инструмент;
пианино . вид : = струнный;
пианино . тип :== ударный;
пианино . клавиатура := true;
пианино . числоклавиш : = 88
Рис. 6.4 показывает синтаксис компоненты записи. Если
компонента записи сама является записью, в тексте программы
К°ДаписоПацЭенпшфикапюр записи.
Рис. 6.4. Синтаксис компоненты записи
Рис. 6.5. Синтаксис оператора присоединения
ссылка на нее указывается с помощью естественного расширения
приведенного ранее способа. Если переменная круг имеет тип
окружность, то
круг . центр
есть координаты его центра, а
круг . центр . абсцисса : = О
присваивает значение нуль координате х центра круга.
На рис. 6.5 показан синтаксис оператора присоединения.
Заметьте, что внутри заголовка оператора присоединения можно
указывать несколько идентификаторов записей. Приведенный
выше оператор присваивания можно записать несколькими спосо-
бами
with круг do
with центр do
абсцисса : = О
или
with круг, центр do
абсцисса : = О
6.2. Записи
193
и ~ t •
with круг . центр do
абсцисса := О
Пример: Построение описанной окружности
Воспользуемся определением типа фигура как исходным пунк-
том для создания программы, которая будет строить окружность,
описанную вокруг произвольного треугольника, или, что то же,
окружность, проходящую через три заданные точки. Будем
применять такой алгоритм:
Пусть даны точки рп р2 и р3. Нарисуем три окружности оди-
наковых радиусов с центрами в этих точках. (Радиус может быть
произвольным, нужно лить убедиться, что окружности пересе-
кутся.) Проведем прямую через точки пересечения сх и с2, и
прямую s2 через точки пересечения с2 и с3. Прямые sx и s2 пересе-
кутся в центре окружности, описывающей данный треугольник,
причем радиусом окружности будет расстояние от получен-
ной точки до любой из данных.
Можно выразить этот алгоритм с помощью следующей схемы
программы:
выбрать радиус, г;
нарисоватьокружность (pv, г, q);
нарисоватьокружность (р2, г, q);
нарисоватьокружность (р3, г, q);
пересечь (q, q, sj;
пересечь (q, q, s2);
найтиточкупересечения (q, s2, центркруга);
радиускруга := расстояние (/q, центркруга);
нарисоватьокружность (центркруга, радиускруга,
окружность)
Иногда такой алгоритм не работает. Две или все три точки
могут совпадать — в этом случае построение невозможно; точки
могут находиться на одной прямой — в этом случае будут совпа-
дать прямые и s2. Так как мы не можем ожидать от наших вычис-
лений абсолютно точных результатов, то надо производить про-
верки не на «совпадение», а на «близость», и говорить о «практи-
ческой параллельности», а не о «параллельности» прямых. Это
позволит нам избежать ряда проблем, касающихся переполнения
и потери значимости, и не делать построения, когда оно может
оказаться неточным. К определению типа форма мы добавим зна-
чение пусто. Процедуры будут возвращать такие значения в тех
случаях, когда они не смогут давать верный результат. Новое
7 № 3388
194
Гл. 6. Сложные типы
определение типа фигура выглядит так:
type
форма = (пусто, точка, прямая, окружность);
фигура =
record
case флаг : форма of
пусто :
( );
точка :
(положение : координаты);
прямая :
(хкоэффициент, укоэффициент, сдвиг
: real);
окружность :
(центр : координаты;
радиус : real)
end;
Написание процедуры нарисоватъокружность не вызывает
никаких особенных затруднений:
procedure нарисоватьокружность (цен : фигура;
рад : real;
var окр : фигура);
begin
with окр do
if (цен . флаг = точка) and (рад > 0)
then
begin
флаг : = окружность;
центр : = цен . положение;
радиус : = рад
end
else флаг : = пусто
end;
Процедура найтиточку пересечения выдает точку пересечения
двух прямых при условии, что они не параллельны. Предполо-
жим, что этими прямыми являются прямая! и прямая2:
прямая! : А.х + В.у + С=0\
прямая2 : D . x+E.y+F
Теперь имеем, если А.Е — B.D—0, то прямые совпадают или
параллельны, в противном случае они пересекаются в точке
х = — (C.E—B.F)/(A.E—B.D);
у = (C.D—A.F) / (А.Е — B.D),
6.2. Записи
195
Процедура пересечь находит линию пересечения двух окруж-
ностей. Предположим, этими окружностями являются окр1
и окр2:
окр1 (х— А)2 + (у—В)2=С2;
окр2: (х — D)2+(y—E)2=F2;
вычитая одно уравнение из другого, получим
— 2.A.x+A+2.D.x —D2—2.B.y—B2A-2.E.y—E2
—C2—F2\
и следовательно, уравнение линии пересечения этих окружностей
выглядит так:
х (A—D)+y (В—Е)+ ((С2—Л2—В2)— (F2—D2—£2))/2 =0.
Это есть уравнение геометрического места точек, равноудален-
ных от центров окружностей, поэтому для наших целей оно будет
подходить даже в том случае, если окружности на самом деле не
пересекаются.
program окружности (input, output);
const
дельта = 1Е-6;
type
форма = (пусто, точка, прямая, окружность);
координаты =
record
абсцисса, ордината : real
end; {координаты}
фигура=
record
case флаг : форма of
пусто :
( );
точка :
(положение : координаты);
прямая :
(хкоэффициент, укоэффициент, сдвиг : real);
окружность :
(центр : координаты;
радиус : real)
end; {фигура}
var
т1,т2, тЗ, окр!, окр2, окрЗ, npl, пр2,
центрописанной, описаннаяокружность : фигура;
рад, радиусописанной : real;
procedure печатьфигуры (шаблон : фигура);
begin
with шаблон do
case флаг of
пусто :
write('Отсутствует фигура');
точка :
7*
196
Гл. 6. Сложные типы
with положение do
write (’Точка: (', абсцисса,
ордината, ')');
прямая :
writefПрямая:', хкоэффициент, '•? Х+',
укоэффициент, •'* Y+',
сдвиг, ' = 0');
окружность :
with центр do
write(,Oкpyжнocть: центр (', абсцисса, %
ордината, ') радиус:' , радиус)
end; {case}
writeln
end; {печатьфигуры}
procedure читатьточку (var тчк : фигура);
begin
with тчк, положение do
begin
флаг := точка;
геад(абсцисса, ордината)
end {with}
end; {читатьточку}
function расстояние (фиг1, фиг2 : фигура) : real;
begin
if (фиг1 .флаг=точка) and (фиг2.флаг точка)
then расстояние := sqrt(
5цг(фиг1 .положение.абсцисса
—фиг2. положение.абсцисса)4-
5цг(фиг1 .положение.ордината
—фиг2. положение.ордината))
else расстояние := 0
end; {расстояние}
function слишкомблизко (фиг1, фиг2 : фигура) : boolean;
begin
слишкомблизко :— расстояние(фиг1, фиг2) < дельта
end; {слишкомблизко}
procedure нарисоватьокружность (цен : фигура; рад ' real;
var окр : фигура),
begin
with окр do
if (цен.флаг = точка) and (рад>0)
then
begin
флаг : = окружность;
центр цен. положение;
радиус : = рад
end
else флаг : = пусто
end, {нарисоватьокружность}
procedure найтиточкупересечения(прм1, прм2 : фигура;
var тчк : фигура);
6.2. Записи
197
var
знам : real;
begin
if (прм1 . флаг = прямая) and (прм2 . флаг = прямая)
then
begin
знам := прм1 . хкоэффициент *
прм2 . укоэффициент
— прм! . укоэффициент *
прм2 , хкоэффициент;
if аЬз(знам) < дельта
then тчк.флаг := пусто
{прямые параллельны}
else
with тчк do
begin
флаг : = точка
with положение do
begin
абсцисса :== —
(прм1.сдвиг * прм2.укоэффициент
— прм1. укоэффициент * прм2.сдвиг)/знам;
ордината :=
(прм 1 .сдвиг*прм2. хкоэффициент
— прм 1.хкоэффициент * прм2.сдвиг)/знам
end {with положение}
end {with тчк}
end
else тчк. флаг := пусто
end; {найтиточкупересечения}
procedure пересечь(ок1, ок2 : фигура;
var линияпересечения : фигура);
begin
if (ок1 . флаг = окружность) and
(ок2 . флаг = окружность)
then
with линияпересечения do
begin
флаг := прямая;
хкоэффициент := ок1 . центр . абсцисса —
ок.2. центр . абсцисса;
укоэффициент := ок1. центр . ордината —
ок 2 . центр . ордината;
сдвиг := ((sqr(oKl. радиус)
— sqr(oKl . центр . абсцисса)
—sqr(oKl . центр . ордината))
— (sqr(oK2 . радиус)
— sqr(0K2. центр . абсцисса)
— sqr(oK2 . центр.opдината)))/2
end {with}
else линияпересечения . флаг := пусто
end; {пересечь}
begin {окружности}
читатьточку(т1);
198
Гл. 6. Сложные типы
читатьточку(т2);
читатьточку (тЗ);
if слишкомблизко(т1, т2)
ог слишкомблизко(т2, тЗ)
ог слишкомблизко(т1, тЗ)
then writeln('Точки не удается различить')
else
begin
рад := расстояние (т1, т2) + расстояние (т2, тЗ);
нарисоватьокружность (т1, рад, окр1);
нарисоватьокружность(т2, рад, окр2);
нарисоватьокружность(тЗ, рад, окрЗ);
пересечь(окр1, окр’2, npl);
пересечь(окр2, окрЗ, пр2);
найтиточкупересечения(пр1, пр2, центрописанной);
радиусописанной := расстояние (т1, центрописанной):
нарисоватьокружность (центрописанной,
радиусописанной,
описаннаяокружность);
печатьфи гуры(описанна яокружность)
end
end. {окружности}
Ввод:
—1000 0
1000 0
0 1
Вывод:
Окружность: центр (0, —499999.5) радиус: 500000.5
Заметьте, что главная программа в этом примере имеет дело
только с расстояниями и фигурами. Подробности представления
фигур (как значений типа фигура) не важны для главной програм-
мы и «из нее не видны». Мы могли бы переписать определение типа
фигура, а также процедур и функций программы, пользуясь со-
вершенно другим представлением (например, полярными коорди-
натами), не изменяя ничего в основной программе. Еще важнее
тот факт, что на уровне главной программы нам нет необходимо-
сти думать о конкретных деталях представления, можно полно-
стью отвлечься от них, решать задачу на нужном уровне абст-
ракции.
Пример: Частотный словарь текста
Последний пример этой главы будет посвящен построению
программы, работающей с массивом, компонентами которого яв-
ляются записи. Эта программа проиллюстрирует также исполь-
зование упакованных массивов. Программа читает текст, а затем
печатает список слов, входящих в него, и число вхождений каж-
дого слова в текст. Назовем этот список частотным словарем
текста. Форма, в которой должен быть представлен результат,
предполагает, что в программе имеется таблица, каждая строка
6.2. Записи
199
которой содержит какое-то слово и счетчик. Начнем с определе-
ний и описаний:
const
размертаблицы = 1000;
type
индекстаблицы = 1 . . размертаблицы;
типстроки ==
record
слово : типслова;
счетчик : типсчетчика
end;
типтаблицы = array [индекстаблицы] of типстроки;
Каждое слово текста можно представлять упакованным масси-
вом, а счетчик положительным целым числом.
const
максимальнаядлинаслова = 20;
type
индекссимвола = 1 . . максимальнаядлинаслова;
типслова = packed array [индекссимвола] of char;
типсчетчика = 1 . . maxint;
Программа будет состоять из трех разделов. Во-первых, таб-
лицу нужно инициировать; во-вторых, нужно прочитать текст и
заполнить таблицу и, наконец, содержимое таблицы нужно вы-
дать на печать. Когда программа читает какое-либо слово тек-
ста, то либо слово уже встречалось ранее, в случае чего нужно
увеличить значение счетчика, либо это новое слово и его нужно
вставить в таблицу. Ясно, что эффективность программы сильно
зависит от метода поиска слов в таблице. В этой программе мы
применим простой линейный поиск, когда прочитанное слово
последовательно сравнивается с каждой строкой таблицы. Для
указания первой свободной строки таблицы будем использовать
переменную следстрока, в каждый момент времени в таблице на-
ходятся следстрока — 1 слов. Мы читаем слово и помещаем его в
эту свободную строчку. Линейный поиск организуется таким
образом:
строка := 1;
while таблица [строка] . слово =# таблица [следстрока] .
слово do
строка : == строка + 1
Цикл будет всегда иметь окончание при условии, что перемен-
ная следстрока 1, что можно обеспечить, инициировав ее
значение единицей,— это будет означать, что в таблице находятся
0 слов,— и увеличивая это значение на 1 всякий раз при встав-
200
Гл. 6. Сложные типы
лении слова. Цикл будет заканчиваться при
строка < следстрока
если слово уже есть в таблице, или при
строка = следстрока
если это новое слово. В последнем случае слово уже будет нахо-
диться на правильном месте (очередная свободная строка таб-
лицы), поэтому все, что нам нужно сделать, это увеличить зна-
чение переменной следстрока и инициировать счетчик.
Программа обращается к двум процедурам: читатъслово
собирает слово из файла в буфере и затем упаковывает его; печа-
татьслово распаковывает слово и печатает его.
program частотныйсловарь (input, output):
const
размертаблицы = 1000;
максимальнаядлинаслова=20;
type
индекссимвола = 1 .. максимальнаядлинаслова;
типсчетчика = 1 .. maxint;
индекстаблицы = 1 ..размертаблицы;
типслова = packed array [индекссимвола] of char}
типстроки =
record
слово : типслова;
счетчик : типсчетчика
end;
типтаблицы = array [индекстаблицы] of типстроки;
var
таблица : типтаблицы;
строка, следстрока : индекстаблицы;
переполнение : boolean;
буквы : set of char;
procedure читатьслово (var упакованноеслово : типслова):
const
пробел = ' ',
var
буфер : array [индекссимвола] of char;
счетсимволов : 0 .. максимальнаядлинаслова;
символ : char;
begin
if not eof
then
repeat
read (символ)
until eof or (символ in буквы);
if not eof
then
begin
счетсимволов := 0;
while символ in буквы do
begin
if счетсимволов < максимальнаядлинаслова
6.2. Записи
201
then
begin
счетсимволов := счетсимволов + 1;
буфер[счетсимволов] := символ
end;
if eof
then символ := пробел
else геаб(символ)
end; {while}
for счетсимволов := счетсимволов + 1
to максимальнаядлинаслова do
буфер[счетсимволов] пробел;
раск(буфер, 1, упакованноеслово)
end
end; {читатьслово}
procedure печататьслово (упакованноеслово : типслова);
const
пробел
var
буфер : array [индекссимвола] of char;
позициясимвола : 1 .. максимальнаядлинаслова;
begin
ппраск(упакованноеслово, буфер, 1);
for позициясимвола := 1 to максимальнаядлинаслова do
write (буфер[позициясимвола])
end; {печататьслово}
begin {частотныйуказатель}
буквы : = ['а' .. ' я'];
переполнение := false;
следстрока := 1;
while not (eof ог переполнение) do
begin
читатьслово(таблица[следстрока] . слово);
if not eof
then
begin
строка 1;
while таблица[строка] . слово =#
таблица[следстрока] . слово do
строка := строка + Г,
if строка < следстрока
then таблица[строка] . счетчик : =
таблица[строка] . счетчик 4- 1
else if следстрока<размертаблицы
then
begin
следстрока := следстрока + 1;
таблица[строка] . счетчик := 1
end
else переполнение := true
end
end; {while}
if переполнение
then writelnf Таблица слишком мала для этого текста')
202
Гл. 6. Сложные типы
else
for строка := 1 to следстрока — 1 do
with таблица[строка] do
begin
печататьслово(слово);
\\тИе1п(счетчик)
end {with}
end {частотныйсловарь}
Упражнения
6.1. Для описания внешности людей используется массив. Каждая ком-
понента массива суть запись с полями, содержащими информацию о поле,
росте, весе, цвете волос и глаз. Напишите подходящее для этого случая опреде-
ление массива и примените его в программе, читающей подобные описания
внешности или печатающей список накопленных описаний. Дополните про-
грамму таким образом, чтобы при добавлении нового описания на печать вы-
давалось наиболее близкое к нему из уже имеющихся в таблице.
6.2. Книга может быть определена в терминах такого описания:
const
длинастроки — 70;
размерстраницы = 55;
толщина = 330;
type
строка = array [1 .. длинастроки] of char;
страница — array [1 .. размерстраницы] of строка;
том = array [1 .. толщина] of страница;
var
книга : том;
Укажите тип и значение каждого из следующих выражений:
книга [25]
книга [187] [50]
книга [46] [7] [10]
Напишите программу для печати содержимого массива книга в соответствую-
щем формате.
6.3. Напишите программу, которая читает текст и печатает распределение
по длине слов. (Сколько в тексте слов из одной буквы, сколько из двух, и т. д.)
Программа должна также печатать среднее значение и дисперсию длины слов.
6.4. Напишите программу, которая печатает список чисел, являющихся
палиндпомическими как в двоичной, так и в десятичной системах. (Палиндро-
мическими называются числа, остающиеся без изменения при чтении в обрат-
ном порядке: число 79488497 палиндромично в десятичной системе.)
6.5. Если массив отсортирован, линейный поиск есть отнюдь не лучший
способ отыскания в нем заданного элемента. Более подходящим в этом случае
будет двоичный поиск Предположим, массив мае упорядочен по возрастанию
и содержит компоненту, значение которой есть ключ. Низ и верх есть границы
изменения индекса. После выполнения операторов
среднее := (низ -J- верх) div 2;
if ключ > мае [среднее]
then низ среднее + 1
else верх := среднее — 1
Упражнения
203
либо мае [среднее] — ключ, и мы имеем искомую компоненту, либо границы
переустанавливаются для поиска в меньшем диапазоне. Напишите рекурсив-
ную и итеративную процедуры, выполняющие двоичный поиск. Что случится,
если в массиве нет искомой компоненты? Модифицируйте программу таким об-
разом, чтобы учесть и такую ситуацию.
6.6. Дополните программу частотныйсловарь таким образом, чтобы она
печатала два списка. В первом списке слова находятся в алфавитном порядке,
а во втором — располагаются в порядке частоты вхождения в текст, причем
первым стоит наиболее часто встречающееся слово. Программа должна содер-
жать сортирующую процедуру, алгоритм которой основан на алгоритме про-
цедуры сортировка программы сортировкашелла, а один из параметров про-
цедуры должен указывать, какой из методов сортировки имеет место.
6.7. Текст содержит слова длиной 10 символов или меньше. Напишите
программу, которая читала бы до 1000 слов текста, а затем печатала прочитан-
ные слова в случайном порядке.
6.8. Выражение множитель * кратное вычисляется во внутреннем цикле
программы простыечисла трижды. Модифицируйте программу так, чтобы в
ней не было этого недостатка.
6.9. Напишите программу, которая бы читала и печатала шестнадцате-
ричные (по основанию шестнадцать) числа, записанные с помощью 16 цифр
0 1 2 3 4 5 6 7 8 9 ABCD EF
6.10. Напишите программу, которая бы переводила числа из одной сис-
темы в другую. Символы '<' и '>' и следующие за каждым из них десятичные
числа в диапазоне от 2 до 10, указывают входную и выходную системы счисле-
ния. Например, входная строка
<8 >2
говорит о том, что надо вводить восьмеричные числа и печатать их двоичные
эквиваленты.
6.11. Пасьянс под названием Часы раскладывается с использованием стан-
дартной колоды из 52 карт. Карты распределяются на стопки по четыре карты.
12 стопок располагаются по кругу, образуя «циферблат» часов, тринадцатая
стопка кладется в центр. Перенос заключается в снятии верхней карты из стоп-
ки и помещении ее под ту стопку, которой она должна принадлежать (туз=1,
. . ., валет — 11, дама = 12, короли кладутся в центр), и из этой стопки
берется очередная карта для следующего переноса. Игра заканчивается, когда
все четыре короля окажутся в центре, пасьянс считается успешным, если все
карты находятся на нужных местах. Напишите программу, которая имитиро-
вала бы расклад такого пасьянса, используя для работы с колодой карт гене-
ратор псевдослучайных чисел.
6.12. Одним из путей преодоления трудностей при работе со строками пе-
ременной длины является организация таблицы строк.
const
размертаблицы = 10000;
максдлинастроки = 100;
type
индекс = 1 .. размертаблицы;
строка =
record
204
Гл. 6. Сложные типы
первый, последний : индекс
end;
var
таблицастрок : packed array [индекс] of char;
буфер : array [1 .. максдлинастроки] of char;
Если таблица строк содержит
'СТОПОРКА...'
то строки 'СТОП', 'СТОПОР', 'ТОПОР' и 'ПОРКА' представляются в виде
записей (1,4), (1,6), (2,6), (4,8). Разработайте процедуру для определения того,
содержит ли таблица данную строку, для добавления в таблицу новой строки,
сравнения строк, генерации подстрок и объединения (конкатенации) строк.
Помните, что ни отдельные строки, ни результат объединения не могут иметь
длину большую, чем значение максдлинастроки.
6.13. Игра Жизнь, придуманная Джоном Хортоном Конвеем из Кембридж-
ского университета, происходит на прямоугольном поле ячеек, каждая из
которых может содержать организм. Ячейка имеет восемь соседей, и с помощью
занято (k) мы будем обозначать число ячеек, соседних с ячейкой k, которые
уже заняты организмами. Конфигурация нового поколения организмов полу-
чается из предыдущего поколения по двум простым правилам:
1. Организм в ячейке k переходит в следующее поколение, если выполнено
условие Ъ^занято (&)<3, в противном случае организм умирает.
2. Организм рождается в пустой ячейке k, если занято (&)=3.
Напишите программу, которая (1) читала бы начальную конфигурацию заня-
тых ячеек, (2) вычисляла последовательность поколений в соответствии с пра-
вилами и (3) печатала каждую конфигурацию. Заметьте, что все изменения
происходят одновременно, поэтому программа должна иметь две копии кон-
фигурации. Проверьте вашу программу для случая исходной ^/-образной кон-
фигурации из 7 ячеек; первые шесть поколений показаны ниже.
* *
* * ** ** ** ** ** ** ** *.* ** **
* ** * * ** **. ******
* * * Л * * * * ***’
*
4? 4е 4$
6.14. Ненаправленный граф с максверш вершинами может быть представ-
лен в виде массива
граф : array [1 . . максверш, 1 . . максверш] of boolean;
причем значение граф [и, и] равно true, если вершины и и v соединены ребром,
и равно false в противном случае.
(а) Опишите эквивалентное представление ненаправленного графа с использо-
ванием массива множеств.
(б) Вершина v может быть достигнута из вершины и с пересечением самое
большее п ребер, если
графп[и, v\—true,
где граф” вычисляется при помощи «перемножения» матриц, в котором обыч-
ные операции сложения и умножения заменены булевскими операциями
or и and.
Упражнения
205
Легко ли вычислить величину граф’1, используя представление посредством
множеств?
(в) Напишите программу, которая читала бы описания ребер, состоящие из
двух чисел, и печатала значения графп для 2, . . ., 5.
(г) .Можете ли вы найти условие окончания работы программы, если считать,
что п — это путь максимальной длины, вместо того чтобы заканчивать
работу после пяти итераций?
6.15. Если вы выполняли у пр. 4 11, то у вас есть программа, вычисляю-
щая значения комплексных выражений. Теперь вы можете написать такую
программу более элегантно, используя определение:
type
комплексные числа =
record
вещественнаячасть, мннмаячасть • real
end,
Глава 7
ФАЙЛЫ
Все программы, которые мы до сих пор изучали, вырабатывали
выходную информацию, а в большинстве случаев также и полу-
чали некоторую входную информацию. Программы могли это
делать с помощью файлов, имена которых являются стандарт-
ными идентификаторами input и output. В данной главе мы более
детально рассмотрим свойства этих и других файлов.
Важность файлов объясняется тремя причинами. Во-первых,
любой процесс может связываться со своим окружением только с
помощью файлов. Во-вторых, процессы обычно длятся недолго:
программа загружается в память и выполняется, а по окончании
выполнения одной программы память поступает в распоряжение
другой. Если программа не изменяет ни одного файла во время
своей работы, эффект будет таким же, как если бы она вовсе не
работала. В-третьих, файлы могут хранить гораздо больше ин-
формации, чем основная память машины.
В языке Паскаль файл представляется в виде переменной. Мы
уже видели на примерах из предыдущих глав, что это несколько
необычная переменная, поскольку она может существовать как
до, так и после выполнения программы и, кроме того, она может
быть намного больше, чем сама программа. В силу этих причин,
те действия, которые Паскаль-программа может производить над
файлами, значительно ограничены, причем при каждом обраще-
нии к фацлу программе доступна только одна компонента файла.
Так как файлы языка Паскаль являются абстрактным обоб-
щением реальных файлов, программа не содержит никакой ин-
формации о физической природе файла. Например, хотя вы знае-
те, что результатом работы стандартной процедуры чтения яв-
ляется передача данных из файла в область памяти, ассоцииро-
ванную с переменной вашей программы, вам не нужно указывать
(и даже знать), вводятся ли эти данные с перфокарт, читаются с
магнитного диска, или набираются на клавиатуре терминального
устройства. Привязка реальных файлов к вашей программе на
время ее выполнения является функцией операционной системы.
Операционная система делает это, выполняя команды, не являю-
щиеся частью вашей программы.
7.1. Последовательные файлы
207
Отношения между формальными файлами, описанными и упот-
ребляемыми в программе, и фактическими файлами, привязы-
ваемыми к программе на время ее выполнения операционной си-
стемой, похожи на отношения между формальными параметрами
процедуры и фактическими параметрами, передаваемыми ей из
вызывающей программы при выполнении этой процедуры. Эта
аналогия отражена в синтаксисе заголовка программы на языке
Паскаль (рис. 2.11), напоминающем синтаксис заголовка про-
цедуры (рис. 4.3).
7.1. Последовательные файлы
Тип файла определяется в программе с помощью соответст-
вующего определения типов.
const
числоколонок = 80;
type
номерколонки = 1 . . числоколонок;
карта = packed array [номерколонки] of char;
файлкарт = file of карта;
Сам файл описывается как переменная:
var
колода : файлкарт;
Имя файла также должно включаться в заголовок программы:
program обработкаперфокарт (input, output, колода);
Стандартные файлы input и output в разделе описания перемен-
ных упоминать нельзя. Однако в заголовке программы необхо-
димо поместить имя input, если в программе есть обращения к
read, eof или eoln без указания в них имени файла, а имя output
помещается в заголовок, если в программме есть процедура записи
без указания имени файла. В некоторых системах имя output
нужно вставлять в заголовок даже в тех случаях, когда в про-
грамме нет таких обращений к процедуре записи, поскольку в этот
файл могут быть переданы сообщения об обнаруженных в ходе
работы программы ошибках.
Базовым типом для файла колода является тип карта. Ком-
понентами файла являются переменные базового типа. В каждый
момент времени программа имеет доступ только к одной компо-
ненте файла. Компонента файла колода, к которой разрешен до-
ступ, есть переменная типа карта, которая обозначается так:
колода f
Компоненты файла традиционно называются записями этого фай-
ла. Слово «запись», относящееся к объектам языка Паскаль, не
208
Гл. 7. Файлы
вносит каких-либо противоречий в терминологию, поскольку в
большинстве случаев каждая запись файла является на самом
деле переменной комбинированного типа, т. е. записью в смысле
языка Паскаль х).
Структурно файл организован как последовательность эле-
ментов, что и показано на рис. 7.1.
Рис. 7.1. Последовательный файл
Запись в файл
Файл создается или пополняется, когда в него что-то записы-
вают. Каждая операция записи добавляет к файлу новую компо-
ненту. Компоненты могут добавляться только к концу последо-
вательного файла. Мы можем себе представить связанный с фай-
лом маркер, который говорит нам, куда можно вставить* новую
компоненту. На рис. 7.2 такой маркер представляется в виде
(CL)
Рис. 7.2. Запись в файл
стрелки. На рис. 7.2(a) показан пустой файл, маркер которого
указывает позицию, куда можно поместить первую запись, а
рис. 7.2(6) показывает файл, в который записаны уже четыре ком-
поненты, причем его маркер установлен в ту позицию, куда надо
помещать пятую компоненту.
Операция установки маркера на начало файла и приготовле-
ния его для записи информации выполняется с помощью вызова
стандартной процедуры
ге\уп!е(колода)
Эта операция одновременно уничтожает всю имеющуюся в дан-
ный момент в файле колода информацию. Ситуация, возникающая
после выполнения этой процедуры, отражена на рис. 7.2(a).
Называть компоненты файла записями не обязательно. Более того, ав-
тор языка Паскаль нигде такого «смешения» не делает, ибо запись в языке —
это нечто другое.— Прим. ред.
7.1. Последовательные файлы
209
Чтобы записать в файл очередную компоненту, мы заносим
нхжпые данные в переменную колода и вызываем процедуру
ргЩколода)
результатом работы которой будет создание новой компоненты
файла в той позиции, куда указывал маркер, после чего маркер
перемещается на одну позицию вправо.
Предположим, что мы описали
var
буфер : карта;
и что буфер содержит информацию, предназначенную для записи
в файл. Тогда операторы
колода f := буфер;
рпЦколода)
можно сокращенно записать так:
\мп1е(колода, буфер)
Чтение из файла
Создав файл, мы можем читать из него информацию. Перед на-
чалом чтения из файла колода выполняется процедура
reset (кол ода)
Эта процедура переместит маркер файла на его начало, как пока-
зано на рис. 7.3(a); она также передаст информацию из первой
(а)
Г
(5)
(В)
Рис. 7.3. Чтение из файла
компоненты файла в переменную колода \ . Чтобы прочитать
следующую компоненту, нужно вызвать процедуру
§е!(колода)
210
Гл 7. Файлы
которая продвинет маркер и скопирует информацию из следую-
щей компоненты в переменную колода \ . Может наступить такой
момент, когда после очередного перемещения маркера вправо мы
не обнаружим там никакой информации — рис. 7.3(b). В этом
случае значение переменной колода^ не может быть определено,
а булевская функция ео{(колода) выдаст значение true. Таким
образом, после обращения к процедуре get* возникает одна из
следующих ситуаций:
еоГ(колода) = false и колода f содержит очередную
компоненту
или
еоГ(колода) = true и колода \ не определена.
Если файл оказывается пустым в тот момент, когда мы читаем из
него первую запись, как на рис. 7.2(a), значение функции
eof (колода) будет равно true непосредственно после вызова про-
цедуры reset. Поэтому лучше сделать проверку, обратившись к
функции eof перед тем, как вызывать get. Можно также исполь-
зовать процедуру read'.
геас!(колода, буфер)
что эквивалентно операторам
буфер : = колода f ;
get(кoлoдa)
Ограничения на чтение и запись
Файловый тип в языке Паскаль является абстракцией магнит-
ной ленты. Операции reset и rewrite соответствуют перемотке
ленты и подготовке ее соответственно к чтению или записи. Не-
посредственно за операцией записи операция чтения следовать
не может, так же как за чтением не может следовать запись.
Следовательно, если файл нужно использовать как для ввода,
так и для вывода, его нужно «перемотать», заполнить информа-
цией, снова «перемотать», а затем уже читать из него. Физичес-
кая природа фактически подключенного файла может наклады-
вать на эту схему еще большие ограничения. Колоду перфокарт
можно только читать, вывод на пульт машины также невозможен,
напротив, печатающее устройство или экран дисплея может
лишь принимать информацию. В связи с этим в языке Паскаль
принято решение запретить как запись в файл input, так и ввод
информации из файла output.
Пример: Дублирование перфокарт
Файлы в отличие от переменных других типов нельзя целиком
копировать с помощью операторов присваивания. Однако их
7.2. Текстовые файлы
211
можно копировать по одной компоненте. Программа копировать-
колоду выполняет копирование колоды перфокарт, читаемой из
файла вхфайл, и передачу дубля в файл выхфайл. Пустые карты,
встречаемые в файле вхфайл, в выходной файл не попадают.
program копироватьколоду (вхфайл, выхфайл, output);
const
числоколонок = 80;
пробел = ' ';
type
номерколонки = 1 . . числоколонок;
карта = packed array [номерколонки] of char;
файлкарт = file of карта;
var
вхфайл, выхфайл : файлкарт;
буфер, пустаякарта : карта;
колонка : номерколонки;
begin
for колонка : = 1 to числоколонок do
• пустаякарта[колонка] := пробел;
reset (вхфайл);
rewrite (выхфайл);
while not eof (вхфайл) do
begin
read (вхфайл, буфер);
if буфер =# пустаякарта
then write (выхфайл, буфер)
end {while}
end. {копироватьколоду}
7.2. Текстовые файлы
Когда в предыдущих главах мы использовали процедуры read
и write без указания параметра, соответствующего имени файла,
мы неявно подразумевали, что нужно работать с файлами input
и output. Эти файлы должны быть упомянуты в заголовке про-
граммы, но описывать их в программе не обязательно. Их подра-
зумеваемое описание выглядит так:
type
text = packed file of char;
var
input, output : text;
Идентификаторы input и output являются подразумеваемыми па-
раметрами процедур read, write и eof. Тип text является стан-
дартным типом, поэтому разрешено описывать ваши собственные
файлы как текстовые.
212
Гл 7. Файлы
Автоматическое преобразование
Текстовыми файлами пользуются наиболее часто, поэтому для
удобства пользования ими язык Паскаль предусматривает мно-
гие специальные средства. Наиболее важным из них является
неявное преобразование типов, выполняемое процедурами чте-
ния и записи. В соответствии с определением этих процедур, дан-
ном в разд. 7.1, операторы
read(x) и write (х)
допускаются только в тех случаях, когда х имеет символьный
тип. Однако, как мы уже видели в гл. 2, х может иметь также
целый или вещественный тип (а для процедуры записи еще и бу-
левский тип), и эти процедуры производят автоматическое пре-
образование типов. Автоматическое преобразование производит-
ся также для всех текстовых файлов, а не только для файлов
input и output.
Предположим, у нас есть текстовый файл, содержащий неко-
торую статистическую информацию. Эта информация собрана в
группы, а каждая группа содержит десять вещественных чисел.
Файл большой, насчитывает, возможно, несколько тысяч таких
групп и должен обрабатываться много раз. Очень невыгодно хра-
нить такой файл в текстовом виде, поскольку при чтении файла
будут производиться тысячи преобразований. Гораздо лучше
произвести одно преобразование всех чисел, а затем записать их в
другой файл в двоичном виде, более подходящем для дальней-
шей работы. Программа преобразованиегрупп читает текст из
файладанных и пишет данные в двоичном виде в двоичныйфайл.
Компонента этого последнего файла представляет собой массив
из десяти вещественных чисел. Преобразование неявно выпол-
няется при вызове процедуры
геас!(файлданных, двоичныйфайл f [инд])
которая читает одно число, записанное с помощью символов,
преобразует его во внутреннее представление для вещественных
чисел и результат записывает непосредственно в компоненту
двоичныйфайл | .
program преобразованиегрупп (файлданных, двоичныйфайл,
output);
const
размергруппы = 10;
type
индекс = 1 . . размергруппы;
группа = array [индекс] of real;
var
файлданных : text;
7 2. Текстовые файлы
213
двоичныйфайл : file of группа;
инд : индекс;
счетчикгрупп : integer;
begin
reset (файлданных);
ге\¥гНе(двоичныйфайл);
счетчикгрупп : = 0;
repeat
инд : = 1;
геасЦфайлданных, двоичныйфайл f [инд]);
if not eof (фай л данных)
then
begin
repeat
инд : = инд 4- 1;
геас!(файлданных, двоичныйфайл f [инд])
until (инд = размергруппы) or
eof (файлданных);
if еоГ(файлданных)
then writein ('Последняя группа
в файле не полна ')
else
begin
риЦдвоичныйфайл);
счетчикгрупп : =
счетчикгрупп + 1
end
end
until eof (файлданных);
writeln('Преобразовано', счетчикгрупп, 'групп')
end. {преобразованиегрупп}
Параметры процедур read и write
Процедуры read и write могут иметь несколько параметров
при условии, что они относятся к текстовым файлам. Каждый
параметр может быть либо целого, либо вещественного, либо сим-
вольного типа. Процедуру записи можно вызывать также с пара-
метрами булевского типа, или параметрами являющимися упа-
кованными массивами символов. В приведенных ниже примерах
иф есть имя текстового файла, а pY, р2,. . ., рп — это параметры
процедур. Обращение
геаё(иф, рп р2.....рп)
эквивалентно обращениям
read (иф, pj;
read (иф, р2);
214
Гл. 7. Файлы
геасЦиф, рп)
аналогично,
write (иф, рп р2,. . рп)
эквивалентно
write (иф, рх);
write (иф, р2);
write (иф, рп)
Имя файла иф можно не указывать, при этом в случае вызова
процедуры read подразумевается работа с файлом input, а в слу-
чае вызова процедуры write — работа с файлом output.
Строчки в файле
Текстовые файлы, встречающиеся в обыденной жизни, такие,
как программы, данные или художественные тексты, не всегда
являются простыми последовательностями символов. Они могут
быть организованы и сложнее, чаще всего в строчки и страницы.
В языке Паскаль имеется набор стандартных процедур и функций,
позволяющих создавать файлы с подобной структурой, а также
распознавать такие структуры во’ входном текстовом файле.
В разных вычислительных машинах и операционных системах
строчки представляются по-разному. Вследствие этого в Паскаль
«встроена» булевская функция eoln, принимающая значение true
в конце строчки и значение false в любом другом состоянии файла,
независимо от конкретного представления ограничителя для кон-
ца строки. Если значение функций eoln равно true, текущим сим-
волом, на который указывает маркер файла, считается символ
пробела. Стандартная процедура readln пропускает во входном
файле все символы до тех пор, пока не встретит конец текущей
строки. Вызов
readln (иф)
где иф есть имя текстового файла, эквивалентен выполнению
следующих операторов:
while not ео1п(иф) do
get (иф);
get^)
Следующее обращение к процедуре read даст первый символ сле-
дующей строчки, если только при этом не был достигнут конец
файла. Имя файла можно опускать, в этом случае подразуме-
вается имя input. Процедура readln может иметь параметры:
readln(иф, рп р2.....рп)
что эквивалентно
7.3. Ввод и вывод
215
геас!(иф, pj;
геасЦиф, р2);
геас!(иф, рп);
геас11п(иф)
Процедурой readin можно пользоваться при чтении для пропуска
избыточной информации, например примечаний. Однако надо со-
блюдать осторожность, чтобы не пропустить, чего не следует.
Процедура writein используется, чтобы окончить текущую
выходную строчку и начать новую. Обращение
writein (иф, рр р2>. . рп)
эквивалентно последовательности;
write (иф, pj;
write (иф, р2);
write (иф, рп);
writein (иф)
Имя файла можно опустить, при этом подразумевается файл
output.
7.3. Ввод и вывод
В реальных вычислительных программах, в противополож-
ность примерам, приводимым в этой книге, ввод и вывод часто
занимает больше всего места. В программах коллективного
пользования любая входная информация должна полностью
контролироваться, а все содержащиеся в ней ошибки должны
быть обнаружены и выведены на печать в специальных диагно-
стических сообщениях. Информация, выводимая на печать,
должна быть оформлена таким образом, чтобы наиболее важные
результаты были сразу видны пользователю. Более того, необ-
ходим разумный компромисс, чтобы количество выводимой
информации не было ни слишком маленьким, ни слишком боль-
шим, когда все полезные или важнейшие результаты теряются в
сплошном потоке бесполезных деталей. Программы, приведенные
в этой книге, следует рассматривать скорее как шаблоны про-
грамм, лишенные правильно оформленных операторов ввода и
вывода. Сделано это в целях экономии места: большинство при-
меров было бы в два или три раза больше, если бы в них была
включена полная проверка входной информации, корректная
диагностика и проводилось правильное оформление выводимой
информации. Кроме того, добавочные операторы повторялись
бы из одной программы в другую и затемняли основные моменты,
иллюстрируемые программой. Цель настоящей главы — дать
216
Гл. 7. Файлы
некоторые рекомендации по составлению разделов ввода и вы-
вода в программах на языке Паскаль.
Соглашения о вводе и выводе делаются в соответствии с тем
режимом, в котором будет работать программа. В общем случае
программы, работающие в пакетном режиме, выводят больше
информации, чем программы, работающие в интерактивном (или
диалоговом) режиме. В частности, пакетная программа должна
копировать весь входной файл или большую его часть в
выходной файл, снабжая его при этом необходимыми замечания-
ми. Это делает, например, и компилятор с языка Паскаль. Вход-
ным файлом для компилятора служит ваша программа, которая и
выводится им на печать, снабженная заголовком, номерами
строчек, а возможно, и диагностическими сообщениями об ошиб-
ках. Компилятор создает также другой файл, содержащий вашу
программу, переведенную на машинный язык. Распечатка, или
«листинг», вашей программы — это одна из важнейших частей
выходной информации компилятора: без нее вам пришлось бы
печатать программу при помощи специальной программы,а при
этом бывает весьма затруднительно соотносить диагностические
сообщения с вашей программой. Аналогичные рассуждения
применимы и к другим программам; если ваша программа
печатает только ответы, вы, затем, можете в течение полугода
гадать, каков же был вопрос г).
Ввод
Программа, не вводящая информации извне, бесполезна, по-
скольку она повторяет одни и те же действия при каждом вы-
зове. Иногда возникает соблазн написать такую программу, осо-
бенно в том случае, когда система снабжена хорошим диалого-
вым редактором текстов. При этом данные вставляются непосред-
ственно в программу в форме определения констант, а затем зна-
чения этих констант изменяются перед каждым вызовом програм-
мы Это плохая методика Определение константы призвано
Это конечно верно Но если вы зададите сразу вопросы на все случаи
жизни и получите от ЭВМ ответы на все эти вопросы, то вам потребуется спе-
циальная программа, которая ищет ответ на интересующий вас в данный мо-
мент вопрос. В этом заключается один из парадоксов использования вычисли-
тельной техники: мы не умеем задавать вопросы, не требуем конкретного от-
вета и в результате получаем из ЭВМ огромные объемы информации, которые
приходится обрабатывать «головой». Например, программа в 6000 строк на
языке Паскаль не такая уж редкость. «Хороший» транслятор, работая с та-
кой программой, выдает том в 100—150 стр., содержащий всю информацию
о конкретной версии данной программы. Как только программа немного (даже
в процессе исправления ошибок) изменилась, вы получаете такой очередной
том, хотя очевидно, что он во многом подобен старому. (Если подходить строго
формально, то он весь новый хотя бы потому, что в начало программы была до-
бавлена новая строка и почти все номера строк текста «сдвинулись».) —
Прим. ред.
7.3. Ввод и вывод
217
служить для определения значений, не меняющихся при различ-
ных вызовах программы. Любая величина, меняющаяся от одного
вызова программы к другому, является входной информацией и
должна находиться во входном файле.
Входные данные могут иметь как свободный, так и фиксиро-
ванный формат. Язык Паскаль больше приспособлен для чтения
данных в свободном формате благодаря тому, что в нем имеется
возможность считывать из входного файла по одному символу.
поле колонки формат
с Зо
имя 1 30 буквенный
адрес 31 70 Куквейна — цифровой
инЗеко 71 80 цифровой
Рис. 7.4. Фиксированный формат
Программа калькулятор из гл. 4 читает данные в свободном фор-
мате. При разработке программы, читающей данные в свободном
формате, часто оказывается полезным рисовать синтаксические
диаграммы для допускаемых структур входных данных. Они
упрощают разработку программы и помогают пользователям
программы при подготовке данных. Ввод в фиксированном фор-
мате является более старым методом, возникшим в то время, когда
для обработки данных интенсивно использовались перфокарты.
Карта (или входная запись) делилась на поля фиксированной
длины, каждый элемент данных был привязан к своему полю.
Рис. 7.4 показывает простой способ расположения на карте фик-
сированных полей. Фиксированные поля записи наиболее легко
обрабатываются на языке Паскаль, когда вся запись считывается
в массив, с последующей проверкой и преобразованием каждого
поля.
Предположим, программа должна читать 80-колоночные кар-
ты с несколькими полями для чисел без знака. Опишем:
const
числоколонок = 80;
type
номерколонки = 1 . . числоколонок;
образкарты = packed array [нолмерколонки] of char;
var
данные : file of образкарты;
входнаякарта : образкарты;
218
Гл. 7. Файлы
Для проверки и преобразования выбранных полей моЯсно вос-
пользоваться следующей процедурой:
procedure преобразованиечисленногополя (карта :
образкарты; первая,
последняя : номерколонки;
var значение : integer;
var ошибка : boolean);
const
пробел = '
основание = 10;
var
колонка : номерколонки;
begin
значение := 0;
ошибка := false;
колонка := первая;
while (карта [колонка] = пробел) and
(колонка < последняя) do
колонка колонка + 1;
if карта [колонка] пробел
then
for колонка := колонка to последняя do
if карта [колонка] in ['0' . . '9']
then значение : = основание * значение +
огс!(карта[колонка]) — ord ('0')
else ошибка := true
end; {преобразованиечисленногополя}
При посимвольной обработке входного файла возникает проб-
лема, заключающаяся в том, что ответы на запросы и диагности-
ческие сообщения об ошибках выдаются в то время, когда чита-
ется входная строка. Если программа работает в пакетном режиме
и копирует входную информацию в выходной файл, то выходной
файл будет выглядеть очень запутанным. Этого можно избежать,
вводя и распечатывая сразу всю строку, но передавая в вызвав-
шую программу только по одному символу. Для этого требуется
организация буфера строки. В главной программе опишем:
const
максимальнаядлинастроки = 100;
type
буфер =
record
строка : array [1 . . максимальнаядлинастроки]
of char;
индекс, длина : 0 . . максимальнаядлинастроки
end;
7.3. Ввод и вывод
219
var
входнойбуфер : буфер;
вхфайл, выхфайл : text;
Выполним следующие инициирующие действия:
reset (вхфайл);
rewrite (выхфайл);
вхбуф . индекс := О
Чтобы прочитать символ из входного файла, будем вызывать та-
кую процедуру:
procedure читатьсимвол (var символ : char;
var входнойбуфер : буфер;
var вхфайл, выхфайл : text);
begin
with вхбуф do
if not eof (вхфайл)
then
begin
if индекс = 0
then
begin
длина := 0;
while not eoln (вхфайл) do
begin
read (вхфайл, символ);
write (выхфайл, символ);
if длина <
максимальнаядлинастроки
then
begin
длина := длина
+ 1;
строка [длина] : =
символ
end
end; {while}
readln (вхфайл);
writein (выхфайл)
end;
символ := строка [индекс + 11;
if индекс < длина — 1
then индекс := индекс +1
else индекс := 0
end
end; {читатьсимвол}
220
Гл. 7. Файлы
Вывод
Все программы должны выдавать некоторую информацию в
файл output, даже если основная часть выдаваемых программой
данных передается еще в какой-нибудь файл. Программа, произ-
водящая слияние двух файлов, должна выдать отчет о своей
работе, например:
14371 записей прочитано в файле 'ОСНОВНОЙ'
7320 записей прочитано в файле 'НОВОСТИ'
11 записей обнаружено в обоих файлах^
21680 записей занесено в файл 'НОВЫЙ ОСНОВНОЙ'
Программа, выдающая больше одной страницы информации,
должна обязательно делить свои результаты на страницы и про-
изводить нумерацию этих страниц. Для перехода со страницы на
страницу очень удобно было бы пользоваться некоторой процеду-
рой. Стандартная процедура ра§е(иф) организует пропуск строк
вплоть до начала очередной страницы в файле иф. Если имя файла
опущено, подразумевается файл output. В главной программе
опишем
const
размерстраницы = 60;
максимальноечислостраниц = 1000;
type
счетчикстрок — 1 . . размерстраницы;
счетчикстраниц = 1 . . максимальноечислостраниц;
var
строкнастранице : счетчикстрок;
номерстраницы : счетчикстраниц;
теперь присвоим начальные значения:
строкнастранице := размерстраницы;
номерстраницы := 1
Теперь, где-бы в программе ни возникла необходимость закончить
выдачу строки, предварительно собранной с помощью стандартной
процедуры write, нужно обращаться не к процедуре writein, а к
процедуре
новаястрока (строкнастранице, номерстраницы)
Определение процедуры новаястрока таково:
procedure новаястрока (var строка : счетчикстрок;
var номстраницы : счетчикстраниц);
const
заголовок = 'Мои результаты';
begin
7 3. Ввод и вывод
221
if строка размерстраницы
then
begin
page; '
\\тИе!п(заголовок, ' ' : 60,'Страница',
номстраницы : 5);
writein; writein;
номстраницы := номстраницы + 1;
строка := 3
end
else
begin
writein;
строка := строка +1
end
end;
Интерактивные программы
В интерактивных программах приняты соглашения о вводе и
выводе, отличные от соглашений, действующих для пакетных
программ. Первое и наиболее важное отличие заключается в том,
чю выходная информация, выдаваемая интерактивной програм-
мой, должна быть минимальной. Пользователь не захочет сидеть
и наблюдать за выдачей на телетайп или экран дисплея длинных
и заранее известных ему сообщений. В частности, интерактивная
программа не должна копировать входные данные в выходной
файл, поскольку пользователь точно знает, что он только что
ввел.
Интерактивная программа должна выдавать сообщения об
ошибках немедленно при обнаружении таковых. Пользователь
будет неприятно удивлен, если после ввода десяти строк выяс-
нится, что у него есть ошибка в первой строке. Интерактивные
программы не должны также сбиваться при вводе ошибочных
данных. Например, вызов
read (значение)
где значение есть целая или вещественная переменная, приведет
к остановке работы программы, если во входном файле находится
синтаксически неправильное число. Это означает, что полезные
интерактивные программы не должны пользоваться автоматичес-
кий преобразованием и, следовательно, должны выполнять пре-
образование типов самостоятельно. Программа калькулятор
з гл. 4 не использует автоматического преобразования. Когда эта
программа читает выражение, она помечает ошибочный символ и
.рисваивает выражению нулевое значение.
222
Гл. 7. Файлы
На языке Паскаль писать интерактивные программы нетруд-
но, если вы будете придерживаться нескольких простых правил.
Трудности проистекают от того, что процедура readln читает не до
конца текущей строки, а до первого символа следующей. Рассмот-
рим такой фрагмент программы:
writein ('ввод первого числа');
readln (первое);
writeln('ввoд второго числа');
геаб1п(второе)
Эта программа выдаст сообщение 'ввод первого числа', затем про-
читает его значение. Возврат из процедуры readln не будет осу-
ществлен до тех пор, пока не будет прочитана следующая строка,
поэтому программе второе число потребуется ввести до того, как
будет выдано соответствующее сообщение. (Этим, в частности,
объясняется несколько необычная структура программы каль-
кулятор в гл. 4.) К сожалению, проблему эту решают в различ-
ных реализациях по-разному, поэтому нет возможности дать
способ ее решения, подходящий во всех случаях. Приведем не-
сколько кратких советов, которые могут оказать помощь при
программировании: ставьте обращение к программе readln непо-
средственно перед вводом очередной строки с терминального
устройства (за исключением первой строки) и никогда не исполь-
зуйте процедуру readln с параметрами. Перед посылкой сообще-
ния на выход удостоверьтесь в том, что обработана вся строка.
7.4. Примеры
Было бы удивительным, проработав в течение длительного
периода времени в области теоретических вопросов программи-
рования, не знать того, что сществуют две школы программиро-
вания и два типа программистов. Одна из школ занимается зада-
чами теоретического характера, а другая тем, что называется
обработкой коммерческой информации. Программисты, работаю-
щие преимущественно в одной из этих областей, склонны считать,
что представители противоположной школы решают неинтерес-
ные, тривиальные или даже бесполезные проблемы. Этот раскол
настолько глубок, что разработано множество специализирован-
ных языков и даже вычислительных машин, предназначенных для
решения либо коммерческих (производственных), либо научных
задач, но никак ни тех и других вместе. Этому есть некоторое
оправдание. Для решения коммерческих задач необходим быст-
рый доступ к большому количеству данных, но при этом не
производится крупных численных расчетов, в то время как науч-
нее задачи часто отличаются небольшим количеством входной
информации, но требуют при этом более интенсивных вычисле-
7.4. Примеры
223
ний. Требования разнятся даже на самом низком уровне пред-
ставления чисел, поскольку для коммерческих расчетов нужны
обычно небольшие диапазоны значений, но большая точность
вычислений, чтобы не терялись даже копейки. Научные же рас-
четы чаще требуют работы с числами в больших диапазонах, но
с меньшей точностью представления.
Подобное разделение в какой-то мере скрывает тот факт, что
основные проблемы программирования в действительности оста-
ются очень похожими. Хотя в некоторых новых языках програм-
мирования была сделана попытка ликвидировать этот разрыв,
чаще это делалось путем включения в язык как средств, ориен-
тированных на «научные приложения», так и средств, облегчаю-
щих программирование «коммерческих» задач. Целью приведен-
ных в этом разделе примеров было показать, что язык Паскаль,
обладающий очень маленьким набором базовых конструкций,
может тем не менее эффективно использоваться и при решении
проблем, не принадлежащих традиционным областям программи-
рования.
Пример: Обработка таблиц
Предположим, что некая фирма пользуется вычислительной
машиной для бухгалтерских расчетов. Пусть имеется файл поступ-
лений, описанный таким образом:
type
сделка =
record
имяклиента, имясделки : имя;
датасделки : дата;
баланс : real
end;
var
файлсделок : file of сделка;
Рис. 7.5 содержит фрагмент файла поступлений, оформленный в
удобном для чтения виде. Файл содержит записи, упорядоченные
по значениям имяклиента и имясделки. Фирма имеет свои кон-
торы во многих местах, поэтому даты указываются в последова-
тельности, не совпадающей с последовательностью номеров сде-
лок; следовательно, записи хронологически не упорядочены.
Задача заключается в следующем:
Просмотрев информацию обо всех клиентах, указанных в
файле, перечислить все операции, произведенные теми клиен-
тами, которые имеют задолженность хотя бы по одной сделке,
заключенной ранее чем за 90 дней до текущей даты.
Существует несколько способов решения поставленной задачи.
Один из путей — отсортировать файл по значениям поля имя-
224
Гл. 7. Файлы
Сегодняшнее число: 1 Мая
имяклиента имясделки датасделки баланс
CR1046 Р1123 Аир 21 66—35
CR1046 Р1127 Апр 23 78—00
CR1046 Р1146 Аир 29 15—50
CU1214 Р1009 Фев 16 216—95
CU1214 Р1114 Апр 18 78-00
CY1249 S851 Янв 2 7-41
CY1249 Р1110 Апр 18 23-90
CY1249 Р1149 Апр 30 78—00
Рис. 7.5 Фрагмент файла поступлений
клиента, одновременно располагая записи, относящиеся к каждо-
му клиенту в хронологическом порядке. Мы, однако, будем пред-
полагать, что для бухгалтерских расчетов предпочтительным
является порядок, в котором главную роль играет поле имясдел-
ки, и что файл слишком велик, чтобы его можно было отсортиро-
вать за разумное время. Другой подход к решению задачи состоит
в том, чтобы прочесть весь файл целиком, составить список долж-
ников, а затем повторить чтение с параллельной печатью запи-
сей, относящихся к тем клиентам, которые были упомянуты в
списке. Имеется и третий путь, которым мы здесь и воспользуемся:
обрабатывать файл не по отдельным записям, а по клиентам.
(Напомним, что для каждого клиента в файле содержится целая
группа записей.) Чтобы иметь возможность работать по этому
методу, необходим большой объем памяти, достаточный для всех
записей, относящихся к любому из клиентов. Лучше всего для
этого подходит массив. Массив записей часто называется таб-
лицей.
Для создания и обработки таблиц применим следующий алго-
ритм. С каждой таблицей будем связывать некий ключ. При чте-
нии записи из файла будем смотреть на ее ключ: если он совпадает
с текущим ключом, к таблице’ будет добавлена новая запись.
Если ключи разные, мы обрабатываем созданную таблицу и
переходим к новой. Ниже приведена схема, в которой переменная
имяклиента используется в качестве ключа, а переменная счет-
чик — для подсчета числа занятых строк таблицы.
while not еоГ(файлсделок) do
begin
геас!(файлсделок, следсделка);
if следсделка . имяклиента =й= ключ
7.4. Примеры
225
then
Begin
обработатьтаблицу(таблица, счетчик);
ключ := следсделка . имяклиента;
счетчик := О
end;
if счетчик < размертаблицы
then
begin
счетчик := счетчик + 1;
таблица [счетчик] := следсделка
end
else таблица переполнена*
end
Эта схема хорошо работает при обработке средней части
файла, но в начале и в конце она не подходит. Чтобы правильно
начать работу, нам нужно установить первый ключ таблицы по
первой записи. Перед оператором цикла с пред-условием необ-
ходимо вставить такие операторы
счетчик := 1;
геаб(файлсделок, таблица [счетчик]);
ключ := таблица [счетчик] . имяклиента
В конце файла остается одна необработанная таблица, поэтому
мы завершим нашу схему оператором
обработатьтаблицу (таблица, счетчик)
В приведенной ниже программе такая схема используется
для построения таблиц записей, имеющих одинаковые значения
поля имяклиента, а процедура обработатьтаблицу печатает
все эти записи, если среди них есть хотя бы одна запись «старше
90 дней». Программа вызывает процедуру читатьдату, которая
выдает дату того дня, когда происходит обработка файла: дата
запрашивается у операционной системы и форма этого запроса
различна для различных реализаций языка, поэтому описания
процедуры читатьдату мы здесь не приводим.
program обработкатаблицы (файлсделок, input, output);
const
размертаблицы = 100;
длинаимени = 10;
давность = 90;
самыйдолгиймесяц = 31;
длинафевраля = 29;
первыйгод = 70;
последний год = 99;
дли на года = 365;
висцикл = 4;
8 № ЗЗЯ8
226
Гл. 7.'Файлы
type
месяц = (январь,февраль,март,апрель,май,июнь.июль,
август,сентябрь,октябрь,ноябрь.декабрь);
день = 1 . самыйдолгиймесяц;
индексименн = 1 .. длинаимени;
индекстаблицы = 0 .. размертаблицы;
имя packed array [индексименн] of char;
дата =
record
гг : первыйгод .. последнийгод;
мм : месяц;
ДД '• день;
end,
сделка =
record
имяклиента, имясделки : имя;
датасделки : дата;
баланс : real
end;
таблицасделок = array [индекстаблицы] of сделка;
var
файл сделок : file of сделка;
таблица : таблицасделок;
счетчик : индекстаблицы;
с л еде делка : сделка;
ключ : имя;
сегодня : дата;
длинамесяца : array [месяц] of день;
procedure обработатьтаблицу (табл. : таблицасделок;
размер : индекстаблицы;
датаобработки : дата);
var
индекс : индекстаблицы;
стараясделка : boolean;
function срок (дт : дата) : integer;
var
мсц : месяц;
дни : integer;
begin
with дт do
begin
дни := дд — 1;
if мм > январь
then
for мсц := январь to pred (мм) do
дни дни -1- длинамесяца[мсц],
if (мм > февраль) and (гг mod висцикл — 0)
then дни := дни + 1;
дни := дни + длинагода * (гг — первыйгод)
end; {with}
срок := дни
end; {срок}
procedure печатьсделки (едл : сделка);
7.4. Примеры
227
procedure печатьимени (им : имя);
var
инд : 1 . . длинаимени;
begin
for инд 1 to длинаимени do
\\тНе(им[инд]);
write(' ')
end, {печатьимени}
begin {печатьсделки}
with сдл do
begin
печатьимени(имяклиента);
печатьимени(имясделки);
with датасделки do
begin
write(aa : 2, 7');
write(ord(MM) 4-1 : 2, '/');
write(rr : 2, ' ')
end, {with датасделки}
wгiteln(бaлaнc : 12 : 2)
end {with сдл}
end; {печатьсделки}
I
i
I
begin {обработатьтаблицу}
стараясделка := false;
индекс := 0;
while (индекс < размер) and not стараясделка do
begin
индекс := индекс + 1;
if срок(датаобработки) —
срок(таблица[индекс] . датасделки) > давность
then стараясделка := true
end; {while}
if стараясделка
then
for индекс := 1 to размер do
печатьсделки(таблица[индекс])
end; {обработатьтаблицу}
begin {обработкатаблицы}
длинамесяца[январь] := 31; длинамесяца[февраль] 28;
длинамесяца[март] 31; длинамесяца[апрель] 30;
длинамесяца[май] 31; длинамесяца[июнь] := 30;
длинамесяца[июль] 31; длинамесяца[август] 31;
длинамесяца[сентябрь] : — 30; длинамесяца[октябрь] : = 31;
длинамесяца[ноябрь] : = 30; длинамесяца[декабрь] := 31;
reset (фа йлсдел ок);
читатьдату (сегодня);
счетчик := 1;
геас1(файлсделок, таблица[счетчик]),
ключ : — таблица[счетчик] . имяклиента;
while not еоГ(файлсделок) do
begin
read (файлсделок, следсделка);
if следсделка . имяклиента =# ключ
8*
228
Гл. 7. Файлы
then
begin
обработатьтабл и цу (таблица, счетчик,
сегодня);
ключ := следсделка . имяклиента;
счетчик := О
end;
if счетчик < размертаблицы
then
begin
счетчик := счетчик + 1;
таблица[счетчик] : = следсделка
end
else \\тИе1п('Таблица переполнена')
end; {while}
обработатьтаблицу(таблида, счетчик, сегодня)
end. {обработкатаблицы}
Пример: Последовательное редактирование
Второй пример, относящийся к обработке файлов, связан с
редактированием последовательного файла. Предположим, что
существует главный файл, содержащий много записей, и файл из-
менений, содержащий относительно немного записей. Программа
последовательного редактирования читает информацию из ста-
рого главного файла с именем старыйфайл и из файла изменений
под именем измфайл, создавая при этом измененный главный файл
с именем новыйфайл. Каждый шаг выполнения программы связан
либо с копированием записи из старого файла в новый, либо с
модификацией записи старого файла в соответствии с содержимым
файла изменений. Существуют три типа модификаций: замена,
исключение и вставка. Замена изменяет значение главной записи,
при исключении запись удаляется из файла, а вставка связана с со-
зданием новой записи. Информация, требуемая для замен и вста-
вок, содержится в файле изменений. Замена записей — по сущест-
ву избыточная операция, поскольку она может быть выполнена с
помощью исключения и вставки, но мы все же оставили эту опера-
цию.
Очевидно, необходимо найти способ идентификации записей.
Мы будем предполагать, что оба файла, как главный файл, так и
файл изменений, содержат ключ и, более того, оба файла были
подвергнуты сортировке по возрастающему значению этого клю-
ча. В качестве простого примера возьмем главный файл, содержа-
щий описания автомобилей, и предположим, что каждая запись
содержит целочисленный ключ. Приведем отрывок из главного
файла:
2 Шевроле красный 76
3 Понтиак зеленый 69
4 Бьюик синий 74
7.4. Примеры
229
6 Олдсмобиль коричневый 75
7 Кадиллак черный 76
К записям главного файла поступили следующие изменения:
3 заменить Понтиак зеленый 70
5 вставить Меркурий серый 74
7 исключить
После внесения изменений новый главный файл будет содержать:
2 Шевроле красный 76
3 Понтиак зеленый 70
4 Бьюик синий 74
5 Меркурий серый 74
6 Олдсмобиль коричневый 75
Если мы хотим, чтобы наша программа была практически по-
лезной, то должны допустить, что одна главная запись может
упоминаться в нескольких записях об изменениях. В частности,
должны быть допустимы такие записи об изменениях:
4 заменить Бьюик серый 74
4 заменить Фольксваген серый 74
Следует также проверять корректность изменений главного
файла. Запись только тогда можно заменять или исключать, если
она уже существует в главном файле. Следовательно, в нашем
первоначальном главном файле нельзя производить такие
изменения:
3 вставить Додж белый 75
5 исключить
Хотя код действия является избыточным для старого и нового
главных файлов, мы будем предполагать, что все три файла имеют
одинаковую структуру и что эта структура может быть представ-
лена следующим описанием:
const
длинаописания = 30;
type
типизменения = (замена, исключение, вставка);
описание = packed array [1. . длинаописания] of char;
записьфайла =
record
изменение : типизменения;
ключ : integer;
модель, цвет : описание;
год : 0 . . 99
end;
230
Гл. 7. Файлы
файлредакций = file of записьфайла:
var
старыйфайл, файлизменений, новыйфайл : файлредакций;
старыйбуфер, буферизменений, новыйбуфер : записьфайла;
Теперь приведем некоторые соображения, которые помогут
нам упростить дальнейшую разработку программы. Во-первых,
для определения того, из какого файла читать, можно сравни-
вать ключи. Во-вторых, мы заранее не можем знать, какой файл
кончится первым. В самом деле, любой файл может оказаться
пустым. Если к каждому файлу добавить фиктивную запись,
содержащую большое значение ключа, и больше никакой инфор-
мации, то все проверки, которые нам будет нужно проводить,
мы сможем осуществлять путем сравнения ключей. Фиктивные
записи нет необходимости «физически» вставлять непосредствен-
но в файлы, они могут добавляться такой процедурой:
procedure читатьзапись (var вхфайл : файлредакций;
var буфер : записьфайла);
begin
if eof (вхфайл)
then буфер . ключ : = максимальныйключ
else геас!(вхфайл, буфер)
end;
Очевидно, что главная программа должна содержать цикл и
что этот цикл не должен заканчиваться до тех пор, пока не будут
полностью прочитаны как исходный файл, так и файл изменений.
Цикл будет иметь такой вид:
while (старыйбуфер.ключ < максимальныйключ) or
(буферизменений.ключ < максимальныйключ) do
begin
обработать по крайней мере одну запись
end
За одну итерацию в цикле мы должны обработать по крайней
мере одну запись, поскольку в противном случае программа мо-
жет никогда не завершить свою работу. Проблема заключается в
том, чтобы определить, как много нужно сделать в течение одной
итерации. Решить эту проблему можно так: выбрать один ключ и
обработать все записи, связанные с этим ключом. В исходном
файле данным ключом может обладать либо нуль, либо одна за-
пись, а в файле изменений один ключ может соответствовать нулю
или более записей. Мы выбираем значение ключа, исследуя оче-
редную запись из каждого файла и беря ту из них, у которой зна-
чение ключа меньше. Если меньшее значение ключа у записи из
исходного файла, эта запись копируется в новый файл без изме-
7.4 Примеры
231
нений; если менылий ключ у записи из файла изменений, это оз-
начает, что типом записи изменения может быть только тип вста-
вить. Если ключи равны, исходная запись должна быть из-
менена.
За одним изменением могут, однако, последовать и другие с
тем же значением ключа, поэтому в этом месте нам нужно будет
построить внутренний цикл для обработки всех записей с выб-
ранным ключом. В результате такой обработки всех записей с
одним значением ключа либо ничего не произойдет, либо появится
одна новая запись в новом файле. Если в новый файл нужно
вставить новую запись, она сначала попадает в новыйбуфер; если
запись вставлять не нужно, значение переменной новыйбуфер.
Ключ устанавливается равным значению болыиойключ. Структура
программы при этом получается такой:
читатьзапись (старыйфайл, старыйбуфер);
читатьзапись (файлизменений, буферизменений);
while (старыйбуфер.ключ < максимальныйключ) ог
(буферизменений.ключ < максимальныйключ) do
begin
выбрать текущий ключ;
инициировать новый буфер;
обработать все записи, в которых
буферизменений.ключ = текущий ключ;
if новыйбуфер.ключ < максимальныйключ
then \сп1е(новыйфайл, новыйбуфер)
end
Если значение текущего ключа оказывается равным ключу из
старого буфера, мы можем сразу же копировать старый буфер в
новый, и из старогофайла читать очередную запись. Если значе-
ние переменной текущийключ меньше, чем значение старыйбуфер.
ключ, то мы даем новому буферу начальное значение, равное кон-
станте максимальныйключ, указывая тем самым, что в новом бу-
фере отсутствуют какие-либо правильные данные. В этом случае
первым изменением должно быть изменение типа вставить.
При обработке записи об изменении должно выполняться
одно из следующих условий:
(1) в буфере новыйбуфер находятся данные (новыйбуфер.ключ<
максимальныйключ), а изменение имеет тип заменить или
исключить; или
(2) в буфере новыйбуфер данные отсутствуют (новыйбуфер.ключ =
максимальныйключ), а изменение имеет тип вставить.
Если ни одно из этих условий не выполнено,, печатается сооб-
щение об ошибке и обрабатыва тся очередная запись
232
Гл. 7. Файлы
program обработатьфайл (output, старыйфайл, файлизменений,новыйфайл);
const
длицаописания = 30;
максимальныйключ = maxint;
type
типизменения = (замена, исключение, вставка);
описание = packed array [1 .. длинаописания] of char;
записьфайла =
record
изменение : типизменения;
ключ : integer;
модель, цвет : описание;
год : 0 .. 99
end; {записьфайла}
файлредакций = file of записьфайла;
var
старыйфайл, файлизмененнй, новыйфайл : файлредакций;
старыйбуфер, буферизменений, новыйбуфер : записьфайла;
текущийключ : integer;
procedure читатьзапись (van вхфайл : файлредакций;
var буфер : записьфайла);
begin
if еоГ(вхфайл)
then буфер . ключ := максимальныйключ
else геад(вхфайл, буфер)
end; {читатьзапись}
begin {обработатьфайл}
reset (ста р ыйфа йл);
гезеЦфайлизменений);
геч¥Г^е(новыйфайл);
читатьзапись(старыйфайл, старыйбуфер);
читатьзапись(файдизменений, буферизменений);
while (старыйбуфер . ключ < максимальныйключ) or
(файлизменений . ключ < максимальныйключ) do
begin
if старыйбуфер . ключ с буферизменений . ключ
then
begin
текущийключ := старыйбуфер . ключ;
новыйбуфер := старыйбуфер;
читатьзапись(старыйфайл, старыйбуфер) •
end
else
begin
текущийключ := буферизменений . ключ;
новыйбуфер . ключ := максимальныйключ
end;
while буферизменений . ключ = текущийключ do
begin
if новыйбуфер . ключ < максимальныйключ
then
case буферизменений . изменение of
замена : новыйбуфер := буферизменений;
исключение : новыйбуфер , ключ
7.5. Многофайловая структура данных
233
:= максимальныйключ;
вставка : writeln('omH6Ka при вставке')
end {case}
. else if буферизменений . изменение = вставка
/ then новыйбуфер := буферизменений
, else writein
('ошибка при замене или исключении')
читатьзапись(файлизменений,буферизменений)
end; {while}
if новыйбуфер . ключ < максимальныйключ
then write(нoвыйфaйл,новыйбуфер)
end {while}
end. {обработатьфайл}
7.5. Многофайловая структура данных
Устройства вспомогательной памяти, такие, как магнитные
ленты или диски, обладают высокой емкостью. Было бы расточи-
тельно хранить на одной ленте или диске только один файл, если
, только этот файл не слишком велик. Диск — это устройство с
произвольным доступом, которое не может быть точно представ-
лено в программе на Паскале. С другой стороны, многофайловая
\ лента обладает более простой структурой и может быть представ-
лена в языке с помощью стандартных типов.
На ленту можно записывать специальный символ, называемый
| меткой ленты и отличающийся от обычных данных. На одну лен-
f ту можно записать несколько файлов, разделяя их метками лен-
( ты. Этй файлы обычно называются подфайлами ленты. Чтобы
прочитать /г-ный подфайл, выполняются операторы:
reset (лента):
for ш : = 1 to п — 1 do
пропустить до метки-ленты
Магнитофон может распознавать метки лент без передачи инфор-
мации из пропускаемых подфайлов в память вычислительной ма-
I шины, тем самым перемотка ленты к нужному подфайлу проис-
ходит эффективнее, чем при чтении всего файла. Многофайловая
структура данных может быть представлена в языке Паскаль с
помощью такого описания:
I var
лента : file of file of t;
где t есть тип компоненты подфайла. Заметьте, что все подфайлы
имеют один и тот же тип. Переменная лента есть подфайл.
Операция get (лента) эквивалентна операции пропустить до
метки-ленты в приведенном выше примере. Функция eof (лента)
принимает значение true в конце ленты. Переменная лента f |
имеет тип t и является записью подфайла. Операция get (лента t )
234
Гл. 7. Файлы
читает один элемент из подфайла, а функция eof (лента | )
принимает значение true в конце подфайла. Операция put
(лента f ) пишет одну запись в подфайл, а операция put (лента)
заканчивает подфайл, проставляя метку ленты.
Хотя файлы с многофайловой структурой, строго говоря, не
являются последовательными файлами, так как подфайлы можно
пропускать, они все же не дают нам возможности имитировать
более мощных устройств с произвольным доступом, например дис-
ков. В большинстве современных вычислительных машин диски
применяются в качестве хранилища с непосредственным досту-
пом, а ленты только для организации архивов, запоминания очень
больших объемов информации и обмена данными с другими
организациями. В связи с распространением и растущим исполь-
зованием методов произвольного доступа отсутствие такового в
стандартной версии языка Паскаль представляется чем-то вроде
анахронизма.
Упражнения
7.1. Напишите программы, генерирующие тестовые данные для программ
обработатьфайл и обработкатаблицы, и воспользуйтесь этими данными для
тестирования указанных программ.
7.2. Модифицируйте программу обработкатаблицы таким образом, чтобы
она вычисляла срок для каждой прочитанной записи и не выполняла бы вовсе
процедуру обработатьтаблицу в том случае, если срок записи не превышает
90 дней.
7.3. Напишите программу, которая бы печатала отчет по срокам записей
о сделках Предположим, что входной файл содержит записи типа сделка,
используемого в программе обработкатаблицы В отчет включаются номер
клиента, номер сделки и дата, которые располагаются в левой части страниц
в соответствующих колонках. Сведения о балансе печатаются в одной из четы-
рех колонок в зависимости от срока записи: текущие записи, срок которых
меньше 30 дней, записи, сделанные от 31 до 60 дней до обработки таблицы,
записи, которые имеют срок от 61 до 90 дней, и записи, сделанные более 90
дней назад В конце отчета печатается баланс для каждой из колонок, а также
общий баланс (по всем колонкам).
7.4. Два файла одного и того же типа содержат записи, отсортированные
по некоторому ключу. Напишите программу, которая бы читала эти два файла
и сливала их, создавая один выходной файл, также отсортированный по тому
же ключу Напишите программу, генерирующую тестовые данные для этой
программы
7.5. В вашем распоряжении имеется вычислительная машина с тремя
магнитофонами и оперативной памятью небольшого объема, а также магнитная
лента, которую надо отсоргировагь Записи на ленте насюлько велики, что
достаточно только двух из них, чтобы заполнить всю оперативную память.
Напишите программу, которая бы сортировала содержимое ленты и помещала
бы отсортированный файл на одну из двух других лент.
Упражнения
235
7.6. Напишите программу, которая читала бы текст, состоящий из слов,
разделенных пробелами, и записывала тот же текст, разбивая его на страницы
установленного размера. Страница может состоять, например, из 40 строчек,
не более чем по 50 символов в каждой. Слова длиннее установленного размера
строки могут укорачиваться до этого размера. Программа должна печатать
заголовок каждой страницы и ее номер.
7.7. Напишите программу, редактирующую текст Эта программа читает
текст из входногофайла и из файларедакций. Она создает выходнойфайл, кото-
рый является копией входного всюду, за исключением тех строчек, к которым
относились директивы из файла редакций. Файл редакций содержит директивы
вида
* 3 m п
строчки текста
что означает «заменить строчки от т до п входного файла на следующий текст», и
* В т
строчки текста
что означает «вставить следующий текст после строчки т входного файла».
Дайте полную спецификацию вашей программы, включая ограничения на по-
следовательность директив и содержимое вставок.
7.8. Дополните программу обработатьфайл таким образом, чтобы она
печатала сводку, содержащую номера строчек, подвергнутых замене, исклю-
чению или вставке, а также количество записей в старом и новом файлах.
Предусмотрите такой режим работы, чтобы пользователь мог, если захочет,
получить список записей, вставленных или замененных при выполнении про-
граммы.
Глава 8.
ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ
Статическими называются такие данные, которые не меняют
свои размеры в течение всего времени своего существования.
Регулярный и комбинированный типы языка Паскаль дают нам
возможность определять статические данные. Мы всегда можем
определить размер статических данных, взглянув на описания,
приведенные в программе х). В противоположность статическим,
данные динамической структуры меняют свои размеры при
выполнении программы. В этой главе будут рассмотрены вопросы
определения и использования данных динамической структуры.
Чтобы понять, где могут оказаться полезными данные дина-
мической структуры, мы обсудим вопросы, связанные с обработ-
кой списков. Каждая компонента списка может быть представ-
лена переменной типа объект. Тип объект может быть простым
типом, например символьным или вещественным, или сложным
типом, регулярным или комбинированным. Можно представлять
список в виде массива:
var
список: array [1 . . размерсписка] of объект;
но такое представление ставит несколько проблем. Во-первых,
нам надо определить количество компонент списка, реально су-
ществующих в момент начала работы. Это необходимо для того,
чтобы указать в описании значение размерсписка. Во-вторых,
добавлять новые компоненты можно только к концу списка.
Исключать компоненты неудобно из-за того, что в массиве оста-
ются «дыры», которые надо каким-то образом отмечать. Наконец,
очень трудно соблюдать порядок среди компонент списка, если
только мы не пойдем на то, чтобы производить сортировку масси-
ва после каждого добавления новой компоненты.
Существует красивое и эффективное решение этой проблемы,
связанное с применением данных такой структуры, которая позво-
лит нам добавлять и исключать компоненты, не заботясь о том,
Э Это не совсем точно; например, запись с вариантами изменяет свой раз-
мер, хотя автор относит ее к статическим данным. Чаще термин «динамичес-
кие» имеет более общий характер и не обязательно связан с размером.—
Прим. ред.
8.1. Ссылки
237
куда поместить новую компоненту, или о том, что происходит со
свободным пространством, возникающим при исключении ненуж-
ных компонент. Для создания данных такой структуры нам нужно
будет пользоваться специальным понятием — ссылкой.
8.1. Ссылки1)
Ссылочный тип — это такой же простой тип, какими являются
целый, вещественный и булевский типы. Однако для этого типа
в языке не зарезервировано никакого стандартного идентифика-
тора. Определение ссылочного типа выглядит таким образом:
type
связь = | объект
Это определение читается так:
'Тип связь есть ссылка на объект'
Стрелка (f ) говорит нам, что связь является ссылочным типом.
Переменная типа объект может быть связана с некоторой ссыл-
кой типа связь; такая ситуация схематично изображена на
рис. 8.1(a). Переменная I на этой диаграмме имеет тип связь.
объект
(&)
(0)
Рис. 8.1. Ссылки и объекты
а тот объект, который связан с ней, обозначается как Z | . Иногда
нам будут нужны ссылки, с которыми не связана ни одна из пере-
Следует различать ссылочные типы и ссылочные переменные. Значениями
последних являются ссылочные значения или просто ссылка. Тершгн «ссыл-
ка» иногда применяется и по отношению к переменным. Английские эквива-
ленты этого слова — pointer, reference — иногда переводят как указатель.—
Прим. ред.
238
Гл. 8. Динамические структуры данных
менных типа объект, в этом случае будем писать
1 : = nil
Ссылка со значением nil схематично изображена на рис. 8.1(6),
именно так мы будем изображать эти ссылки в нашей книге.
В некоторых других книгах можно встретить также изображение
пустых ссылок, которое похоже на знак электрического «заземле-
ния» (см. рис. 8.1(b)). В теоретических работах по структурам
данных для обозначения пустых ссылок часто употребляется
символ Л (заглавная греческая буква лямбда). В языке Пас-
каль слово nil зарезервировано.
Важно понимать разницу между ссылкой и тем объектом, на
который она ссылается. Рис. 8.2(a) показывает две переменные
Р
(а)
(6)p:=q
Р
Рис. 8.2. Присваивание ссылки и записи
р и q типа связь, ссылающихся на различные объекты. В резуль-
тате выполнения присваивания
Р q
8.1. Ссылки
239
значение ссылки q будет присвоено ссылке р. Ситуация, возни-
кающая после выполнения этого оператора, показана на
рис. 8.2(6). Обе ссылки ссылаются на самолет, а корабль оказался
потерянным, если только не было еще одной ссылки, связанной с
ним,— с одним объектом может быть связана не одна ссылка.
После выполнения присваивания
Pf := qf
возникнет совершенно другая ситуация. Теперь мы копируем
значение объекта q f в объект р | , результат такого присваи-
вания показан на рис. 8.2(b). Ссылки не изменились, но изме-
нилось значение р f .
Ссылка иногда называется указателем, поскольку она указы-
вает на объект, а не представляет его. Операция ' | ' часто назы-
вается в литературе операцией разыменования. На рис. 8.2(a)
указателем является р, а результатом разыменования р будет
значение р f , т. е. корабль.
Для определения данных динамической структуры нужно
задать объект, в состав которого будет входить ссылка. Канди-
датами могут быть объекты трех следующих типов: ссылочного,
регулярного и комбинированного. Ссылочный тип мы можем исклю-
чить сразу же, поскольку объекты этого типа не могут содержать
никакой информации, кроме значения самой ссылки. Компоненты
массива должны быть одного типа, что представляет собой дово-
льно значительное ограничение, поэтому остановим внимание на
объектах комбинированного типа, т. е. записях. Мы можем
определить, например, следующий объект, содержащий, кроме
ссылки, еще некоторую информацию:
type
объект =
record
следующий : связь;
данные : типданных
end;
Здесь мы столкнулись с проблемой «курицы и яйца»: что надо оп-
ределять в первую очередь — связь или объект? К счастью
разработчики языка Паскаль предвидели эту проблему, поэтому
нам разрешено определять ссылки на объекты перед описанием
самих объектов. Можно, следовательно, писать:
type
связь = f объект
объект =
record
следующий : связь
240
Гл. 8. Динамические структуры данных
данные : типданных
end;
В следующем разделе мы увидим, как можно применять эти
определения для решения задач, связанных с обработкой спис-
ков. Но прежде чем перейти к рассмотрению этих задач, хотелось
бы обратить ваше внимание еще на одну особенность определения
ссылочных типов. Ссылка жестко связана с переменными того
типа, для которого она была определена. После введения описа-
ний
type
Р = f А;
Q = t В;
var
в которых А и В — это два разных типа, запрещается писать
такие операторы присваивания, как
Р:=Ч и q:=p
После выполнения первого из них, например, окажется, что р,
т. е. ссылка на объект типа Д, будет на самом деле ссылаться на
объект типа В, что в языке Паскаль не разрешается.
8.2. Связанные списки
Связанный список является простейшим типом данных дина-
мической структуры. Компоненты связанного списка можно
вставлять и исключать произвольным образом.
На рис. 8.3 изображена структура данных, построенная на ос-
нове одиночной ссылочной переменной начало и трех компонент
начало
Рис. 8.3. Связанный список
типа объект. Данные такой структуры и называются связанным
списком. Теперь продемонстрируем, каким образом можно
строить списки, изображенные на рис. 8.3, в программах на языке
Паскаль.
Предположим, что ссылочная переменная начало принимается
в качестве исходной точки для построения списка, как это изоб-
8.2. Связанные списки
241
ражено на рис. 8.3. Сначала список должен быть пуст, поэтому
мы пишем
начало := nil
Теперь надо вставить в список одну компоненту. Компоненты со-
здаются динамически с помощью стандартной процедуры new,
аргументом которой является ссылка. Опишем
var
р : связь;
и обратимся к процедуре new(p).
Тем самым мы создадим компоненту типа объект с именем pf.
Создавшаяся теперь ситуация показана на рис. 8.4(a). Следую-
щим шагом будет присваивание новой компоненте некоторого
значения. Так как pf это запись, то поле данные этой записи р\
начало
(а) пеаг(р)
начало
(Е) pf. Занное;='Х'
начало
(8) pt. следующий; = начало
начало
Рис. 8.4. Создание списка
(г) начало: = р
242 Гл. 8 Динамические структуры данных
можно указывать как р\.данные, и предполагая, что в поле
данные можно поместить одиночный символ, запишем:
pf.данные := 'X'
Результат выполнения этого оператора показан на рис. 8.4(6).
Следующим шагом, необходимость которого скоро станет очевид-
ной, должно быть выполнение оператора
pf. следующий : = начало
До этого у переменной начало было значение nil, поэтому в ре-
зультате выполнения оператора окажется, что значением поля
р\.следующий будет такое же значение, что и изображено на
рис. 8.4(b). Наконец, надо выполнить оператор
начало := р
после чего мы получим желаемый результат, показанный на
рис. 8.4(г). Построен список, состоящий из одной компоненты.
Однако на самом деле последовательность операторов
pf.следующий : = начало;
начало := р
использованная при создании первой компоненты, является
частью общего алгоритма добавления компоненты в начало спи-
ска. Рис. 8.5 (а—б) показывают, каким образом с помощью этих
двух операторов происходит включение в список информации 'У'.
(a) pt следующий. = начало
начало = р
Рис. 8.5. Расширение списка
8.2. Связанные списки
243
Просмотр связанного списка
Вернемся теперь к проблеме доступа к компонентам списка.
Взгляните опять на рис. 8.3. Доступ к первой компоненте осу-
ществить легко, поскольку ее имя есть начало], следовательно,
начало|. данные = 'Z'
Вторая компонента списка становится доступной благодаря на-
личию в первой компоненте ссылки с именем следующий. Именем
второй компоненты является начало].следующий], поэтому
начало!.следующий!.данные = 'Y'
Можно продолжить этот процесс, но для доступа к компонентам
длинного списка такой метод совершенно не подходит. Нужен
алгоритм динамического доступа. Этот алгоритм основан на том
факте, что если р ссылается на некоторую компоненту списка,
то после выполнения оператора присваивания
р := р!«следующий
р будет ссылаться на компоненту, следующую в списке за дан-
ной. Выполнение этого оператора можно продолжать до тех пор,
пока значением р не станет nil, т. е. пока мы не достигнем конца
списка. В соответствии с этим, алгоритм просмотра списка будет
выглядеть так:
р := начало;
repeat
р := р!.следующий
until р = nil
Мы можем несколько улучшить его, если вместо этого напишем
р начало;
while р ф nil do
р := р!-следующий
Теперь у нас есть алгоритм, который не будет сбиваться в тех
случаях, когда список окажется пустым. Процесс последователь-
ного обращения к компонентам списка называется просмотром
списка. Программа переупорядочитьсписок, использующая изу-
ченные нами методы построения и просмотра списков, читает по-
следовательность символов, строит из них список, а затем печа-
тает их в обратном порядке.
program переупорядочитьсписок (input,output);
type
связь = !объект;
объект =
record
244
Гл. 8. Динамические структуры данных
следующий : связь;
данные : char
end;
var
начало, р : связь;
begin
начало := nil;
while not eof do
begin
new (p);
read (pf.данные);
pf.следующий := начало;
начало := р
end; {while}
р : = начало;
while р =/= nil do
begin
write (pf.данные);
p := pf.следующий
end {while}
end. {переупорядочитьсписок }
На этом примере видно, что простой связанный список соот-
ветствует так называемой структуре (LIFO последний пришел,
первый вышел).
Структура с такими свойствами называется стеком, или, если
есть риск появления неоднозначностей, LIFO-стеком,
Очереди
Хотя в некоторых приложениях простой связанный список
действительно оказывается весьма полезным, его применение
сдерживается из-за недостаточно удобного доступа ко, всем
головной
Рис. 8.6. Очередь
компонентам списка, кроме первой. Нетрудно реорганизовать
список таким образом, чтобы превратить его в очередь. Очередь
это структура, у которой доступна лишь компонента, находя-
щаяся в этой очереди наибольшее время. Термин «очередь» ис-
8.2. Связанные списки
245
пользуется в данном случае по аналогии с очередями, организуе-
мыми людьми, где последний присоединившийся человек встает
в конец очереди, а обслуживается тот, кто в данный момент ока-
зался в начале. Такая структура представляет собой уже изу-
ченный нами связанный список, но с одним добавлением: потре-
буется еще одна ссылка на конец очереди, как это показано на
рис. 8.6. Как и раньше, мы можем описать:
type
связь — ^объект;
объект =
record
следующий : связь;
данные : типданных
end;
var
головной, замыкающий г связь;
Процедура убратьизочереди исключает из очереди первую
компоненту и настраивает на нее ссылку первый. Если при этом
исключается последняя компонента очереди и очередь становится
пустой, в процедуре предпринимаются некоторые специальные
действия.
procedure убратьизочереди (var первый, головной,
замыкающий : связь);
begin
первый := головной;
if головной #= nil
then
begin
головной := головHofif.следующий;
if головной = nil
then замыкающий := nil
end
end;
Если при вызове процедуры убратьизочереди оказывается,
что очередь пуста, будет возвращено значение nil, указывающее,
что убирать из очереди нечего. Если очередь становится пустой в
результате исключения из нее последней компоненты, значение
nil получают обе переменные: и головной, и замыкающий. Проце-
дура вставитьвочередь помещает в конец очереди новую компо-
ненту. Ссылка на новую компоненту содержится в переменной
новый, причем устанавливается она в вызывающей программе.
В случае работы с пустой очередью в этой процедуре так же,
как и в предыдущей, выполняются специальные действия.
246
Гл. 8. Динамические структуры данных
procedure вставитьвочередь (новый : связь;
var головной, замыкающий : связь);
begin
if головной = nil
then головной := новый
else замыкающий.следующий := новый;
замыкающий := новый
end;
Общий алгоритм добавления и исключения
Рассмотрим теперь проблему добавления и исключения ком-
понент, находящихся в середине списка. Совсем нетрудно вста-
вить компоненту в середину списка, когда в нашем распоряжении
имеется ссылка на компоненту, предшествующую данной в реор-
ганизованном списке. Предположим, что у нас есть компонента
ваня\, после которой мы хотим вставить компоненту вася\.
Процедура вставитьпосле это сделает:
procedure вставитьпосле (ваня, вася : связь);
begin
вася f. следующий := ваняf.следующий;
ваня|.следующий : = вася
end;
Результат работы процедуры вставитьпосле иллюстрируется
на рис. 8.7. Ситуация, имеющая место перед выполнением про-
цедуры, показана на рис. 8.7(a), а на рис. 8.7(6) изображено по-
ложение, возникающее после выполнения указанной процедуры.
Процедура вставитьпосле будет правильно работать и в том слу-
чае, если компонента ваня будет последней компонентой списка,
если же ваня = nil, то выполнение процедуры приведет к ошиб-
ке. Если в списке уже присутствует компонента вася\, то при вы-
полнении процедуры вставитьпосле возникнет имеющая весьма
важные последствия ситуация, при которой в списке образуется
кольцо. Существование этого кольца будет приводить к тому, что
все попытки произвести просмотр списка не дадут никакого ре-
зультата.
Несколько труднее реализовать процедуру вставитьперед,
которая используется в тех случаях, когда в нашем распоряже-
нии оказывается ссылка на компоненту списка, перед которой
надо вставить новую компоненту. Трудности эти связаны с тем,
что у нас нет непосредственного доступа к ссылке, подлежащей
модификации при реорганизации списка. На рис. 8.7(b) изобра-
жена ситуация, которая должна возникнуть после выполнения
процедуры вставитьперед (ваня.вася) (исходная ситуация ос-
тается такой же, как и раньше,— см. рис. 8.7(a)). Во-первых,
8.2 Связанные спи ска
247
Вася
Вася
(5}
W
Рис. 8.7. Вставление в список
нужно будет организовать просмотр списка:
var
здесь : связь;
while здесь|.следующий #= ваня do
здесь :^= здесь|.следующий
Если предположить, что ваня #= nil, то окажется, что список
пустым быть не может. Однако компонента ваня] может оказать-
ся первой компонентой списка, и приведенный выше алгоритм
просмотра не найдет ее. Следовательно, нужно будет специально
проверять этот выделенный случай, когда ваня] является первой
компонентой списка^. Процедуру вставитьперед можно составить
таким образом;
248
Гл. 8. Динамические структуры данных
procedure вставитьперед(ваня, вася : связь;
var начало : связь);
var
здесь : связь;
begin
if ваня = начало
then
begin
вася|.следующий := ваня;
начало := вася
end
else
begin
здесь := начало;
while здесь|. следующий ваня do
здесь : = здесь|.следующий;
здесь|.следующий := вася;
вася|.следующий := ваня
end
end;
Имеется еще один способ реализации процедуры вставитьпе-
ред, которым можно пользоваться в тех случаях, когда количест-
во информации, содержащееся в компонентах списка, не очень
велико. Сначала с помощью процедуры вставитьпосле новая ком-
понента помещается в список (попадет она, однако, не на свое
место), а затем меняются местами содержимое компонент ваня\
и вася\.
procedure вставитьперед(ваня, вася : связь);
var
временноехранение : типданных;
begin
вася^.следующий := ваня|.следующий;
ваня|.следующий : = вася;
временноехранение := ваня|.данные;
ваня|.данные : = вася|.данные;
вася|.данные := временноехранение
end;
Если список достаточно длинен, такая версия процедуры
вставитьпосле оказывается предпочтительнее, так как отпадает
необходимость в просмотре списка. Предыдущей версией лучше
пользоваться в случае короткого списка, состоящего из компо-
нент большого размера.
С похожей проблемой приходится сталкиваться и при необхо-
димости исключить из списка некоторую компоненту. Простой
8.2. Связанные списки
249
операция исключения будет только тогда, когда у нас имеется
ссылка на компоненту, предшествующую в списке исключаемой
компоненте, пусть она имеет имя предыдущий.
предыдущий!.следующий := предыдущий!.следующий!.
следующий
Чаще однако встречаются ситуации, когда ссылка указывает
именно на ту компоненту, которая подлежит исключению.
При этом для нахождения предыдущей компоненты приходится
просматривать весь список. Кроме того, нужно особо обращать
внимание на проблему исключения первой компоненты списка.
procedure исключить (ваня : связь;
var начало : связь);
var
здесь : связь;
begin
if ваня — начало
then начало : = ваня!.следующий
else
begin
здесь := начало;
while здесь!.следующий =# ваня do
здесь := здесь!.следующий;
здесь!.следующий ваня!.следующий
end
end;
Рекурсивная обработка списков
Рекурсивное определение списка выглядит так:
Список может быть либо пустым, либо состоять из узла,
содержащего ссылку на список.
Для обработки списков можно написать рекурсивную процедуру.
Ее структура будет соответствовать определению в том смысле,
что она должна содержать условный оператор, одна ветвь кото-
рого выполняет действия, соответствующие обработке пустого
списка, в то время как другая обрабатывает информацию, содер-
жащуюся в одиночном узле, а для обработки оставшейся части
списка процедура рекурсивно обращается к самой себе. В приве-
денной ниже программе используются две рекурсивные процеду-
ры — одна для чтения последовательности символов, а другая
для печати их в исходном порядке. Для копирования последова-
тельности символов имеются, конечно, более простые методы, но
мы хотели на этом примере проиллюстрировать возможности ре-
курсивной обработки списков.
250 Гл. 8 Динамические структуры данных
program копировать список (input,output);
type
связь = [объект;
объект =
record
следующий : связь;
данные : char
end;
var
начало : связь;
procedure добавить (var ссылка : связь);
begin
if ссылка = nil
then
begin
new (ссылка);
ссылка|.следующий := nil;
read (ссылка|.данные)
end
else добавить (ссылка!.следующий)
end; {добавить}
procedure печататьсписок (ссылка : связь);
begin
if ссылка #= nil
then
begin
write (ссылка!.данные);
печататьсписок (ссылка!.следующий)
end f
end; {печататьсписок} ।
begin {копироватьсписок} j
начало := nil; )
while not eof do *
добавить(начало);
печата гьсписок {начало) !
end. {копироватьсписок} |
Двусвязные кольца
Несколько изменив структуру списка, можно избавиться от
неудобств, связанных с необходимостью особой обработки спе-
циальных случаев. В каждой компоненте списка можно хранить
две ссылки — одну на предыдущую компоненту, а другую — на
8.2. Связанные списки
251
следующую. Модифицированное описание списка будет выгля-
деть так:
type
связь = ^объект;
объект =
record
вес, нсс : связь;
данные : типданных
end;
Вес — это ссылка на следующую компоненту списка, так называе-
мая ссылка вперед. Нсс ссылается на предыдущую компоненту и
называется ссылкой назад. Для полной симметрии можно связать
первую и последнюю компоненты списка между собой. В резуль-
тате получится двусвязнное кольцо, которое изображено на
рис. 8.8(a). Если мы определим пустое кольцо как кольцо, со-
стоящее из фиктивной компоненты, ссылающейся сама на себя,
начало
Рис. 8.8. Двусвязное кольцо
как это показано на рис. 8.8(6), процедура обработки кольца
заметно упростится. Ниже приведены процедуры, производящие
вставку и исключение компонент кольца.
{вставить объект вася| после объекта ваня[}
procedure вставитьпосле (ваня, вася : связь);
begin
вася|\вес := ваня|.всс;
252
Гл. 8. Динамические структуры данных
вася|.нсс := ваня;
BaHHf.Bccf.HCC := вася;
ваня|.всс := вася
end;
{вставить объект вася| перед объектом ваня|}
procedure вставитьперед (ваня, вася : связь);
begin
вася|.всс ваня;
вася|.нсс := ваня|.нсс;
ваня|.нсс|.всс := вася;
ваня|.нсс := вася
end;
{исключить объект ваня|}
procedure исключить (ваня : связь);
begin
ваня|.всс|.нсс := ваня|.нсс;
ваня|.нсс|.всс := ваня}.вес
end;
Процедуры эти работают во всех случаях, и, что интересно
отметить, в них совершенно не используется значение перемен-
ной начало, т. е. исходной точки кольца. Естественно, что доби-
лись мы этого не бесплатно: компоненты кольца стали больших
размеров, поскольку в них появилась вторая ссылка, а про-
цедуры стали медленнее, так как в них теперь обрабатывается
большее количество ссылок. Однако в целом двусвязаное кольцо
представляет собой более элегантную структуру, простые опера-
ции над кольцами не требуют последовательных просмотров,
кроме того, не возникает необходимости обрабатывать никаких
специальных случаев.
Поскольку мы не можем встретиться с пустой ссылкой, имею-
щей значение nil, просмотр кольца несколько отличается от про-
смотра простого списка. При таком просмотре нам нужно лишь
помнить то место, с которого мы начали. В примере, приведенном
ниже, переменные старт и кольцо являются ссылками:
кольцо := старт|.всс
while кольцо =# старт do
begin
S;
кольцо := кольцо|.всс
end
Оператор S будет выполняться по одному разу для каждой
компоненты кольца. В момент выполнения этого оператора пере-
8.2. Связанные списки
253
менная кольцо будет указывать на текущую компоненту. Опера-
тор S не будет выполняться вовсе, если кольцо пусто в том
смысле, как это показано на рис. 8.8(6). Если вместо переменной
вес использовать переменную нсс, просмотр кольца будет идти в
обратном направлении.
Операции над ссылками
Единственной операцией, которую разрешено выполнять над
переменными ссылочного типа в языке Паскаль, является наст-
ройка ссылки на некоторый объект (или настройка на фиктивный
объект, если ссылке дается значение nil). Такой подход представ-
ляется весьма разумным, поскольку большинство операций над
ссылками или вовсе бессмысленны, или их результат слишком
сильно зависит от конкретной машины. Однако иногда может
возникнуть такая ситуация, что нам потребуется выдать на пе-
чать значение ссылки, например, при аварийном завершении
программы. Во многих реализациях значением ссылки является
адрес памяти вычислительной машины, который можно рассмат-
ривать как целое число. Если это так, то для печати значения
ссылки можно пользоваться такой процедурой:
procedure печататьссылки (елк : ссылка);
type
вид = (видссылки, видцелого);
типхамелеон =
record
case вид of
видссылки :
(езначение : ссылка);
видцелого :
(цзначение : integer)
end;
var
хамелеон : типхамелеон;
begin
хамелеон . езначение := елк;
write (хамелеон . цзначение)
end;
Пользуясь сведениями, полученными в гл. 6, вы можете ска-
зать, что типхамелеон является свободным объединенным ти-
пом. Это программистский трюк, который противоречит духу
языка Паскаль. В некоторых реализациях такие программы мо-
гут не сработать, кроме того, они могут иногда сбиваться даже на
тех машинах, где в некоторых случаях выполнение программы
идет успешно (например, для значения nil). Программа эта
является существенно машинно-зависимой. Мы привели ее здесь
254
Гл. 8. Динамические структуры данных
только потому, что в некоторых случаях распечатка значения
ссылки может оказаться очень полезной, в частности, при отлад-
ке программы. Однако такие процедуры ни в коем случае нельзя
оставлять в законченных программах, и уж во всяком случае
не -оставляйте их при переносе программы на другую машину.
8.3. Пример: Моделирование дискретных событий
Вычислительные машины часто используются для моделиро-
вания естественных явлений. Правильно выбрав теоретическую
модель, при таком моделировании можно получить очень полез-
ные результаты, особенно в тех случаях, когда проведение на-
стоящего эксперимента либо связано со значительными затрата-
ми, либо вовсе невозможно.
Программы моделирования классифицируются по типам ими-
тируемых процессов, а для удобства моделирования многие про-
цессы часто рассматриваются как последовательность дискрет-
ных событий.
Для примера мы построим программу, моделирующую рабо-
ту автобусного транспорта. Начнем мы с разработки абстрактной
модели, использующей параметры, изменяющиеся только в дис-
кретные моменты времени, хотя реально, конечно, автобусное
обслуживание есть процесс непрерывный. Например, будем счи-
тать, что автобус может находиться в одном из двух состояний:
он может либо двигаться от остановки к остановке, либо стоять
на остановке. Пассажир может либо ожидать автобуса, либо вхо-
дить в автобус, либо сидеть в автобусе.
Автобусная компания высылает на линию автобусы в коли-
честве числоавт штук, которые курсируют по замкнутому марш-
руту, останавливаясь по пути на числост остановках. Автобусная
компания делает все возможное для соблюдения регулярности
обслуживания. Гарантируется, что как время следования автобу-
са от одной остановки до другой, так и время посадки одного
пассажира в автобус всегда остается постоянным. Обслужива-
ние было бы абсолютно регулярным, если бы не тот факт, что пас-
сажиры подходят к остановкам через произвольные интервалы
времени. Будем различать события трех типов:
(а) Человек становится в очередь у автобусной остановки.
(б) К автобусной остановке подходит автобус.
(в) Человек входит в автобус.
При такой постановке задачи игнорируется процесс высадки
пассажиров: мы предполагаем, что пассажиры выходят из авто-
буса через переднюю дверь быстрее, чем туда через заднюю са-
дятся новые. Для каждого из перечисленных выше событий мы
можем указать событие, которое должно следовать за данным:
8 3. Пример: Моделирование дискретных событий
255
(а) Через случайный промежуток времени к очереди присо-
единяется еще один человек.
(б) Если очереди нет, автобус идет к следующей остановке, и
его прибытие туда становится очередным событием; в
противном случае в автобус заходит человек, стоящий
в очереди первым;
(в) Длина очереди уменьшается на единицу Если очередь за-
канчивается, автобус отъезжает к следующей остановке,
в противном случае в автобус заходит еще один пассажир.
Каждое событие будем представлять в нашей программе в виде
записи.
type
типсобытия = (человек, прибытие, посадка);
событие =
record
тип : типсобытия
end;
var
текущеесобытие : событие,
Приблизительная схема нашей программы будет выглядеть так:
repeat
определить текущее событие',
case текущеесобытие . тип of
человек :
begin
длинаочереди длинаочереди + 1;
геисобытие (человек)
end;
прибытие :
if длинаочереди = О
then генсобытие (прибытие)
else генсобытие (посадка);
посадка :
begin
длинаочереди := длинаочереди — 1;
if длинаочереди = О
then генсобытие (прибытие)
else генсобытие (посадка)
end
end
until конец процесса моделирования
При разработке этой схемы предполагалось существование
процедуры генсобытие, которая определяет последовательность
256
Гл. 8. Динамические структуры данных
реализации событий во времени. Мы написали оператор опреде-
лить текущее событие. Тем самым предполагалось существова-
ние такой структуры данных, которая подходит для запомина-
ния событий в хронологическом порядке, чтобы можно было
легко осуществлять доступ к «следующему» событию. Заметьте,
однако, что генерируются события не в хронологическом поряд-
ке, и, значит, для их запоминания очередь не подходит. Наибо-
лее подходящей структурой для такого запоминания является
кольцо, каждая его компонента — это событие. Параметрами
события должны быть типсобытия и время, когда оно происхо-
дит, а также номера автобусных остановок и автобусов, к кото-
рым оно относится. Теперь можно составить более подробное опи-
сание как записи о событии, так и кольцевого списка событий:
type
связь =|событие;
событие =
record
вес, нсс : связь;
тип : типсобытия;
остном : остномер;
автном : автномер
end;
var
начало : связь;
текущеесобытие : связь;
Различные поля записи о событии представлены на рис. 8.9(a),
в то время как рис. 8.9 (б) иллюстрирует форму кольцевого спи-
ска событий, возникшего во время моделирования. Заметьте,
что все события хронологически упорядочены. В соответствии
с соглашением, принятым в разд. 8.2, в список включена фиктив-
ная запись, причем список будет считаться пустым в том случае,
если в нем нет других компонент, кроме этой фиктивной записи.
Текущим событием будет.
текущеесобытие := начало!.вес
поэтому текущим временем будет
текущеесобытие|:время
После обработки текущего события оношсключается из списка,
и сразу же текущим становится следующее событие. Приведен-
ные ниже операторы присваивания выполняют именно такое
исключение; результат их работы показан на рис. 8.9(b):
начало!, вес := текущеесобытие!. все;
текущеесобытие!.всс|.нсс := начало
Поскольку нас интересует только количество людей на останов-
8 5. Пример: Моделирование дискретных событий
257
W
начало
Рис. 8.9. Структура данных программы автобусы
ке, будем представлять очередь на остановке одним целым чис-
лом. Длины всех очередей можно собрать в одном массиве:
очередь : array [остномер] of integer
9 № 3388
258
Гл. 8. Динамические структуры данных
Используя все эти описания, мы можем написать новую версию
основной программы.
repeat
текущеесобытие := начало]4.вес;
with текущеесобытие]4 do
case тип of
человек :
begin
очередь[остном] : = очередь[остном] + 1;
генсобытие (человек, время, остном,0)
end;
прибытие :
if очередь[остном] = О
then генсобытие (прибытие,время,
следост, автном)
else генсобытие (посадка,время,
остном,автном);
посадка :
begin
очередь[остном] := очередь[остном] — 1;
if очередь[остном] > О
then генсобытие (посадка,время,
остном, автном)
else генсобытие (прибытие,время,
следост, автном)
end
end;
начало]4, вес := текущеесобытие]4.вес;
текущеесобытие]4.вес]4.нес : = начало
until начало|.всс|.время > максимальноевремя
После отхода автобуса от остановки мы вызываем процедуру
генсобытие (прибытие, время, следост, автном)
и получаем в результате событие, представляющее собой прибы-
тие автобуса к следующей остановке. Если предположить, что
все автобусные остановки пронумерованы от 1 до числост, то
следост = (остном mod числост) + 1
Процедура генсобытие должна вычислять время, когда собы-
тие будет иметь место, строить запись о событии и вставлять ее
в список событий. Время реализации нового события зависит от
его типа. Поскольку мы работаем с событиями трех типов, три
типа временных данных попадают в массив, предназначенный
для запоминания хронологии событий:
var
времясоб : array [типсобытия] of real;
8.3. Пример: Моделирование дискретных событий 259
Времясоб [прибытие] и времясоб [посадка]— это постоянные
величины, тогда как появление человека на автобусной остановке
есть событие случайное, вследствие чего величина времясоб
[человек] должна каждый раз умножаться на некоторое случай-
ное число. Новое событие вставляется в список событий после
просмотра списка в порядке уменьшения времени, при этом оно
попадает на место после события, непосредственно предшест-
вующего данному. Такой механизм будет работать при условии,
что фиктивному событию присвоено нулевое время. Процедуру
генсобытие запишем таким образом:
procedure генсобытие (типсоб : типсобытия;
новоевремя : real;
остановка : остномер;
автобус : автномер);
var
соб, новоесоб : связь;
задержка : real;
begin
if типсоб = человек
then задержка := времясоб[типсоб1*
случайноечисло
else задержка := времясоб[типсоб];
new (новоесоб);
with новоесоб! do
begin
тип := типсоб;
время : = новоевремя + задержка;
остном := остановка;
автном := автобус
end;
соб := начало;
repeat
соб := соб!. нсс
until новоесоб|.время соб!.время;
новоесоб!.вес := соб!.вес;
новоесоб!.нсс := соб;
соб!.вес!.нсс := новоесоб;
соб!.всс := новоесоб
end;
Если мы хотим, чтобы наше моделирование оказалось хотя бы
немного полезным, то нужно с особым вниманием рассмотреть во-
прос об определении начальных условий. Моделирование авто-
бусного сообщения начинается в тот момент, когда все автобусы
равномерно распределены по всему маршруту, а очереди на ос-
тановках отсутствуют. Цель моделирования — определить воз-
9*
260
Гл. 8. Динамические структуры данных
можность поддержания интервалов между автобусами на одина-
ковом уровне. Результат эксперимента известен всем диспетче-
рам движения — автобусы имеют тенденцию сбиваться в группы.
Интуитивно это понятно: если автобус следует на небольшом
расстоянии от предыдущего* очереди на остановках не успевают
выстраиваться, и автобус движется быстрее. Напротив, сильно
отставший автобус, задерживаемый длинными очередями, дви-
жется медленнее. В описанной выше схеме моделирования эф-
фект группирования настолько сильно выражен, что автобусы,
быстро сбиваясь в длинную колонну, никогда уже не расходятся
друг от друга. Ниже приведена программа, в которой для ликви-
дации колонн используется простой метод, в силу ряда причин
запрещенный многими автобусными компаниями.
program автобусы (input,output);
const
числоостановок = 100;
числ©автобусов = 100;
максимальнаяочередь = 100;
type
остномер = 0 .. числоостановок;
автномер = 0 .. числ©автобусов;
длинаочереди — 0 .. максимальнаяочередь;
типсобытия = (человек,прибытие,посадка);
связь = |событие;
событие =
record
вес, нсс : связь;
тип : типсобытия;
время : real;
остном : остномер;
автном : автномер
end;
var
очередь : array [остномер] of длинаочереди;
наостановке : array [остномер] of boolean;
времясоб : array [типсобытия] of real;
интервал, остановка, всегоостановок : остномер;
автобус, всегоавтобусов : автномер;
максимальноевремя : real;
текущеесобытие, начало : связь;
случайнаязаготовка : integer;
индекссобытия : типсобытия;
function случайноечисло (var заготовка : integer) :
real;
begin
8.3. Пример: Моделирование дискретных событий
261
случайноечисло := —In ((заготовка + 1) / 65536);
заготовка : = (25173 * заготовка + 13849)
mod 65536
end; {случайноечисло}
procedure генсобытие (типсоб : типсобытия;
новоевремя : real;
остановка : остномер;
автобус : автномер);
var
соб, новоесоб : связь*
задержка : real;
begin
if типсоб = человек
then задержка : = времясоб[типсобытия]#
случайноечисло (случайнаязаготовка)
else задержка : = времясоб[типсобытия];
new (новоесоб);
with новоесоб! do
begin
тип := типсоб;
время :== новоевремя + задержка;
остном : = остановка;
автном : = автобус;
end; {with}
соб начало;
repeat
соб := co6f.Hcc
until новоесоб!.время соб!.время;
новоесоб!.вес := соб!.вес;
новоесоб}.нсс := соб;
соб!.всс!.нсс := новоесоб;
соб!.всс : = новоесоб
end; {генсобытие}
begin {автобусы}
{читать параметры процесса моделирования}
read (всегоостановок, всегоавтобусов);
for индекссобытия := человек to посадка do
read (времясоб[индекссобытия]);
read (максимальноевремя);
{создание пустого списка событий}
262
Гл. 8. Динамические структуры данных
new (начало);
with начало} do
begin
вес := начало;
нсс := начало;
время : = О
end; {with}
{равномерное распределение автобусов по маршруту}
if всегоостановок < всегоавтобусов
then интервал := 1
else интервал : = всегоостановок div
всегоавтобусов;
остановка := 1;
for автобус : = 1 to всегоавтобусов do
begin
генсобытие (прибытие,0,остановка,автобус);
if остановка + интервал всегоостановок
then остановка := остановка + интервал
else остановка := 1
end; {for}
{создание на каждой остановке отдельной очереди}
for остановка : = 1 to всегоостановок do
begin
очередь[остановка] := 0;
генсобытие (человек,0,остановка,0);
наостановке[остановка] := false
end; {for}
{моделирование}
repeat
текущеесобытие : = начал of.вес;
with текущеесобытие! do
case тип of
человек :
begin
очередь[остном] : =
очередь[остном] + 1;
генсобытие (человек,время,остном,0)
end;
прибытие :
if наостановке[остном] or
(очередь[остном] = 0)
8.3. Пример: Моделирование дискретных событий
2G3
then генсобытие (прибытие,время,
(остном mod всегоостановок)
+ 1, автном)
else
begin
наостановке[остном] := true;
генсобытие (посадка,время,
остном,автном);
write (время : 8 : 3);
write (' ' : 3 х остном);
writein (автном : 1)
end;
посадка :
begin
очередь[остном] : =
очередь[остном] — 1;
if очередь[остном] > О
then генсобытие (посадка,время,
остном,автном)
else
begin
наостановке[остном] : =
false;
генсобытие (прибытие,время,
(остном mod всегоостановок)
+ 1,автном)
end
end
end; {with,case}
начало|.вес := текущеесобытие!.вес;
текущеесобытие!.вес!.нсс начало
until начало!.вес!.время максимальноевремя
end. {автобусы}
Кольцевой список событий оказался очень полезной струк-
турой данных, и все благодаря тому, что, используя эту струк-
туру, МЫ получили возможность с помощью одного процессора
моделировать параллельно идущие процессы. Сама программа
автобусы является последовательным процессом, но в ней очень
точно моделируется поведение произвольного числа автобусов и
очередей. Мы можем следующим образом обобщить требования
к моделированию параллельных процессов:
(1) Для каждого моделируемого процесса должна существовать
некоторая запись о текущем состоянии этого процесса.
(2) Один процессор должен по очереди обрабатывать все процес-
264
Гл. 8. Динамические структуры данных
сы. После обслуживания процесса в кольцевой список со-
бытий вставляется либо измененная запись о состоянии этого
процесса, либо запись о новом процессе, как это делается
в программе автобусы.
(3) Нужно иметь некоторый механизм, управляющий прохож-
дением процессов. В программе автобусы такое управление
осуществляется с помощью задания времени свершения со-
бытий.
8Л. Деревья
Ссылки можно использовать не только для представления '
простых списков и колец, но также и для представления струк-
тур более общего вида. Предположим, у нас есть некоторая ,
структура, состоящая из записей, связанных между собой сис-
темой ссылок, причем каждая запись может содержать ссылки на :
несколько других записей. Так представляются направленные ,
графы. Вершинами, или узлами, графа являются записи, ссылки :
играют роль ребер. Частным случаем графа является дерево, >
которое имеет следующие свойства: подструктуры, связанные с !
некоторым узлом, не связаны между собой, кроме того, сущест-
вует узел, называемый корнем: из него просмотром конечного ;
числа ребер можно достичь любого узла дерева. Рис. 8.10 изобра- ;
жает дерево с корнем А. Узлы Г, Д, Е, Ж, И, К и Л называются j
терминальными узлами или листьями дерева. j
Глядя на рис. 8.10, вы можете подумать, что он перевернут, |
так как корень дерева А расположен сверху. Однако традиция
Рис. 8.10. Дерево
изображать деревья перевернутыми сложилась давно. Мы при-
выкли начинать рисунок в верхней части листа и, рисуя, перехо-
дить ниже и ниже, а рисовать корень дерева до того, как будут
изображены его листья, нам легче.
Дерево является рекурсивной структурой. Его можно опреде-
лить так: дерево либо пусто, либо состоит из узла, содержащего
8.4. Деревья
265
ссылки на непересекающиеся деревья. Это определение весьма
похоже на рекурсивное определение списка. В то время как спи-
ски можно с одинаковой легкостью обрабатывать как рекурсив-
ными, так и итеративными алгоритмами, мы увидим, что деревья
намного легче обрабатывать с помощью рекурсивных алгоритмов.
Двоичные деревья
Каждый узел двоичного дерева имеет не больше двух узлов-
отростков. Если у узла действительно два отростка, они называ-
ются левым и правым отростками. Такие отростки в двоичных
деревьях не являются взаимозаменяемыми. На рис. 8.11 пока-
заны три различных двоичных дерева: деревья, показанные на
рис. 8.11 (б) и рис. 8.11 (в), считаются разными. Для представле-
Рис. 8.11. Двоичное дерево
(5) (6)
ния узла двоичного дерева в языке Паскаль удобно пользоваться
записями:
type
связь = |узел;
узел =
record
левое, правое : связь;
данные : типданных
end;
Просмотр двоичного дерева может производиться рекурсивно:
для каждого узла нужно выполнить три действия. Для обозначе-
ния операции, которую нужно выполнить над каждым узлом де-
рева, будем использовать термин 'исследовать'.
Исследовать узел.
Просмотреть левое поддерево.
Просмотреть правое поддерево.
Эти три шага могут быть выполнены шестью разными последова-
266
Гл. 8. Динамические структуры данных
тельностями, поэтому для просмотра дерева существуют шесть
разных способов. Благодаря существованию традиционного со-
глашения о том, что левое поддерево всегда просматривается пе-
ред правым, количество различных способов просмотра снижает-
ся с шести до трех. Три оставшихся способа имеют специальные
наименования: при прямом просмотре сначала исследуется узел,
а затем левое и правое поддеревья: при обратном просмотре ис-
следуется левое поддерево, узел, и затем правое поддерево;
наконец, при концевом просмотре узел исследуется после про-
смотра поддеревьев х). Мы легко можем написать рекурсивную
процедуру, выполняющую просмотр двоичного дерева:
procedure просмотр (дерево : связь);
begin
if дерево =/= nil
then
begin
исследовать (дерево);
просмотр (дерево!.левое);
просмотр (деревоj.правое)
end
end;
Такая процедура осуществляет прямой просмотр дерева.
Другие виды просмотра могут быть получены путем перестановки
трех операторов, входящих во внутренний составной оператор.
При различных порядках просмотра дерева, изображенного на
рис. 8.11(a), узлы исследуются в такой последовательности:
Прямой: Д Б А Г В Е 3 Ж И
Обратный: А Б В Г Д Е Ж 3 И
Концевой: А В Г Б Ж И 3 Е Д
Обратный просмотр дерева привел к возникновению упорядо-
ченности узлов по алфавиту. Это не случайное совпадение,
поскольку рис. 8.11(a) был специально нарисован таким обра-
зом, чтобы добиться этого эффекта. Дерево, изображенное на
рис. 8.11(a), называется двоичным деревом поиска, такая струк-
тура часто оказывается весьма полезной, в чем мы еще убедимся,
изучая приведенные ниже процедуры. Процедура вставить до-
бавляет к двоичному дереву поиска один узел, сохраняя упоря-
доченность дерева.
procedure вставить (var дерево : связь;
новыеданные : типданных);
В оригинале preorder, inorder, postorder. Отметим, что в книге Д. Кнута
«Искусство программирования для ЭВМ», т. 1, эти порядки прохождения соот-
ветственно называются preorder, postorder и endorder. Во избежание путаницы
мы переводим эти термины, следуя переводу книги Д. Кнута.— Прим, ред.
8.4. Деревья
267
begin
if дерево = nil
then
begin
new (дерево);
with дерево! do
begin
левое := nil;
правое := nil;
данные := новыеданные
end {with}
end
else
with дерево! do
if новыеданные < данные
then вставить (левое,новыеданные)
else if новыеданные > данные
then вставить (правое,новыеданные)
else {дублирование информации}
end;
Следующая функция выдает в результате работы ссылку на узел
дерева, содержащий необходимые данные, или пустую ссылку,
если нужная информация в дереве отсутствует.
function найти (дерево : связь;
ключ : типданных) : связь;
begin
if дерево = nil
then найти := nil
else
with дерево! do
if ключ < данные
then наити := найти (левое,ключ)
else if ключ > данные
then найти := найти (правое,ключ)
else найти : = дерево
end;
Функцию найти можно написать и не прибегая к рекурсии:
function найти (дерево : связь;
ключ : типданных) : связь;
var
конец : boolean;
begin
конец false;
repeat
268
Гл. 8. Динамические структуры данных
if дерево = nil
then конец := true
else
with дерево! do
if ключ < данные
then дерево := левое
else if ключ > данные
then дерево := правое
else конец := true
until конец;
найти := дерево
end;
Заметим, наконец, что элементы двоичного дерева поиска
могут быть перечислены в правильной последовательности с по-
мощью обратного просмотра. Единственной операцией, требую-
щей значительных усилий, является операция исключения узла,
не являющегося листом дерева. Двоичное дерево поиска пред-
ставляет собой более удобную структуру для хранения и поиска
данных, чем массив. При работе с таким деревом надо предвари-
тельно убедиться, что новые данные не поступают в порядке воз-
растания или убывания, так как в этом случае дерево выродится
в линейный список. Во многих практических случаях, однако,
данные поступают в существенно случайном порядке, что позво-
ляет строить более или менее эффективные деревья поиска. В слу-
чайном дереве, состоящем из N узлов, время, нужное для добав-
ления или исключения узла, пропорционально log2(Af), тогда как
соответствующее время проведения линейного поиска по массиву
пропорционально N.
Пример: Частотный словарь
В гл. 6 была приведена программа частотныйсловарь, кото-
рая читала текст и составляла список слов, входящих в этот текст,
сопровождая его указанием частот употребления этих слов.
Здесь мы приведем программу, выполняющую те же действия,
но использующую для хранения слов не массив, а двоичное
дерево поиска. Новая программа превосходит старую по двум
параметрам: она будет работать быстрее, так как поиск по дереву
эффективнее линейного поиска, кроме того, печать слов в алфавит-
ном порядке может производиться без предварительной сорти-
ровки. С другой стороны, новая версия программы для таблицы
заданного размера потребует памяти большего объема, так как
каждый элемент таблицы должен теперь, кроме самого слова
и соответствующего счетчика, содержать еще и две ссылки,
program частотныйсловарь (input,output);
const
8.4. Деревья
269
максимальнаядлинаслова = 20;
type
индекссимвола = 1 .. максимальнаядлинаслова;
типсчетчика = 1 .. maxint
типслова = packed array [индекссимвола] of
* char;
ссылка = |типстроки;
типстроки =
record
левое, правое : ссылка;
слово : типслова;
счетчик : типсчетчика
end;
var
деревослов : ссылка;
следслово : типслова;
буквы : set of char;
procedure читатьслово (var упакованноеслово :
типслова);
{см. страницу 200}
procedure печататьслово (упакованноеслово :
типслова);
{см. страницу 201}
procedure создатьстроку (var дерево : ссылка;
строка : типслова);
begin
if дерево = nil
then
begin
new (дерево);
with дерево! do
begin
слово : = строка;
счетчик := 1;
левое := nil;
правое := nil
end; {with}
end
else
with дерево! do
if строка < слово
then создатьстроку(левое,строка)
270
Гл. 8. Динамические структуры данных
else if строка > слово
then создатьстроку(правое,строка)
else счетчик := счетчик + 1
end; {создатьстроку}
procedure печататьдерево (дерево : ссылка);
begin
if дерево У= nil
then
with дерево! do
begin
печататьдерево (левое);
печататьслово (слово);
writein (счетчик);
печататьдерево (правое)
end {with}
end; {печататьдерево}
begin {частотныйсловарь}
буквы ['а' .. 'я'];
деревослов := nil;
while not eof do
begin
читатьслово (следслово);
if not eof
then создатьстроку (деревослов,следслово)
end; {while}
печататьдерево (деревослов)
end. {частотныйсловарь}
Деревья общего вида
Теперь рассмотрим проблему представления деревьев, узлы
которых могут содержать ссылки более чем на два поддерева.
Если максимальное число поддеревьев ограничено некоторым до-
статочно маленьким значением, то практически бывает возможно
для указания на поддеревья пользоваться массивом ссылок.
В этом случае описание структуры данных будет выглядеть так:
const
максимальноечислоузлов = 6;
type
связь = |узел;
узел =
record
подузел : array [1 .. максимальноечислоузлов]
Упражнения
271
of связь;
данные : типданных
end;
Заранее устанавливая значение максимального числа узлов,
мы вносим в программу довольно серьезное ограничение. Если
сделать это значение слишком маленьким, то рано или поздно
мы столкнемся с ситуацией, когда дерево представить невозмож-
но, если же значение максимального числа поддеревьев сильно
увеличить, возрастет объем памяти, теряемой из-за наличия не-
используемых ссылок. Можно предложить альтернативное ре-
шение, заключающееся в том, что каждый узел дерева должен
Рис. 8.12. Преобразованное дерево
содержать ссылку на связанный список его поддеревьев. Рис. 8.12
показывает то же дерево, что и рис. 8.10, но преобразованное
указанным образом. При таком представлении каждый узел
будет содержать только две ссылки (каждая из которых может
быть пустой): первая указывает на ближайший из узлов-отрост-
ков данного узла, а вторая на один из нескольких узлов того же
уровня, что и данный. На рис. 8.12 ссылки на поддеревья-от-
ростки показаны вертикальными линиями, горизонтальными ли-
ниями — ссылки на поддеревья, начинающиеся от узлов, имею-
щих общий предшествующий узел с данным. Таким образом, мы
преобразовали наше дерево в двоичное.
Упражнения
8.1. Нарисуйте диаграмму, иллюстрирующую выполнение операций
вставления и исключения элементов двусвязного кольца. Что получится в ре-
зультате исключения из пустого кольца?
8.2. Покажите, что в любом двоичном дереве пустыми являются более
половины всех имеющихся в нем ссылок. (Это очень расточительно, поэтому в
эффективных программах, обрабатывающих двоичные деревья, часто исполь-
зуются свободные поля для запоминания ссылок на узлы, расположенные на
более высоком уровне.)
272
Гл. 8. Динамические структуры данных
8.3. (а) Нарисуйте диаграммы для нескольких возможных форм двоич-
ных деревьев, которые вы можете придумать. В частности, укажите наилуч-
шую и наихудшую форму деревьев, используемых для хранения и поиска
имен.
(б) Разработайте алгоритм для исключения из двоичного дерева поиска
нетерминального узла без нарушения упорядоченности дерева.
8.4. Уровень корня дерева равен 0. Уровень любого другого узла всегда
больше на единицу, чем уровень узла, ссылающегося на данный. Глубиной
дерева называется уровень того узла, чей уровень максимален. Длина внутрен-
него пути по дереву есть общее число ребер, находящихся в дереве. Напишите
процедуры для нахождения:
(а) уровня указанного узла дерева;
(б) глубины данного дерева;
(в) длины внутреннего пути по данному дереву.
8.5. Существует несколько способов изображения двоичных деревьев на
бумаге, отличающихся от топологического способа, приведенного на рис. 8.11.
Рис. 8.11(a), например, может быть описан такими двумя способами:
(а) Д(Б(А,Г(В,)),Е(,3(Ж,И)))
(б) Д
Б
Е
Напишите программу, которая бы читала дерево в формате (а) и затем печатала
это дерево в формате (б).
Рис. 8.13. Дерево для выражения
8.6. Двоичные деревья можно применять для представления алгебраиче-
ских выражений. Каждый узел дерева содержит знак операции (+, —, *, или^
и ссылки на два подвыражения. На рис. 8.13 показано дерево, соответствую-
щее такому выражению
(а ъ Ь) Д- с — a < (b Д- с)
Приняв в качестве отправной точки программу калькулятор, напишите про-
грамму, которая бы читала выражение и строила соответствующее дерево.
Упражнения
273
8.7. Программа автобусы работаете 15 автобусными остановками и 5 ав-
тобусами. Нарисуйте график, приблизительно показывающий размер кольце-
вого списка событий как функцию времени.
8.8. Опишите применяемый в программе автобусы механизм, позволяю-
щий избежать «скучивания» автобусов, и объясните его действие на реальном
примере.
8.9. Напишите процедуру, которая делала бы 'мгновенный снимок' те-
кущей ситуации, сложившейся при работе программы автобусы. Процедура
должна активироваться в самом начале работы программы и затем выпол-
няться через фиксированные промежутки времени. Воспользуйтесь этой
процедурой, чтобы нарисовать график расположения автобусов как функцию
времени»
8Л0. Модифицируйте программу автобусы таким образом, чтобы в ней
производилось моделирование высадки пассажиров. Можно принять одно из
следующих предположений:
(а) Пассажиры выходят из автобуса с той же скоростью, что и встают в очередь,
(б) Дальность поездки пассажира есть случайная величина, подчиняющаяся
экспоненциальному закону распределения с отрицательным показателем.
8.11. Разработайте такую структуру данных, которая позволила бы моде-
лировать семейные отношения между людьми. Сведения о каждом человеке
представлены в записи, содержащей имя, а также ссылки на родителей, су-
пруга и детей. Напишите процедуру, которая позволяла бы вставлять в такие
данные описания новых людей, а также процедуры, которые могли бы уста-
навливать отношения нового члена семьи с остальными ее членами, например:
рождение (родитель, ребенок)
брак (жена, муж)
Напишите булевскую функцию кузен, выдающую значение true в том случае,
если ее аргументами являются ссылки на двоюродных братьев или сестер.
8.12. Напишите интерактивную программу, моделирующую геометриче-
ские операции. Пользователь программы должен иметь возможность давать
простые команды о создании объектов различной формы, которым он может
при этом давать некоторые имена. Такие команды, например, могли бы иметь
вид:
А — точка О О
Б — точка 2 3
П = прямая Л Б
8.13. На очередь вычислительной системы поступают задания со средней
скоростью X, которые выполняются со средней скоростью р. Напишите моде-
лирующую программу, которая определяла бы среднюю величину очереди как
функцию X и ц. Модифицируйте свою систему таким образом, чтобы организо-
вать в ней две очереди, одну — для заданий более высокого приоритета, по-
ступающих со скоростью Хб, а другую — для остальных заданий, кото-
рые поступают со скоростью Хм. Во время выполнения не может быть прер-
вана никакая работа, но работы низкого приоритета могут начинаться только
тогда, когда очередь более приоритетных работ пуста. Определите величины
длины очереди для различных значений Хб и Хм.
Замечание: Если некоторый ряд событий совершается со средней ско-
ростью С, то время между двумя последовательными событиями есть слу-
чайная величина, определяемая по закону
/=—In (х)/С,
где х — величина, равномерно распределенная в диапазоне от 0 до I.
Глава 9.
ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ
ЯЗЫКА
Это последняя глава, посвященная описанию возможностей
языка Паскаль. Многие программы, даже довольно сложные,
могут быть написаны без привлечения тех конструкций, которые
обсуждаются в этой главе, поэтому, перед тем как начинать поль-
зоваться ими, вам необходимо добиться свободного владения
языком.
9.1. Оператор перехода
Оператор перехода (goto) это единственный оператор языка,
еще ни разу не упомянутый в книге. Это не случайно, поскольку
программы можно писать и без этого оператора. Некоторые вер-
сии Паскаля вообще не признают операторов перехода п.
Оператор перехода позволяет производить непосредственную
передачу управления от одной части программы к другой, имею-
щиеся на этот счет в языке ограничения будут рассмотрены нами
позднее. Наиболее часто оператор перехода употребляется при
выходе из цикла. Приведенный ниже цикл полностью взят из
программы обработкатаблицы, гл. 7.
стараязапись := false;
индекс := 0;
while (индекс < размер) and not стараязапись do
begin
индекс : = индекс + 1;
if срок (датаобработки) — срок (таблица[индекс] .
датасделки) > давность
then стараязапись := true
end;
if стараязапись
then
Существует, очевидно, очень много способов трансформации одного
языка в другой. Если в языке нет оператора перехода, то говорить о версии
языка Паскаль уже нельзя. Это другой язык. В самом же языке Паскаль воз-
можности создания его версий, зависящих от машины, оговорены.— Прим,
ред.
9.1. Оператор перехода
275
for индекс : = 1 to размер do
печатьсделки (таблица[индекс])
Глядя на оператор цикла с пред-условием, мы видим, что фак-
тически это замаскированный оператор цикла с параметром. По-
этому можно записать:
стараязапись := false;
for индекс := 1 to размер do
if срок (датаобработки) — срок (таблица[индекс].
датасделки) > давность
then стараясделка := true;
if стараясделка
then
for индекс := 1 to размер do
печатьсделки (таблица[индекс!)
Единственное различие между этими фрагментами программ
состоит в том, что цикл в первом фрагменте закончится ровно в
тот момент, когда будет найдена строка таблицы, удовлетворяю-
щая указанному условию, в то время как второй цикл будет
проверять все компоненты таблицы, независимо от того, имеется
в ней искомая строка или нет. Нам становится понятно, что опе-
ратор цикла с проверкой пред-условия был нужен для того,
чтобы получить возможность окончить выполнение цикла в се-
редине таблицы. Кроме того, чтобы управлять условием, прове-
ряемым в операторе цикла, пришлось ввести одну совершенно
лишнюю переменную стараясделка. Можно достичь того же ре-
зультата и таким образом:
for индекс := 1 to размер do
if срок (датаобработки) — срок (таблица[индекс].
датасделки) > давность
then goto 1;
goto 2;
1 :
for индекс : = 1 to размер do
• печатьсделки (таблица[индекс])
2 :
Оператор-goto вызывает передачу управления на оператор,
помеченный меткой 1.
Переход можно также использовать и при решении проблемы,
с которой мы сталкивались в гл. 4: каким образом функция, вы-
зываемая из любого места программы, может выдавать понятные
сигналы о возникновении ошибки? Одно из решений заключается
в том, чтобы в конце главной программы поставить метку, на-
пример 99, на которую передается управление всякий раз при об-
276
Гл. 9. Дополнительные возможности языка
наружении какой-либо ошибки. Функцию квадратныйкорень из
гл. 4 можно теперь написать таким образом:
function квадратныйкорень (значение : real) : real;
const
эпсилон = IE — 6;
var
корень : real;
begin
if значение < 0
then
begin
writein ('аргумент функции значение);
goto 99
end
else if значение = 0
then
В конце главной программы напишем:
99 : writein ('выполнение программы прекращается')
end.
Метка в операторе перехода должна обязательно быть описа-
на. Раздел описания меток помещается в блоке перед всеми дру-
гими описаниями и определениями, его синтаксис показан на
рис. 9.1. Заметьте, метка должна быть положительной целой кон-
стантой.
описание меток—label
целое &ез
Знака
Рис. 9.1. Синтаксис описания метки
Ha использование оператора перехода наложены некоторые
весьма важные ограничения. Переходы можно осуществлять вну-
три одного уровня или при передаче управления из внутреннего
уровня во внешний, но нельзя переходить из внешнего уровня во
внутренний. В частности, хотя можно выйти из процедуры, пе-
рейти внутрь процедуры вам не удастся. Ниже приведены пра-
вильные операторы перехода.
label 2;
procedure выпрыгнуть;
label 1;
9.1. Оператор перехода
277
begin
goto 1;
goto 2;
1 : Onepamopl
end;
begin
if условие
then goto 2;
2 : Onepamop2
end
А вот такие операторы, напротив, запрещены в языке:
goto 3;
if условие
then Onepamopl
else 3 : Onepamop2
while другое условие do
begin
Onepamop3\
4 : Onepamop4
end; 1
goto 4;
procedure впрыгнуть;
label 5;
begin
5 : Оператор^
end;
begin
goto 5;
Метка и следующее за ней двоеточие составляют оператор
даже в том случае, если после двоеточия не указано выполнение
каких-либо действий. В соответствии с этим в следующем приме-
278
Гл. 9. Дополнительные возможности языка
ре точка с запятой, стоящая после оператора writein, необходима
для разделения оператора writein и пустого оператора с меткой 99.
begin
goto 99;
writein;
99 :
end
9.2. Процедуры и функции как параметры
Мы уже видели, что процедуры и функции могут вызывать
друг друга. Если бы не это, было бы очень трудно написать по-
настоящему нетривиальную программу. Часто мы сталкиваемся
с ситуациями, в которых нужно написать процедуру или функ-
цию, вызывающую другую процедуру или функцию, причем
результат работы последней невозможно определить до тех пор,
пока программа не будет выполняться. Например, может быть
потребуется написать функцию интегрировать такую, чтобы при
вызове
интегрировать (f,a,b)
мы получили значение
ъ
5 f (х) dx.
а
Тогда можно было бы писать такие операторы:
интегрировать (sin,0,t)
На Паскале подобные функции составлять можно. Мы проил-
люстрируем методику их построения на примере функции реше-
ние, которая находит приближенное решение уравнения
Л*)=о,
где f есть функция, передаваемая функции решение как пара-
метр.
Любой процесс поиска приближенного решения уравнения
не может начинаться, пока нет хотя бы одного начального при-
ближения, поскольку функция f может иметь несколько нулей.
Для нахождения нуля функции мы выбрали метод секущих,
который требует даже двух начальных приближений. Иллюстра-
цией к этому методу служит рис. 9.2. Приближениями настоя-
щего корня являются их2. Точки A1=(x1,f(x1)) и A2=(x2,f(x2))
находятся на кривой
У=Нх).
9.2. Процедуры и функции как параметры
279
Мы вычисляем третье приближение х3, которое есть х-коор-
дината точки пересечения прямой с осью абсцисс. Если
(х, у) есть точка на прямой АГА2, то:
У~У2 = У2 — У1
х — х2 х2 — х1 ’
где у± есть а у2 есть f(x2). Мы знаем, что точка (х3, 0) нахо-
дится на этой прямой, следовательно:
Хо — Xi
X з — X о У 2 * ' •
3 2 J2 У2-У1
Из этой формулы можно вывести рекуррентное соотношение
х„+г = хя+1-Дх„+1) • .
которое определяет новое приближение хп+2 по значениям двух
предыдущих приближений хп и хп+1, при условии, что
f (Хп + 1) f (*„)•
Можно показать, что limxrt=g, если только первичные при-
П-* 00
ближения достаточно хороши, f(g)y=O, a f"(x) непрерывна вну-
три рассматриваемого интервала. Из этих условий практически
Рис. 9.2. Приближение корня уравнения /(х)=0
самым важным является условие /'(£)=^0, которое не позволяет
применять данный метод для нахождения кратных корней. На-
пример, мы не можем найти нуль функции, изображенной на
рис. 9.3. Математическое доказательство сходимости не обяза-
тельно означает, что программа, написанная на основе этого
метода, всегда будет правильно работать, поскольку ошибки ок-
280
Гл. 9. Дополнительные возможности языка
ругления, возникающие при вычислениях, могут сделать мате-
матические результаты недействительными. Метод секущих до-
статочно хорошо работает для большинства «реальных» функций.
Рис. 9.3. Кратный корень
Так как имеющиеся в нашем распоряжении вычислительные
машины обладают конечной скоростью выполнения операций, то
надо указывать условие окончания вычислений. Один шаг ите-
рации изменяет значение текущего приближенного корня на ве-
личину
8==__f (у \ — xn + i~xn
+ f(Xn + 1)-f(xn) ’
и мы будем заканчивать вычисления, как только будет справед-
ливо
I 6/x„+i К е.
Функции решение нужно передавать четыре параметра: два при-
ближения корня, значение 8 и функцию /. Заголовок функции
надо записывать следующим образом:
function решить
(xl, х2, эпсилон : real;
function f(x : real) : real) : real;
Формальный параметр f имеет тот же вид, что и заголовки
функций, которые будут передаваться как фактические пара-
метры при выполнении программы. Должны быть указаны имя,
тип и формальные параметры. Имя х указано произвольно; оно не
обозначает ровным счетом ничего и служит только для указания
наличия у функции f одного параметра.
Программа уравнения использует функцию решение для отыс-
кания наименьшего положительного корня уравнений
cos(x) cosh(x)—1=0, (9.2.1)
cos(x) cosh (x)+1—0, (9.2.2)
9 2. Процедуры и функции как параметры
281
которые выводятся в элементарной теории колебаний. На рис. 9.4
показаны графики функций
£/=cos(x) cosh(x),
у= — 1-
у=1.
Из этих графиков мы видим, что первый ненулевой корень
уравнения (9.2.1) удовлетворяет условию л<|1<;2л, а первый
Рис. 9.4. Решение уравнений cos(x) cosh(x) ±1=0
корень уравнения (9.2.2) £2 удовлетворяет условию 0<^2<л.
В программе уравнения мы определим функции
ccn=cos(x) cosh(x)+l,
ccm=cos(x) cosh(x)—1
и вызовем функцию решение дважды.
program уравнения (input, output);
const
допуск = IE—6;
var
приближение!, приближение2 : real;
function решить(х1,х2,эпсилон : real;
function f(x : real) : real) : real;
var
хЗ,у 1,у2,дельта : real;
282 Гл. 9. Дополнительные возможности языка
решено : boolean;
begin
решено := false;
yl := f(xl);
у2 := f(x2);
repeat
дельта := —у2 * (х2 — х!) / (у2 — yl);
хЗ х2 + дельта;
if abs (дельта/х2) < эпсилон
then решено := true
else
begin
xl := х2;
х2 := хЗ;
yl := у2;
у2 := f(x2)
end
until решено;
решить := хЗ
end; {решить}
function ссп(х : real) : real;
var
expx : real;
begin
expx := exp(x);
ccn := cos(x) * (expx + 1/expx) /2+1
end; {ccn}
function ccm(x : real) : real;
var
expx : real;
begin
expx := exp(x);
ссм := cos(x) * (expx + 1/expx) /2 — 1
end; {ссм}
begin {уравнения}
read (приближение!, приближение2);
writein (решить (приближение!,приближение2,
допуск,ccn));
writein (решить (приближение!,приближение2,
допуск,ссм))
end. {уравнения}
Ввод:
5 7
9.3. Распределение памяти
283
Вывод:
4.694092
4.730040
Метод секущих основан на более известном методе Ньютона —
Рафсона, в котором применяется формула
Все, что мы сделали, это аппроксимировали f'(xr) выражением
хп xn-i
Оба начальных значения нам были нужны для приближенного
нахождения первого значения f'(x). Метод Ньютона — Рафсона
можно применять всегда, когда есть аналитическое выражение
для /'(х), но он не всегда позволяет найти решение с максималь-
ной скоростью. При использовании метода Ньютона — Рафсона
для решения уравнения (9.2.1) нам пришлось бы вычислять зна-
чение
cos (х) cosh (х) — 1
cos (х) sinh (х) — sin (х) cosh (х)
трижды на каждом шаге итерации.
Не каждая процедура и функция может быть передана в ка-
честве параметра другой. Параметры процедуры или функции,
передаваемой как параметр, сами должны быть параметрами-
значениями. Для большей наглядности предположим, что у нас
есть определения следующих процедур:
procedure a (procedure р);
procedure б (м,н : integer);
procedure в (var х,у : real);
procedure г (function д : real);
В программах, использующих эти определения, мы можем
писать а (б), имея в виду выполнить процедуру а, используя в
качестве фактического параметра процедуру б', но нам не разре-
шается писать
а (в) или а (г),
поскольку в имеет параметр-переменную, а г имеет параметр-
функцию.
9.3. Распределение памяти
В гл. 8 мы уже видели, как можно пользоваться стандартной
процедурой new, чтобы динамически создавать новую запись
и связывать с ней значение некоторой ссылки. Конечно, на самом
284
Гл, 9. Дополнительные возможности языка
деле процедура new не «создает» запись, она просто находит
не использованную до сих пор область памяти нужного размера
и соответственно настраивает ссылку. Область памяти, резерви-
руемая для динамических переменных, часто называется кучей, в
противоположность области, отводимой для статических пере-
менных (глобальных и локальных переменных процедур), кото-
рая называется стеком.
Если программа вызывает процедуру new неоднократно, может
наступить такой момент, что вся память, отведенная для динами-
ческих переменных, будет исчерпана, и программа не сможет
дальше выполняться. Однако во многих случаях программа может
уничтожать те динамические переменные, которые больше не по-
требуются для выполнения. Например, программа автобусы
из гл. 8 никак не использует запись о некотором событии после
того, как это событие обработано. Так как процедура new не
может знать, какие области памяти все еще нужны, то уничтожае-
мые записи следует отметить в программе явно. Для этого
существует процедура dispose, уничтожающая запись, ссылка
на которую передается ей в качестве параметра. Процедуры new
и dispose дополняют друг друга:
new (р);
{обработка записи pf }
dispose (р)
Точный результат работы процедуры dispose не определен в
предлагаемом стандарте языка Паскаль, поэтому ее реализация
целиком зависит от разработчиков компилятора. В простейших
версиях процедура dispose не выполняет вообще никаких действий.
Более сложные версии дают возможность повторно использовать
некоторую часть или вообще всю память. Вы можете получить
некоторые сведения об эффективности работы процедуры осво-
бождения памяти на вашей машине, попробовав выполнить про-
грамму проверкаосвобождения. Очень немногие машины могут
выделять программам по 1 000 000 слов памяти, поэтому если эта
программа дошла до конца, то значит на вашей машине работает
какая-то схема перераспределения памяти.
program проверкаосвобождения (output);
const
размер = 1000;
числоузлов = 1000;
type
связь =|узел;
узел = array [1 . . размер] of integer;
var
ссылка : связь;
9.3. Распределение памяти
285
индекс : 1 . . числоузлов;
begin
for индекс := 1 to числоузлов do
begin
new(ccbMKa);
dispose(ccbMKa)
end {for}
end. {проверкаосвобождения}
Создание специальных программ для проверки свойств язы-
ка — это порочная практика: все сведения вы должны черпать
из описания языка. В данном случае мы проверяем свойства
конкретной реализации, не определенной в предлагаемом стан-
дарте Паскаля. Разумеется, если в вашем распоряжении имеется
руководство для пользователей вашей реализации языка, лучше
всего получить необходимую информацию оттуда, а не из про-
граммы проверкаосвобождения.
Если процедура dispose не производит реального освобожде-
ния памяти, вам придется взять все обязанности по управлению
этим процессом на себя. Простейшим способом является образо-
вание связанного списка неиспользуемых записей, называемого
списком свободной памяти. Как только появится нужда в новой
записи, она будет браться из этого списка, процедурой new записи
будут порождаться только в том случае, если список свободной
памяти оказался пустым. Когда программа кончает пользоваться
какой-либо записью, эта запись возвращается в список свобод-
ных записей. Все записи, находящиеся в списке свободной памя-
ти, являются вариантами типа свободный; в качестве начала
списка используется ссылочная переменная. В этих описаниях
мы показываем только свободный вариант записи:
type
вид = (свободный, . . .);
связь = j ячейка;
ячейка =
record
case флаг : вид of
свободный : (следующий : связь);
end;
var
свободныйсписок : связь;
Определим далее две процедуры, создать и уничтожить, каж-
дая из которых имеет один параметр типа связь.
procedure создать (var ссылка : связь);
286
Гл. 9. Дополнительные возможности языка
begin
if свободныйсписок = nil
then пе\у(ссылка)
else
begin
ссылка : = свободныйсписок;
свободныйсписок : =
свободныйсписок |. следующий
end
end; {создать}
procedure уничтожить (ссылка : связь);
begin
ссылка|.флаг := свободный;
ссылка|.следующий := свободныйсписок;
свободныйсписок := ссылка
end; {уничтожить}
В главной программе должен содержаться оператор инициа-
лизации
свободныйсписок := nil
так что сначала список свободной памяти пуст. Первоначально
новые записи создаются с помощью процедуры new, поскольку
свободныйсписок = nil. По мере уничтожения некоторых записей
(с помощью процедуры уничтожить) эти записи будут накапли-
ваться в списке свободных записей, что позволит затем использо-
вать их повторно в процедуре создать. Заметьте, что поскольку
в языке Паскаль ссылку можно связывать с записью определен-
ного типа, то список свободных записей может содержать записи
только одного этого типа.
Вообще говоря, различные варианты записи могут иметь раз-
личные размеры. При использовании процедуры new для отве-
дения памяти под новую запись память будет отведена в расчете
на максимальный возможный размер. Для организации эффек-
тивного распределения памяти следует стараться делать варианты
записей примерно одинаковыми по размерам, хотя это может ока-
заться и нелегкой задачей, если вы не'знаете, как различные струк-
туры данных представляются на вашей машине. Прежде всего
постарайтесь избежать ситуаций, в которых один вариант записи
намного превосходит по размерам все остальные. В языке Пас-
каль имеются хорошие механизмы, позволяющие эффективно
распределять память. Процедуру new можно вызывать, пере-
давая ей более одного параметра:
new (ссылка, t)
9.3. Распределение памяти
287
где t есть константа поля признака для требуемого варианта
записи. Переменным полям этой записи можно присваивать
значения самым обычным образом, но полю признака записи
присваивается значение t, причем менять его запрещено. Соз-
данная таким образом запись может быть уничтожена вызовом
dispose (ссылка, t)
Программа окружности из гл. 6 уже использовала записи с ва-
риантами. Приведем определение типа форма, фигурировавшего
в этой программе:
type
форма — (пусто, точка, прямая, окружность);
координаты —
record
абсцисса, ордината : real
end;
фигура =
record
case флаг : форма of
пусто :
( );
точка :
(положение : координаты);
прямая :
(хкоэффициент, укоэффициент, сдвиг : real);
окружность :
(центр : координаты;
радиус : real)
end;
var
f, g : фигура;
коор : координаты;
Вызов new(f) создаст новую запись достаточно больших разме-
ров, чтобы разместить любой вариант. Эта новая запись может
быть уничтожена вызовом dispose (f).
Вызов new(f, точка) создаст запись такого размера, чтобы
разместился вариант описания точки. Эта запись может иметь
недостаточные размеры для хранения описаний прямой или
окружности. Она должна уничтожаться вызовом dispose (f,
точка)
Нельзя менять тип записи, созданной таким образом, и нельзя
288
Гл. 9. Дополнительные еоалюжноети языка
осуществлять присваивание полной переменной. Мы не можем
записать:
new (g);
ft := gt
ff.признак := окружность
Однако можно производить присваивания тем компонентам
которые соответствуют выбранному нами значению поля
признаков. Например, можно написать
ff.позиция := коорд
При наличии в записи «вложенных» вариантов любой конкрет-
ный вариант можно указывать с помощью значения признака
каждого уровня. В этом случае вызовы процедур new и dispose
примут такой вид:
new(рп tx, t2,. . t„)
dispose (p1( tn t2>. . t„)
Упражнения
9.1. Напишите две эквивалентные программы, одну с операторами пере-
хода, а другую без них. Выберите задачу, в которой использование переходов
укорачивает, упрощает и ускоряет работу программы по сравнению с версией
без переходов.
9.2. Постройте функцию max, такую, что
max (f, а, b)
находит максимальное значение функции f (х) в интервале жх<Ь и возвра-
щает соответствующее значение х.
9.3. Постройте вещественную функцию интегрировать, такую, что
интегрировать (f, а, Ь)
будет выдавать приближенное значение
ь
5 f W dx.
а
9.4. Напишите процедуру срубитьдерево. которая будет уничтожать
все узлы двоичного дерева. Процедура имеет одни параметр — ссылку на кор-
невой узел дерева.
Глава 10.
РАЗРАБОТКА ПРОГРАММЫ
В первой главе этой книги мы определили программу для вы-
числительной машины как последовательность команд. Большая
часть последующих восьми глав была посвящена описанию раз-
личных форм, в которые могут облекаться команды в языке Пас-
каль, а также результатов выполнения этих команд. В этой пос-
ледней главе мы обсудим различные аспекты задачи, стоящей
перед программистом.
Будем считать, что нам поручено решить некоторую задачу и
что мы намерены написать программу для вычислительной маши-
ны, которая и будет в каком-то смысле решением данной задачи.
Мы будем также предполагать, что задача поддается решению
на машине: выдаваемые в процессе работы результаты являются
хорошо определенной функцией от исходных данных. Повсюду в
этой главе подразумевается, что речь идет о разработке програм-
мы значительного размера. Нетрудно написать правильную про-
грамму, помещающуюся на одной странице, гораздо сложнее на-
писать программу в сотню страниц и с уверенностью сказать, что
она совершенно правильна.
Машина не решает задачу, решение — это дело программиста.
Решить задачу — значит найти алгоритм, с помощью которого
входные данные можно правильно преобразовать в выходные.
Написание программы состоит в последовательном выражении
этого алгоритма в виде, понятном вычислительной машине. Так
как машина начисто лишена воображения и какого бы то ни было
здравого смысла, алгоритм ей надо представлять в ясной и недвус-
мысленной форме. Чтобы уметь программировать, не надо быть
математиком, но ясно выражать свои мысли просто необходимо.
Общеизвестно, что некоторые программы не всегда выдают
правильные результаты. Этому есть целый ряд возможных объяс-
нений: неправильная постановка задачи; нечеткое выражение
задачи в алгоритме программы; выполнение программы с невер-
ными данными. Мы коснемся здесь только двух первых источников
ошибок: выбора неправильного алгоритма и неправильной реали-
зации выбранного алгоритма.
Есть два способа, чтобы убедиться в том, что программа
верна. Либо нужно руководствоваться такой системой, которая
Ю № 3388
290
Гл 10 Разработка программы
неминуемо приводит к написанию правильной программы, либо
разработать специальные средства, позволяющие продемонстри-
ровать отсутствие ошибок в законченной программе.
Иногда вычислительная машина используется просто как обык-
новенная, хотя довольно сложная, игрушка, на которой вполне
можно применять метод проб и ошибок, поскольку последствия
от ошибок в программах не приводят к значительным неприят-
ностям. Однако в последнее время машины все чаще начинают
действовать там, где последствия какой-либо ошибки оказываются
весьма серьезными. В таких случаях отсутствие систематической
методологии программирования не только безответственно, но и
просто опасно. Нам не нужны инженеры, которые строят редко
разрушающиеся здания, обходимся мы и без бухгалтеров, ба-
ланс у которых, как правило, сходится, по той же причине не
нуждаемся мы и в программистах, программы которых время от
времени выдают неправильные результаты.
10.1. Составление программы
Все современные методы программирования по существу есть
вариации старого традиционного метода, поэтому мы начнем с
обзора этого метода. Создание программы традиционно разделя-
ется на три фазы: разработка, реализация и тестирование.
Фаза разработки, обычно проводимая так называемым «ана-
литиком», заключается в составлении описания программы, обыч-
но на естественном языке. Это описание передается программис-
ту, который на его основе пишет программу.
Традиционные методы программирования окутаны покровом
таинственности, но, благодаря тому что некоторые программисты
открыли свои секреты, мы точно знаем, что программирование
состоит из комбинации предположений, предчувствий, трюков,
вдохновения, а также некоторых специальных принципов прог-
раммирования.
После того как программист заявляет, что его работа законче-
на, программу тестируют сначала сам программист, а затем (иног-
да) еще и аналитик. Тестирование заключается в выполнении
программы со специально подобранными для этого данными,
чем достигается ее полная проверка. Позднее тестирование может
проводиться с реальными данными, т. е. в окружении, в котором
программа будет работать при настоящей эксплуатации.
В программе могут быть обнаружены расхождения по отно-
шению к описанию, в случае чего ее модифицируют и снова тести-
руют. Иногда результаты тестирования программы приводят к
сомнениям в правильности описания, тогда повторяется весь
цикл — производится модификация описания, модификация
программы и повторное тестирование; необходимо также сопо-
10.1. Составление программы
291
ставить результаты работы с модифицированным описанием про-
граммы.
При соблюдении некоторых мер предосторожности такой метод
приводит к созданию работающих программ. Однако иногда метод
не срабатывает, и от проекта приходится отказаться. Во многих
случаях, даже в значительном большинстве случаев, подобный
метод приводит к появлению программ, правильно работающих
большую часть времени, но время от времени останавливающихся
в середине процесса решения или выдающих неправильные ре-
зультаты. Люди, подолгу работающие с программой, часто очень
хорошо знакомы с ее поведением, привыкают к ее недостаткам,
но тех, кто пользуется программой редко, ошибки в ней очень
смущают.
Множество людей выражало свое неудовольствие по поводу
отсутствия строгости в традиционных методах программирова-
ния, и »было предложено много различных путей к улучшению
ситуации. Большая часть предложений касалась создания фор-
мальной системы программирования.
Математики провели последние несколько веков в попытках
исключить из своих теорий и выводов'всевозможные двусмыслен-
ности и поэтому лучше других знакомы с преимуществами фор-
мальных систем. У программистов есть и свои дополнительные
надежды на формальную систему программирования: если удаст-
ся определить набор правил для создания программ, можно бу-
дет выразить эти правила в форме программы и поручить созда-
ние программ вычислительной машине. Машина, конечно, никог-
да не сможет выполнять всю работу по программированию, но если
она возьмет на себя хотя бы часть рутинной работы, мы сможем
большую часть времени и своей энергии посвятить более интерес-
ным творческим аспектам программирования.
Наиболее крупным вкладом в дело автоматизации программи-
рования было создание языков высокого уровня, а также компи-
ляторов для этих языков. Как мы уже видели, программа — это
есть выражение решения некоторой проблемы в терминах языка,
понятного вычислительной машине, а программирование есть
процесс перевода решения на такой язык. Значительную часть
подобного перевода осуществляет для нас компилятор.
Языки высокого уровня и компиляторы заметно расширили х)
диапазон задач, решаемых с помощью вычислительных машин.
Другим важным усовершенствованием явилось широкое исполь-
Ни языки программирования, ни компиляторы не расширяют диапазо-
на задач, а лишь облегчают процесс программирования, и это приводит к то-
му, что число решаемых задач в некотором фиксированном диапазоне трудно-
сти увеличивается Сам же диапазон задач расширяется либо за счет увеличе-
ния мощности машин, либо в результате исследований в области алгорит-
мов—. Прим. ред.
10*
292
Гл. 10. Разработка программы
зование стандартных функций и библиотек программ. Стандарт-
ные, т. е. часто встречающиеся задачи, лучше всего решать
одному квалифицированному программисту либо группе таких
программистов, а затем передать эти решения всем пользователям,
вместо того чтобы каждому программисту решать такие задачи
самостоятельно. Возможно, большинство программистов, поль-
зующихся стандартными функциями Паскаля In и sqrt, не знают,
как вычисляются натуральные логарифмы и квадратные корни,
именно так и должно быть.
Кроме встроенных в компилятор стандартных функций языка
существуют еще библиотеки процедур и функций, и на совре-
менных машинах программист, владеющий алгеброй в объеме
средней школы, без особых затруднений может выполнить обра-
щение матрицы или быстрое преобразование Фурье. Все эти весь-
ма важные аспекты не являются, однако, предметом обсуждения
этой главы. В ней мы опишем популярные современные методы
создания программ и некоторые основополагающие идеи верифи-
кации программ.
С точки зрения разработчика самым главным в программе
является ее иерархическая структура. Как мы уже знаем из
гл. 4, иерархическая структура программы строится с помощью
процедур. Программа состоит из процедур высокого уровня,
которые выполняют большие трудные задачи, к таким процеду-
рам относится, например, процедура «решить систему уравнений».
Процедуры высокого уровня обращаются к процедурам более
низких уровней и так далее, вплоть до примитивных процедур
самого низкого уровня, таких, как «ввести один символ». Проце-
дуры высокого уровня выражают общие очертания метода реше-
ния задачи, в то время как процедуры низкого уровня содержат
детали реализации этого решения.
Первый важный выбор мы должны сделать в самом начале
создания новой программы; начинать ли с верхней части иерархи-
ческой структуры, с наиболее абстрактного уровня, либо начи-
нать снизу, с уровня наибольшей детализации, и продвигаться
вверх. Следуя советам ряда авторитетных специалистов, мы
предпочитаем первый из двух указанных методов: программи-
рование сверху - вниз. Создание программы начинается с вы-
работки общего подхода к решению и продолжается затем путем
последовательной конкретизации до тех пор, пока это решение не
будет полностью выражено на выбранном языке программиро-
вания.
Процедуры дают нам возможность выр азить необходимые кон-
кретизации в удобном для нас виде. Существуют три основных
метода конкретизации решения; для каждого из них принято
некоторое мнемоническое наименование: «разделяй и властвуй»,
«метод последовательных приближений» и «анализ вариантов».
10.1. Составление программы
293
Применение первого из перечисленных методов состоит в разде-
лении задачи на относительно независимые части и в последую-
щем решении каждой отдельной части. ЛАногие задачи обработки
данных, например, можно легко разделить на три последователь-
ных шага решения:
ввести данные-,
выполнить вычисления-,
напечатать результаты
Если данных больше, чем можно хранить в памяти одновременно,
такая схема не подходит. В этом случае лучше пользоваться сле-
дующей схемой:
repeat
ввести часть данных9,
выполнить вычисления-,
напечатать результаты9,
until данных больше нет
Это и есть реализация второго метода конкретизации — метода
последовательных приближений. Идея его состоит в том, что
если есть какой-то способ за один шаг немного приблизиться к
решению и его можно повторять много раз, то рано или поздно мы
несомненно получим полное решение. Точное определение «ме-
тода последовательных приближений» очень важно, и мы еще
вернемся к нему позднее в том месте этой главы, где будет обсуж-
даться верификация программ. Для приведенного выше примера
предположим, что элементы данных могут быть пронумерованы
от 1 до N и что оператор ввести часть данных вводит по крайней
мере один элемент данных. Можно заключить отсюда, что для
получения полного решения понадобится не более N шагов.
Третьим методом конкретизации программы является анализ
вариантов. Предположим, что в рассмотренном примере данные
можно разделить на четыре класса: овцы, свиньи, козы и ослы.
Для каждого такого класса естественно использовать различные
процедуры:
repeat
читать один элемент данных9,
case класс of
овца :
обработать овцу;
свинья :
обработать свинью;
коза :
обработать козу;
осел :
обработать осла
294
Гл. 10. Разработка программы
end;
напечатать результаты
until данных больше нет
Методы конкретизации программы применяются на всех уров-
нях до тех пор, пока не останется ничего из того, что еще можно
было бы конкретизировать. В этом примере у нас есть теперь шесть
новых подзадач:
ввести элемент данных
обработать
обработать
обработать
обработать
напечатать
W
свинью
козу
осла
результаты
и следующим шагом будет сведение их к еще более простым за-
дачам с помощью любого из трех методов конкретизации про-
граммы.
Обратите внимание на то, как естественно эти методы соотно-
сятся с операторами языка Паскаль. В результате применения
принципа «разделяй и властвуй» — появляются последователь-
ности операторов или составные операторы. Итеративный метод
(метод последовательных приближений) моделируется различны-
ми операторами цикла, а анализ вариантов можно моделировать
либо оператором варианта, либо, в тех случаях, когда варианты
легче различать с помощью булевских выражений, условным опе-
ратором или составным условным оператором.
Кроме того, мы можем на любом уровне писать процедуры.
При использовании в программировании сверху-вниз процедур
окончательный вариант программы сохранит следы процесса
разработки, что в свою очередь сделает программу более по-
нятной.
Однако реально разработка программы не всегда может вес-
тись в полном соответствии с описанным здесь методом сверху-
вниз. Иногда конкретизация программы может приводить к воз-
никновению не независимых или неразрешимых подзадач. Перед
тем как решиться на какую-либо конкретизацию программы,
обязательно нужно заглянуть вперед и попытаться предусмотреть
ее последствия. Иногда, несмотря на такие попытки, мы все же
можем ошибиться, поэтому приходится возвращаться назад,
уничтожая некоторые из уже сделанных конкретизаций. Число
просмотров вперед и возвратов назад, необходимых для решения
задачи, зависит как от сложности этой задачи, так и от нашего
опыта.
Разработка сверху-вниз — это не единственный метод систе-
10.2. Тестирование и верификация
295
матического программирования. В нашей книге мы выбрали имен-
но этот метод, поскольку он является наиболее документирован-
ным, надежным и гарантирующим безопасную работу. Метод
разработки сверху-вниз не всегда приводит к самому эффективно-
му решению, он не всегда позволяет глубоко вникнуть в существо
решаемой задачи. Применение метода разработки программы
сверху-вниз будет на примере типичной задачи программирования
продемонстрировано ниже, в разд. 10.4.
10.2. Тестирование и верификация
Как любой другой продукт производства, программа для вы-
числительной машины перед использованием должна быть про*
верена. Одним из путей проверки или тестирования программ
является выполнение этой программы по одному разу с каждой
из возможных комбинаций входных данных. Легко видеть, что в
большинстве случаев это совершенно непрактично. Обычная
вычислительная машина может представлять около миллиарда
различных целых чисел, поэтому, если мы хотим исчерпывающим
образом проверить работу программы, которая читает всего одно
целое число, нам придется выполнить ее миллиард раз и проверить
миллиард наборов выходных результатов. Ясно, что нам надо как-
нибудь уменьшить число необходимых проверок.
Одним из путей такого уменьшения является выполнение
случайных тестов. Хотя случайное тестирование может обнару-
жить в программе крупные ошибки, например программа посто-
янно выдает неправильный результат, очень маловероятно,
чтобы случайное тестирование обнаружило какие-либо нерегуляр-
ности в поведении программы. Программа, использующая, на-
пример, значения функции Zg'(x), может выдавать ненадежные
результаты при х ж л/2, но случайное тестирование этого, веро-
ятно, не обнаружит.
При тестировании другой продукции, например автомобиля,
мы не будем применять ни исчерпывающего тестирования, кото-
рое должно, по-видимому, выражаться в полной обкатке автомо-
биля по всем дорогам страны, ни случайного тестирования. На-
против, сначала мы классифицируем все поверхности дорог, по
которым, возможно, придется ездить нашему автомобилю, и
построим соответствующие тесты: мы будем чередовать участки
гладкие и прямые, извилистые и ровные, наклонные и т. п. Затем
мы определим те части автомобиля, которые могут ломаться, и
проверим, действительно ли они сломались в испытаниях. Мы
будем основывать наши тесты на знании внутренней структуры
автомобиля. Аналогичным образом для разработки тестов про-
грамм надо знать внутреннюю структуру программы.
296
Гл. 10. Разработка программы
Предположим, у нас есть программа, содержащая оператор,
который присваивает z значение тах(х, £/),т. е. наибольшего из ,
чисел х и у. j
if х > у
then z : = х
else z : = у i
Программист, разрабатывающий тест для этой программы, j
заметит, что существуют два случая, уих^у, и позаботит- j
ся о том, чтобы тест включал в себя данные, проверяющие оба ।
этих случая. Вообще говоря, согласно этому методу, во время |
тестирования нужно выполнить операторы на всех ветвях про- I
граммы. Например, программа квадратное из гл. 3 показана с I
пятью наборами входных данных, каждый из которых соответ- f
ствует одному из путей в программе. I
Здесь учитывается то свойство программы, что решения, I
принимаемые в программе, делят набор входных данных на пять |
классов, причем каждый класс входных данных обрабатывается |
на отдельном пути выполнения программы. Вообще говоря, j
тестировать все пути или даже все комбинации путей недостаточ- i
но. Ошибка может быть и в условии, как, например, в следующем I
случае: I
var 1
х, у : integer;
if ((х + y)div 2) = х
then write ('x = y')
else write ('x Ф y')
Обе ветви этого оператора могут быть проверены специаль-
ными тестирующими данными (х = 2, у = 2) и (х = 3, у =- 2),
а ошибка не будет обнаружена, хотя оператор неверен для дру-
гого набора данных (х = 2, у — 3). Несмотря на эти трудности,
для поисков подходящих наборов тестирующих данных были
предложены систематические методы.
Существует множество больших и сложных программ, кото-
рые, хотя и прошли весьма тщательное тестирование, иногда сби-
ваются. Например, компиляторы работают сотни раз в день, но
вдруг могут отказаться компилировать правильную программу.
Многие операционные системы непрерывно работают целыми дня-
ми, но могут вдруг неожиданно сбиваться и останавливать-
ся. В этих случаях некоторые редкие комбинации событий
привели процесс к состоянию, не предусмотренному програм-
мистом.
10.2. Тестирование и верификация 297
Подобные сложные программы никогда не могут быть оттес-
тированы полностью. Например, компилятор должен обрабаты-
вать любую строку символов конечной длины и, если строка
соответствует определению языка, должен сформировать экви-
валентную рабочую программу. Операционные системы тестиро-
вать еще труднее, поскольку они управляют множеством парал-
лельных процессов. Когда сбивается операционная система,часто
бывает невозможно даже воспроизвести те условия, которые при-
вели к сбою.
Некоторые специалисты предлагают, вместо того чтобы тести-
ровать программы, доказывать, что они правильно выполняют
свои функции. Так как доказанный оператор надежен не более,
чем предположения, лежащие в основании доказательства, перед
тем, как доказывать правильность программы, написанной на
данном языке, нам надо определить смысл каждой языковой кон-
струкции. Это осуществимо только в том случае, если в нашем
распоряжении имеется идеальный процессор, поэтому обычно мы
не можем гарантировать правильную работу доказанной про-
граммы на реальной вычислительной машине. Однако, можно
утверждать, что программа правильна, даже если вычислитель-
ная машина не сможет правильно ее выполнить.
Подробное изложение теории доказательства правильности
программ или их верификации, как обычно называют этот про-
цесс, не входит в задачу этой книги. Мы дадим здесь некоторые
вводные сведения о верификации программ, основываясь на не-
формальной интерпретации семантики языка Паскаль. Это не
будет бесполезной тратой времени, так как понимание техники
верификации программ поможет понять и технику программиро-
вания. В самом деле, концепции, используемые при верификации
программ, представляют собой не более чем формализованные
версии идей, возникающих в голове хорошего программиста,
когда он пишет программу.
Предположим, мы пишем оператор
у : = 1/х
где х и у — вещественные переменные некоторой программы.
Написав этот оператор, мы должны иметь доводы в пользу того,
что х в данный момент не равен нулю, поскольку иначе нельзя
будет вычислить выражение 1/х, кроме того, мы ожидаем, что
после выполнения оператора у будет иметь значение 1/х. В раз-
витие этих соображений мы и опишем методику верификации про-
граммы.
С каждым оператором программы мы свяжем два булевских
выражения. Первое из них будет называться пред-условием опе-
ратора, оно выражает наше знание о состоянии программы перед
298
Гл. 10. Разработка программы
началом выполнения оператора. Второе выражение назовем
пост-условием оператора, оно будет выражать наше знание о
состоянии программы после выполнения оператора. Эти условия
будут записываться в виде комментариев перед и после оператора.
{пред-условие},
оператор
{пост-условие}
Предположим, в некоторой точке программы, где есть уверен-
ность что х > 0, мы пишем оператор
у := 1/х
Далее мы будем знать, что и после выполнения этого оператора
условие х > 0 останется справедливым, так как присваивание
не изменит значение х, мы будем также знать, что у = 1/х. По-
этому:
{X > 0}
у := 1/х
{(X > 0) Д (Y = 1/Х)}
Буквами X и Y мы обозначили текущие значения переменных
х и у. В последующем обсуждении мы будем всегда использовать
заглавные буквы для обозначения текущих значений. Знак опе-
рации Д соответствует операции and, везде в дальнейшем при
составлении пред- и пост-условий мы будем пользоваться этим
символом, а также символом V, означающим операцию or, и
соответствующим операции not, поскольку полные обозначения
сильно загромоздили бы наши записи.
Будем предполагать, что программа выполняется идеальным
процессором, и будем игнорировать возможность выхода резуль-
татов за пределы допустимого, а также возможность возникнове-
ния неточных результатов.
В примере, приведенном выше, оператор дополнил наше зна-
ние о состоянии процесса, что, однако, бывает далеко не всегда.
В нижеследующем примере после выполнения оператора мы будем
знать о значении переменной х меньше, чем до его выполнения:
{X =0}
read(х)
{?}
После окончания работы оператора чтения мы ровным счетом
ничего не будем знать о значении переменной %, поэтому пост-
условие должно быть истинно для всех значений х. Единственным
10.2. Тестирование и верификация
299
пост-условием, имеющим истинное значение для всех значений х,
является значение true. Мы можем записать:
{X =0}
read(x)
{true}
Хотя это может показаться странным, но в контексте верифика-
ции программы условие true выражает полное отсутствие пред-
ставления о состоянии программы.
Однако просто расписать все пред- и пост-условия х) програм-
мы явно недостаточно. В общем случае нашей задачей будет до-
казать истинность пост-условия в предположении, что истинно
пред-условие. Чтобы добиться этого, мы будем рассматривать
оператор за оператором всю программу, доказывая, что из пер-
вого пред-условия следует последнее пост-условие. Так как
отношения между первым пред-условием и последним пост-усло-
вием характеризуют спецификацию программы, нам фактически
надо доказать, что программа будет функционировать в точном
соответствии со своей спецификацией.
В качестве простейшего примера рассмотрим «программу»:
с : = а + Ь;
d : = с * с
Отсутствие каких-либо знаний о начальных значениях перемен-
ных выразим, записав пред-условие true. Очевидно, что пост-
условием первого оператора будет С = А + В. Это будет также
и пред-условие для второго оператора. Из второго оператора мы
можем вывести, что/) = С * С, поэтому пост-условием программы
будет
(С = А + В) д (D =С * С)
что можно упрощенно записать как D = (А + В)2, и следова-
тельно, мы можем написать
{true}
с : а + b;
{С = А + В}
d : = с * с
{D =(А + В)2}
Все это может показаться довольно очевидным. Верификация
становится более интересной при рассмотрении операторов
цикла и принятия решений. Прежде всего рассмотрим уже
1 Не совсем точно: просто расписать все пред- и пост- условия невозможно,
не доказывая нечто о программе. Когда же вы их все напишите, то на этом до-
казательство закончится.— Прим. ред.
300
Гл. 10. Разработка программы
приводившуюся программу вычисления тах(х, у):
if х > у
then z : = х
else z : = у
Повторимся, сказав еще раз, что мы ничего не знаем о начальных
значениях всех переменных, поэтому первое пред-условие есть
true. Из нашего знания о работе условного оператора можно
вывести пред-условия для обеих альтернатив оператора:
{true}
И х > у
then
{X>Y}
z : = х
else
{- (X > Y)
z := у
Соответствующие пост-уравнения выводятся из знания работы
операторов присваивания:
{true}
if х > у
then
{X>Y}
z * — x
{(X>Y)A{Z=X)}
else
{- {X > Y)
z := У
{~(X>Y) Д (Z = Y)}
Чтобы получить пост-условие всего оператора, обратим внимание
на то, что при выполнении условного оператора выполняется
только одна его альтернатива. В соответствии с этим, истинным
может быть только одно из пост-уравнений, и окончательное пост-
условие будет выглядеть так:
{(X > Y) A (Z = X) V ~ (X > Y) Д (Z = Y)}
что можно записать в таком виде:
Z = max(X, Y)
Следующим шагом будет попытка верифицировать программу
с циклом, например, таким:
{(X > 0) Д (Y > 0)}
и : = х;
10.2. Тестирование и верификация
301
V := у;
while u #= v do
if u > v
then u : = u — v
else v : = v — u
{U = нод{Х, Y)}
Мы будем верифицировать цикл, отыскивая выражение, остаю-
щееся истинным во время выполнения цикла; такое выражение
называется инвариантом цикла. Исследование программы пока-
зывает, что значения U и V уменьшаются, а истинным под-
держивается соотношение
нод(и, V) = нод(Х, Y),
т. е. имеет место теорема
если А > В то нод(А — В, В) = нод(А, В).
Заметим далее, что U и V первоначально положительны и что
меньшее всегда вычитается из большего, так что они всегда
остаются положительными. Это приводит к выбору такого
инварианта:
{(U > 0) Л (V > 0) Л (нод (U, V) = нод (X, Y))}.
Мы можем начать, написав
{(X > 0) Д (Y > 0)}
и : = х;
v : = у;
while и ф v do
{(U > 0) Л (V > 0) Л (нод(и, V) = нод(Х, Y))}
Условный оператор различает два случая, U > V и U < V.
Заметим, что случай U = V исключается оператором цикла с
пред-условием. Имеем
if u > v
then
{(U > V > 0) Л (нод(и, V) = нод(Х, Y))}
и : = ц — v
else
{(V > и > 0) л (нод(и, V) = нод(Х, Y))}
Для случая U > V напишем U' = U — V. Так как U > V,
мы знаем, что U — V > 0 и, следовательно, U' > 0. Аналогично
(U', V') = нод(и — V, V) = нод(и, V) = нод(Х, Y),
и поэтому
{(U' > 0) Л (V > 0) Л (нод(и', V) = нод(Х, Y))}.
302
Гл. 10 Разработка программы
Если теперь вместо U' мы напишем (/, а именно в этом выража-
ется результат выполнения оператора присваивания, то уви-
дим, что после выполнения первой альтернативы условного опе-
ратора предложенный инвариант останется истинным. Пользуясь
аналогичными аргументами, можно показать, что присваивание
v : = v — и
стоящее во второй альтернативе условного оператора, также не
изменяет истинности предложенного инварианта. Следовательно,
доказаны следующие два утверждения:
Предложенный инвариант истинен перед началом первого
шага работы цикла.
Если инвариант истинен в начале одного шага цикла, он также
истинен в начале следующего шага цикла.
Согласие методу математической индукции, предложенный нами
инвариант остается истинным до конца выполнения оператора •
цикла с пред-условием. Именно такой инвариант мы и искали.
Оператор цикла заканчивает работу, когда (/ = V, так что
пост-условием для него будет
(U > 0) Д (V > 0) Д (нод(и, V) = нод(Х, Y)) Д (U = V).
Из этого легко вывести, что
нод(Х, Y) - нод(и, V) - нод (U, U) - U,
что и требовалось доказать. Тем самым мы продемонстрировали,
что после окончания работы цикла в программе будет найден нод
(X, У). Мы, однако, еще не доказали, что условие и =^= v будет
выполняться не всегда. Чтобы доказать, что в конце концов будет
выполнено условие U = V, заметим, что
0< min(U, V) max(U, V),
где min(t/, V) есть наименьшее, a max(t/, X) есть наибольшее
из чисел U и У. Во время работы одного шага цикла мы выпол-
няем:
либо и : = и — v
либо v : = v — и
Какой бы из этих двух операторов мы не выполняли, остается ис-
тинным соотношение
0< min(U, V) max(U, V),
но при этом max(t/, V) должен уменьшиться по крайней мере
на 1, так как (/ #= V, а оба этих числа — целые. Через конечное
число шагов мы, следовательно, будем иметь
0< min(U, V) = max(U, V),
10.2. Тестирование и верификация 303
откуда следует, что теперь U = V. Отсюда можно сделать вывод,
что через конечное число шагов программа остановится.
Цикл с параметром можно верифицировать, построив эквива-
лентный ему цикл с пред-условием. Рассмотрим программу, вы-
числяющую сумму элементов массива а, индекс которого меня-
ется от 1 до п.
s :=0;
for i : = 1 to n do
s : = s + a [i]
Это же можно записать в эквивалентном виде:
s : = 0;
i := 1;
while i n do
begin
s : = s + a [i];
i := i + 1
end
i
Пусть sz означает У a [&], a s0 положим равным нулю. Тогда при
/? = 1
входе в цикл будем иметь
(I = 1) Л (5 = 0).
Воспользовавшись определением So, получим
S — 0 — So — Sj--] = Sj-j,
поэтому для оператора цикла можно написать такое пред-усло-
вие:
(I = 1) д (S = 5^).
После выполнения первого оператора присваивания внутри
цикла
S + All] =SP
Обозначим новое значение /, получающееся после выполнения
второго оператора присваивания в цикле, через Г, тогда
I' = I + 1,
Тело оператора с пред-условием будет теперь выглядеть так:
begin
{S =5,-3
s : = s + a [i];
{S = S,}
304
Гл. 10. Разработка программы
i : = i + 1
{S = Sw}
end
Следовательно, S = S7_f действительно будет инвариантом
цикла. По выходе из цикла, как мы знаем, выполняется/> Af,
а на предыдущем шаге цикла I N. Поэтому
I = N + 1,
S = = S^,
что нам и требовалось. Теперь можно доказать обязательность
остановки цикла, заметив, что / начинает изменяться с 1, при-
чем на каждом шаге увеличивается на 1. Выполнение цикла, сле-
довательно, закончится ровно через N шагов за конечное время.
Для верификации процедур и функций нам не нужны ника-
кие дополнительные соображения. Пред-условия для процедур
будут определяться ограничениями на значения их фактических
параметров. Хорошо написанные процедуры проверяют значения
своих параметров и имеют поэтому менее ограничительные пред-
условия. Пост-условия процедур выражают ожидаемый резуль-
тат выполнения процедуры. Эти условия вставляются в вызы-
вающую программу во всех местах, где есть обращения к данной
процедуре.
Когда процедура имеет побочный эффект, записать эти усло-
вия будет несколько сложнее. Это и есть одна из причин, по ко-
торым мы стараемся избежать использования или изменения
глобальных переменных в процедурах 1}. Больший интерес
представляют рекурсивные процедуры и функции, верификацию
которых мы обсудим сейчас на примере простой рекурсивной
функции. Функция f(n) =п\ может быть определена рекурсивно:
0! - 1
п! = п* (и — 1)! для п > 0.
Соответствующая функция для положительных аргументов может
быть описана так:
function f (n : integer) : integer;
begin
{N>0}
if n = 0
then
{N =0}
Если исходить из таких соображений, то не следует использовать даже
операторы присваивания. Это вовсе не шутка. О том, почему стремятся огра-
ничить побочные эффекты, мы уже упоминали.— Прим. ред.
10.2. Тестирование и верификация
305
f : = 1
{(N =0) Д (F(0) =1)}
else
{N > 0}
f : = n * f (n — 1)
{(N > 0) Д (F(N) = N * F(N — 1))}
{.(N = 0) A (F (0) = 1) V (N > 0) A (F(N) = N *
*F(N— 1))}
end;
Доказательства здесь довольно бесхитростные. Заметим для
начала, что мы должны доказать, правильно ли отработает вызов
f(n—1). Мы знаем, что N > 0 и, следовательно, N — 1^0 так,
что для функции /' допустим фактический параметр п — 1. Мы
доказали, что
f (0) = 1,
f (n) = n * f (n — 1) для n > 0.
Из определения n! следует, что
f (0) = 0!
и если f (n — 1) = (n — 1)!, то f (n) = n!
С помощью второго из этих двух соотношений для п — 1, 2, 3,...
можно доказать, что
f(l) =1!
f(2) = 2!
f (3) =3!
и, следовательно, по методу индукции,
f (п) = п!
Теперь для каждого типа оператора мы можем сформулиро-
вать общие правила вывода пост-условия из имеющегося пред-
условия. Для оператора присваивания V:=E пост-условие
получается с помощью замены V на Е в пред-условии. Для ус-
ловного оператора имеем
{р}
if b
then
{Р А Ь}
St
{qj
else
{Р A ~
S2
{q2}
Kv q2}
b}
306
Гл. 10. Разработка программы
Мы должны доказать, что выполнение оператора Sx с пред-усло-
вием р Д b дает пост-условие qY, а выполнение оператора S2 с
пред-условием р д ~ b дает пост-условие q2. Так как выполня-
ется один из операторов Sx или S2, то пост-условием для услов-
ного оператора будет qx \Jq2. Оператор варианта рассматривается
аналогично, на каждую метку варианта требуется отдельное
доказательство. Для циклов доказательство проводится по ин-
дукции.
Если мы имеем
{р}
while b do
i}
S
{i A ~ b}
то нам нужно будет доказать, что р необходимо влечет за собой
i и что s сохраняет истинность i. Для оператора цикла с пост-
условием
(р)
repeat {i}
S.; S2; . . . . S„
until q
{i A q)
мы должны доказать, что из р следует i и что последователь-
ность операторов S2;. . ., S/z не изменяет истинности i. Мы
уже видели, как для оператора цикла с параметром можно по-
строить оператор цикла с пред-условием.
Для доказательства окончания часто оказывается полезным
найти функцию, обладающую следующими свойствами:
Первоначально f = f0 > 0.
На каждом шаге итерации f уменьшается по крайней мере
на 1.
Процесс заканчивается, когда f <1 0.
Если удалось найти такую функцию, то легко доказать, что цикл
будет выполняться не более чем/0 раз. В приведенном выше приме-
ре мы могли бы выбрать f = N— 1. В важности выбора подходя-
щей функции можно легко убедиться на следующем примере:
var
слагаемое, сумма : real;
begin
слагаемое : = 1;
сумма : = 0;
repeat
10.3. Отладка
307
слагаемое : == слагаемое / 2;
сумма := сумма + слагаемое
until сумма 1
end
Предположим, что на нашей вычислительной машине точ-
ность вычислений не ограничена. Тогда на каждом шаге итерации
к переменной сумма будет прибавляться некоторая конечная ве-
личина, и, следовательно, условие окончания цикла никогда не
будет удовлетворено. На реальной машине, либо слагаемое через
некоторое время станет равным нулю, и программа будет выпол-
няться до тех пор, пока ее не прервет операционная система,
либо при выполнении очередной операции деления произойдет
переполнение, что опять-таки заставит операционную систему
прекратить выполнение этой программы.
В этом разделе нам удалось лишь едва коснуться проблемы
верификации программ. Мы не рассматривали трудности верифи-
кации программ, работающих с вещественными переменными,
производящих присваивания элементам массивов, использую-
щих ссылки, или операторы перехода. В самом деле, все фрагмен-
ты программ, верификация которых была здесь проведена, до-
вольно тривиальны.
Верификация нетривиальных программ — дело трудное и
утомительное; большие программы, возможно, вообще не будут
верифицированы до тех пор, пока не появятся методы автомати-
ческой верификации. Однако несколько поучительных уроков
все же можно извлечь.
Если вы самостоятельно попробуете верифицировать несколь-
ко программ, то увидите, что доказательство правильности про-
грамм помогает значительно расширить представление о проб-
лемах разработки программ. Если при написании программы вы
будете принимать во внимание вопросы, связанные с ее дальней-
шей верификацией, ваш стиль программирования заметно изме-
нится к лучшему. Введение в программу в качестве коммента-
риев важнейших пред- и пост-условий сделает программу более
удобной для чтения.
Наконец, старайтесь вместо «самодельных» методов использо-
вать при решении стандартных задач программирования надеж-
ные алгоритмы, правильность которых так или иначе уже доказа-
на.
10.3. Отладка
Целью структурного программирования, метода разработки
сверху-вниз, верификации программ является производство безо-
шибочных программ. Следовательно, разговор об «отладке» как
308
Гл. 10. Разработка программы.
будто становится лишь данью вежливости. К сожалению, от того
что об ошибках не говорят, они не исчезают. Самый сознатель-
ный «структурный» программист, тщательно проверяющий каж-
дую строчку, все же может вместо «+» написать «—» или вместо
sqrt написать sqr, внося тем самым в свою программу ошибку.
В этой книге мы часто вносили предложения по улучшению
читаемости программ. Например, мы рекомендовали наглядно
располагать операторы на строчках, определять все константы,
давать переменным смысловые имена, вводить явные поля при-
знаков, определяющие используемый вариант, избегать побочных
эффектов. Если вы будете следовать этим рекомендациям, то про-
граммы станут не только более удобными для чтения, в них
также будет меньше ошибок. Более того, если они будут рабо-
тать не так, как следует, отыскать в них ошибку будет легче.
Для отыскания ошибок не существует никаких общих мето-
дов — каждая ошибка уникальна. Для отладки нужны интуи-
ция, вдохновение и опыт. Здесь мы предложим несколько сове-
тов о том, как проводить отладку, некоторые из них — общего
характера, другие годятся только для отладки программ на Пас-
кале.
Если в вашей программе есть ошибки, исследуйте все улики.
«Улики» состоят из текста самой программы, входных данных,
введенных в нее, выходных данных, полученных в результате
работы программы, а также диагностических сообщений, выдан-
ных операционной системой в том случае, если ошибка в програм-
ме привела к ненормальному окончанию работы.
Чтобы найти ошибку, вы должны обратить внимание на все, что
хотя бы немного отличается от ожидаемых вами результатов.
Если этих улик недостаточно, можно попытаться создать допол-
нительные: вы можете вновь запустить программу с другими дан-
ными или вставить несколько операторов печати для проверки
промежуточных результатов и запустить ее с теми же данными.
Если вам кажется, что вы уже нашли ту часть программы,
которая работает неправильно, но в ней ничего обнаружить не
удалось, поищите где-нибудь еще. В частности, полезно просмот-
реть те участки программы, которые выполняются перед подозри-
тельным местом. Если вы не можете найти в программе ничего
неправильного, покажите ее своему товарищу. Однако не надо
объяснять ему, как программа должна работать, поскольку это
может помешать ему или ей увидеть ошибку п. Когда вы найдете
ошибку, убедитесь, что ею объясняются все странности поведе-
ния программы. Если это не так, надо искать другие ошибки.
Можно рекомендовать и обратное: обязательно начните рассказы-
вать! Очень часто во время объяснения вы обнаружите ошибку сами.—
Прим. ред.
10.4. Пример: Генератор перекрестных ссылок
309
В Паскале имеется несколько конструкций, которые часто
приводят к возникновению плохо обнаруживаемых ошибок.
Проверьте, все ли условные операторы построены правильно,
при этом пусть вас не вводит в заблуждение неправильная запись
оператора, например,
if р
then
if q
then St
else S2
его действие четко выражается при помощи такой записи:
if Р
then
if q
then Sx
else S2
Циклы с параметрами и пред-условиями могут вообще не выпол-
няться. Оператор S в конструкции
while р do
S
и
for i : = j to k do
S
не будет выполняться, если р равно false или соответственно j >k.
С другой стороны, тело оператора цикла с пост-условием всегда
выполняется по крайней мере один раз.
Проверяйте, перед всеми ли формальными параметрами-
переменными в определениях процедур проставлен описатель
var: его отсутствие может привести к очень коварным ошибкам!
Наконец, если вы пользуетесь записями с вариантами, проверяй-
те, чтобы все поля, к которым вы обращаетесь, были согласованы
со значениями полей дискриминантов.
10.4. Пример: Генератор перекрестных ссылок
Этот раздел будет посвящен разработке очень полезной- прог-
раммы, использующей некоторые из методов, описанных в настоя-
щей главе. Для разработки как алгоритма, так и структуры дан-
ных этой программы мы будем следовать методу разработки
сверху-вниз. Мы не будем пытаться доказать, что наша программа
правильна. Программа будет формировать перекрестные ссылки,
и ее описание таково:
310
Гл. 10. Разработка программы
Прочитать входной текст, содержащий до 999 строчек. На-
печатать список, состоящий из одной записи для каждого
отдельного слова текста. Каждая запись состоит из самого
слова, за которым следует список номеров строчек, на которых
данное имя встречалось. «Слово» есть последовательность букв.
Буквы, стоящие за двадцатой буквой слова, следует игнориро-
вать. Слова располагаются в списке в алфавитном порядке.
Если на некоторой строчке слово встречается дважды, номер
строчки должен попасть в список только один раз. Выходной
текст должен быть разбит на страницы размером 60 стро-
чек, причем ни в одной строчке не должно быть более 80 сим-
волов.
Эта программа во многом похожа на программу частотный словарь
из гл. 6, 8, но с каждым словом в этой программе связано боль-
ше информации. Сначала мы «разделяем и властвуем». Программа
естественным образом разбивается на две части: построение таб-
лицы слов и ее печать. Альтернативное решение (немного прочи-
тать, немного отпечатать, начать сначала) не подходит из-за того,
что перед началом печати нам надо прочитать весь текст.
Будем предполагать, что памяти у нас достаточно, чтобы хра-
нить всю необходимую информацию. Это предположение вполне
разумно ввиду того, что текст не может быть длиннее 999 строчек.
Нам нужно работать с данными такой структуры, в которую мы
сможем вставлять вновь появляющиеся слова и из которой мы
сможем извлекать слова для печати в алфавитном порядке. Под-
ходящим будет двоичное дерево поиска. Теперь можно написать
первый, наиболее абстрактный вариант программы.
program перекрестныессылки (input, output);
type
ссылканадерево = |узел;
узел =
record
запись : типзаписи;
левое, правое : ссылканадерево
end;
var
деревослов : ссылканадерево;
begin
деревослов : = nil;
построитьдерево(деревосл ов);
напечататьдерево(деревослов)
end.
Пока что эта программа как будто бы правильна. Выполнить
ее, однако, мы не сможем, поскольку не определены объекты тип-
записи, построитьдерево и напечататьдерево.
10.4. Пример: Генератор перекрестных ссылок
311
Для конкретизации процедуры построитьдерево воспользу-
емся двумя принципами. Во-первых, она содержит цикл: на каж-
дом шаге мы обрабатываем по одному слову текста. Это дает га-
рантию, что процесс закончится через конечное число шагов.
Второй принцип — это опять «разделяй и властвуй»: во-первых,
взять слово, во-вторых, вставить его в дерево.
procedure построитьдерево (var дерево : ссылканадерево);
var
текслово : типслова;
текстрока : счетчик;
begin
текстрока : = 1;
while not eof do
begin
взятьслово(текслово, текстрока);
if not eof
then
вставитьвдерево(дерево, текслово, текстрока)
end {while}
end;
В этой процедуре представлены четыре новых неопределен-
ных объекта: типы типслова и счетчик, а также процедуры взять-
слово и вставитьдерево. Новые типы мы можем определить без
особых трудностей.
const
всегострок = 999;
длинаслова = 20;
type
счетчик = 1 . . всегострок;
индекс = 1. . длинаслова;
типслова = packed array [индекс] of char;
Установив это, мы можем продолжить конкретизацию процедуры
взятьслово, которая основана на похожих процедурах, прояв-
лявшихся в этой книге ранее.
procedure взятьслово (var слово : типслова;
var строка : счетчик);
var
тексимвол : char;
индекс, индекспробелов : 0 . . длинаслова;
begin
тексимвол := пробел;
while not (eof or (тексимвол in буквы)) do
взятьсимвол(тексимвол, строка);
312
Гл. 10. Разработка программы
if not eof
then
begin
индекс : = 0;
while тексимвол in буквы do
begin
if индекс < длинаслова
then
begin ‘
индекс : = индекс + 1;
слово [индекс] : = тексимвол
end; ।
if eof j
then тексимвол : = пробел •
else взятьсимвол(тексимвол, строка) i
end; {while} I
if индекс < длинаслова j
then I
for индекспробелов : = индекс + 1 |
to длиннаслова do f
слово [индекспробелов] : = пробел ’
end
end; <
Процедура взятьсимвол, вызываемая процедурой взятьслово,
вводит из входного файла очередной символ и, если надо, уве-
личивает значение счетчика строчек. Процедура взятьсимвол
разрабатывается по принципу анализа вариантов. Рассматри-
ваются варианты: конец файла, конец строчки, а также когда не
имеет место ни то, ни другое. Процедура взятьсимвол вставляет 1
между строчками пробел, чтобы избежать присоединения пер- <
вого слова очередной строчки к последнему слову предыдущей J
строчки. Обе процедуры, и взятьслово, и взятьсимвол, могут окан- <
чиваться с признаком eof = true. |
procedure взятьсимвол (var символ : char; |
var стр : счетчик);
begin ;
if eof
then символ : = пробел
else if eoln
then
begin
символ := пробел; ;
стр : = стр + 1;
readln
end
10.4. Пример: Генератор перекрестных ссылок
313
else геаб(символ)
end;
Мы продолжим конкретизацию процедуры построитьдерево
и займемся вызовом
вставитьдерево (дерево, текслово, текстрока)
Сначала надо более подробно рассмотреть тип записи, исполь-
зуемой для хранения информации о словах текста. В запись надо
иметь возможность вставлять не только само слово, но также и
несколько номеров строчек. Примем во внимание, что на фазе пе-
чати дерева нам необходимо будет печатать номера строк в возрас-
тающем порядке. Но именно в таком порядке они и появляются
в программе. Следовательно, данные должны быть организованы
по типу «первым вошел — первым вышел», для чего подходит
структура очереди. Как мы уже видели в гл. 8, очередь может
быть реализована в виде списка со ссылкой на его начало и еще
одной ссылкой — на конец. Следовательно, можно написать:
type
типзаписи =
record
значениеслова : типслова;
первыйвочереди, последнийвочереди :
ссыл кан аочередь
end;
Каждый из элементов очереди содержит номер строчки и ссылку
на следующий элемент очереди:
type ;
ссыл кан аочередь = |элементочереди;
элементочереди =
record
номерстроки : счетчик;
следующийвочереди : ссылканаочередь
end;
На этом месте лучше остановиться и дать полную сводку всех
констант и типов, определенных на данный момент.
const
всегострок = 999;
длинаслова = 20;
type
счетчик = 1 . . всегострок;
индекс == 1. . .длинаслова;
типслова = packed array [индекс] of
ссылканаочередь = ^элементочереди;
314
Гл. 10. Разработка программы
элементочереди =
record
номерстроки : счетчик;
следующийвочереди : ссылканаочередь
end;
типзаписи =
record
значениеслова : типслова;
первыйвочереди,последнийвочереди :
ссылканаочередь
end;
ссылканадерево = !узел;
узел =
record
запись : типзаписи;
левое, правое : ссылканадерево
end;
Процедура вставитьвдерево разрабатывается с учетом различ-
ных вариантов. Одним из ее параметров является ссылка на слово
дерева или на одно из поддеревьев. Если эта ссылка равна nil,
мы создаем новую запись и устанавливаем на нее ссылку. В про-
тивном случае мы сравниваем слово, записанное в узле дерева,
со словом, которое только что было прочитано. Если они совпа-
дают, все, что нужно сделать, это добавить к списку текущее зна-
чение счетчика строчек, если только его в этом списке еще нет;
если же он там есть, на него должна указывать ссылка последний-
вочереди. В противном случае мы организуем поиск по дереву,
либо по левому поддереву этого узла, либо по правому, рекурсив-
но вызывая процедуру вставитьвдерево. Это традиционный алго-
ритм вставок в двоичное дерево поиска.
procedure вставитьвдерево (var поддерево: ссылканадерево;
слово : типслова;
строка : счетчик);
begin
if поддерево = nil
then создать новую запись
else if слово = поддерево!.запись.значениеслова
then добавить данный номер строки к списку
else it слово < поддерево!.запись.значениеслова
then вставитьвдерево(поддерево f .левое,
слово, строка)
else вставитьвдерево(поддерево f .правое,
- слово, строка)
end;
10.4. Пример: Генератор перекрестных ссылок
315
Действия создать новую запись и добавить номер строки к
списку можно конкретизировать при помощи стандартных алго-
ритмов.
{создать новую запись}
begin
пе\\’(поддерево);
with поддерево] do
begin
левое : = nil;
правое : = nil;
with запись do
begin
значениеслова : = слово;
пеху(первыйвочереди);
последнийвочереди : = первыйвочереди;
with первыйвочереди ! do
begin
номерстроки : = строка;
следующийвочереди : = nil
end {with первыйвочереди!}
end {with запись}
end {with поддерево}
end
{добавить новый элемент списка}
with поддерево!, запись do
if последнийвочереди!.номерстроки =/= строка
then
begin
new(cлeдy ющийэл емент);
with следующийэлемент! do
begin
номерстроки : == строка;
следующийвочереди : = nil
end; {with следующийэлемент!}
последнийвочереди f .следующийвочереди : =
следующийэлемент;
последнийвочереди : = следующийэлемент
end
else {слово встретилось в той же строке}
Тем самым завершается разработка фазы ввода, и мы можем
вернуться к подробной разработке процедуры
напечататьдерево (деревослов)
Так как мы будем печатать записи из двоичного дерева поиска,
то следует ожидать, что для печати записей и поддеревьев неко-
316
Гл. 10. Разработка программы
торого узла нам придется просматривать дерево. Полезно будет
несколько заглянуть вперед и представить себе, как можно орга-
низовать выходную информацию. Деление на страницы можно
осуществлять с помощью процедуры печататьстроку, которая
будет следить за текущей позицией строки на странице. На этой
стадии мы можем решить, сколько номеров строк можно поме-
стить на одну строку.
Слово может занимать до 20 позиций. Так как максималь-
ная длина строчки составляет 80 символов, для номеров строк
остается 60 символов. Наибольший номер строки равен 999, т. е.
состоит из трех цифр, поэтому, если для каждого номера выделять
пять позиций на строке, между соседними номерами будут по
крайней мере два пробела. Отсюда следует, что на одной строке
мы можем печатать до 12 номеров строк. Программа сама может
выполнить эти вычисления, и будет лучше, если она их сделает.
В соответствии с этим определим константы и ограниченные типы:
const
максимальнаядлинастроки = 80;
максимальныйразмерстраницы = 60;
числоинтервалов = 2;
основание = 10;
type
индексстраниц = 1. . максимальныйразмерстраницы;
индексстрок = 1. . максимальнаядлинастроки;
Запишем первую версию процедуры напечататьдерево;
procedure напечататьдерево(дерево : ссылканадерево);
var
положениенастранице : индексстраниц;
длиначисла, длинастроки : индексстрок;
begin
длиначисла := 1гипс(1п(всегострок)/1п(основание))
+ 1 + числоинтервалов;
длинастроки : ^(максимальнаядлинастроки —
длинаслова) div длиначисла;
положениенастранице := максимальныйразмерстраницы;
напечататьстроку(дерево, положениенастранице)
end;
Величины длиначисла и длинастроки нет нужды передавать
процедуре печататьзапись в качестве параметров. Так как их
значения постоянны, достаточно просто обеспечить, чтобы печать
записи производилась в нужном диапазоне. Процедура печатать-
запись выполняет просмотр дерева слов по следующей схеме:
напечатать узлы из левого поддерева;
10.4. Пример: Генератор перекрестных ссылок
317
напечатать текущий узел\
напечатать узлы из правого поддерева.
Печать записи состоит в печати слова, входящего в нее, а также
в просмотре элементов, содержащих номера строк. Процедура
печататьстроку управляет переходом со страницы на страницу,
а константа интервалзаписей определяет число пустых строк
между соседними словами.
{Генератор перекрестных ссылок
Автор программы: Питер Грогоно
Программа написана: 24 сентября 1977 года
Последнее исправление внесено: 30 сентября 1977 года
Цель:
Генерация списка перекрестных ссылок произвольного
текстового файла.
Описание работы программы:
Программа читает текст из файла 'input' и передает в файл 'out-
put' список перекрестных ссылок. Этот список состоит из одной
записи на каждое отдельное слово, встреченное во входном тек-
сте. Слово представляет собой строку букв. В запись о слове
включаются как само слово, так и номера всех строк, в которых
оно встретилось в тексте. Ограничение, накладываемые про-
граммой, см. в разделе определения констант. }
program перекрестныессылки
({читать текст из файла} input,
{писать список в файл} output);
const
всегострок = 999; {максимальный размер списка}
длинаслова = 20; {максимальное слово,
читаемое без укорачивания}
максимальнаядлинастроки = 80; {длинастроки выходного текста}
максимальныйразмерстраницы=60; {размер выходной
страницы}
размерзаголовка=3; {число строк для заголовка}
интервалзаписей= Г, {число пустых строк между записями}
числоинтервалов=2; {число пробелов между
номерами строк}
основание = 10;
пробел = ' ';
сима='а';
симя='я';
заголовок^'список перекрестных ссылок';
type
счетчик=1 .. всегострок;
индекс=1 .. длинаслова;
индексстраниц=1 .. максимальныйразмерстраницы;
индексстрок=1 .. максимальнаядлинастроки;
типслова=раске<1 array [индекс] of char;
318
Гл. 10. Разработка программы
ссылканаочередь= f элементочереди;
элементочереди=
record
номерстроки : счетчик;
следующийвочереди : ссылканаочередь
end;
типзаписи =
record
значениеслова : типслова;
первыйвочереди, последнийвочереди : ссылканаочередь
end;
ссылканадерево= fузел;
узел==
record
запись : типзаписи;
левое, правое : ссылканадерево
end
var
деревослов : ссылканадерево;
буквы : set of char:
procedure построитьдерево (var дерево : ссылканадерево);
var
текслово : типслова;
текстрока : счетчик;
procedure взятьслово (var слово : типслова;
var строка : счетчик);
var
тексимвол : char;
индекс, индекспробелов : 0 .. длинаслова;
procedure взятьсимвол (var символ : char;
var стр : счетчик);
begin {взятьсимвол}
if eol
then символ := пробел
else if eoln
then
begin
символ := пробел;
стр := стр 1;
readin
end
else геаб(симвЪл)
end; {взятьсимвол}
begin {взятьслово}
тексимвол := пробел;
while not (eof or (тексимвол in буквы)) do
взятьсимвол (тексимвол,строка);
if not eof
then
begin
индекс := 0;
10.4. Пример: Генератор перекрестных ссылок
319
while тексимвол in буквы do
begin
if индекс < длинаслова
then
begin
индекс ~ индекс + 1;
слово[индекс] := тексимвол
end;
if eof
then тексимвол := пробел
else взятьсимвол(тексимвол,строка)
end; {while}
if индекс<длинаслова
then
for индекспробелов := индекс + 1
to длинаслова do
слово[индекспробелов] := пробел
end
end; {взятьслово}
procedure вставитьвдерево (var поддерево : ссылканадерево;
слово : типслова;
строка : счетчик);
var
следующийэлёмент : ссылканаочередь;
begin {вставитьвдерево}
if поддерево=пП
then
begin {создать новую запись}
new (поддерево)
with поддерево! do
begin
левое := nil;
правое := nil;
with запись do
begin
значениеслова := слово,
new(nepBbiftBO4epeAH);
последнийвочереди
первыйвочереди;
with первыйвочереди! do
begin
номер стр оки := строка;
следующийвочереди := nil
end {with первыйвочереди!}
end {with запись}
end {with поддерево}
end
else {добавить новый элемент списка}
with поддерево ! , запись do
if слово = значениеслова
then
begin
if последнийвочереди !.номерстроки
=#= строка
then
begin
320
Гл. 10. Разработка программы
new (следующийэлемент);
with следующий элемент f do
begin
номерстроки := строка;
следу ющийвочереди
:= nil
end {следующийэлемент^}
последнийвочереди f.
следующийвочереди :=
следующийэлемент;
последнийвочереди :=
следующийэлемент
end
end
else if слово<значениеслова
then вставитьвдерево(левое,слово,строка)
else вставитьвдерево(правое,слово,строка)
end; {вставитьвдерево}
begin {построитьдерево}
текстрока := 1;
while not eof do
begin
взятьслово(текслово, текстрока);
if not eof
then вставитьвдерево(дерево,текслово,текстрока)
end {while}
end; {построитьдерево}
procedure напечататьдерево (дерево : ссылканадерево);
var
положениенастранице : индексстраниц;
длиначисла, длинастроки : индексстрок;
procedure печататьзапись (поддерево : ссылканадерево;
var положение : индексстраниц);
var
инд : индекс;
счетчикэлементов : 0 .. максимальнаядлинастроки;
ссылканаэлемент : ссылканаочередь;
procedure печататьстроку (var текпозиция : индексстраниц;
новыестроки : индексстраниц);
var
счетчикстрок : индексстраниц;
begin {печататьстроку}
if текпозиция + новыестроки <
максимальныйразмерстраницы
then
begin
for счетчикстрок := 1 to новыестроки do
writein;
текпозиция := текпозиция + новыестроки
end
else
begin
page(output);
10.4. Пример: Генератор перекрестных ссылок 321
\\гЛе1п(заголовок);
for счетчик 1 to размерзаголовка — 1 do
write In;
текпозиция размерзаголовка + 1
end
end; {печататьстроку}
begin {печататьзапись}
if поддерево #= nil
then
with поддерево fdo
begin
печататьзапись(левое, положен не);
печататьстроку(положение,интсрвалзаписей +1);
with запись do
begin
for инд := 1 to длинаслова do
write^HaHeHHeciOBahiru]);
счетчикэлементов 0;
ссылканаэлемент первыйвочереди;
while ссылканаэлемент nil do
begin
счетчикэлементов: =
счетчикэлементов + 1;
if счетчикэлементов >
длинастроки
then
begin
печататьстроку
(положение, 1);
write(пробел :
длинастроки);
счетчикэлементов
:= 1
end;
итИе(ссылканаэлемент|.
номерстроки :
длиначисла);
ссылканаэлемент :=
ссылканаэлемент!.
следующийвочереди
end {while}
end; {with запись}
печататьзапись(правое,положение)
end {with поддерево f }
end; {печататьзапись}
begin {напечататьдерево}
длиначисла := 1гипс(1п(всегострок)/1п(основание))
+ 1 + числоинтервалов;
длинастроки := (максимальнаядлинастроки — длинаслова)
div длиначисла;
положениенастранице максимальныйразмерстраницы;
печататьзапись(дерево,положениенастранице)
end; {напечататьдерево}
322
Гл. 10. Разработка программы
begin {перекрестныессылки}
буквы := ['а' .. 'я'];
деревослов := nil;
построитьдерево(деревослов);
напечататьдерево(деревослов)
end. {перекрестныессылки}
10.5. Оценка языка Паскаль
Язык, которым пользуется человек, оказывает глубокое влия-
ние на стиль его мышления. И хотя вдохновение, например, не
является результатом «словесного» мышления, все же большая
часть процессов, протекающих в нашем сознании, лингвистична
по своей природе. При переводе с одного языка на другой тонкие
оттенки часто оказываются потерянными. Это же оказывается
верным и для языков программирования: мы думаем о путях
решения задачи в терминах привычного языка программирования.
Задача покажется тем легче, чем проще ее программировать на
этом языке. Профессиональные программисты обычно владеют
несколькими языками программирования, и, сталкиваясь с ка-
кой-либо задачей, они уже на ранней стадии ее решения выби-
рают наиболее подходящий для этого язык 1}.
В этой книге мы говорили исключительно о языке Паскаль.
В качестве примеров мы выбирали задачи, для которых весьма
подходит именно этот язык, те же задачи, для решения которых
он не очень подходит, мы не рассматривали. Теперь попробуем
взглянуть на язык Паскаль более объективно, обращая внимание
как на его достоинства, так и недостатки.
Важнейшим свойством Паскаля является относительно не-
большой набор базовых конструкций. Тем не менее он отлича-
ется значительной мощностью, поскольку эти конструкции можно
комбинировать самыми различными способами. В частности, как
алгоритмы, так и структуры данных можно строить иерархически.
Подобная природа языка является его наиболее ценным качест-
вом, благодаря которому этот язык выигрывает по крайней мере
в четырех отношениях.
Во-первых, с Паскаля относительно просто компилировать, и
программы, скомпилированные с Паскаля, эффективны для боль-
шинства вычислительных машин. Во-вторых, правильно написан-
ные программы на Паскале легко читать и понимать. В-третьих,
рекурсивная природа конструкций Паскаля позволяет просто и
Возможно, именно этот факт заставляет задуматься о природе мышле-
ния программиста-профессионала. Возможно, что язык программирования и
не играет особой роли в процессе нашего мышления (если вообще играет ка-
кую-либо роль). Этим мы, конечно, не пытаемся поставить под сомнение линг-
вистическую природу нашего сознания, а только еще раз подчеркиваем, что
языки программирования суть языки кодирования.— Прим, ред»
10.5« Оценка языка Паскаль
323
естественно применять при разработке программ метод разработ-
ки сверху-вниз. В-четвертых, за небольшим исключением, управ-
ляющие конструкции языка Паскаль достаточно легко верифици-
ровать, поскольку семантика их проста.
С другой стороны, базовых конструкций в языке Паскаль не
так уж и мало. Существует целый ряд языков программирования,
где их еще меньше, но, хотя любой алгоритм, который можно
запрограммировать на Паскале, можно запрограммировать и на
этих языках, получающаяся при этом программа может оказаться
весьма невразумительной. В языке Паскаль был достигнут
компромисс: он достаточно широк, чтобы кратко выражать все
основные алгоритмы, но при этом в нем нет никаких излишеств.
Критики этого языка обычно выдвигают три аргумента.
Первый — это утверждение о том, что в Паскале недостаточно
развиты средства работы с файлами. В языке допускается авто-
матическое преобразование данных для текстовых файлов, но ча-
ще всего им невозможно пользоваться в реальной программе, по-
скольку для ошибочных ситуаций не предусмотрено никаких
разумных реакций.
Хотя Паскаль — это язык, наиболее подходящий для обу-
чения программированию, чрезвычайно важные вопросы, касаю-
щиеся работы с файлами произвольного доступа, не нашли в
нем никакого отражения. Второй недостаток языка связан с мас-
сивами: размер массива в языке Паскаль определяется на стадии
компиляции. Из-за отсутствия в языке динамических массивов
очень трудно оказывается написать полезную программу числен-
ного анализа и обработки строк символов. Третий аргумент крити-
ков языка связан с отсутствием конструкции, позволяющей про-
извольно прерывать выполнение цикла. Очень часто возникают
ситуации, когда необходимо выйти из цикла, не дожидаясь его
окончания, а единственный способ сделать это в языке Паскаль —
это вводить лишние булевские переменные или воспользоваться
оператором перехода.
Наконец, стремление достичь эффективности привело к сниже-
нию мобильности программ, написанных на Паскале. Разработ-
чик языка программирования определяет стандарт, которому
нужно следовать при любой его реализации. Этот стандарт должен
обеспечить переносимость любой программы, написанной на этом
языке. (Мобильной или переносимой называется программа, ко-
торую без каких-либо изменений можно выполнять на различных
вычислительных машинах.)
Мы проиллюстрируем отношения между стандартизацией и
эффективностью на следующем простом примере. При разработке
языков программирования представляется вполне разумным тре-
бовать наличия в языке средств, позволяющих производить вы-
числения с вещественными числами, поскольку на большинстве
11*
324 Гл, 10. Разработка программы
машин имеются или аппаратные, или программные средства, обес-
печивающие подобные вычисления. Однако было бы весьма нера-
зумно настаивать на том, чтобы результаты вещественных вычис-
лений имели точность в десять десятичных цифр (не больше и не
меньше), так как почти на всех вычислительных машинах это при-
вело бы к резкому снижению эффективности программ.
Для языка Паскаль стандарты кажутся слишком снисходи-
тельными. Тотфакт, чтов Паскале не определено, например, пред-
ставление символов, значительно затрудняет написание про-
грамм, независимых от конкретного набора символов, имеющего-
ся на той или иной вычислительной машине. Наиболее полезной
структурой при составлении программ, не зависящих от пред-
ставления символов, является множество символов (set of char),
но такая структура запрещена во многих реализациях языка!
Однако недостатки языка Паскаль кажутся не очень значи-
тельными, если сопоставить их с преимуществами, которые имеет
Паскаль над другими современными языками. Паскаль уверенно
сохраняет место среди немногих языков программирования, до-
стигших широкого распространения и 'завоевавших всемирную
популярность. В печатных работах, посвященных программиро-
ванию, все чаще применяется нотация Паскаля; Паскаль все
больше и больше используется как в академических исследова-
ниях, так и в производственном программировании; существует
множество новых языков, обязанных своим появлением Паскалю;
каждый год все больше и больше учебных заведений выбирают
именно Паскаль в качестве первого языка, с которым они знако-
мят своих студентов.
Паскалю уже больше десяти лет, и весьма возможно, что в
своем теперешнем виде язык не просуществует много лет. Нет,
однако, сомнения в том, что основным принципам, заложенным в
этом языке, будут еще много лет следовать многие разработчики
языков программирования. Несомненно и то, что в настоящее
время знание языка Паскаль совершенно необходимо всем, кто
хочет быть профессиональным программистом.
Упражнения
10.1. Если вы знакомы с каким-либо другим языком высокого уровня,
сравните его с Паскалем. Среди прочих рассмотрите такие вопросы:
(а) константы, типы и описания переменных;
(б) область действия описаний переменных;
(в) динамические структуры данных,
(г) ввод и вывод.
Напишите программы на Паскале и на другом языке, который вы знаете,
и сравните их по
(д) легкости программирования;
(е) легкости отладки;
(ж) легкости верификации;
(з) времени компиляции и выполнения;
Упражнения
325
(и) памяти, требуемой для рабочей программы.
10.2. Выберите программу на Паскале либо из этой книги, либо еще откуда-
нибудь, и попробуйте ее верифицировать.
10.3. Ниже приведена процедура, выполняющая двоичный поиск по упо-
рядоченному массиву век. Найдите инвариант для содержащегося в процедуре
оператора цикла.
type
вектор = array [минимум .. максимум] of real;
procedure двоичныйпоиск (век : вектор;
ключ : real;
var найдено : boolean;
var середина : integer);
var
низ, верх : integer;
begin
{век[минимум]<ключ<век[максимум] Л век упорядочен}
низ := минимум;
верх := максимум;
repeat
середина := (низ + верх) div 2;
if ключ>век[середина]
then низ := середина + 1
else верх := середина — 1
until (век[середина]=ключ) or (низ>верх);
найдено := низ<верх
end;
10.4. Программа перекрестныессылки используется для формирования
списка перекрестных ссылок программ, написанных на Паскале. Однако, для
того чтобы эта программа была по-настоящему полезна, она должна читать
не слова, а идентификаторы, пропускать зарезервированные слова, игнори-
ровать комментарии. Модифицируйте ее соответствующим образом.
10.5. Напишите нерекурсивную программу, формирующую список опе-
раций, которые необходимо выполнить, чтобы решить задачу Ханойских ба-
шен (разд.4.3). Подробно опишите все сделанные вами дополнения программы.
10.6. Напишите программу, которая производила бы ввод синтаксически
правильной программы на Паскале в произвольном формате и печатала бы
текст этой программы в соответствии с тем форматом, который приведен в
приложении Г.
10.7. Усовершенствуйте программу калькулятор из гл. 4 таким образом,
чтобы она стала действительно полезным инструментом. В частности, внесите
модификации, которые позволят производить:
(а) вычисление функций (т. е. синуса, косинуса, квадратного корня, экспо-
ненты, натурального логарифма);
(б) преобразование оснований (ввести возможность вычислений над числами
двоичными, восьмеричными и т. д.);
(в) управление форматом вывода;
(г) вычисления с комплексными числами. Не следует накладывать никаких
«заплаток» на исходную программу. Напишите полную спецификацию
вашего калькулятора, а затем проведите его реализацию Методом сверху-
вниз.
10.8. Проблема размена суммы денег (см. упр. 3.6.) становится более инте-
ресной, если ограничить число имеющихся монет. Предположим, например, что
у вас есть монета в 15 копеек и четыре десятикопеечные монеты, а вам надо за-
платить ровно 40 копеек. Простой алгоритм, выбирающий сначала монеты на-
ибольшего достоинства, не срабатывает. Напишите программу, которая бы
разменивала заданные суммы денег, если это возможно, с помощью заданного
числа монет.
ЛИТЕРАТУРА ДЛЯ ДАЛЬНЕЙШЕГО ЧТЕНИЯ
Книги о Паскале
Наиболее важной книгой для всех программирующих на
Паскале является Паскаль. Справочное руководство и сообщение
(Pascal User Manual and Report) Енсен и Вирта [50]. Вирт напи-
сал еще две книги, использующие способ записи, похожий на
Паскаль, но в целом для обучения языку не предназначенные:
Систематическое программирование—Введение [99] и «Алгоритмы
+ Структуры данных — Программы [103]. Многие из принци-
пов, на которых основан Паскаль, могут быть найдены в класси-
ческой книге Структурное Программирование, авторами кото-
рой являются Дал, Дейкстра и Хоор [27].
Книги по теории
Для хорошего программирования теоретические познания
требуются не меньше, чем свободное владение языком программи-
рования. Кнут [56, 57] приводит подробное и полное описание
теории алгоритмов и структур данных. Баас [11] описывает тео-
рию и разработку алгоритмов; а Ахо, Хопкрофт и Ульман [3]
предложили еще более развернутое рассмотрение данного вопро-
са. В этих книгах алгоритмы анализируются в терминах их пра-
вильности, времени выполнения и требований на рабочую па-
мять. Другим важным аспектом разработки алгоритмов является
анализ точности, которая может быть достигнута при вычисле-
ниях с плавающей точкой. Если вас интересует численный ана-
лиз, вы можете обратиться к книгам Далквиста и Бьорка [28],
Джонсона и Риса [51], Кнута [56, 57] или Стербенца [86].
Программа окружности в гл. 6 основана на примере, приве-
денном Бертвистлом и др. [15] в их книге о языке программиро-
вания SIMULA. SIMULA — это элегантно построенный язык,
опередивший свое время; он был разработан в середине шести-
десятых годов, но важность его средств, позволяющих работать
с абстрактными данными и сопрограммами, не была признана в те-
чение более чем десяти последующих лет.
Программа обработатьфайл из гл. 7 основана на примере,
приведенном Дейкстрой [34] (алгоритм принадлежал Фейену)
Литература для дальнейшего чтения
327
в книге, в которой, кроме демонстрации формального подхода к
разработке программ, были приведены некоторые весьма интерес-
ные алгоритмы.
Статьи о Паскале
Постепенное признание языка Паскаль в среде программистов
достаточно хорошо отражено в научной литературе. Первое офи-
циальное описание языка появилось в журнале Акта Информа-
тика [Вирт, 95]. Паскаль был подвергнут критическому разбору
Хаберманом [42]; ответ на его критику был дан Лекармом и Дежар-
динсом [64]. Важнейшим положением, дискутировавшимся во
многих статьях, является понятие типа в языке Паскаль. Уэлш,
Снирингер и Хоор [94] осветили вопросы, связанные с этим поня-
тием, в очень важной работе. Вирт [102] также опубликовал свою
собственную оценку Паскаля. Среди других критических работ,
посвященных Паскалю, можно упомянуть работы Конради [25]
и Эдвардса [36], чья статья представляет собой скорее личные
нападки, чем серьезный критический разбор.
Стандарт языка Паскаль
Полное название документа, часто упоминаемого в этой книге
как предлагаемый стандарт языка Паскаль,— это Рабочий Доку-
мент/3, выпущенный. Британской Рабочей Группой по Стандар-
там DSP/13/14. Этот документ был опубликован Эддиманом и
др. [1, 2].
Реализация языка Паскаль
Первоначально Паскаль был реализован на машинах серии
CDC 6600; методика разработки компилятора была описана Ам-
маном [5, 6, 7] и Виртом [97]. В середине 70-х г. Паскаль прив-
лек к себе значительное внимание, что привело к его реализации
на многих других машинах. Различные реализации были описаны
Броном и де Врие [19], Дежардинсом [30], Фейересеном [37],
Гроссе-Линдеманном и Нагелем [41], Хансеном и др. [43], Рудми-
ком [82], Тиболтом и Мануэлем [87], а также Уэлшем и Куинном
[93].
Чанг и Юн [1978] показали, каким образом можно реализо-
вать язык на микромашине, используя в качестве отправной точ-
ки компилятор с языка Бейсик. Бэйтс и Кайе [12], а также Берри
113] описали свой опыт работы с P-компилятором с языка Пас-
каль. Это машинно-независимый компилятор, формирующий про-
граммы, называемые P-программами, для гипотетической стеко-
вой машины. Другие аспекты P-программ обсуждались Нельсо-
ном [75], Перкинсом и Сайтесом [78, 85]. Большинство компиля-
торов с Паскаля отводят для работы с динамическими перемен-
ными кучу, размещая статические переменные в стеке; Марлин
328
Литература для дальнейшего чтения
[68] показал, что возможны реализации с использованием кучи
для всех переменных.
Расширения Паскаля
Было много попыток расширить Паскаль. К наиболее интерес-
ным работам принадлежат статьи Бидла [141, Иглецки и др. [49],
Китлица [54], а также Леблана [61, 62]. Попытки введения в
язык динамических массивов специально обсуждались в работах
Кондикта [23], Конради [25], Китлица [55], Мак-Леннана [70],
Покровского [81] и Вирта [104].
Единственный диалект языка, получивший широкое распро-
странение, Параллельный Паскаль, является расширением языка,
допускающего программирование параллельных процессов. Этот
диалект был разработан Бринч-Хансеном [17, 18]. Компилятор с
Параллельного Паскаля для машины PDP 11 был создан Харт-
маном [44].
GYPSY [Амблер и др., 4], MESA [Гешке и др., 38], Euclid
[Лэмпсон и др., 60], CLU [Лисков и др., 67] и TELOS [Травис и
др., 88] — это совсем новые языки программирования, которые
разрабатывались под сильным влиянием Паскаля.
Вспомогательное программное обеспечение
Мастер только тогда работает по-настоящему эффективно,
когда у него под рукой имеются все необходимые инструменты.
Программные инструменты, или программное обеспечение,—
это программы, предназначенные для того, чтобы помочь програм-
мистам сделать их работу более производительной. Кернигхен и
Плогер [53] описали весьма полезный набор программ, написан-
ных на языке RATFOR (RATional FORtran — Рациональный
Фортран).
Растет библиотека стандартных программ, написанных на
Паскале специально с целью помочь в разработке других программ
на Паскале. Кондикт и др. [24] и Гуерас и Ледгард [47, 48] опуб-
ликовали сведения о программах, осуществляющих форматную
распечатку программ, написанных на Паскале. Важным аспектом
программирования, не получившим, однако, освещения в этой
книге, является анализ поведения программы при ее выполнении
(часто называемый измерением программы)} методы подобного
анализа были описаны Матвином и Миссалой [69], Микелем [71]
и Ювалом [106]. Майнер [72] представил программу, осуществ-
лявшую сравнение двух текстовых файлов и выдающую информа-
цию о всех обнаруженных между ними различиях.
Структурное программирование
Статья Дейкстры [32] вызвала начало полемики, всколыхнув-
шей весь программистский мир; вопросы, поднятые в этой работе,
Литература для дальнейшего чтения
329
продолжают дискутироваться до сих пор. В особенности Дейкстра
выступил против оператора перехода, что вызвало ответы
Бохмана [161, Кнута [58] и Вульфа [105]. К сожалению, эти
дебаты привели к тому, что другие, более важные аспекты струк-
туризации программ остались в тени. Интересные для программи-
рующих на Паскале статьи о структурном программировании
были опубликованы Сичелли [31], Лекармом [63], Ледгардом
[65], Ледгардом и Маркотти [661, Петерсоном и др. [80] и Виртом
[96, 101]. Статьи с критикой в адрес структурного программиро-
вания принадлежат Аткинсону [8] и Вейнбергу и др. [92].
Расположение текста программ
Принципы структурного программирования оказались благот-
ворными во многих отношениях. В частности, привлекли к себе
внимание вопросы, связанные с наглядностью текста программы.
Для облегчения чтения программы была предложена специальная
система расположения текста программы на страницах распеча-
ток. Расположение текста, соответствующее логической структу-
ре программы, в значительной мере зависит от конкретного син-
таксиса языка программирования.
К сожалению, синтаксис Паскаля представляет собой не сов-
сем удачное совмещение алгольных (язык Паскаль основан в значи-
тельной мере на Алголе) и чисто паскалевских конструкций (на-
пример, разделов описаний и определений, операторов цикла с
пост-условием, операторов варианта). Наилучшие способы фор-
матирования программ на Паскале были предложены Крайдером
[26], Грогоно [40], Гуерасом и Ледгардом [47, 48], Ковачем [59],
Могильнером [74], Петерсоном [79] и Сэйлом [83].
Другие аспекты стиля программирования обсуждались Ат-
кинсоном [9, 10]. Очень хорошее определение стиля программи-
рования дано в книге Кернигхэна и Плогера [521, где, к сожале-
нию, язык Паскаль не рассматривался.
Верификация программ
Создание формальной системы верификации программ требует
прочной теоретической основы. Для большого подмножества
Паскаля аксиоматическая база была создана Хоором и Виртом
[46], несколько позднее Уатт [91] разработал расширенную
атрибутную грамматику языка Паскаль.
Формализация подхода к программированию не могла не
вызвать возражений. Демилло и др. [29] выступили с критикой
методов верификации программ, причем их статья вызвала резкий
OTnoj) со стороны Дейкстры [35]. Парнас [77] определил более
умеренную практическую позицию, промежуточную между эти-
ми двумя крайними точками зрения.
330
Литература для дальнейшего чтения
Новости Паскаля
Новости Паскаля — это официальный бюллетень Группы поль-
зователей Паскаля. Он рассылается всем членам Группы, и со-
держит богатую информацию о Паскале в виде писем, статей и
заметок по вопросам реализации. Если вас интересует современ-
ное состояние языка Паскаль, его будущее, или вопросы реализа-
ции языка на вашей машине, Новости Паскаля несомненно вам
помогут. Бюллетень выходит четыре раза в год, обычно в сентяб-
ре, ноябре, феврале и мае. Напишите по одному из следующих
адресов:
Европа, Северная Африка, Западная и Центральная Азия (стои-
мость 4 фунта стерлингов).
Pascal User’s Group
с/о Computer Studies Group
Mathematics Department
The University
Southampton SO9 5NH
United Kindom
Телефон 44-703-559122 доб. 700
Австралия, Новая Зеландия, Восточная Азия, Япония
(8 австралийских долларов).
Pascal User’s Group
с/о Arthur Sale
Department of Information Science
University of Tasmania
GPO Box 252C
Hobart
Tasmania 7001
Australia
Телефон 61-02-23 0561
В прочих странах (6 американских долларов):
Pascal User’s Group
с/о Rick Shaw
Digital Equipment Corporation
5775 Peachtree Dunwoody Road
Atlanta
Georgia 30342
U. S. A.
Литература
1. Addyman, A. M., et al. (1979a)
“The BSI/ISO Working Draft of Standard Pascal"
Pascal News, 14, January 1979, 4—50.
Литература для дальнейшего чтения 331
2. Addyman, А. М., et al. (1979b)
“A Draft Description of Pascal44
SOFTWARE: Practice and Experience, 9:5, May 1979, 381—424.
3. Aho, A. L., J. E. Hopcraft, and J. D. Ullman (1974)
The Design and Analysis of Computer Algorithms, (Addison—Wesley).
(Русский перевод: Ахо А., Хопкрофт Дж., Ульман Дж. Постро-
ение и анализ вычислительных алгоритмов.— М.: Мир, 1979.)
4. Ambler, A. L., et al. (1977)
“GYPSY: A Language for Specification”
SIGPLAN Notices, 12 : 3, March 1977, 1—10.
5. Amman, U. (1973)
“The Method of Structured Programming Applied to the
Development of a Compiler”
Proceedings ACM International Computing Symposium:
Davos, 1973, ed., Gunther.
6. Amman, U. (1977a)
“On Code Generation in a Pascal Compiler”
SOFTWARE: Practice and Experience, 7(1977),
7. Amman, U « (1977b)
“The Zurich Implementation”
Proceedings of a Symposium on Pascal—The Language and Its Imp-
lementation, Southampton, 1977.
8. Atkinson, G. (1977)
“The Non-Desirability of Structured Programming in User Languages”
SIGPLAN Notices, 12 : 7, July 1977, 43—50.
9. Atkinson, L. V. (1978)
“Know the State You are In”
Pascal News, 13, December 1978, 66—69.
10. Atkinson, L. V. (1979)
“Pascal Scalars as State Indicators”
SOFTWARE: Practice and Experience, 9 : 6, June 1979, 427—32.
11. Baase, S. (1979)
Computer Algorithms: Introduction to Design Analysis (Addison-
Wesley).
12. Bates, D., and R. Cailliau (1977)
“Experience with Pascal Compilers on Minicomputers”
SIGPLAN Notices, 12:11, November 1977, 10—22.
13. Berry, R. E. (1978)
“Experience with the Pascal P-Compiler”
SOFTWARE: Practice and Experience, 8 : 5 (1978), 617—28.
14. Biedl, A. (1977)
“An Extension of Programming Languages for Numerical Computation
in Science and Engineering with Special Reference to Pascal”
SIGPLAN Notices, 12 : 4, April 1977, 31—33.
15. Birtwistle, G. M., O—J Dahl, B. Myrhaug, K. Nygaard (1973) SIMULA
begin, (Auerbach).
16. Bochmann, G. W. (1972)
“Multiple Exits from a Loop without GOTO”
Communications of the ACM, 16 : 7, July 1972, 443—44.
17. Brinch Hansen, P. 1975
“The Programming Language Concurrent Pascal”
IEEE Transactions on Software Engineering, SE-1 : 2, June 1975,
199—207.
18, Brinch Hansen, P. (1976)
“The SOLO Operating System”
Practice and Experience, 6, (1976), 139—205.
332
Литература для дальнейшего чтения
19. Bron, С., and W. deVries. (1976)
“A Pascal Compiler for the PDP11“
SOFTWARE'. Practice and Experience. 6, (1976), 109—16»
20. Chung, К—M, and H. Yuen 1978
“A‘Tiny’ Pascal Compiler”
Part 1: BYTE, 3 : 9, September 1978;
Part 2: BYTE, 3 : 10, October 1978.
21. Cichelli, R. J. (1976)
“Design Data Structures by Step-Wise Refinement”
Pascal News, 5, September 1976, 7—13.
22. Clark, R. G. (1979)
“Interactive Input in Pascal”
SIGPLAN Notices, 14 : 2, February 1979, 9—13.
23. Condict, M. N. (1977)
“The Pascal Dynamic Array Controversy and a Method for Enforcing
Global Assertions”
SIGPLAN Notices, 12 : 11, November 1977, 23—27,
24. Condict, M. N., R. L. Marcus, and A, Mickel (1978)
“Pascal Program Formatter”
Pascal News, 13, December 1978, 45—58.
25. Conradi, R. (1976)
“Further Critical Comments on Pascal, Particularly as a System Pro-
gramming Language”
SIGPLAN Notices, 11:11, November 1976, 8—25.
26. Crider, J. E. 1978
“Structured Formatting in Pascal Programs”
SIGPLAN Notices, 13 : 11, November 1978, 15—22.
27. Dahl, O—J, E. W. Dijkstra, and C. A. R. Hoare (1972)
Structured Programming (Academic Press). (Русский перевод: Дал
У., Дейкстра Э., Хоор К- Структурное программирование.— М.,
Мир, 1975.)
28. Dahlquist, G., and A. Bjorck (1974)
Numerical Methods, tr., Ned Anderson (Prentice Hall).
29. Demillo, R. A., R. J. Lipton, and A. J. Perlis (1977)
“Social Processes and Proofs of Theorems and Programs” Conf. Rec.
ACM Symposium on the Principles of Programming Languages, 1977,
206—14.
Rpt. Communications of the ACM, 22 : 5, May 1979, 271—80.
30. Desjardins, P. (1976)
“A Pascal Compiler for the Xerox Sigma"6”
SIGPLAN Notices, 8 : 6, June 1976, 34—36.
31. Desjardins, P. (1978)
“Type Compatibility Checking in Pascal Compilers”
Pascal News, 11, February 1978, 33—34.
32. Dijkstra, E. W. (1968)
“The Goto Statement Considered Harmful”
Communications of the ACM, 18 : 8, August 1975, 145—48.
33. Dijkstra, E. W. (1975)
“Guarded Commands, Nondeterminancy, and Formal Derivation of
Programs”
Communications of the ACM, 18 : 8, August 1975, 453—57.
34. Dijkstra, E. W. (1976)
A Discipline of Programming (Prentice Hall). (Русский перевод: Дей-
кстра Э. Дисциплина программирования.— М.: Мир, 1978.)
35. Dijkstra, Е. W. (1978)
“On a Political Pamphlet from the Middle Ages”
SIGSOFT Notes, 3 : 2, April 1978, 14—15,
У
Литература для дальнейшего чтения
333
36. Edwards, R. (1977)
Is PASCAL a Logical Subset of Algol-68 or Not?
SIGPLAN Notices, 12:6, June 1977, 184—91.
37, Feiereisen (1974)
“Implementations of Pascal on the PDP11/45”
Proceedings DECUS Conference, Zurich, 1974.
38. Geschke, M., et aL
“Early Experience with NESA”
Communications of the ACM, 20 : 8, August 1977, 540—52.
39. Goodenough, J. B., and S. L. Gerhart (1975)
“Toward a Theory of Test Data Selection”
IEEE Transactions on Software Engineering, SE—1 : 2, 156—73, June
1975.
40. Grogono, P. (1979)
“On Layout, Identifiers, and Semicolons in Pascal Programs” SIGP-
LAN Notices, 14 : 4, April 1979, 35—40
41. Grosse-Lindemann, С. O., and H. H. Nagel (1976)
“Postlude to a Pascal Compiler for the DEC System 10”
SOFTWARE' Practice and Experience, 6 : 1 (1976), 29—42.
42. Habermann, A. N. (1973)
“Critical Comments on the Programming Language Pascal”
Acta Information, 3 : 1 (1973), 45—57.
43. Hansen, G. J., G. A. Shoults, and J. D. Cointment (1979)
“Construction of a Portable, Multi—Pass Compiler for Extended Pascal”
SIGPLAN Notices, 14:8, August 1979, 117—26.
44. Hartmann, A. C. (1977)
A Concurrent Pascal Compiler for MiniComputers (Springer-Verlag).
45. Hoare, C. A. R. (1973a)
“Hints on Programming Language Design”
SIGACT/SIGPLAN Symposium on Principles of Programming Lan-
guagls, 1973.
46. Hoare, C. A. R., and N. Wirth (1973b)
“An Axiomatic Defenition of Pascal”
Acta Informatica, 3 (1973), 335—55.
47. Hueras, J., and H. Ledgard (1977)
“An Automatic Formatting Program for Pascal"
SIGPLAN Notices, 12 : 7, July 1977, 83—84.
48. Hueras, J., and H. Ledgard (1978)
“Pascal Prettyprinting Program”
Pascal News, 13, December 1978, 34—45.
49. Iglewski, M., J Madey, and S. Matwin (1978)
“A Contribution to an Improvement of Pascal”
SIGPLAN Notices, 13 : 1, January 1978, 45—58.
50. Jensen, K-, and N., Wirth (1976)
Pascal User Manuel and Report, 2nd. ed. (Springer-Verlag).
51. Johnson, L. W., and R. D. Riess (1977)
Numerical Analysis (Addison-Wesley).
52. Kernighan, B. W., and P. J. Plauger (1974)
The Elements of Programming Style (McGraw-Hill).
53. Kernighan, B. W., and P. J. Plauger (1976)
Software Tools (Addison-Wesley).
54. Kittlitz, E. N. (1976)
“Block Statements and Synonyms in Pascal”
SIGPLAN Notices, 11 : 10, October 1976, 32—35.
55. Kittlitz, E. N. (1977)
“Another Proposal for Variable Size Arrays in Pascal”
SIGPLAN Notices, 12 : 1, January 1977, 82—86.
334
Литература для дальнейшего чтения
56. Knuth, D. Е. (1969)
The Art of Computer Programming, Vol. 1: Fundamental Algorithms,
2nd. ed. (Addison—Wesley). (Русский перевод: Кнут Д. Искусство
программирования для ЭВМ. Т. 1. Основные алгоритмы.— М.з
Мир, 1976.)
57. Knuth, D. Е. (1969)
The Art of Computer Programming, Vol. 2: Seminumerical Algorithms
(Addison—Wesley). (Русский перевод: Кнут Д. Искусство програм-
мирования для ЭВМ. Т. 2. Получисленные алгоритмы.— М.: Мир,
1978.)
58. Knuth, D. Е. (1974)
“Structured Programs with GOTO Statesments”
Computing Surveys, 6 (1974), 261—301,
59. Kovats, T. A. (1978)
“Program Readability, Closing Keywords, and Prefix—Style Inter-
mediate Keywords”
SIGPLAN Notices, 13 : 11, November 1978, 30—42,
60. Lampson, B. W., et al, (1977)
“Report on the Programming Language Euclid”
SIGPLAN Notices, 12 : 2, February 1977, 1—77.
61. LeBlanc, R. J. (1978)
“Extensions to Pascal for Separate Compilation” SIGPLAN Notices,
13 : 9, September 1978, 30—33.
62. LeBlanc, R. J., and C. N. Fischer (1979)
“On Implementing Separate Compilation in Block-Strucktured Lan-
guages”
SIGPLAN Notices, 14 : 8, August 1979, 139—43.
63. Lecarme, 0. (1974)
“Structured Programming, Programming Teaching, and the Language
Pasca 1”
SIGPLAN Notices, 9 : 7, July 1974, 15—21.
64. Lecarme, O., and P. Desjardins (1974)
“Reply to a Paper by A. N. Habermann on the Programming Language
Pasca 1”
SIGPLAN Notices, 9 : 10, October 1974, 21.
65. Ledgard, H. (1973)
“The Case for Structured Programming”
BIT, 13 (1973), 45—57.
66. Ledgard, H., and M. Marcotty (1975)
“A Genealogy of Control Structures”
Communications of the ACM, 18 : 11, November 1974, 629—39.
67. Liscov, B., et al. (1977)
“Abstraction Mechanisms in CLU”
Communications of the ACM, 20 : 8, August 1977, 564—76.
68. Marlin, C. D. (1979)
“A Heap-based Implementation of the Programming Language Pascal”
SOFTWARE: Practice and Experience, 9 : 2, February 1979, 101—20.
69. Matwin, S., and Missala (1976)
“A Simple Machine Independent Tool for Obtaining Rough Measure-
ments of Pascal Programs”
SIGPLAN Notices, 11:8, August 1976, 42—45,
70. McLennan, B. J. (1975)
“A Note on Dynamic Arrays in Pascal”
Pascal News, 12, 10 : 9, September 1975, 39—40.
71. Mickel, A. (1978)
“Augment and Analyze”
Pascal News, 12, June 1978, 23—32.
Литература для дальнейшего чтения
335
72. Miner, J. F. (1978)
“Compare Two Text Files”
Pascal News, 12, June 1978, 20—23.
73. Mohilner, P. R. (1977)
“Using Pascal in a Fortran Environment”
SOFTWARE: Practice and Experience, 7, (1977), 357—62,
74. Mohilner, P. R. (1978)
“Prettyprinting Pascal Programs”
SIGPLAN Notices', 13 : 7, July 1978, 34—40.
75. Nelson, P. A. (1979)
“A Comparison of Pascal Intermediate Languages”
SIGPLAN Notices, 14 : 8, August 1979, 208—13.
76. Nutt, G. J. (1978)
“A Comparison of Pascal and FORTRAN as Introductory Programming
Languages”
SIGPLAN Notices, 13 : 2, February 1978, 57—62.
77. Parnas, D. L. (1978)
“Another View of the Dijkstra-dMLP Controversy”
SIGSOFT Notes, 3 : 4, October 1978, 20—21.
78. Perkins, D. R. and R. L. Sites (1979)
“Machine Independent Pascal Code Optimization”
SIGPLAN Notices, 14 : 8, August 1979, 201—07,
79. Peterson, J. L. (1977)
“On the Formating of Pascal Programs”
SIGPLAN Notices, 12 : 12, December 1977, 83—83.
80. Peterson, W., T. Kasami, and N. Tolura (1973)
“On the Capabilities of While, Repeat, and Exit Statements”
Communications of the ACM, 16 : 8, August 1973, 503—12.
81. Pokrovsky, S. (1976)
“Formal Types and Their Application to Dynamic Arrays in Pascal”
SIGPLAN Notices, 11 : 10, October 1976, 36—42.
82. Rudmik, A. (1979)
“Compiler Design for Efficient Code Generation and Program
Optimization”
SIGPLAN Notices, 14 : 8, August 1979, 127—38.
83. Sale, A. H. J. (1978)
“Stylistics in Languages with Compound Statements”
Australian Computer Journal, 10:2, May 1978, 58—59.
84. Singer, A., J. Hueras, and H. Ledgard (1977)
“A Basis for Executing Pascal Programmers”
SIGPLAN Notices, 12 : 7 July 1977, 101—05.
85. Sites, R. L. (1979)
“Machine Independent Register Allocation”
SIGPLAN Notices, 14:8, August 1979, 221—25.
86. Sterbenz, P. H. (1974)
Floating Point Computation (Prentice Hall).
87. Thibault, D., and P. Manuel (1973)
“Implementation of a Pascal Compiler for the CH Iris 80 Computer”
SIGPLAN Notices, 8 : 6, June, 894-90.
88. Travis, L., et al. (1977)
“Design Rationale for TELOS, a Pascal Based Al Language”
SIGPLAN Notices, 12:8, August 1977, 67—76.
89. Ulman, J. D, (1976)
Fundamental Concepts of Programming Systems (Addison-Wesley).
90. Venema, T., and J. de Rivieres (1978)
“Euclid and Pascal”
SIGPLAN Notices, 13:3, March 1978, 57—69.
333
Литература для дальнейшего чтения
91. Watt, D. А. (1979)
“An Extended Attribute Grammar for Pascal”
SIGPLAN Notices, 14 : 2, February 1979, 60—74.
92. Weinberg, G. M., D. P. Geller, and T. W. Plum (1975) «If-Then-Else Consi-
dered Harmful»
SIGPLAN Notices, 10 : 8, August 1975, 34—43.
93. Welsh, J., and C. Quinn (1972)
“A Pascal Compiler for the ICL 1900 Series Computer”
SOFTWARE: Practice and Experience, 2 (1972), 72—77.
94. Welsh, J., W. J. Sneeringer, and C. A. R. Hoare (1977)
“Ambiguities and Insecurities in Pascal”
SOFTWARE: Practice and Experience, 7 (1977), 685—96.
95. Wirth, N. (1971a)
“The Programming Language Pascal”
Acta Informatica 1, (1971), 35—64.
96. Wirth, N. (1971b)
“Program Development by Stepwise Refinement”
Communications of the ACM, 14 : 4, April 1971, 221—27,
97. Wirth, N. (1971c)
“Design of a Pascal Compiler”
SOFTWARE: Practice and Experience, 1 (1971), 309—33.
98. Wirth, N. (1972)
“The Programming Language Pascal and its Design Criteria” Infotech
State of the Art Report No. 7: High Level Languages (1972), 451—73.
99. Wirth, N. (1973)
Systematic Programming—An Introduction (Prentice Hall).
100. Wirth, N. (1974a)
“On the Design of Programming Languages”
North Holland Information Processing: Programming Methodology.
101. Wirth, N. (1974b)
“On the Construction of Well Structured Programs” Computing Surveys,
6 : 4, December 1974, 247—59.
102. Wirth, N. (1975)
“An Assessment of the Programming Language Pascal” IEEE Transac-
tions on Software Engineering, SE—1 : 2, June 1975, 192—98.
103. Wirth, N., (1976a)
Algorithms Data Structures = Programs (Prentice Hall).
104. Wirth, N. (1976b)
“Comments on a Note on Dynamic Arrays in Pascal”
SIGPLAN Notices, 11:1, January 1976, 37—38.
105. Wulf, W. A. (1971)
“Programming Without the GOTO”
Proceedings IFIP Conference, 1971.
106. Yuval, G. (1975)
“Gathering Run Time Statistics Without Black Magic”
SOFTWARE: Practice and Experience, 5 (1975), 105—08.
Приложение А.
СЛОВАРЬ ЯЗЫКА ПАСКАЛЬ
Программы на языке Паскаль пишутся с использованием букв,
цифр и других символов. Большинство из этих символов представ-
лено на всех пишущих машинках и периферийных устройствах
вычислительных машин. Однако некоторые символы иногда от-
сутствуют, поэтому для них определяются синонимы.
В этой книге при описании программы на Паскале мы пользо-
вались как заглавными, так и строчными буквами. Многие вычис-
лительные машины не читают и не печатают строчных букв, и
если на вашей машине именно такое положение, то для своих
программ вам придется пользоваться только заглавными буква-
ми. Если на вашей машине использовать строчные буквы разре-
шено, вам следует сначала изучить вопрос о том, как их воспри-
нимает работающий на этой машине компилятор.
А.1. Зарезервированные слова
Нижеперечисленные зарезервированные слова рассматрива-
ются в языке Паскаль как неделимые символы. Их нельзя исполь-
зовать ни в каком месте программы для каких-либо целей, не
определенных грамматикой, за исключением примечаний.
and array begin case const
div do downto else end
file for function goto if
in label mod nil not
of or packed procedure program
record repeat set then to
type until var while with
A.2. Идентификаторы
Идентификаторы — это имена, выбираемые программистом
для обозначения констант, типов, переменных, процедур и функ-
ций. Идентификатор состоит по крайней мере из одной буквы, за
которой могут следовать буквы и цифры в произвольных комби-
нациях. Два идентификатора, отличающиеся в первых восьми
символах, будут распознаны компилятором как различные.
338
Приложение А. Словарь языка Паскаль
Существует некоторое число стандартных идентификаторов, ко-
торые могут распознаваться компилятором без предварительного
определения в программе. Стандартный идентификатор можно
переопределять во всей программе или в некоторых ее частях.
Ниже следует список стандартных идентификаторов.
Стандартные константы:
false true maxint
Стандартные типы:
integer boolean real char text
Стандартные файлы:
input output
Стандартные функции:
abs arctan chr cos
eof eoln exp In
odd ord pred round
sin sqr sqrt succ
trunc
Стандартные процедуры
get new pack page
put read readln reset
rewrite unpack write writein
▲.3. Знаки препинания
Все оставшиеся символы языка перечислены в последней таб-
лице. Первая колонка этой таблицы содержит сам стандартный
символ; вторая колонка, если в ней присутствует какой-либо сим-
вол, содержит символ-синоним, которым можно пользоваться
вместо стандартного символа, если по каким-либо причинам стан-
дартный символ не допускается; третья колонка содержит очень
крроткое и не обязательно полное описание функции этого симво-
ла. Символы и их синонимы не универсальны, поэтому в таблице
оставлено место, чтобы вы могли вписать туда те символы, кото-
рые используются в вашей конкретной реализации языка.
+ плюс
— минус
* умножить
/ разделить
< меньше
<== меньше или равно
= равно
О не равно
А.З. Знаки препинания
339
> = больше или равно больше
Л and V or — not логическое умножение логическое сложение логическое отрицание присвоить
t > разделитель элементов списка разделитель операторов языка
/ разделитель имени переменной и типа ограничитель символьных и строковых конс- тант
• десятичная точка, селектор записи и символ конца программы
t ( ) задание диапазона индикатор файловой и ссылочной переменной начало списка параметров или вложенного выражения конец списка параметров или вложенного
1 (• выражения начало списка индексов или множественного
{ (* } •) 1 •) выражения начало примечания конец примечания конец списка индексов или множественного выражения
Приложение Б.
СИНТАКСИС ЯЗЫКА ПАСКАЛЬ
В это приложение включен полный набор синтаксических диаграмм языка Паскаль. Во всех случаях рас-
хождения между этими синтаксическими диаграммами и синтаксическими диаграммами^ приведенными в
этой же книге ранее, нужно помнить, что правильными являются диаграммы этого приложения.
программа *->Q)ROGRAMj—>] идентификатор
идентификатор
Блок
.t - Z
mun
список
полей ‘
простой
тип ~
^идентификатор типа
переменная
-^идентификатор переменной
^►1 идентификатор поля L
Б\[ква
цифра
Приложение В,
РЕАЛИЗАЦИЯ ЯЗЫКА ПАСКАЛЬ
В этом приложении мы попытаемся дать обзор одной конкрет-
ной реализации языка Паскаль. Это даст возможность лучше уяс-
нить себе разницу между тем, что называется языком Паскаль, в
том виде, как он описан в этой книге, и его реализацией для ка-
кого-либо семейства вычислительных машин. Первый компилятор
с языка Паскаль был написан для машины CDC 6600 Урсом Ам-
маном и Никлаусом Виртом в Высшей федеральной технической
школе в Цюрихе.
Первая версия много раз подвергалась различным модифика-
циям, и это приложение основано на версии компилятора, на-
зываемой «Паскаль 6000 Версия 3». За состоянием этого компиля-
тора в настоящее время следит группа миннесотского универси-
тета, состоящая из Джона Истона, Энди Микеля и Джона Стей-
та. Используется он довольно широко, особенно в университе-
тах. В этом приложении язык, воспринимаемый компилятором
Паскаль 6000 Версия 3, называется Паскаль В3(даал. R3).
Во всем, что касается программирования, машины серий
CDC 6000 и Cyber 170 очень похожи на исходную CDC 6600,
которая была разработана в начале шестидесятых годов. Машина
CDC 6600 была предназначена преимущественно для проведения
научных расчетов, а так как среди тех, кто проводил такие рас-
четы, в то время был наиболее популярен язык Фортран, руково-
дящим принципом при создании CDC 6600 было обеспечить быст-
рое выполнение фортрановских программ. На новых сериях 6000
и 170 также имеются возможности очень быстрого исполнения
фортрановских программ, однако реализации других языков, в
частности PL/1 и Лиспа, оказались не столь удачными. Вопрос о
том, насколько эти машины подходят для систем с языком Пас-
каль, неоднократно обсуждался Амманом [6, 7] и Виртом [97].
Паскаль ВЗ значительно расширен по отношению к стандарту
языка, есть, например, описание value, динамические массивы,
конструкция otherwise в операторах варианта. В настоящем
приложении эти расширения не описаны.
В.1. Стандартные типы
351
в.1. Стандартные типы
Машины серии 6000 имеют длину слова, равную 60 разрядам.
При проведении численных расчетов слово можно рассматривать
как состоящее из одного знакового разряда, 11 разрядов порядка
и 48 разрядов мантиссы, а при работе с символами можно считать,
что слово состоит из 10 групп, каждая по 6 разрядов. Следова-
тельно:
(a) maxint = 248— 1 = 281474976710655.
(б) Если х вещественное, то либо х = 0, либо
10~293< lx | < 10322 приблизительно.
(в) Вещественные числа представляются примерно с точно-
стью около 14 десятичных цифр.
(г) Допускается работа с 64 различными символами. Конкрет-
ные множества символов могут быть разными, поэтому ни
один из них здесь не приведен. Символ, ord которого
равен 0, может оказаться запрещенным, так как опера-
ционная система для обозначения конца строки исполь-
зует комбинацию из 12 нулевых разрядов (00008). К сожа-
лению, именно этот символ обозначает на многих системах
двоеточие, и для ' : ', тем самым, приходится исполь-
зовать другой символ (например, '%')• Множества допус-
тимых символов и их упорядоченность меняются от одной
реализации к другой, а иногда даже разнятся на разных
периферийных устройствах. Обычно,
'а' < 'Ь' <'с' <. . . C'z' < 9 9
что вызывает странные последствия при попытке провести
лексикографическое упорядочение какого-либо текста.
Одним из решений этой проблемы может быть замена
пробела символом c/zr (0), но нельзя при этом забывать
производить обратное преобразование, перед тем как
печатать пробелы.
В Паскале ВЗ имеются две дополнительные стандартные
константы:
col = c/ir(0) двоеточие,
per — chr (51) процент.
(д) Множества могут иметь до 59 элементов. Отсюда следует,
что множественная переменная может быть записана в
одно 60-разрядное слово. Символ не может быть элементом
множества, если ord от этого символа больше 58. К сожа-
лению, приходится приносить такие жертвы ради повы-
шения эффективности. В частности, не допускается работа
с типом
set of char
352
Приложение В. Реализация языка Паскаль
что весьма неудобно. Например, выражения, присутство-
вавшие в программе поисксмежных (гл. 6):
[тексим, предсим] 1сма. . . смя]
в Паскале ВЗ не допускаются, поскольку программа не
сможет быть выполнена, если ord (тексим) > 58 или ord
(предсим) > 58.
Стандартную функцию ord можно использовать с па-
раметрами-множествами; в этом случае она выдает цело-
численное представление множества.
(е) Вводится стандартный тип alfa, который эквивалентен
packed array [1 . . 10] of char
Переменные этого типа можно сравнивать, им можно
присваивать значения. Однако сравнения производятся
над представлением в коде машин серии 6000, и упоря-
доченность символов иногда кажется более^чем стран-
ной. Например,
'compile '>'compiler !
так как
•' '>'г'.
Переменные типа alfa используются и в самом компи-
ляторе ВЗ для хранения во время компиляции иденти-
фикаторов. Следовательно, он различает те идентифика-
торы, у которых есть различия в первых десяти симво-
лах. Сообщение о Паскале говорит, что все компиляторы
должны различать идентификаторы по первым восьми
символам, поэтому компилятор ВЗ вполне удовлетворяет
этому стандарту. Многие ранние компиляторы следовали
более предпочтительному соглашению, по которому все
символы идентификаторов считались значащими.
(ж) Восьмеричные (по основанию 8) константы можно упо-
треблять в программе в следующей форме:
ццц. . . .ццВ
где каждый символ ц соответствует цифре в диапазоне
['О'. . .'7']. Например,
100В 7777777777777777В
это восьмеричные константы, представляющие соответст-
венно 6410 и maxint.
(з) Ссылки представляются с помощью 17-разрядных чисел,
если только они не проверяются при выполнении про-
граммы, в последнем случае ссылки представляются с по-
мощью 36 разрядов. Расширены возможности использова-
В.З. Стандартные процедуры и функции
353
ния функции ord, которая может вызываться с парамет-
ром-ссылкой: выдаваемое при этом вызове значение
ord(p) есть адрес pf.
Стандартная процедура dispose (р) возвращает в спи-
сок свободной памяти область, занимаемую р|, оттуда
она может быть затем снова забрана стандартной проце-
дурой new. В Паскаль ВЗ включена одна дополнительная
стандартная процедура release(р), которая освобождает
pf и все динамические переменные, размещенные в памя-
ти с того момента, как была выполнена процедура new(p).
Эта процедура фактически превращает кучу во второй
стек, которым в программе можно явно управлять.
Процедура release изменяет лишь одно значение (указа-
теля кучи), поэтому она работает намного быстрее, чем
процедура dispose, но по той же причине она и не столь
полезна. Процедуры release и dispose несовместимы друг
с другом, и их не следует совместно использовать в одной
программе.
В.2. Арифметика
Процессор 6000 никак не отмечает тот факт, что при выполне-
нии целочисленных вычислений получился результат, превышаю-
щий значение maxint. Более того, числа N, такие, что
248<= |ЛА|<259,
вполне можно и складывать, и вычитать друг из друга; нельзя их
только перемножать, делить и печатать. Выражение
abs(n)>maxint
в этих условиях всегда будет иметь значение true.
Для обозначения бесконечных и неопределенных значений
вещественных чисел процессор CDC 6000 использует специаль-
ные коды. «Бесконечное» значение может возникать в тех случа-
ях, когда результат вычисления превышает самое большое пред-
ставимое на машине число; неопределенный результат возникает
при попытке произвести деление на нуль. Если вещественное чис-
ло х имеет бесконечное или неопределенное значение, то булев-
ская функция
undefined (х)
будет иметь значение true.
В.З. Стандартные процедуры и функции
В дополнение к списку стандартных процедур и функций,
приведенному в приложении А, имеются следующие:
12 № 3388
354
Приложение В. Реализация языка Паскаль
procedure time(var время : alfa);
{устанавливает значение переменной время, равным
текущему времени в формате ЧЧ.ММ.СС}
procedure message (стр : строка);
{в файл операционной системы, содержащий информацию о
сегодняшнем ходе работ на машине, выдается произвольное
сообщение (не превышающее по длине 80 символов)}
procedure date (var дата : alfa);
{устанавливает значение переменной дата, равным
текущей дате в формате ГГ/ММ/ДД}
procedure linelimit (var текстовыйфайл : text;
число : integer);
{снимает задачу когда указанное число строк будет
записано в указанный текстовый файл}
procedure halt (стр : строка);
{снимает задачу, печатает сообщение стр,
не превышающее по длине 80 символов, а затем выдает
отладочную информацию}
procedure release (с : ссылка);
{ликвидирует все динамические переменные, память под ко-
торые была выделена при выполнении обращения new (с)}
function card (множество : set of типэлемента) : integer;
{возвращает значение мощности множества, т. е. число его
элементов}
function clock : integer;
{возвращает в качестве своего значения число миллисекунд,
затраченных процессором на выполнение данного задания,
не только данной программы к моменту вызова функции}
function expo (f : real) : integer;
{возвращает содержимое поля порядка вещественного числа f}
function undefined (f : real) : boolean;
{возвращает значение true, если значение f неограничено
или неопределено, и значение false в противном случае}
Еще две дополнительные процедуры, getseg и putseg, а также
функции eos определены в разд. В.6.
В 4. Ввод и вывод
Если а есть переменная типа alfa, то оператор
write (а)
В.4. Ввод и вывод
355
вполне допустим и приведет к передаче в файл значения а как
строки из десяти символов, однако оператор
read (а)
не допускается, поэтому значения типа alfa необходимо читать
символ за символом.
Целые числа можно печатать в восьмеричном виде; если i
есть целая переменная, то оператор
write (i : ширина поля oct)
выдаст значения i как восьмеричного числа (но без последующего
символа 'В'), состоящего из ширинаполя цифр. Нули впереди
числа пробелами не заменяются. Если ширинаполя 20, то
печатаются сначала ширинаполя — 20 пробелов, а затем уже
20 цифр числа. Если ширинаполя < 20, то печатаются только
самые правые цифры числа, даже если при этом будут потеряны
некоторые ненулевые цифры. Например, после выполнения
число := 40000000000000005555В;
write (число : 4 oct)
будет отпечатано
5555
Если указана ширинаполя, равная нулю, результат будет такой
же, как при ширинеполя, равной 20.
Оператор
write (i : ширинаполя hex)
приведет к печати значения целого числа i в шестнадцатеричном
виде. Для преобразования чисел как в восьмеричный, так и в
шестнадцатеричный вид указывать ширинуполя всегда обяза-
тельно.
Для других типов по умолчанию принимаются следующие
значения шириныполя'.
10 для целого;
22 для вещественного;
10 для булевского;
1 для символьного;
Вещественные числа, для которых не указаны ни ширинаполя,
ни точность, печатаются в виде
□ ± ц. цццццццццццццЕ ± ццц
где каждой букве ц соответствует десятичная цифра. После деся-
тичной точки размещается 13 цифр, а символ '[]' соответствует
12*
356
Приложение В. Реализация языка Паскаль
пробелу. Первая (перед десятичной точкой) цифра всегда будет
отлична от нуля.
Строчка текстового файла представляется всегда целым чис-
лом полностью заполненных слов; каждое слово содержит де-
сять 6-разрядных символов. Конец строчки представляется с по-
мощью по крайней мере двенадцати расположенных справа ну-
левых разрядов в последнем слове. Процедура writein обеспе-
чивает, чтобы каждая строчка всегда содержала четное число
символов (не считая нулевых разрядов), и добавляет один про-
бел к концу строчки, если это необходимо.
Процедура reset не может работать с файлом input, а процеду-
ра rewrite — с файлом output.
В.5. Файлы
В системе CDC имена файлов не должны быть длиннее семи
символов. В самой программе файлам можно давать и более длин-
ные имена, но при этом для связи с операционной системой зна-
чащими будут только первые семь символов. Например, програм-
ма, начинающаяся с заголовка
program управлениефайлами
(первичныйфайл, вторичныйфайл, output)
будет на самом деле работать с файлами первичн, вторичн и output.
Не допускается тип file of file. Однако файл может быть
компонентой массива или записи.
В операционной системе CDC имеются различные способы ор-
ганизации файлов. Большинство файлов, содержащих символы,
организованы тем способом, который в Паскале называется тек-
стом. Операционная система преобразует перфокарточные файлы
к такому виду, чтобы избежать хранения большого количества
следующих друг за другом пробелов. Следовательно, не нужно
ожидать, что образ перфокарты будет обязательно состоять из
80 символов; напротив, при чтении карт вам нужно вводить один
символ за другим, постоянно осуществляя проверку на конец с
помощью функции eoln.
Тип
файлкарт =
record
packed array [1 . . 80] of char;
использовать можно, но он предполагает совершенно другой спо-
соб организации файла, когда каждая строка файла содержит
ровно 80 символов. Программа копироватьфайл из гл. 7 будет
правильно работать именно с таким файлом, но при работе с фай-
лом input будет сбиваться.
В.6. Сегментированные файлы
357
Перед тем как начинать работу с каким-либо файлом ф (от-
личающимся от файла input), необходимо выполнить процедуру
reset (ф). Однако, прежде чем начинать запись в файл, необяза-
тельно выполнять процедуру rewrite (ф)\ поэтому в программе
можно как добавлять некоторые данные к уже существующему
файлу, так и создавать новые файлы.
В.6. Сегментированные файлы
Любой файл может быть описан как сегментированный
(segmented). Например, можно написать такое определение
type
фф = segmented file of char;
Последовательность слов segmented file of char может быть
сокращена до слова segtext\ типы segtext и text совместимы. За-
метьте, что слово segmented зарезервировано, а слово text нет.
Сегментированный файл состоит из отдельных сегментов. Для
доступа к ним можно пользоваться стандартными процедурами
putseg и getseg, а также булевской функцией eos. В приведенных
ниже примерах сегфайл есть сегментированный файл, чис есть
целое число.
rewrite (сегфайл)
Производит подготовку файла к записи с самого его начала.
rewrite (сегфайл, чис)
Производит подготовку файла к перезаписи сегмента с номе-
ром чис, считая от текущего положения указателя сегментов.
Заметьте, что rewrite (сегфайл, 1) означает «подготовиться
к перезаписи, начиная со следующего сегмента», а это не то
же самое, что означает процедура rewrite (сегфайл).
putseg (сегфайл)
Завершить сегмент. Эта процедура выполняется после того,
как сегмент будет заполнен информацией с помощью процеду-
ры put или write.
getseg (сегфайл)
Подвести указатель файла к началу следующего сегмента.
Сегфайл\ будет первой компонентой этого сегмента, или, если
сегмент пуст, становится истинным значением еов(сегфайл).
Сегмент можно читать с помощью обращений к процедурам
get (сегфайл\) или read (сегфайл, буфер) вплоть до того момен-
та, когда значение функции ео5(сегфайл) не станет истинным.
358
Приложение В. Реализация языка Паскаль
В конце последнего сегмента становятся истинными значения
обеих функций — eos (сегфайл) и eof (сегфайл).
getseg (сегфайл, час)
Пропустить чис сегментов вперед (чис > 0) или назад (чис <
< 0). Выполнение процедуры getseg(сегфайл, 0) приведет к
подводу указателя файла к началу текущего сегмента. Вызов
getseg (сейфайл, 1) эквивалентен вызову getseg(сегфайл).
В.7. Внешние процедуры
Процедуры, не определенные в программе, называются внеш-
ними по отношению к данной программе. Если программа вызы-
вает внешние процедуры (либо функции: все, что мы будем в этом
разделе говорить о процедурах, полностью применимо и к функ-
циям), то рабочая программа будет содержать внешние ссылки,
и их нужно обязательно определить до того, как программа нач-
нет выполняться.
Определением ссылок на внешние функции занимается про-
грамма-загрузчик, делается это при помощи просмотра опреде-
лений процедур, собранных в библиотеке. Определения стан-
дартных процедур также содержатся в библиотеке, поэтому за-
грузчик, выполняя загрузку программы, почти всегда будет
осуществлять просмотр библиотеки. Компилятору с Паскаля
6000 можно указать произвести поиск определения некоторой
процедуры и в другой библиотеке. В программе такие процедуры
описываются, но вместо тела процедуры пишется слово extern или
fortran. Например:
procedure (еда первоеблюдо,второеблюдо : real;
var наелся : boolean);
extern;
function cosh (var x : real) : real;
fortran;
На основании указаний extern или fortran компилятор форми-
рует обращение к процедурам того или другого типа. Обращения
к процедурам «паскалевского» и фортрановского типа отличаются
друг от друга, поэтому важно правильно указать тип. Заметьте
при этом, что команды обращения, формируемые для Фортрана,
соответствуют командам, формируемым компилятором с Фортра-
на FTN, поэтому для создания библиотеки фортрановских под-
программ нужно использовать именно этот компилятор.
Компилятор с Паскаля не может компилировать незакончен-
ные программы, поэтому для формирования библиотеки проце-
дур на Паскале нужно идти на некоторые ухищрения. Компи-
лятор создает файл из нескольких записей: по одной записи на
В.7. Внешние процедуры
359
каждый модуль, где каждый модуль есть процедура, которая, воз-
можно, содержит несколько вложенных процедур, и, кроме того,
еще одну (последнюю) запись для главной программы. Вам, сле-
довательно, нужно написать программу, содержащую все про-
цедуры, которые вы хотели бы записать в библиотеку, а за ними
поместить пустую главную программу:
begin
end.
Откомпилируйте эту программу, а затем скопируйте все записи,
кроме последней, в другой файл, который станет вашей библио-
текой.
При пользовании библиотеками возникает принципиальная
опасность, состоящая в том, что компилятор не может проверить
соответствие формальных и фактических параметров. Это озна-
чает, что ответственность за соответствие списков параметров как
по числу, так и по типам параметров, ложится целиком и пол-
ностью на вас. При пользовании библиотечными процедурами
лучше всего совсем не пользоваться сложными типами, за исклю-
чением неупакованных массивов. Если воспользуетесь фортра-
новскими подпрограммами, обращайте внимание на следующее:
(1) Фортрановские типы INTEGER и REAL соответствуют
паскалевским integer и real, за исключением того, что фортранов-
ская подпрограмма может выдавать значение —0, чего Паскаль
не понимает. К счастью —0+0=0, поэтому вы можете решить
эту проблему, прибавляя ко всем фортрановским результатам
перед их передачей из процедуры целый нуль.
(2) Фортрановский логический тип не соответствует булев-
скому типу Паскаля. Фортрановские логические переменные
представляются целыми числами, причем
.FALSE. > 0
.TRUE. < 0
(3) Фортрановские типы, используемые для представления
комплексных чисел и чисел с двойной точностью, отводят для
переменных по два машинных слова. Оба этих типа могут пред-
ставляться в Паскале в качестве массива или записи, содержа-
щих две вещественные компоненты. В Паскале не предусмотрены
какие-либо средства для выполнения операций с двойной точ-
ностью, поэтому с переменными двойной точности в Паскале ни-
чего делать не удается, за исключением их запоминания.
Библиотеки откомпилированных программ появились в то
время, когда компиляторы были громоздкими и медленными, а
загрузчики простыми и быстрыми. С тех пор компиляторы стали
значительно более эффективными, а загрузчики превратились в
360
Приложение В. Реализация языка Паскаль
«причудливые чудовища». Создавать библиотеки откомпилиро-
ванных программ стало гораздо менее удобно, чем раньше, более
привлекательно хранить непосредственно тексты программ.
В частности, это справедливо и для Паскаля, поскольку про-
грамма на Паскале может быть легко откомпилирована за один
просмотр текста программы.
В.8. Режимы компиляции
Компилятор CDC 6000 распознает специальные директивы,
записанные в виде
{$(последовательность режимов)(примечание)}
Последовательность режимов состоит из режимов, разделенных
запятыми. Режим записывается в виде буквы, за которой следует
знак ' или целое число. Каждый режим имеет значение,
принимаемое по умолчанию. Значения режимов, устанавливае-
мые по умолчанию, тщательно подобраны, и мы не рекомендуем
пользоваться директивами без особой на то нужды. Набор режи-
мов, принимаемых по умолчанию, выглядит таким образом
(* §А+, В4, Е—, G—, L+, Р+, R+, Т+, U—, WO, Х4 *)
Результат установки каждого режима описан ниже. Результат
установки режима по умолчанию описан первым.
Эти директивы можно вставлять в текст программы в произ-
вольном месте. Тем самым появляется возможность управлять
распечаткой отдельных частей программы, производить или не
производить проверки во время выполнения в отдельных частях
большой программы.
А+ Выбрать множество символов ASCII.
А— Выбрать множество символов CDC.
В4 Буфера файлов будут содержать по меньшей мере 512 слов.
Вп Буфера файлов будут содержать по меньшей мере 128*п
слов, 1^п^9.
Вп Буфера файлов будут содержать по меньшей мере п слов,
п>10.
Е— Компилятор формирует уникальные символы для локаль-
ных имен модулей, это означает, что идентификаторы, кото-
рые вы даете своим процедурам и функциям, заменяются на
имена типа PRC0017. Заметьте, что такая замена остается
для вас невидимой до тех пор, пока вы не посмотрите на ра-
бочую программу, формируемую компилятором. Такая за-
мена необходима, так как операционная система CDC рас-
В.8. Режимы компиляции
361
познает в качестве имен точек входа только те идентифика-
торы, которые не превышают в длину 7 символов. Это зна-
чит, что паскалевские идентификаторы, отличающиеся
далее чем в седьмом символе, будут восприниматься как
одинаковые. Сказанное не относится к именам процедур,
определенных с помощью описателей extern или fortran'.
эти процедуры получают имена, эквивалентные иденти-
фикаторам процедур или функций, но укороченные до семи
символов.
Е+ В качестве имени точки входа используются первые семь
символов имени локального модуля. Если вы работаете в
этом режиме, вам необходимо убедиться, что все ваши
процедуры и функции можно различать по первым семи сим-
волам.
G— Только компилировать.
G+ Компилировать и исполнить.
I За этой директивой должно обязательно следовать указание
имени записи или имени записи с именем файла. Результа-
том выполнения директивы будет включение в исходную
программу текста из указанной записи указанного файла.
Если имя файла отсутствует, подразумевается имя стандарт-
ного файла. Этот режим дает возможность организовать
библиотеки паскалевских программ в их исходном виде.
L+ Организовать в выходном файле распечатку исходной про-
граммы.
L— Передавать в выходной файл только сообщения об ошибках.
Р+ Сформировать команды, необходимые для «посмертной» вы-
дачи, организуемой либо при выполнении процедуры halt,
либо если программа была снята в результате ошибки.
Р— Не надо формировать команды, необходимые для «посмерт-
ной» выдачи.
R + Длина поля откомпилированной программы будет установ-
лена компилятором.
R— В качестве длины поля откомпилированной программы бу-
дет выбрано наибольшее из чисел, одно из. которых вычис-
ляется компилятором, а другое равно текущей длине поля.
Т + Вставить проверки времени выполнения. Эти проверки
включают:
(1) проверку индексов массивов и диапазонов, чтобы убе-
диться, что переменная входит в эти диапазоны; (2) провер-
ку делителей, чтобы убедиться, что делитель не равен нулю;
(3) проверку преобразования целых чисел в вещественные,
362
Приложение В, Реализация языка Паскаль
которая необходима, так как преобразование для целых
чисел Z, таких, что abs(i)>rnaxint, не может быть выпол-
нено; (4) проверку операторов варианта, чтобы убедиться,
что селектор варианта соответствует какой-либо метке ва-
рианта.
Т— Не включать проверки времени выполнения. Идея состоит
в том, что вы разрабатываете и тестируете программу с
включенными проверками, а после того, как вы убедитесь,
что программа работает правильно, проверки выключаются.
По этому поводу есть одно забавное сравнение: моряки носят
спасательный пояс во время учений на земле, и снимают его
при выходе в море.
U— Компилятор будет обрабатывать только первые 120 симво-
лов каждой входной строчки.
U+ Компилятор будет обрабатывать только первые 72 символа
каждой входной строчки.
W0 Компилятор самостоятельно вычисляет размеры памяти,
отводимой для стека и кучи. Результат такого вычисления,
конечно, носит оценочный характер, но подобных оценок
достаточно для большинства программ.
Wn В программе для стека и кучи будет выделено пространство
из п слов. Если вы хотите указать размеры в восьмеричном
виде, поставьте букву В, например W4000B. Эта директива
используется в паре с директивой R. Режим, устанавливае-
мый по умолчанию (R + , W0), возлагает все на компилятор
и операционную систему; выбрав какой-либо другой режим
(например, R—, W10000B), вы можете изменить эти заранее
принятые значения.
Х4 Для передачи дескрипторов параметров процедурам и функ-
циям используются регистры Хо, Х15 Х2 и Х3. Этот режим
позволяет формировать компактные последовательности
быстро выполняемых команд, при этом возникает риск, что
регистров может не хватить. Реально, однако, для разумных
программ регистров компилятору вполне хватает.
Хп Для передачи дескрипторов параметров будут использо-
ваться регистры Хо, Х15 ... , Хп_!,
ХО Передача дескрипторов параметров будет осуществляться
через стек. Это несколько замедлит выполнение программы
и потребует больше памяти, но зато риск нехватки регистров
будет полностью исключен.
Приложение Г.
СТАНДАРТЫ ПРОГРАММИРОВАНИЯ
Для написания программ не существует каких-то общепри-
нятых стандартов. Многие организации устанавливают свои соб-
ственные стандарты и настаивают на том, чтобы персонал следо-
вал им. Время от времени кое-кто публикует свои индивидуаль-
ные стандарты в надежде привлечь к ним других программистов.
Если вы пользуетесь при программировании стандартами, то де-
лаете ваши программы доступными для изучения другими людь-
ми, работающими в вашей организации, или даже вообще любы-
ми программистами.
Стандарты, приведенные в этом приложении, не следует рас-
сматривать как строгий устав, а скорее — это рекомендаций.
Если вы будете ими пользоваться, то можете быть уверены, что
любой человек, программирующий на Паскале, будет легко раз-
бираться в ваших программах, а вы будете, возможно, совершать
при программировании меньше ошибок.
Вы наверное заметите, что примеры программ, приведенные
в этой книге, написаны с учетом не всех правил, собранных в
приложении. Это объясняется тем, что программы-примеры —
это всего лишь модели программ: они разрабатывались для ил-
люстрации того или иного понятия. Из них исключались приме-
чания, поскольку соответствующие объяснения приводились в со-
провождающем тексте: это делало дополнительные примечания
излишними. Строчки не пропускались в наших примерах просто
для экономии места. Распечатки вводимой информации и органи-
зация выдачи в надлежащем формате также не делались по тем
же причинам.
Г.1. Описание программы
Все программы начинаются с примечания, содержащего по
крайней мере следующую информацию:
короткое название программы;
имя программиста (или ведущего программиста);
описание того, что делает программа;
описание входной информации, потребной для работы про-
граммы, и выдаваемой ею выходной информации.
364
Приложение Г. Стандарты программирования
Если в программе реализуется сложный алгоритм и для его
объяснения требуется много места, дайте ссылки либо на свою
собственную документацию, либо на книгу или журнал, в кото-
рой вы нашли этот алгоритм. Например:
Символическая таблица — это сбалансированное двоичное
дерево. См. Д. Кнут «Искусство программирования для
ЭВМ. Том 3. Сортировка и поиск», Алгоритм А, стр. 455.—
М.: Мир, 1978.
Каждая процедура должна описываться по той же самой схе-
ме. Если программа пишется одним человеком, он не обязан
ставить свое имя перед каждой процедурой, но если программа
создается коллективом авторов, то имя создателя отдельной про-
цедуры нужно ставить обязательно. Документация на процедуру
должна включать описание того, что делает данная процедура,
как она это делает, а также роль каждого параметра.
Г.2. Примечания
Для разъяснения потенциально непонятных мест в вашей
программе пользуйтесь примечаниями или комментариями. Не
засоряйте программу примечаниями, без которых можно обой-
тись, не пишите примечаний, не содержащих ничего, кроме
того, что и так ясно из текста программы. Не пишите, например,
так:
число := число + 1 {увеличить число}
Примечание организуйте яснее. Если оно занимает несколько
строк, все строки должны начинаться в одной колонке, даже если
слева от примечания нет ни одного оператора программы. На-
пример:
var
сумма 1, сумма2, {сумма попыток}
сумкв1, сумкв2, {сумма квадратов попыток}
сумпр {сумма произведений}
: real;
Г.З. Описания и определения
Константы должны быть глобальными. Гораздо легче искать
определение константы, когда имеется только один раздел опре-
деления констант в начале программы, чем когда константы раз-
бросаны по всем процедурам. Вне раздела определения констант
в программе не следует пользоваться константами без имени, от-
личающимися от 0 и 1. При записях в файл можно, однако, непо-
Г.4. Расположение
365
средственно пользоваться такими константами, как строки сим-
волов.
Типы также в большинстве случаев должны быть глобальны-
ми. Однако, если некоторый тип используется только в одной про-
цедуре, он должен быть определен как локальный тип этой про-
цедуры.
Переменные следует описывать в соответствии с их ролью.
Процедуры должны иметь доступ только к своим параметрам и
локальным переменным. Если в процедуре необходим доступ к не-
локальной переменной, нужно по этому поводу подробное при-
мечание. Функции не должны пользоваться нелокальными пере-
менными ни под каким видом.
Процедуры и функции можно определять локально, т. е.
вкладывать одно описание в другое, но программу от этого не
обязательно легче читать. Кроме того, некоторые компиляторы
ограничивают возможные уровни вложенности.
В качестве имен констант, типов, переменных, процедур и
функций старайтесь выбирать имена, проясняющие роль этих
объектов.
Г.4. Расположение
Перед разделами определения меток, констант, типов и разде-
лом описания переменных оставляйте по одной пустой строке.
Перед описанием процедуры или функции оставляйте по две или
более пустые строки. Для разделения сложных частей программ
на блоки, достаточно простые, чтобы быть понятыми другим
программистом, также пользуйтесь пустыми строками.
В дальнейших примерах S2, ... , Sn— это операторы
(возможно,составные), b — булевское выражение, I — скаляр-
ная переменная, а выр{ и выр2 — скалярные выражения. Распо-
ложение составного оператора:
begin
Sn
S2;
s’
end
Расположение условного оператора
if b
then Sx
else S2
Если и S2 есть составные операторы, используйте такое рас-
положение
366
Приложение Г. Стандарты программирования
if b
then
St
else
S2
Расположение оператора цикла с пост-условием:
repeat
until b
Расположение оператора цикла с пред-условием:
while b do
Sx
Расположение оператора цикла с параметром:
for i := выр1 to выр2 do
Sx
Расположение оператора варианта (с есть селектор варианта,
а alf а21 ... , ап — метки вариантов):
case с of
end {case}
Расположение оператора присоединения (з есть идентификатор
записи):
with з do
Si
Если оператор, стоящий внутри какого-либо оператора цикла
или оператора присоединения,— составной, дайте примечание
к заключительному слову end. Например:
while b do
begin
Si;
S2;
Г.5. Переносимость
367
end {while}
Оставляйте по одному пробелу с каждой стороны знаков '=' и
в разделах описания и ' в операторах присваивания (если
только для этого есть место). В примечаниях после символа '{'
ставьте пробел, не забывайте его и перед символом
Слово program всегда пишите в самой левой позиции. В этой
книге мы выделяли слова const, type, var, procedure и function,
отступая от крайней позиции, но этого делать не обязательно,
если не возникает путаницы. Вложенность процедур должна обя-
зательно отмечаться соответствующим отступлением от края.
Г.5. Переносимость
Программа называется переносимой (или мобильной), если без
каких-либо изменений она может выполняться в системах, от-
личных от той, для которой она разрабатывалась. Когда бы вы ни
писали программу, всегда учитывайте возможность ее переноса
на другую систему. Если для такого предположения есть какие-
либо основания, вам следует, насколько это возможно, сделать
программу машинно-независимой. К сожалению, Паскаль не
был определен таким образом, чтобы облегчить написание пере-
носимых программ. Ниже перечислены правила, которым нуж-
но следовать при написании переносимых программ, однако
переносимости они еще не гарантируют.
Не делайте предположений о допустимом множестве симво-
лов. Безопасными являются предположения, что
'a'C'b'. . .<'z'
'0'<Т. . .<'9'
Если в программе есть преобразования чисел, возможно, вам
захочется воспользоваться тем, что
ord ('и')—ord ('О')=п, 0^п<^9,
но гораздо безопаснее:
case и of
'О' : значение : = 10 * значение;
'Г : значение := 10 * значение +1;
Не надо предполагать, что
succ('a') = 'b'
succ('b') = 'c'
и т. д.,
368 П риложение Г. Стандарты программирования
поскольку есть по крайней мере одно множество символов, для
которого это неверно.
Не пользуйтесь типом set of char, а также ограничениями
символьного типа.
Ограничение
'О' . . '9'
довольно безопасно, но ограничения типа
. 7'
могут привести к неверным результатам или вообще могут
оказаться запрещенными.
Не используйте записей с вариантами без полей признаков,
и, следовательно, не пользуйтесь свободными объединенными
типами для трюков вроде печати значения ссылки.
Всюду, где вы печатаете значения целых или вещественных
чисел, включайте указание параметров ширины поля и точности.
Не используйте стандартных процедур или стандартных функ-
ций в качестве фактических параметров процедур или функций.
Не пользуйтесь стандартными идентификаторами, не описан-
ными в Сообщении о Паскале.
Не предполагайте нереально высокой точности проведения
вещественных вычислений. Для определения сходимости алго-
ритма применяйте константы (с именем), определения которых
вставляйте в начало программы с соответствующими приме-
чаниями.
Тщательно документируйте все части программы, которые,
как вам кажется, могут оказаться машинно-зависимыми.
Г.6. Автоматическое форматирование
Многие вычислительные системы располагают программами,
которые производят автоматическое форматирование паскалев-
ских программ. Делать это относительно легко, так как уровень
вложенности и, следовательно, требуемые отступы могут быть
определены путем анализа зарезервированных слов программы.
Пользоваться или нет форматирующей программой — это во мно-
гом дело вкуса. Программистов, которые пишут программы,
располагая операторы по смыслу, никогда не заставишь прибе-
гать к автоматическому форматированию программ. Однако мно-
гие придерживаются мнения, что с подсчетом пробелов машина
справится лучше, и предпочитают пользоваться форматирующей
Г.6. Автоматическое форматирование
369
программой. Иногда форматную распечатку производит сам ком-
пилятор, не оставляя программистам никакого выбора:
В пользу автоматического форматирования имеются два аргу-
мента, важные даже для тех, кто гордится своим собственным спо-
собом расположения операторов. Во-первых, форматирующие
программы универсальны и одинаково доступны всем, поэтому
если в группе программистов все пользуются одной и той же фор-
матирующей программой, то им легко разбираться в программах,
написанных коллегами. Во-вторых, хотя в связи с новой про-
граммой не возникает трудностей в расположении, исправление
старой программы с сохранением правильного расположения
текста может оказаться чрезвычайно трудным делом.
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ
Абстрактные типы (abstract types) 134
Абстракция (abstraction) 91
Автоматическое преобразование (automatic conversion) 212
— форматирование (formatting) 368—369
Алгоритм (algorithm) 289, 322, 368
Аргумент (argument) 17, 22, 41, 45, 49, 60, 95
— скалярный (scalar) 136
Базовый тип (base type) 165, 207
----массива (of array) 173
----множественный (set) 139—140
----скалярный (scalar) 137—138
----файла (of file) 207
Блок (block) 57, 58, 103—104
Библиотека программ (program library) 292
Булевская переменная (boolean variable) 51—54
Булевский массив (boolean array) 179—182, 268
— тип (type) 7, 38, 51—54, 179, 212—213
Булевское выражение (boolean expression) 40—41, 51—53, 67—68, 298
— значение (value) 28, 51—54, 136
Вариантная часть записи (variant part of record) 187
Верификация программ (program verification) 292, 297—307
Верхняя граница (upper bound) 138
Вершина (vertice) [см. Узел]
Вещественная переменная (real variable) 19, 85
Вещественный тип (real type) 19, 38, 43, 46—50, 85, 106, 137, 149, 165, 212—213
Виртуальная машина (virtual machine) 20—23
Вложенные процедуры (nested procedures) 103, 105
Внешние процедуры (external procedures) 358—360
Вызов процедуры (procedure call) 91, 105—106
----рекурсивный (recursive) 112
Выполнение (executing) 13, 20
Выражение (expression) 26, 31, 39—41, 42, 61—63, 102—103
— булевское (boolean) 40—41, 51—53, 67—68, 298
— вещественное (real) 48, 103, 106
— ограниченного типа (subrange) 151
— простое (simple) 61, 62
— скалярное (scalar) 151
— целое (integer) 103, 106
Генератор псевдослучайных чисел (pseudo-random generator) 128
Глобальные идентификаторы (global identifiers) 34,94, 155—156
Предметный указатель
371
— переменные (variables) 94, 100—103, 105, 304
Граница (bound) 138
— верхняя (upper) 138
— нижняя (lower) 138
Данные (data) 13, 19, 20—24, 31
— динамические (dynamic) 236—237
— локальные (local) 96
— статические (static) 236
Двусвязные кольца (doubly linked rings) 250—253
Дерево (tree) 264—271
— двоичное (binary) 265—268
----поиска (search) 266—268, 310
Диапазон (range) 137, 138—139
— представления чисел 46—47
Динамические данные (dynamic data) 236—237
— переменные (variables) 284, 353
Директива (directive) 110, 358
Дискриминант (discriminant) 189, 309
Доказательство правильности (proof of correctness) [см. Верификация программ]
Заголовок (heading) 57, 58
— программы (program) 92, 103—104, 207
— процедуры (procedure) 93, 103
— функции (function) 107
Загрузчик (loader) 358—360
Запись (record) 163, 184—193, 264
— с вариантами (with variants) 186—190
— упакованная (packed) 190
Зарезервированные слова (reserved words) 8, 25, 31, 183, 337
Значение (value) 34—35, 40, 45, 106
— булевское (boolean) 28, 51—54, 136
— скалярное (scalar) 136—137
— стандартное (standard) 237
— функции (function) 165, 185
Идентификатор (identifier) 25, 31—34, 183, 337
— глобальный (global) 34, 94, 155—156
— локальный (local) 34, 94—96
— нелокальный (non-local) 103
— процедуры (procedure) 103
— стандартный (standard) 31, 103, 147, 338, 368
— типа (type) 147
Иерархическая структура (hierarchical structure) 17, 91, 292? 322
Избыточность (redundance) 9, 24
Имя (name) 34—35, 40, 91
Инвариант цикла (loop invariant) 301—304
Индексный тип (index type) [см. Тип индексов]
Исходная программа (source program) 20
Итерация (iteration) 115—117
Ключевые слова (key words) [см. Зерезервированные слова]
Команда (instruction) 13—17, 289
372
Предметный указатель
Комбинированный тип (record type) 163, 184—193, 236
Комментарий (comment) [см. Примечание]
Компилятор (compiler) 20—24, 85—86, 291, 296
Компиляция (compilation) 20
Компонента (component)
— записи (record) 184—185, 192
— массива (array) 163—165
— файла (file) 207—208
Конечный автомат (finite state machine) 154
Конкретизация (refining) 292—295
Константы (constants) 34—38, 42, 46, 364
— вещественные (real) 46
— скалярного типа (scalar) 135
— целые (integer) 42, 46
Корень дерева (root) 264
Куча (heap) 284, 353
Лексема (symbol) 146
Лист (leaf) [см. Терминальный узел]
Локальные идентификаторы (local identifiers) 34, 94—96
— переменные (variables) 94—95, 100—101
— типы (types) 134, 157, 365
Массив (array) 163—184, 323
— булевский (boolean) 179—182, 268
— многомерный (multidimensional) 173—175
— упакованный (packed) 177—179
Метка (label) 275—278
— варианта (case label) 152
— ленты (tape-mark) 233
Метод последовательных приближений 292—293
— «разделяй и властвуй» 292—293
Многомерный массив (multidimensional array) 173—175
Многофайловая структура (subfile structure) 233—234, 356
Множественный тип (set type) 139—140, 149
Множество (set) 139—148, 179, 351
— пустое (empty) 140
— символов (character) 145
Множитель (factor) 61,63, 106, 109—110, 165
Мобильность (portability) 323, 367—368
Моделирование параллельных процессов (simulation of concurrent processes)
263-264
Направленный гр^ф (directed graph) 264
Нелокальная переменная (non-local variable) 126—127, 365
Нижняя граница (lower bound) 138
Область значений (range) 41
— определения (domain) 41, 95
Общая часть записи (fixed part of record) 187
Объединение множеств (union of sets) 140
Объединенный тип (union type) 189, 253
Ограниченный тип (subrange type) 137—139
Предметный указатель
373
Окружение (environment) 18, 206
Оператор (statement) 58—60, 91, 277, 294
— варианта (case statement) 151 —155, 188—189, 294, 306
— перехода (goto statement) 274—278
— присваивания (assignment operator, assignment statement) 26, 40, 59—60,
102, 141, 164, 185, 210, 238—239, 287—288, 305
— присоединения (with statement) 186, 192
— составной (compound) 58—59, 68—69, 78, 93, 294
— условный (if statement) 16, 25, 28, 67—73, 294, 305
• составной (compound) 70—71, 294
— цикла 294
----с параметром (for statement) 84—86, 137, 306
•---------постусловием (repeat statement) 19, 73—77, 306
----------предусловием (while statement) 59, 77—79, 137, 306
Операции (operators) 41—43
— инфиксные (двуместные) (infix) 41—42
— над ссылками (on pointers) 253—254
— отношения (relational) 51—52, 55, 136, 140—141
— разыменования (dereferencing) 239
— сравнения (comparison) [см. Операции отношения]
— унарные (одноместные) (unary) 42
Операционная система (operating system) 18, 206, 296, 307, 308
Описание (declaration) 14, 39—40, 57
— массива (array) 164
— переменных (variable) 39—40
— программы (program description) 363—364
Определение (definition) 34, 35—36, 57, 364—365
— типа (type) 134
Отладка (debugging) 254, 307—309
Очередь (queue) 244—246
Ошибки (errors) 21—24, 136—137, 146—147, 215—216, 308—309
— во входных данных (input data) 24
— времени выполнения (run-time) 22, 23—24, 49, 138
----------компиляции (compile-time) 21—22, 23, 24
Параметр (parameter) 49, 95, 100—103, 185, 190, 364
— значение (value) 102—103, 150—151, 164, 283
— переменная (variable) 96, 101 —103, 164, 177, 190
— фактический (actual) 49, 96, 101 —103, 105—106, 149—150, 207
— формальный (formal) 96, 101 — 103, 105—106, 149—150, 207
Паскаль-машина (Pascal machine) 20—24
Переменная (variable) 19, 38—39, 46, 96, 100—103, 365
— булевская (boolean) 51—54
— вещественная (real) 19, 85
— глобальная (global) 94, 100—103, 105, 304
— динамическая (dynamic) 284, 353
— локальная (local) 94—95, 100—101
— множественного типа (set type) 139, 141
— нелокальная (non-local) 126—127, 365
— ограниченного типа (subrange type) 138
— полная (entire) 287
— простая (simple) 163
— сложная (structured) 163
— ссылочная (pointer) 285
— строковая (string) 183
— целого типа (integer) 139
374
Предметный указатель
Переносимость (portability) [см. Мобильность]
Пересечение множеств (intersection of sets) 140
Перечислимый тип (enumerated type) 134
Побочный эффект (side effect) 126—127, 304, 308
Повторение (repetitive structure) 16
Подмножество (subset) 140
Поле записи (record field) 190
Полная переменная (entire variable) 287
Последовательность (sequence) 13, 16, 18
Постусловие (post-condition) 298—307
Предварительное описание (forward) ПО
Предикат (predicate) [см. Условие]
Предусловие (pre-condition) 298—307
Преобразование типов (type conversion) 41,43—44, 48
— неявное (implicit) 43—44, 212
Признак (tag) 187—190, 308, 368
Примечание (comment) 26, 61—63, 153—155, 364
Пробелы (spaces) 43—44, 49—50, 53—54, 56, 61—63
Программа (program) 13, 57, 103—104, 289
— автобусы 260—263
— выборка 144—145
— гаммы 142—143
— гармонический ряд 84—85
— головоломка 78—79
— делители 27
— исключитьпримечания 154—155
— калькулятор 117—126, 155—161
— квадратное 72—73
— квадратныекорни 19—20, 21, 22—23, 24, 25
— квадратныйкоренъ 18—19, 22
— квадратныйкореньиздвух 17—18
— копироватьколоду 211
— копироватьсписок 250
— минимакс 28
— найтиквадратныекорни 76—77
— обработатьфайл 232—233
— обработкатаблицы 225—228
— объемсферы 130
— окружности 195—198
— перекрестныессылки 317—322
— переупорядочитьсписок 243—244
— подсчетсимволов 29, 30
— поисксмежных 176—177
— преобразованиегрупп 212—213
— преобразовать 80—82
— проверкаосвобождения 284—285
— простая 100—102
— простыечисла 180—181
— ряды 74
— сдвоенныесимволы 29
— системысчисления 167
— сортировкашелла 172—173
— среднее 82—84
— суммагармоник 98—100
— сфера 63—64
— таблицастепеней 26
— убратьмножители 92—97
Предметный указатель
375
— уравнения 281—283
— форматы 44
— ханойскиебашни ПО—115
— частотныйсловарь 199—202, 268—270
— частоты 86—88
Производный множественный тип (associated set type) 139—140
Произвольный доступ (random access) 233—234
Просмотр (traversing)
— двоичного дерева (binary tree) 265—266
— вперед (look ahead) 294
— списка (list) 243
Простая переменная (simple variable) 163
Простое выражение (simple expression) 61,62
Простой тип (simple type) 147
Процедура (procedure) 16, 17, 91 —106, 126—127, 292
— вложенная (nested) 103, 105
— внешняя (external) 358—360
— как параметр (as parameter) 2££—283
— локальная (local) 105, 365
— рекурсивная (recursive) 117
Процесс (process) 13, 19—20, 53, 206, 263—264, 297
Процессор (processor) 13, 19—20, 263—264
Псевдослучайная последовательность (pseudo-random sequence) 127—128
Пустое множество (empty set) 140
Рабочая программа (object program) 21—24, 297
Раздел описания меток (label declaration section) 276
---переменных (variable) 39—40
— определения констант (constant definition) 35—36, 50, 157
---типа (type) 134
Разделитель (separator) 58
Разность множеств (difference of sets) 140
Разработка (design) 290
Расположение текста (layout) 365—367
Распределение памяти (memory allocation) 283—288
Реализация (implementation) 290, 350—362
Ребро (edge) 264
Регулярный тип (array type) 163, 183, 236
Режимы компиляции Версии 3 (compiler options) 360—362
Результат (effect) 13, 106
— процедуры (procedure) 106
Рекурсивная обработка списков (recursive list processing) 249—250
— структура данных (data structure) 117, 264—265
Рекурсивные функции (recursive functions) 116—117
Рекурсивный вызов (recursive call) 112
Рекурсия (recursion) 59, 103, НО—117
— косвенная (indirect) 117
— прямая (direct) 117
Решение (decision) 14, 16, 19, 25
Решето Эратосфена (sieve of Eratosthenes) 180—181
Свободное объединение (free union) 189, 253, 368
Связанные списки (linked lists) 240—250
Сегментированные файлы (segmented files) 357—358
Селектор варианта (case selector) 152, 188
376
Предметный указатель
— записи (record) 184
Символьный тип (character type) 29, 38—39, 54—57, 137, 139, 212—213
Синтаксические диаграммы (syntax disgrams) 32—33, 36—38, 58—60, 62—63,
68—69, 75, 79, 80, 86, 103—105, 109, 147—148, 155, 165, 190, 276, 341—349
Скалярное выражение (scalar expression) 151
— значение (value) 136—137
Скалярный аргумент (scalar argument) 136
— тип (type) 134, 137, 139, 188
----базовый (base) 137—138
Сканер (scanner) 146
Слагаемое (term) 61—62
Слово (word) 15—16, 177, 351
Сложная переменная (structured variable) 163
Сложный тип (structured type) 163
Случайная последовательность (random sequence) 127—128
Сортировка (sorting) 168—173
— Шелла 169
Составной оператор (compound statement) 58—59, 68—69, 78, 93, 294
— условный оператор (if statement) 70—71, 294
Составные символы (compound symbols) 61
Список (list) 240—271
— двусвязный (doubly linked) 250—253
— параметров (parameter) 96, 103—104
— полей (field) 190
— свободной памяти (free list) 285
— связанный (linked) 240—250
Ссылка (pointer) 237—240, 264
— вперед (forward) 251
— назад (backward) 251
Ссылочная переменная (pointer variable) 285
Ссылочный тип (pointer type) 237, 253
Стандарт языка Паскаль 9, 32, 36, 54—55, 350
--------, Версия 3 350—362
Стандартное значение (standard value) 237
Стандартные идентификаторы (standard identifiers) 31, 103, 147, 338, 368
— константы (constants) 28, 41, 51, 338, 351—352
----Версии 3 351
— процедуры (procedures) 338, 368
----Версии 3 353, 354, 357—358
— типы (types) 31,38—57, 311,338
----Версии 3 352
— файлы (files) 207, 338
---- Версии 3 356—358
— функции (functions) 41, 45—47, 103, 106, 291—292, 338, 368
---- Версии 3 354
Статические данные (static data) 236'
Стек (stack) 244, 284, 353
Строка (string) 25
— символов (character) 182—184
Строковая переменная (string variable) 183
Структура (structure) 14—16, 20, 25, 31, 264, 295
— данных (data) 286, 322
----динамическая (dynamic) 236—271
— многофайловая (subfile) 233
Таблица (table) 224
Тело процедуры (procedure body) 93, 96, 103
Предметный указатель
317
Терминальный узел (terminal node) 264
Тестирование (testing) 290
— исчерпывающее (exhaustive) 295
— случайное (sandom) 295
Тип (type) 38—41, 365
— абстрактный (abstract) 134
— базовый (base) 165, 207
---массива (of array) 173
---скалярный (scalar) 137—138
---файла (of file) 207
— булевский (boolean) 7, 38, 51—54, 179, 212—213
— вещественный (real) 19, 38, 43, 46—50, 85, 106, 137, 149, 165, 212—213
— выражения (expression) 40—41
— индексов (index) 163—165
— комбинированный (record) 163, 184—193, 23§
— компонент (component) 163—165
— локальный (local) 134, 157, 365
— множественный (set) 139—140, 149
---базовый (base) 139—140
• производный (associated) 139—140
— объединенный (union) 189, 253
— ограниченный (subrange) 137—139
— перечислимый (enumerated) 134
— простой (simple) 147
— регулярный (array) 163, 183, 236
— скалярный (scalar) 134, 137, 139, 188
— сложный (structured) 163
— символьный (character) 29, 38—39, 54—57, 137, 139, 212—213
— ссылочный (pointer) 237, 253
— стандартный (standard) 31,38—57, 211, 338
— упорядоченный (ordered) 40, 149
— файловый (file) 163, 207, 210
— функции (function) 107, 109—110
— целый (integer) 38, 41—46, 106, 137, 139, 149, 212—213
Типы (types)
— идентичные (identical) 148—150, 184
— совместимые (compatible) 148—150, 184
---для присваивания (assignment-compatible) 148—151
Точность представления чисел (precision) 46—48, 368
Узел (node) 264
— терминальный (terminal) 264
Указатель (reference) [см. Ссылка]
Упакованные записи (packed records) 177—179
— массивы (arrays) 190
Упорядоченность (ordering) 136
Упорядоченный тип (ordered type) 40, 149
Уровень (level) 25, 91, 276, 292, 294, 368
Условие (condition) 67—68, 73, 77
— окончания цикла (termination condition of a loop) 67, 73—75
Условный оператор (if statement) 16, 25, 28, 67—73, 294, 305
Файл (file) 29, 57, 82—83, 206—222, 323
— последовательный (sequential) 207—211
— стандартный (standard) 207, 338
378
Предметный указатель
— текстовый (text) 211—215
Файловый тип (file type) 163, 207, 210
Фактический параметр (actual parameter) 49,96, 97, 101 —103, 149-
Формальный параметр (formal parameter) 96, 97, 149—150, 207
Функция (function) 106—117, 126—127
— как параметр (as parameter) 278—283
— рекурсивная (recursive) 116—117
— стандартная (standard) 41, 45—47, 103, 106, 291—292, 338, 368
150, 207
Целый тип (integer type) 38, 41—46, 106, 137, 139, 149, 212—213
Цикл (loop) 16—17, 19, 26, 67, 73—88
Эффективность (efficiency) 323
abs 45—46, 49
al fa 352
and 8, 29, 51, 61
arctan 45—46, 49
array 8, 163
begin 8, 18, 25, 28, 58
boolean 7, 8, 28
case 8, 151
chr 45, 55—56
const 8, 35
cos 45—46, 49
dispose 283—288, 353
div 8, 41—43
do 8, 28
downto 8, 86
else 8, 67—68
end 8, 18, 25, 28, 58, 155, 188
eof 29, 45, 53, 207, 210, 211, 233—234
eoln 45, 53, 207, 214
exp 45—46, 49
false 28, 51—54, 68
file 8, 207
for 8, 26, 84
forward 110
function 8, 109
get 209—210, 214, 233—234
goto 8, 275
Предметный указатель
379
halt 24
if 8, 67
in 8, 141
input 18, 53, 206—207, 210, 211,214, 215
integer 26
label 8
In 45—46, 49
maxint 28, 41, 128, 351
mod 8, 27, 41—42
nil 8, 237—238
new 241,283—288, 353
not 8, 29, 51, 61
odd 45, 53, 77
or 8, 51, 61
ord 45, 55—56, 136—137, 351
of 8
output 18, 206—207, 210, 211, 214, 215, 220
pack 178—179
packed 8, 177
page 220
pred 45, 136
procedure 8
program 8, 18, 57
put 209, 234
read 18, 43, 49, 54, 56, 60, 79—80, 82—83, 138, 183, 207, 211—215
readln 214—215, 222
record 8, 139—140
repeat 8, 19, 25, 75
reset 209
rewrite 208
round 41, 45, 49
set 8
sin 45—46, 49
sqr 22—23, 26, 45—46, 49
sqrt 17, 19, 22, 45-46, 49, 106—107
succ 45, 85, 136—137
text 211
then 8, 67—68
to 8, 85—86
380
Предметный указатель
true 28,51,68
trunc 45, 49
type 8
undefined 353
unpack 178—179
until 8, 19, 25, 75
var 8, 19, 96, 98, 309
while 8, 28, 77
with 8, 186
write 17, 24, 43—44. 49, 53—54, 56—57, 60, 138, 207, 211—215
writein 26, 60—61, 214—215, 222
ОГЛАВЛЕНИЕ
От редактора перевода................................... . , 5
Предисловие к пересмотренному изданию.......................... 9
Предисловие к первому изданию................................... И
Глава 1. Принципы программирования...................... . . . 13
1.1. Программы............................................. 13
1.2. Структура программы................................... 14
1.3. Неформальное введение в Паскаль....................... 17
1.4. Компиляция и выполнение............................... 20
1.5. Представление программы и примеры..................... 25
Глава 2. Данные, выражения и присваивания ..................... 31
2.1. Идентификаторы........................................ 31
2.2. Константы............................................. 34
2.3. Данные................................................ 38
2.4. Целый тип............................................. 41
2.5. Вещественный тип...................................... 46
2.6. Булевский тип......................................... 51
2.7. Символьный тип ....................................... 54
2.8. Построение программы.................................. 57
Глава 3. Условия и циклы....................................... 67
3.1. Условный оператор..................................... 67
3.2. Оператор цикла с пост-условием...................... 73
3.3. Оператор цикла с пред-условием........................ 77
3.4. Оператор цикла с параметром . . . .................... 84
Глава 4. Процедуры и функции.................................
4.1. Процедуры............................................. 91
4.2. Функции.............................................. 106
4.3. Рекурсия.............................................. ПО
4.4. Нелокальные переменные и побочные эффекты............ 125
4.5. Псевдослучайные числа................................ 127
Глава 5. Переменные типы...................................... 134
5.1. Скаляры.............................................. 134
5.2. Ограниченные типы.................................. 137
5.3. Множества............................................ 139
5.4. Отношения между типами............................... 148
5.5. Оператор варианта.................................... 151
5-6. Пересмотр программы калькулятор...................... 155
382
Оглавление
Глава 6. Сложные типы.......................................... 163
6.1. Массивы............................................... 163
6.2. Записи ............................................... 184
Глава 7. Файлы ................................................ 206
7.1. Последовательные файлы................................ 207
7.2. Текстовые файлы....................................... 211
7.3. Ввод и вывод.......................................... 215
7.4. Примеры............................................... 222
7.5. Многофайловая структура данных........................ 233
Глава 8. Динамические структуры данных......................... 236
8.1. Ссылки................................................ 237
8.2. Связанные списки...................................... 240
8.3. Пример: Моделирование дискретных событий.............. 254
8 4. Деревья .......................................... 264
Глава 9. Дополнительные возможности языка...................... 274
9.1. Оператор перехода..................................... 274
9.2. Процедуры и функции как параметры..................... 278
9.3. Распределение памяти.................................. 283
Глава 10. Разработка программы................................. 289
10.1. Составление программы.............................. 290
10 2. Тестирование и верификация.......................... 295
10.3. Отладка.............................................. 307
10.4. Пример: Генератор перекрестных ссылок................ 309
10.5. Оценка языка Паскаль................................. 322
Литература для дальнейшего чтения ...... ...................... 326
Приложение А. Словарь языка Паскаль........................... 337
А.1. Зарезервированные слова............................... 337
А.2. Идентификаторы........................................ 337
А.З. Знаки препинания ..................................... 338
Приложение Б. Синтаксис языка Паскаль........................ 340
Приложение В. Реализация языка Паскаль........................ 350
В.1. Стандартные типы...................................... 351
В.2. Арифметика............................................ 353
В.З. Стандартные процедуры и функции...................... 353
В.4. Ввод и вывод.......................................... 354
В 5. Файлы................................................. 356
В 6. Сегментированные файлы............................ 357
В.7. Внешние процедуры..................................... 358
В.8. Режимы компиляции..................................... 360
Приложение Г. Стандарты программирования...................... 363
Г. 1. Описание программы................................... 363
Г.2. Примечания........................................... 364
Г 3. Описания и определения................................ 364
Г.4. Расположение ......................................... 365
Г.5. Переносимость......................................... 367
Г.6. Автоматическое форматирование . ..................... 368
Предметный указатель .......................................... 370
УВАЖАЕМЫЙ ЧИТАТЕЛЬ!
Ваши замечания о содержании
книги, ее оформлении, качестве пере-
вода и другие просим присылать по
адресу: 129820, Москва, И-110, 1-й Риж-
ский пер., д. 2, издательство «Мир».
Питер Грогоно
ПРОГРАММИРОВАНИЕ НА ЯЗЫКЕ ПАСКАЛЬ
Научный редактор К. Г. Батаев
Мл. научный редактор Э. Г. Иванова
Художник В. М. Новоселов
Художественный редактор В. И Шаповалов
Технический редактор В. П. Сизова
Корректор Л. В. Байкова
ИБ №2949
Сдано в набор 26.10.81.
Подписано к печати 14.01.82.
Формат бОХЭО1/^-
Бумага типографская № 2.
Гарнитура литературная. Печать высокая.
Объем 12 бум. л. Усл. печ. л. 24-Усл. кр. готт. 24,48.
Уч.-изд. л. 17,22 Изд. № 1/1607.
Тираж 30 000 экз. Заказ Ка 3388. Цена 1 р. 30 к.
ИЗДАТЕЛЬСТВО «МИР»
Москва, 1-й Рижский пер., 2
Ордена Октябрьской Революции
и ордена Трудового Красного Знамени
Первая Образцовая типография имени А. А. Жданова
Союзполиграфпрома при Государственном комитете СССР
по делам издательств, полиграфии и книжной торговли.
Москва, М-54, Валовая, 28
"СКИТ
ы
* k л!
О
МАТЕМАТИЧЕСКОЕ ОБЕСПЕЧЕНИЕ ЭВМ