Текст
                    г.джонстон
УЧИТЕСЬ
ПРОГРАММИРОВАТЬ

ЕДЖОНСТОН УЧИТЕСЬ ПРОГРАММИРОВАТЬ
LEARNING TO PROGRAM HOWARD JOHNSTON Department of Computer Science Queens University Belfast Prentice-Hall International UK Ltd
г.джонстон УЧИТЕСЬ ПРОГРАММИРОВАТЬ Перевод с английского С.В. АРУТЮНОВА Г.В. СЕНИНА И.Ф.ЮГАНОВА Под редакцией и с предисловием Г.В. СЕНИНА Москва «Финансы и статистика» 1989
ББК 24.4.1 Д40 Джонстон Г. Д40 * Учитесь программировать/Пер. с англ.; под ред. и с предисл. Г.В.Се- нина. - М.: Финансы и статистика, 1989. - 368 с.: ил. ISBN 5-279-00251-8 Книга британского исследователя Г.Джонстона представляет собой проинтерпре- тированный и развитый автором курс лекций профессора К.Хоара по искусству программи- рования. Методика построения и анализа программ, выбора алгоритма решения иллюстриро- вана программами минимальной сложности, реализованными на языке Паскаль. Является удачным руководством для систематического освоения начал профессионального програм- мирования. Для специалистов, постигающих и совершенствующих мастерство программиро- вания, преподавателей вузов и школ, студентов. 2404010000-136 010(01)-89 ББК 24.4.1 131-89 ISBN 5-279-00251-8 (СССР) ISBN 0-13-527754-Х (Великобритания) © 1985 by Prentice-Hall International, UK Ltd © Перевод на русский язык, предис- вие, ’’Финансы и статистика”, 1989
ОГЛАВЛЕНИЕ ПРЕДИСЛОВИЕ К РУССКОМУ ИЗДАНИЮ......................................9 ПРЕДИСЛОВИЕ РЕДАКТОРА СЕРИИ........................................14 ПРЕДИСЛОВИЕ...................................................... 15 1 ПРОЦЕССЫ, ПРОЦЕССОРЫ И ПРОГРАММЫ...................................18 1.1. Обработка информации..........................................18 1.2. Компьютерные системы..........................................20 1.3. Небрежные люди и послушные компьютеры.........................22 1.4. Выбор языка программирования..................................23 1.5. Паскаль-системы...............................................24 2 КАК ЧИТАТЬ ПРОГРАММЫ...............................................26 2.1. Переменные, типы данных и операторы...........................26 2.2. Трассировка программ..........................................28 2.3. Константы.....................................................30 2.4. Что такое структура в программировании........................32 2.5. Последовательное выполнение...................................32 2.6. Выбор.........................................................33 2.7. Повторение....................................................36 2.8. Сочетание программных конструкций.............................39 2.9. Расположение текста программы и пунктуация....................40 Упражнения.........................................................42 3 ПРОСТЕЙШИЙ ВВОД И ВЫВОД............................................44 3.1. Символы.......................................................44 3.2. Текстовые файлы input и output................................46 3.3. Ввод и вывод символов.........................................48 3.4. Вывод на новую строку.........................................52 3.5. Ввод и вывод целых чисел......................................56 3.6. Вывод символьных строк........................................58 Упражнения.........................................................63 4 КОММЕНТАРИИ И ПРОЦЕДУРЫ............................................67 4.1. Комментарии...................................................67 4.2. Процедуры.....................................................69 4.3. Многократный вызов процедур...................................72 4.4. Локальные константы и переменные в процедурах.................72 4.5. Доступ к локальным константам и переменным....................77 Упражнения.........................................................80 5 СИНТАКСИС..........................................................84 5.1. Синтаксические диаграммы......................................85 5.2. Расширенная форма Бэкуса-Паура................................87 5.3. Синтаксический анализ.........................................89 Упражнения.........................................................92
6 Оглавление 6 НЕКОТОРЫЕ СВЕДЕНИЯ О СТАНДАРТНОМ ПАСКАЛЕ..............95 6.1. Лексемы Паскаля...................................................95 6.2. ’’Пустоты” в программе............................................101 6.3. Выражения........................................................102 6.3.1. Старшинство операций и скобки в выражениях..................106 6.4. Операторы........................................................108 6.5. Структура программы..............................................114 6.5.1. Определение идентификатора и область его действия...........114 6.5.2. Доступ к константам, переменным и подпрограммам.............118 Упражнения............................................................120 7 ПОСТРОЕНИЕ ПРОГРАММ....................................................122 7.1. Программы и процессоры...........................................122 7.2. Поэтапное уточнение..............................................123 7.3. Программы без процедур...........................................124 7.3.1. Простая программа обработки текста..........................124 7.3.2. Программа численного расчета................................130 7.4. Программы с процедурами..........................................132 7.5. Альтернативы при разработке......................................135 7.6. Влияние языка....................................................141 Упражнения.......................................................... 142 8 ОШИБКИ ПРОГРАММИРОВАНИЯ................................................144 8.1. Логические ошибки................................................144 8.2. Некорректные данные..............................................146 8.3. Ограничения, накладываемые оборудованием.........................147 8.4. Устойчивые программы.............................................149 Упражнения............................................................154 9 ПРЕДОТВРАЩЕНИЕ, ОБНАРУЖЕНИЕ И ИСПРАВЛЕНИЕ ОШИБОК156 9.1. Ответственность программиста.....................................156 9.2. Предотвращение ошибок............................................157 9.3. Подбор тестовых данных...........................................158 9.4. Обнаружение ошибок: ручная проверка..............................160 9.5. Обнаружение ошибок: тестирование программы.......................171 9.6. Исправление ошибок...............................................173 Упражнения............................................................174 10 БУЛЕВЫ ВЫРАЖЕНИЯ И ПЕРЕМЕННЫЕ..........................................175 10.1. Булевы переменные...............................................175 10.2. Булевы операции.................................................177 10.3. Скобки в булевых выражениях.....................................179 10.4. Законы де Моргана...............................................180 10.5. Ввод и вывод булевых значений...................................180 10.6. Булевы выражения и циклы........................................182 Упражнения............................................................187
Оглавление 7 11 ПРОЦЕДУРЫ С ПАРАМЕТРАМИ..................................................................189 11.1. Доступ к информации...............................................................189 11.2. Процедуры с параметрами............................... .. ......................190 11.2.1. Параметры, передаваемые по значению..........................................191 11.2.2. Параметры, передаваемые по ссылке........................................................................... 195 11.3. Синтаксис параметров процедуры....................................................199 11.4. Чем выгодны параметры.............................................................201 11.5. Какой вид параметра выбрать ......................................................204 11.6. Программа с параметрами...........................................................205 Упражнения...............................................................................210 12 ФУНКЦИИ..................................................................................213 12.1. Функции, определяемые программистом...............................................213 12.2. Синтаксис функций.................................................................215 12.3. Некоторые встроенные функции......................................................216 12.3.1. Арифметические функции abs и sqr.............................................216 12.3.2. Порядковые функции ord, chr, succ, pred...................................................................... 216 12.3.3. Булевы функции odd, eof, coin................................................218 12.4. Использование функций при составлении программы...................................219 12.4.1. Программа, обрабатывающая текст.......................................................................... 219 12.4.2. Программа численного расчета.................................................223 Упражнения...............................................................................227 13 ВЕЩЕСТВЕННЫЕ ЧИСЛА.......................................................................229 13.1. Представление вещественных чисел..................................................229 13.2. Усечение и округление.............................................................230 13.3. Опасные свойства вещественной арифметики;.........................................231 13.4. Вещественные числа в стандартном Паскале..........................................234 13.4.1. Синтаксис вещественных чисел.................................................234 13.4.2. Чтение и вывод чисел.........................................................235 13.4.3. Арифметические операции и встроенные функции.................................236 Упражнения...............................................................................238 14 ПРОГРАММЫ, РАБОТАЮЩИЕ С ВЕЩЕСТВЕННЫМИ ЧИСЛАМИ ...240 14.1. Решение квадратного уравнения................................................... 240 14.2. Другая форма оператора цикла и вычисление кубического корня.......................244 14.3. Губительные погрешности округления........................................... . 254 Упражнения.............................................................................. 259 15 ЭФФЕКТИВНОСТЬ ПРОГРАММ...................................................................263 15.1. Профили и анализ программ.........................................................263 15.1.1. Правила анализа программ.....................................................266 15.1.2. Примеры анализа программ............................................... .... 269 15.2. Объем памяти.................................................................. . ... 274 15.3. Выбор алгоритма............................................................... . ... 275 15.4. Сокращение накладных расходов.....................................................276 15.5. Эффективность вычислений..........................................................278 Упражнения...............................................................................281
8 Оглавление 16 НОВЫЕ ТИПЫ ДАННЫХ.................................................................285 16.1. Определение типа...........................................................285 16.2. Диапазоны..................................................................287 16.3. Структурные типы............................................................290 16.4. Одномерные массивы.........................................................291 16.4.1. Определение типа массива и описание переменных........................292 16.4.2. Индексированные переменные............................................293 16.5. Оператор цикла с шагом for.................................................298 16.6. Записи......................................................................300 16.6.1. Определение типа запись и описание переменных.........................300 16.6.2. Выборка поля записи........................................................................................................ 301 16.7. Оператор присоединения with.................................................305 16.8. Сочетание структур данных...................................................306 Упражнения........................................................................307 17 ИСПОЛЬЗОВАНИЕ СТРУКТУРНЫХ ПЕРЕМЕННЫХ..............................309 17.1. Представление справочной таблицы...........................................309 17.2. Представление строки текста..................................................312 17.3. Сведения о рабочем..........................................................316 17.4. Многомерные массивы.........................................................320 17.5. Представление шаблонов......................................................323 Упражнения..................................... ...................................327 18 ОБРАБОТКА ПОСЛЕДОВАТЕЛЬНОСТЕЙ ОБЪЕКТОВ............................................330 18.1. Строки и новый тип phrasetype..............................................331 18.2. Новый тип itemtype.........................................................332 18.3. Новый тип sequencetype......................................................333 18.4. Вставка элемента...........................................................335 18.5. Исключение элемента........................................................338 18.6. Поиск элемента.............................................................339 18.6.1. Полный поиск..........................................................339 18.6.2. Линейный поиск........................................................342 18.6.3. Двоичный поиск........................................................344 18.6.4. Сравнение линейного и двоичного поиска................................346 18.7. Примеры применения вставки, исключения и поиска...........................347 18.8. Сортировка.................................................................348 18.8.1. Сортировка вставками..................................................348 18.8.2. Пирамидальная сортировка..............................................350 18.8.3. Сравнение процедур сортировки.........................................355 18.9. Полная программа............................................................355 Упражнения........................................................................358 ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ..............................................................360
ПРЕДИСЛОВИЕ К РУССКОМУ ИЗДАНИЮ Анализируя направление, в котором развивается программирование, хо- чется вспомнить ’’парадокс о программистах”. Если не ошибаюсь, академиком А.П.Ершовым впервые был высказан тезис ’’программирование - вторая грамот- ность”, что предсказывало быстрый рост числа людей этой профессии. Возника- ло опасение, что программирование превратится в основное занятие человече- ства. С другой стороны, довольно стойким было представление о сложности про- граммистского труда. Можно ли научить ему всех и каждого? Разрешение парадокса, не замедлившее себя ждать, лучше всего вырази- лось в метком сравнении, прозвучавшем, кстати сказать, не из уст програм- миста1. Владение компьютером было уподоблено искусству вождения автомоби- ля. Когда-то казалось, что каждому нужен личный шофер (читай - програм- мист). Оказалось: не так. Больше стало комфорта, легче управление, и половина человечества водит ныне автомашины (во всяком случае способна это делать). Водитель и пассажир соединились в одном лице. Управлять компьютером тоже стало легче. Пахари в поле информации са- дятся на свой личный трактор. И снова соединяются чувство хозяина и комфорт пассажира: программа, создать которую самостоятельно мы были бы не в силах, сама отвезет куда нужно. Это избавляет от заботы о ’’болтах и гайках”: позво- ляет нам оставаться профессионалами в своей области, не становясь профессио- нальными программистами. Техника вообще и вычислительная техника в частности все больше приру- чается человеком. Подобному же процессу гуманизации подвергается и програм- мирование. Исследователи, эспериментаторы, педагоги напряженной работой на- вели порядок в этой области знаний, выявили ее элементарную сторону, научи- лись излагать ее начала и сделали тем самым доступной многим. Еще пять-шесть лет назад ситуация, как мне кажется, была иной. Про- граммирование воспринималось как область весьма замкнутая, как говорится, ’’для тех, кто понимает”. ’’Если Вы первый раз окунаетесь в ’’море программи- рования” и не знаете, что такое ЭВМ, то лучше оставьте эту книгу на прилавке, пусть ее купят другие: ведь программистов так много, а хороших книг для них, к сожалению, так мало”. Это из предисловия к одной хорошей книжке по 1 Кочетков Г.Б. Персональные компьютеры на работе и дома. Персональные компьютеры: информатика для всех. - М.: Паука, 1987.
(> Предисловие к русскому изданию программированию.1 Фраза в то время естественная и совершенно правомерная - ведь книга Н.Вирта адресована уже сложившимся программистам, а для них книг и в самом деле не хватало (хотя не будем забывать о трехтомнике Д.Кнута ’’Искусство программирования для ЭВМ”). Однако то, что ясно теперь, тогда еще только приоткрывалось. Было ощущение другого читателя (который только ’’окунался” и еще ”не знал”) но необходимость литературы для него ощущалась не вполне. А может быть, не было и читателя? Ясно одно: времена меняются, ибо именно такую книгу Вы, новый чита- тель, держите в руках. Ее можно назвать ’’Элементарное программирование”. Она для тех, кто поступает в начальные классы программирования, кого до сей поры отпугивали замысловатые учебники для высшей школы. Элементарное не есть примитивное: школьные знания здесь умело сопрягаются с ’’университет- скими”, являются их зачатком. Наметившееся расслоение программирования, конечно, знаменует пору его созревания как научной дисциплины. Чудовищным по сложности должен казать- ся начинающему трехтомник Д.Кнута. Но ведь с тем же чувством и школьник берет в руки, положим, курс дифференциальной геометрии... В предисловии к своей книге Н.Вирт утверждает: ”Из ремесла программи- рование превратилось в академическую дисциплину”. Думается, что превратив- шись в академическую дисциплину, программирование не перестало быть и ре- меслом. Забавна перекличка этих определений с заглавием книги Д.Кнута: ремесло, наука, искусство... Поистине место программирования в нашей жизни в должной степени пока не осознано. Мне кажется весьма уместной параллель программирования с архитекту- рой, которая своей инженерной стороной несомненно опирается на точную нау- ку, а в своих высших проявлениях, безусловно, является искусством. Но и соб- ственно строительство, мастерство простого каменщика отделить от архитектуры нельзя. Проектируя программу, автор, как зодчий, предвосхищает облик буду- щего ’’здания”, все его линии и инженерию. Это важнейший, центральный эле- мент программирования, но оно включает и сумму практических приемов, ’’ре- месло обычной кладки”. С этой точки зрения ’’Алгоритмы + ...” - блестящее введение в научную дисциплину, а ’’Учитесь программировать” ближе к основам ремесла. Обращение к книге Н.Вирта здесь не случайно, ибо многое сближает ее с книгой Г.Джонстона, толкает к их сопоставлению. Обе книги посвящены разра- ботке программ и основываются на языке Паскаль. В них исповедуются одни и те же идеи нисходящего проектирования. Совпадает часть фактического матери- ала, касающегося элементов языка, обе книги щедро снабжены упражнениями для читателя и т.п. Эта общность черт станет более понятной и естественной, если вспомнить, что книга Г.Джонстона восходит к лекциям профессора К.Хоара, который наравне с Н.Виртом заслуженно считается одним из столпов европейского течения программистской мысли. Но остановимся на том, что отличает книгу Джонстона как более позднюю. Прежде всего, Джонстон ’’начинает с нуля”. Если для чтения ’’Алгорит- мов...”, как указывает сам Н.Вирт, нужен некоторый фундамент, и в частности знакомство с языком Паскаль, то книга Г.Джонстона, напротив, может быть вашей первой книгой по программированию и заодно служить введением в этот $?зык2. Вирт И. Алгоритмы з структуры данных = программы М.: Мир, 1985; под редакцией и с предисловием Д Б.11одшивалоиа. ; Умести здесь назвать и неплохую отечественную книжку: Абрамов В.]'., 'Грифонов Н.П. Введение l Паскаль - М.: Паука,1988.
Предисловие к русскому изданию 11 Второе. Начиная ’’дальше”, маэстро Вирт и движется быстрее. Но то, что лучше для подготовленного читателя, не подходит для новичка, поэтому изло- жение у Джонстона идет более мелкими шагами, и это отражается во всем, вплоть до характера упражнений. Естественно, что к одним и тем же понятиям или темам авторы подходят с разной детальностью. Это касается многого, напри- мер типов данных, динамических структур, алгоритмов сортировки, вопросов описания формальных языков и синтаксического разбора. Некоторые проблемы выходят за рамки книги Г.Джонстона, и мы находим их только у Н.Вирта. При- мером может служить и такая мелкая тема, как чтение вещественного числа из файла, и такая крупная, как рекурсия. В известном смысле Г.Джонстон заканчивает там, где Н.Вирт и другие только начинают, и его труд можно считать тем самым фундаментом, который обеспечивает плавный переход к более глубоким и сложным материям. (Стоит отметить, что книга Г.Джонстона входит в серию книг по информатике изда- тельства ”Прентис-Хол”, включающую несколько десятков наименований). Наконец, обратим внимание на ’’человеческий” облик книги Г.Джонстона. У нас, мне кажется, распространено мнение (и соответствующая практика), со- гласно которому изложение строгих понятий должно быть чисто дедуктивным, исчерпывающе полным и математически точным, где примеры только иллюстри- руют ранее введенные общие положения. Пытаясь освоить безупречно гладкий материал, бедный читатель быстро обнаруживает, что у автора-то все хорошо, но у него - все плохо, и что в двух шагах от идеального шоссе начинаются такие ухабы, на которых реальное продвижение вперед невозможно. В книге Г.Джонстона мы не найдем ни стремления к полноте (например, полноте описания Паскаля), ни следования заповеди ”от общего к частному”. При построении программ он, например, с самого начала пользуется некоторыми управляющими структурами, хотя систематизирует их гораздо позднее, ближе к концу книги. Не менее важно то, что автор подробно останавливается на практических вещах, на том, с чем неминуемо столкнется читатель: на ошибках, на ложных путях рассуждений, рассматривает неудачные и неверные шаги в построении программы. Останавливается он и на таком практически важном навыке, как умение читать и анализировать программы. Глубоко продуманы курсивные выделения в книге: если читатель сведет их воедино, то станет обладателем своеобразного кодекса, т.е. суммы правил и заповедей, знакомых всякому опытному программисту2, но чаще всего действу- ющих подспудно, чуть ли не на уровне подсознания. Заслуга автора, что он сделал их общим достоянием, расставив в тексте как путеводные вехи. Теперь немного о методике, о том, как автор преподносит материал. Центральным для всей книги, безусловно, является то, что можно назвать процессом появления программы на свет. С той или иной степенью подробности процессы рождения программы мы наблюдаем на протяжении всей книги, и это в конечном счете складывается в живую, реальную картину программистского труда. Автор выделяет основные стадии, которые проходит в своем построении программа, и рассматривает сопровождающие их ’’документы”: задание на составление программы - спецификация; собственно составление программы, распадающееся, в свою очередь, на ряд этапов, которые описываются в таблице разработки; наконец, прослеживание ее работы (тесно связанное с тестирова- нием), отражаемое в таблицах трассировки. На основной стадии разработки программы проводится классическая уже методика нисходящего проектирования, иначе называемая поэтапным уточнени- ем, состоящая в том, что программа, первоначально сформулированная обычным 2 Особенно программисту на Паскале.
12 Предисловие к русскому изданию языком, постепенно разворачивается, получая все более детальную форму и во- площаясь, в конце концов, в операторах языка программирования, т.е. в тре- буемом виде. Таблицы разработки фиксируют эти трансформации программы. Ремесло программирования, как и всякое ремесло, изобилует элементами, которые постоянно повторяются и, будучи однажды освоены, в дальнейшем тре- буют рутинного, почти механического применения. На этом пути в программу незаметно вкрадываются ошибки, причиняющие немало хлопот. Автор книги позаботился о том, чтобы начинающий программист научился избегать самых типичных из них благодаря применению простых формальных приемов. Это касается построения логических выражений, записи оператора цикла с предусловием и др. Внимание читателя также обращается на ’’краевые случаи” работы программы - всегда практически важные. Отметим также, что особое место отведено в книге тестированию про- грамм. Крылатое выражение Э.Дейкстры (’’тестирование доказывает наличие ошибок в программе, а не их отсутствие”) может объяснить то как бы снисходи- тельное отношение к тестированию, которое существует в ’’академических” программистских кругах. Доказательству правильности программ посвящено целое направление, и именно оно, разумеется, принадлежит строгой науке. Но ремесло программирования, к которому причастно подавляющее большинство программистов-практиков, без тестирования пока никак обойтись не может. Честь и хвала автору за его отнюдь не академическое решение! Трудность особого рода представляло при переводе стечение двух обстоя- тельств: англоязычный характер языка Паскаль и доступный уровень изложения материала, который безусловно хотелось сохранить. Читатель оригинала перехо- дит к Паскалю непосредственно от английского. Нашему читателю нужен допол- нительный шаг - через паскалеподобные, но русские программные конструкции. Несколько слов о терминологии. Пара ключевых для программирования терминов в английском звучит совершенно неповторимо: bug, debugging. Второе из них давно нашло удачный перевод: ’’отладка”, для первого же мы довольствуемся безликим ’’ошибка”. В книге делается попытка несколько исправить положение посредством введения парного термина: ’’нелад” (предложено Е.Н.Веселовым). Как лаконично могли бы выяснять отношения программисты: ”Да будь она неладна, твоя программа!”. Впрочем, используются и синонимичные термины: ’’изъян”, ’’недочет”, ’’оши- бка” и проч. Английское statement почти всюду переводится как ’’оператор”, однако ис- пользуется также и слово ’’предложение”, когда речь идет о ’’неразвернутых” элементах программы. ’’Постепенная замена предложений операторами” - так, уже чисто по-русски, можно сформулировать основную идею нисходящего про- ектирования. Термин ’’листинг” всюду служит для обозначения окончательного текста программы или ее части; в этом виде она может быть исполнена непосредствен- но Паскаль-процессором. В нескольких случаях при переводе явственно ощущается сопротивление нашего языка. ’’Лобовые” переводы сочетаний array variable, record variable - ’’переменная типа массив”, ’’переменная типа запись” - сильно загромождали бы текст. В недавнем русском издании стандарта Паскаля1 появились более краткие варианты перевода (и в синтаксическом отношении более гибкие): ’’массивовая переменная” и т.п., но переводчикам они показались неудобными. Вместо этого сделана попытка синтаксического калькирования: переменная-массив, перемен- ная-запись и т.п. 1 Йенсен К., Вирт И.. Паскаль. Руководство для пользователя. - М.: ’’Финансы и статистика”, 1989; пер. с англ. Д.Б.Подшивалова.
Предисловие к русскому изданию 13 Еще одно ’’несчастье” русского языка: двусмысленность слова строка в контексте Паскаля. Его применяют и для обозначения ’закавыченной цепочки букв’ (то, что вы сейчас прочитали, в Паскале принято называть строкой символов), англ, string, и для обозначения ряда букв как линейного элемента текста, англ, line. Оба смысла очень активно используются в программировании вообще и на Паскале в частности. Строки в первом значении являются данны- ми, которые находятся внутри программы и ею обрабатываются (как один из видов константы в Паскале). Строки во втором значении (нашем обыденном) образуют текст самой программы {строка программы), а также являются еди- ницей членения внешнего для программы мира, который программе ’’представ- ляется” чаще всего в виде текстового файла {строка файла, ввод строки, выходная строка). Это значение наиболее универсально отражается, по-види- мому, термином строка текста. Чаще всего никаких недоразумений не возникает. Так, строки программы в книге почти всюду нумеруются, и строка с номером всегда отсылает нас к тек- сту программы. Иногда же эти два смысла тесно переплетаются (оператор readin может, например, читать строку в смысле 2 и делать ее содержимым строки в смысле 1; двусмысленно звучит и пустая строка). При переводе для второго значения нами употребляется термин литерал (семантика этого слова отвечает его употреблению). Положение усугубляется тем, что термин (символьная) строка не совсем аккуратно употребляется, в том числе и автором книги, для обозначения не только закавыченной цепочки, но также и переменной, и типа данных. В этих случаях более строгими, различительными являются термины строковая пере- менная, строковый тип. Наконец, обратите внимание, как выглядит книга, которая у вас в руках. Текст перевода подготовлен на персональном компьютере типа IBM в тек- стовом процессоре Microsoft Word.* Распечатка оригинал-макета за исключением небольшого количества рисунков осуществлена на лазерном принтере. Русифи- цированный вариант программы и набор шрифтов для лазерного принтера были любезно предоставлены совместным предприятием ’’Параграф”. Работа переводчиков распределилась следующим образом: предисловие и главы 1-7 переведены И.Ф.Югановым, предисловие редактора серии и главы 8-14 - Г.В.Сениным, главы 15-18 - С.В.Арутюновым. Г. В,Сенин Торговая марка фирмы Microsoft (США).
14 Предисловие к русскому изданию ПРЕДИСЛОВИЕ РЕДАКТОРА СЕРИИ Эта книга - для тех, кто приступает к изучению программирования. Она не предполагает у читателя никаких предварительных знаний и в то же время во всей полноте охватывает идеи и подоплеку программирования, которые чрез- вычайно продуманно изложены, так что очень трудно понять их превратно. Новизна книги в том, что она приглашает учиться методом проб, но избе- гая ошибок, обычно их сопровождающих. Печально, но факт: ошибки програм- мирования проявляются симптомами, настолько далекими от причин, их вызы- вающих, что лишь большой опыт, зрелое мастерство, тонкое наитие, неистощи- мая энергия да терпение помогают в их диагностике и исправлении. Здесь и про- фессиональный программист может опустить руки, не говоря уж о новичке. Многие программисты-профессионалы значительно повысили качество и продуктивность своего труда, научившись сводить процент ошибок практически к нулю. Эта же технология доступна ныне и новичку, только начинающему про- граммировать. Вы сумеете избегать ошибок, но лишь серьезно потрудившись и достигнув полного понимания задачи, глубокого осмысления программы и уве- ренного владения языком программирования. Именно этой цели и служит книга. Проработайте ее со старанием, и она облегчит процесс обучения, сделает его бо- лее приятным и станет хорошим фундаментом вашей будущей программистской работы либо в качестве любителя, либо программиста ”по случаю”, либо как профессионала. К.А.Р.Хоар
Предисловие редактора серии 15 ПРЕДИСЛОВИЕ В основе предлагаемого руководства лежит опыт, накопленный в ходе пре- подавания вводного курса программирования в Королевском университете в Белфасте. В 1974 г. двенадцатинедельный курс был прочитан К.А.Р.Хоаром, а затем, с 1976 г., его читал и далее развил автор этой книги. Свои главные методические принципы профессор Хоар изложил в статье ”Структурное про- граммирование во вводных курсах” в ’’Infotech International State of the Art Report on Structured Programming” (1976). Большинство этих принципов сохра- нено, но вместе с тем некоторые акценты расставлены по-новому. Обучение программированию немного напоминает обучение игре в футбол. Кое-как и то и другое может делать любой, а вот достичь высокого класса весь- ма непросто. Здесь имеют значение три фактора: способности, заложенные от природы, обучение и практика. Что касается способностей, то они либо есть, ли- бо нет; обучать может тот, кто обладает необходимым опытом, если он потруди- лся подумать, как научить других; практиковаться же каждый должен сам, причем, чем больше, тем лучше. Важно с самого начала выработать хороший стиль программирования, потому что избавляться от дурных привычек очень трудно. Это руководство рассчитано на то, чтобы ’’подковать” начинающих программистов. В него включены и тщательно подобранные упражнения, которые будут полезны для практики. Что же касается способностей, то будем надеяться, что они есть! Программы пишутся на каком-нибудь языке программирования, поэтому необходимой, хотя и не первостепенно важной задачей является обучение языку. В этой книге используется широко известный и получивший признание язык Паскаль. Одновременно он является своеобразным аппаратом для распростра- нения принципов хорошего программирования, которые в равной степени могут применяться и во многих других языках. В полном объеме Паскаль здесь не нужен; попытка преподать весь язык во вводном курсе привела бы только к нехватке времени на изучение других, более важных вопросов. Кроме того, всякий, кто наблюдал за работой начинающих программистов, знает, что большой объем знаний о языке, полученный слишком рано, скорее мешает им, нежели помогает. И конечно же, преподаватель, опирающийся на данное руководство, может включить в свой курс дополнительные сведения о языке, если сочтет это целесообразным. Описываемые характеристики Паскаля соответствуют международному стандарту, который определен в ’’Описании компьютерного языка прэграммиро-
16 Предисловие вания Паскаль”1. Этот документ опубликован Британским институтом стандар- тов под номером BS 6192:1982 и Международной организацией по стандартиза- ции под номером ISO 7185:1983. Книга обеспечит читателю хорошую подготовку для продолжения карьеры программиста либо на пути дальнейшего изучения Паскаля, либо в результате переключения на другой язык. Выработанный стиль программистского мыш- ления, основанный на использовании коротких, максимально независимых друг от друга, аккуратно составленных подпрограмм, послужит хорошей базой для написания программ на любом языке последовательного программирования (на- пример, на Фортране или Коболе) и для освоения более современных языков (например, Ады™ или Оккама™), обеспечивающих возможности параллельного программирования. В гл. 1 сообщаются исходные сведения и вводится терминология. Гл. 2-4 посвящены чтению и пониманию программ. Дело в том, что принцип ’’слово - серебро, молчание - золото” вполне применим и к программированию. Здесь он означает, что сначала нужно научиться разбираться в структуре программы и понимать, в чем ее смысл, и только после этого самому приниматься за написа- ние программ. Сопутствующие этим главам упражнения закрепляют изложен- ные идеи и приглашают совершить первый шаг к написанию программ, а имен- но: модифицировать уже существующие программы. При выполнении упражне- ний читателю становится также ясно, что не всякая программа и не всегда работает правильно. Если книга используется как учебник, начальные главы можно изучать в то время, пока студенты знакомятся с компьютерной системой, на которой им предстоит работать. Они могут приобретать навыки ввода в компьютер програмгл и данных, выполнять существующие программы (желательно, чтобы в них были .тщательно подобранные синтаксические и логические ошибки), разбираться в результатах компиляции и редактировать программы и файлы с данными. Прак- тическая деятельность дополнит первые теоретические уроки, поможет понять, что люди всегда делают ошибки, а компьютеры выполняют то и только то, что им предписано. Следующие две главы, гл. 5 и 6, касаются формального определения син- таксиса вообще и синтаксиса стандартного Паскаля в частности. Причина, по которой введены эти главы, следующая. Выполняя упражнения, читатель на собственном опыте убедится, что синтаксические ошибки являют собой реальную проблему, и, вероятно, захочет узнать, как избегать их. Один из путей - во всех деталях понять структуру применяемого языка. Впрочем, те, кому не терпится начать писать программы как можно скорее, могут отложить чтение этих глав и прочитать их после гл. 9. В гл. 7-9 решается задача конструирования программ путем их постепен- ного уточнения, причем акцент делается на их правильности и ясности. Практи- чески с самого начала приветствуется употребление подпрограмм, а основная ло- гика выполнения программы включается не в ’’головную программу”, а в ’’го- ловную процедуру”. Подпрограммы не вкладываются друг в друга, и хотя это не традиционный способ написания коротких программ на Паскале, по ряду сообра- жений он является наилучшим. (Тем специалистам, которым такая точка зрения кажется сомнительной, предлагается изучить программы, рассматриваемые на протяжении книги). В этих же главах досконально обсуждается природа про- граммных ошибок и даются практические рекомендации по их предупреждению, обнаружению и исправлению. 1 Русский перевод: Йенсен К., Вирт Н. Паскаль. Руководство для пользователя. - М.: Финансы и статистика, 1989. - Примеч.,ред.
Предисловие 17 В следующей группе глав, гл. 10-14, читатель продолжает знакомиться с элементами Паскаля и методами построения правильных программ. Подчеркива- ется центральная роль булевских выражений, выступающих в качестве утверж- дений, пред- и постусловий, хотя они трактуются довольно неформально. При изучении процедур с параметрами (гл. И) категорически не рекомендуется при- менять глобальные переменные; здесь же рассматриваются простые рекоменда- ции по конструированию надежных процедур и функций. В последних двух гла- вах этой группы отмечаются основные опасности, обусловленные тем, что боль- шинство чисел нельзя точно представить в компьютере. Несмотря на то, что эти главы по необходимости оказались более математизированными, чем остальные, были приложены усилия к тому, чтобы и здесь свести математику к минимуму. Главным качеством хорошо написанной программы после правильности и ясности является эффективность. Факторы, влияющие на эффективность, иссле- дуются в гл. 15, вслед за обсуждением способов оценки времени выполнения простой программы и занимаемого ею объема памяти. Наиболее важным из этих факторов является выбор хороших алгоритмов. Остальные главы посвящены структурам данных. Преобладающей методи- кой является представление новых типов данных с помощью записей (как пра- вило, содержащих массивы) и использование независимых подпрограмм для вы- полнения операций с этими данными. Особое внимание уделено представлению и работе с одной из наиболее общих структур данных - последовательностью элементов. Дается несколько хороших, широко известных алгоритмов поиска и сортировки. Руководство для преподавателя, содержащее решения упражнений, можно приобрести у издателя1. Я искренне благодарен Тони Хоару за то, что он расши- рил мои знания о программировании, заложил основы курса, на котором базиру- ется эта книга, за то, что он предложил мне ее написать, и за поддержку и помощь, оказанные мне в период ее подготовки. Я также весьма признателен за помощь Генри Хиршбергу, Тому О’Дохерти, Дэвиду Слоуну, Бобу Лингарду, Морису Клинту и множеству анонимных рецензентов. И наконец, слова особой благодарности - моей жене Дженни, которая на протяжении всей работы была и главным консультантом, и контролером, и моим терпеливым помощником. Говард Джонстон, Белфаст, апрель 1984 1 Имеется в виду английское издание книги. - Примеч. пер.
1 ПРОЦЕССЫ, ПРОЦЕССОРЫ И ПРОГРАММЫ 1.1. Обработка информации Компьютер - машина, обрабатывающая информацию, изучать программи- рование - это учиться объяснять компьютеру, что он должен делать. Его работа состоит в выполнении команд, инструкций. Давайте рассмотрим для начала простую канцелярскую задачу и те инструкции, которые могут быть даны не компьютеру, а человеку - служащему, обязанному их выполнять. Книготорговец издал каталог, содержащий аннотации к книгам по опреде- ленному предмету, и хочет разослать этот каталог некоторым из своих клиентов. У него есть картотека, где хранятся фамилии и адреса всех его клиентов и - для каждого клиента - список предметов, которыми этот клиент интересуется. Нуж- но просмотреть картотеку и надписать конверт каждому клиенту, в чью карточ- ку вписан предмет каталога. Служащему могут быть даны инструкции типа следующих: запомнить предмет каталога; искать в картотеке сведения о тех клиентах, в сферу интересов которых . входит этот предмет, и записывать фамилию и адрес каждого такого 1 клиента на отдельном конверте. Предположим, что предмет каталога - ’’психология” и что информацион- ная картотека представляет собой совокупность заполненных карточек. Давайте подробно рассмотрим, что будет делать служащий. Сначала он прочитает и за- помнит название предмета ’’психология”, а затем начнет по одной просматри- вать карточки. Он может посмотреть на первую карточку и сразу перейти к сле- дующей, потому что в первую карточку не вписана ’’психология” как предмет, интересующий клиента. Он может пропустить еще несколько карточек, но рано или поздно ему попадется карточка, в которой записано ’’психология”. Тогда фамилию и адрес с этой карточки он перепишет на конверт. Так он будет действовать, пропуская одни карточки и надписывая конверты на основании других, до тех пор, пока не просмотрит всю картотеку. В результате, несмотря на то, что инструкций всего две, служащий выполнит длинную повторяющуюся последовательность действий. Мы будем называть такую последовательность действий процессом, а человека или машину, которые осуществляют процесс, - процессором. Говоря, что компьютер - это машина, обрабатывающая информа- цию, мы тем самым утверждаем, что, получив выполнимые инструкции (коман- ды) и информацию, представленную в подходящем виде, компьютер может
Гл. 1. Процессы, процессоры и программы 19 выполнить последовательность действий и, в свою очередь, произвести информацию, В обработке информации независимо от того, осуществляется она служа- щим или компьютером, центральное место занимает сама обрабатываемая ин- формация. Единичный элемент информации называется данным:, если же эле- ментов информации больше, чем один, то они называются данными. Полезно различать информацию, которой снабжается процессор, информацию, порождае- мую процессором, и информацию, поддерживаемую процессором в течение про- цесса. Определим, соответственно, и три вида данных: Входные данные - элементы информации, существующие во внешнем мире и читаемые процессором в ходе процесса. Результаты, или выходные данные - элементы информации, которые записываются процессором в ходе процесса. Внутренние данные - элементы информации, содержащиеся внутри про- цессора на каком-либо этапе процесса. В этом контексте чтение означает перенос информации из внешнего мира в процессор; запись означает перенос информации из процессора во внешний мир1. В нашем примере входными данными являются предмет нового каталога и картотека записей о клиентах, а результатами - кипа надписанных конвертов. В общем случае некоторые внутренние данные образуются непосредственно из входных данных, другие имеют временный характер и служат для поддержки процесса, третьи становятся результатами. Важно, что внутренние элементы данных находятся внутри процессора. Если вернуться к нашему примеру, то внутренние элементы данных это то, что служащий держит в своей памяти, на- пример, слово ’’психология”, которое он должен помнить на протяжении про- цесса после того, как прочитал его из входных данных. Когда говорят о компьютерных системах, программой называют последова- тельность команд, даваемых процессору. В общем виде схема обработки инфор- мации представлена на рис. 1.1. Важно отметить, что наряду с командами, ком- пьютерная программа содержит те сведения об элементах внутренних данных, крторые нужны в ходе ее выполнения. Это станет понятнее, когда во второй главе мы рассмотрим некоторые программы. программа > входные данные > процессор > результаты Рис. 1.1. Общая схема обработки информации Введем теперь еще два термина: программист и пользователь. Если прог- рамма сложна, в ее создание вовлекается большая группа людей, выполняющих различные функции, но мы будем употреблять общий термин ’’программист” для обозначения человека, который проектирует, записывает и совершенствует новую программу или исправляет старую. Человек, для которого пишется про- грамма и который, вероятно, будет снабжать ее входными данными и получать 1 Англ, read, ’’читать” и write, ’’писать”. На протяжении книги мы будем использовать также термины ввод данных и вывод данных. Термины, предлагаемые автором, во многом объясняются тем, что выбранный язык программирования ’’англоязычен” и имеет операторы read, для ввода данных, и write, для вывода данных. - Примеч. ред.
20 Гл. 1. Процессы, процессоры и программы результаты, называется пользователем. И хотя один и тот же человек может быть и пользователем, и программистом, важно различать эти две роли. 1.2. Компьютерные системы Можно, к счастью, программировать и использовать компьютер, не зная досконально того, как он работает, так же, как можно водить машину и ездить в транспорте, не зная подробностей их механического устройства. Однако для тех читателей, у которых нет соответствующей подготовки, может оказаться полезным краткое перечисление основных компонентов простой компьютерной системы. Две основные составляющие компьютерной системы - это аппаратура и программное обеспечение. Аппаратура - это собственно оборудование; она обеспечивает пять осново- полагающих возможностей, аналогичных тем, которые нужны служащему, чтобы справляться со своими обязанностями, а именно: возможность запоминать информацию на короткое время; возможность следовать инструкциям (выполнять команды); возможность читать (вводить данные); возможность писать (выводить данные); возможность подолгу сохранять информацию. В компьютерной системе эти возможности реализуются за счет ее состав- ных частей. Таковыми соответственно являются: основная память; центральный процессор; устройства ввода; устройства вывода; внешние запоминающие устройства. Так же, как служащий помнит полученные инструкции и фрагменты обра- батываемой информации, основная память' хранит программу и внутренние дан- ные, нужные при ее выполнении. Если бы процесс надписывания конвертов вы- полнялся компьютером и если бы мы остановили компьютер в середине процесса и исследовали содержимое основной памяти, то обнаружили бы программу, записанную в определенной форме, слово ’’психология”, также закодированное некоторым образом, и, вероятно, информацию с одной карточки. После выпол- нения программа, а также связанные с ней внутренние элементы данных удаля- ются из основной памяти, т.е. ’’забываются”. Центральный процессор - сердце компьютерной системы; он может извле- кать команды из основной памяти и подчиняться им. Мы говорим, что процессор выполняет команды. Они должны быть выражены в машинном коде, т.е. на ма- шинном языке данного процессора. Каждая команда задает элементарное дей- ствие, которое может быть совершено процессором с очень большой скоростью - порядка миллиона в секунду и более. Эта скорость обязательно соответствует быстродействию основной памяти. Устройства ввода используются для переноса информации под управлени- ем центрального процессора из внешнего мира в основную память. К этим уст- ройствам относятся клавиатуры, позволяющие вводить информацию в компьютер знак за знаком с помощью клавиш, и устройства ввода с перфокарт, которые считывают информацию, записанную в виде отверстий в перфокартах. Устройства вывода переносят информацию из основной памяти во внешний мир. К наиболее распространенным из них относятся устройства построчной 1 Употребляется также термин ’’оперативная память”. ‘Примеч. ред.
Гл. 1. Процессы, процессоры и программы 21 печати - обычная принадлежность компьютеров, телетайпы, печатающие данные посимвольно, и дисплеи с экраном, похожим на телевизионный. По человеческим меркам устройства ввода и вывода работают очень быст- ро, но по сравнению со скоростью обмена информацией между процессором и основной памятью скорость этих устройств крайне низка. К внешним запоминающим устройствам относятся накопители на магнитных дисках, накопители на магнитных лентах и - в малых компьютерах - накопители на гибких дисках и кассеты. Они обеспечивают память большой емкости, позволяют подолгу и без значительных затрат хранить большие объемы инфор- мации, будь то программы или данные. Если основную память компьютера можно сравнить с памятью служащего, то внешние запоминающие устройства сопоставимы с картотекой или библиотекой. Служащему доступна хранящаяся в них информация, но прежде, чем ею воспользоваться, он должен ее прочитать. Точно так же информация, хранящаяся во внешней памяти, должна быть пере- несена в основную память, чтобы с ней мог работать центральный процессор. Можно спорить о том, находится информация, хранящаяся во внешней па- мяти, ’’внутри” компьютера или нет. Если, например, информация записана на гибкий диск, то его можно вынуть из накопителя и полностью отделить от ком- пьютера. Таким образом, внешние запоминающие устройства могут рассматри- ваться не как память внутри компьютера, а как устройства ввода и вывода. По сравнению с основной памятью и процессором они работают довольно медленно, но все же намного быстрее тех типов устройств ввода и вывода, которые упоми- нались выше; в этом их преимущество перед последними. Следует, однако, от- метить, что информация, записанная на внешнее запоминающее устройство, может быть прочитана только с помощью компьютера или специального обору- дования. Информация, предназначенная для человека, должна быть выдана на одно из других устройств вывода*. Общий термин программное обеспечение применяется для обозначения программ, используемых в компьютерной системе. Эти программы подразделя- ются на два класса: прикладное программное обеспечение и системное програм- мное обеспечение. Прикладное программное обеспечение образуют программы, написанные пользователями или для пользователей и осуществляющие инфор- мационные процессы, которые нужны пользователям. Системное программное обеспечение предоставляется (чаще всего изготовителями компьютера) для того, чтобы облегчить подготовку программ и входных данных и выполнение про- грамм. К системному программному обеспечению относятся, в частности, опера- ционные системы, компиляторы и редакторы. Операционная система является, в сущности, внутренним распорядителем компьютера. Как и любая другая программа, она выполняется центральным про- цессором. В наиболее простом случае ее основные задачи - управление операци- ями ввода и вывода и перемещение программ и данных между компьютером и внешними запоминающими устройствами. Компилятор - это программа, которая принимает в качестве исходных дан- ных другую программу, написанную на языке, отличном от машинного кода, с которым имеет дело процессор, и переводит эту программу в машинный код. Чуть позже мы обсудим этот процесс более подробно. Редактор обеспечивает пользователю возможность исправлять программы и данные, хранящиеся в компьютерной системе. Это средство очень полезно при разработке программ и подготовке больших объемов данных. Итак, полная компьютерная система, включающая аппаратуру и програм- мное обеспечение, создаст обстановку, в которой могут разрабатываться, храниться и выполняться программы. ’ Например, на дисплей или устройство печати. - Примеч. ред.
22 Гл. 1. Процессы, процессоры и программы 1.3. Небрежные люди и послушные компьютеры Успешным можно считать лишь такой процесс, который дает правильные результаты. В общем случае к достижению успеха ведет выполнение двух главных требований: а) Программа должна правильно описывать то, что следует сделать. Если для процесса надписывания конвертов вторая инструкция сформу- лирована небрежно: искать в картотеке сведения о тех клиентах, в сферу интересов которых входит указанный предмет, и записывать фамилию каждого такого клиента на конверте, то исполнительный служащий надпишет кипу конвертов с фамилиями, но без адресов. В ситуации, когда каталоги предстоит рассылать по почте, такое положение дел окажется неудовлетворительным. б) Входные данные должны быть правильными. Если служащему ошибоч- но сообщат, что предмет нового каталога - ’’физиология”, конверты бу- дут адресованы не тем людям (за исключением счастливчиков, которые интересуются как психологией, так и физиологией). Не будут полнос- тью правильными результаты процесса и в том случае, если в картотеке записей о клиентах некоторые адреса или интересы зафиксированы не- верно. На практике достижение успеха зависит от того, в какой мере учитывают- ся следующие факты: люди редко делают что-либо совершенно правильно; компьютеры делают только то, что им предписано, и ничего более. Давайте рассмотрим сначала простую немеханизированную систему, в которой люди формулируют инструкции, готовят данные и производят обработ- ку. Ошибки в этом случае могут вкрадываться всюду. Однако им в какой-то сте- пени противодействует здравомыслие людей, участвующих в процессе. Кроме того, можно ’’разрешить” некоторые наиболее вероятные ошибки. Например, служащий, прочитавший небрежно составленную инструкцию, где сказано ’’на- писать фамилию каждого такого клиента на конверте”, зная, что конверты придется посылать по почте, попросит уточнить инструкцию. Другие огрехи могут быть выявлены, если в конце процесса кто-нибудь еще просмотрит надписи на конвертах, отыскивая в них ошибки. Так немеханизированная система может произвести правильные результаты. Когда процессором в системе является компьютер, ситуация существенно отличается от описанной. Программа и входные данные подготавливаются людь- ми и поэтому, вероятно, содержат ошибки, а обработка выполняется компью- тером, который здравомыслием не обладает и делает только то, что сказано. Это сочетание почти наверняка породит результаты, неверные по крайней мере отчасти, если не принять самых действенных мер. Главная ответственность здесь ложится на программиста. Важно поэтому, чтобы он взял себе за правило такое тщательное построение программ, которое позволило бы противодействовать слабостям системы. Для этого ему следует: а) избегать ошибок и при написании программ, и при ее последующих переделках; б) выявлять ошибки уже сделанные; в) предполагать, что ошибки с высокой вероятностью окажутся во входных данных. Большинство из нас склонно считать себя аккуратными и внимательными и полагать, что, взявшись за программирование или подготовку данных, мы
Гл. 1. Процессы, процессоры и программы 23 сможем работать без ошибок. Эти иллюзии обычно рассеиваются очень быстро, и программист или пользователь скоро узнает, как это огорчительно - иметь дело с абсолютно послушным партнером! Процесс нахождения и устранения ошибок в программе называют отладкой программы, а саму ошибку, изъян программы будем называть также неладом. Об ошибочных входных данных часто говорят как о некорректных, а процесс поиска ошибок во входных данных называют проверкой корректности (правиль- ности) данных. Хотя, как мы увидим, компьютер может существенно помочь при отладке программ и проверке корректности данных, найти все ошибки он не в силах. Но если программист сознает это и профессионально подходит к делу, он сможет создавать надежные программы. 1.4. Выбор языка программирования Программа существенно коммуникативна. Одна из ее целей - передать опи- сание некоторого процесса от программиста, который ее разработал, процессору, который должен ее исполнить. Другая возможная цель программы - сообщение о некотором процессе, которое от одного человека, его разработавшего и запро- граммировавшего, поступает другому с тем, чтобы тот мог понять его. Успешная коммуникация достигается благодаря языку, понятному программистам и про- цессору. Как мы уже говорили, команды, выполняемые центральным процессо- ром компьютерной системы, должны быть выражены в машинном коде. Каждая команда при этом выступает в виде последовательности нулей и единиц. Про- граммист может, затрачивая значительные усилия, понимать и составлять про- граммы на машинном языке, для создания больших надежных программ машинный код совершенно неприемлем. Трансляция - один из способов преодолеть ’’языковой” барьер. Если воспо- льзоваться языком, который понятен программисту, а затем обеспечить перевод с него в машинный код, проблема будет решена. Может показаться, что для этой цели лучше всего было бы выбрать естественный язык, например, английский, но на самом деле удобнее сконструировать особый язык. Сегодня создано много таких языков и они известны как языки программирования высокого уровня. Хорошо сконструированные языки высокого уровня обладают целым рядом достоинств, основанных на следующих фактах: а) Средства, предоставляемые языком, позволяют удовлетворить потребности конкретной прикладной области. Например, один язык может быть разработан для научных, преимущественно численных рас- четов, другой - для коммерческих приложений, сопряженных с обработ- кой больших объемов нечисловой информации, а третий будет применя- ться в таких прикладных областях, где компьютер служит для модели- рования какой-либо другой системы, скажем, самолета. В каждом слу- чае средства, предоставляемые языком, и используемая в нем терми- нология могут быть выбраны так, чтобы обслуживать конкретную при- кладную область и специалистов, в ней работающих. б) В визуальном отношении программа должна быть такой, чтобы ее легко было читать и чтобы ясна была ее структура. Проектирование и написание больших программ - сложная интеллектуальная задача, и для се успешного решения программист должен предельно сосредото- читься на том, что он делает. Идеальна ситуация, когда сам текст программы настолько удобен для восприятия, что помогает вникнуть в нее. Это полезно не только тому, кто пишет программу, но и вообще всякому, желающему понять, как программа работает, и особенно программисту, которому поручено се усовершенствовать.
24 Гл. 1. Процессы, процессоры и программы в) В язык могут быть встроены средства, помогающие выявлять и пре- дупреждать ошибки. Учитывая важность того, чтобы законченная про- грамма была правильной, и принимая во внимание естественную под- верженность программиста ошибкам, такие средства следует признать главным достоинством языков высокого уровня. В данном руководстве в качестве языка программирования выбран Паскаль или, если говорить более точно, стандартный Паскаль в том виде, как он определен Международной организацией по стандартизации в документе ISO 7185:1983 и Британским институтом стандартов в документе BS 6192:1982. Основное преимущество работы со стандартизованным языком (по сравнению с любым другим) состоит в возможности сделать программы мобильными. О программе говорят, что она мобильна, если ее можно легко перенести с одного компьютера на другой1. Паскаль создан профессором Никлаусом Виртом, одной из принципиаль- ных целей которого было ’’разработать язык, пригодный для изучения програм- мирования как систематической дисциплины, которая исходит из определенных фундаментальных понятий, ясно и естественно отраженных в языке”. Время, однако, показало, что Паскаль обладает свойствами, которые делают его пригод- ным не только для изучения принципов программирования, но также и для пра- ктического применения - для создания как прикладного, так и системного про- граммного обеспечения. Поэтому те, кто начинают программировать на Паскале, могут быть уверены в том, что они: а) изучают набор принципов, исходя из которых можно ’’хорошо” про- граммировать практически на любом языке; б) оснащают свой арсенал языком, который все шире применяется за пре- делами сферы обучения. Мы уже упоминали о системной программе, называемой компилятором и предназначенной для перевода программы, написанной на конкретном языке, в машинный код, с которым имеет дело процессор. И хотя существуют и другие методы выполнения программ на языках высокого уровня, компиляция - наибо- лее распространенный метод трансляции программ на Паскале. Процессор, осна- щенный компилятором Паскаля, превращается в Паскаль -процессор или, иначе говоря, начинает ’’понимать” Паскаль. О таком процессоре мы говорим, что Паскаль реализован на нем и что тем самым решена проблема поддержания удобного для программиста общения между ним и процессором. 1.5. Паскаль-системы Два самых важных этапа запуска программы на компьютере - компиляция и исполнение. На этапе компиляции программа высокого уровня, называемая исходной программой, переводится в форму, называемую объектной программой. На этапе исполнения объектная программа запускается с входными данными, задаваемыми пользователем, и выдает результаты. Не всегда, однако, программа проходит сразу два этапа. Дело в том, что на этапе компиляции могут быть обнаружены некоторые простые ошибки. Они называются ошибками этапа компиляции, и о каждой найденной при компиляции ошибке выдается сообщение. Если обнаружена хотя бы одна такая ошибка, объ- ектная программа не создается и этап исполнения остается неосуществленным. Ошибка (или ошибки) в исходной программе должна быть исправлена, и компиляцию следует повторить. 1 Т.е. заставить работать программу на другом компьютере, Далеко не всегда это просто из-за различия процессоров. - Примеч. ред.
Гл. 1. Процессы, процессоры и программы 25 Будучи однажды помещенной в компьютер, исходная программа обычно сохраняется во внешней памяти. Целесообразность сохранения в том, что для дальнейших исправлений и изменений можно использовать редактор. Объектная программа тоже может храниться во внешней памяти. Это позволяет многократ- но выполнять ранее компилированную программу, не производя компиляцию всякий раз заново. И только в том случае, если исходная программа изменена, нужно заново ее компилировать, чтобы получить исправленную версию объект- ной программы. Таким образом, создание и прохождение через компьютер программы на Паскале включает множество разнообразных видов деятельности, или ’’работ”. К ним относятся: а) занесение исходной программы и данных в компьютер; б) компиляция программы и получение сообщений об ошибках этапа компиляции, если таковые будут найдены; в) редактирование программы, осуществляемое с целью исправления ошибок или внесения изменений; г) запуск объектной программы, снабженной входными данными, для получения результатов; д) получение распечаток текста исходной программы и, возможно, другой информации. Систему, которая обеспечивает средства, необходимые для выполнения указанных работ, на протяжении книги мы будем называть Паскаль-системой. На практике Паскаль-система может воплощаться различными способами и состоять из нескольких системных программ. Итак, к чему мы пришли? Мы рассмотрели понятия процесса, процессора и программы и вкратце основополагающие свойства системы, на базе которой могут разрабатываться и исполняться программы на Паскале. Цель последую- щего изложения - помочь вам сделать первые шаги на пути становления в каче- стве программиста. Начнем мы с того, что будем учиться читать и понимать не- которые простые программы, а затем попробуем такие программы составлять.
2 КАК ЧИТАТЬ ПРОГРАММЫ 2.1. Переменные, типы данных и операторы Просмотрите текст программы, приведенный на листинге 2.11. Это закон- ченная программа, написанная на стандартном Паскале. Вам она, вероятно, по- кажется текстом, написанным на незнакомом языке и совершенно непонятным. Насладитесь своим невежеством сейчас, ибо когда вы прочитаете эту главу, ско- рее всего от него не останется и следа! 1 2 3 4 5 6 7 8 9 10 11 12 program Account! (input,output)’, var postage, number, price, cost: integer, begin postage 5; read (number, price)’, cost number * price’, cost cost + postage’, write (cost) end . Листинг 2.1. Текст программы Расчет! Эта программа предназначена для расчета общих затрат на приобретение определенного количества единиц товара по известной цене в фирме ’’товары - почтой”, где, помимо основной стоимости, взимается пять долларов за пересыл- ку. Не самое сложное из возможных вычислений! Цель настоящей главы - испо- льзовать эту и четыре другие простые программы для того, чтобы помочь вам понять содержание любой программы, написанной на Паскале, и увидеть, как при ее выполнении производятся вычисления. Одно небольшое, но важное замечание. Колонка чисел слева от текста не является частью программы. Числа приведены просто для облегчения ссылок. Первый шаг к пониманию программы - обнаружить три основные части, которые в ней можно выделить: 1 Большинство элементов программы в стандартном Паскале не может содержать русские буквы. Это затрудняет восприятие программ для тех, кто не владеет английским. Помогут читателю комментарии (о них речь пойдет в разд. 4.1), которые везде даются на русском языке, а также пояснения в тексте книги. - Примеч. ред.
Гл. 2. Как читать программы 27 строка 1 строки 3-5 строки 6-12 : заголовок программы; : раздел описания переменных; : раздел операторов. Заголовок программы сообщает нам, что имя программы - Accountl {Расчет!). Это имя используется программистом и пользователями программы для ее идентификации. Остальная информация, содержащаяся в круглых скобках после имени программы, касается источников входных данных и места назначения для результатов. Эти вопросы обсуждаются в гл. 3. postage (почтовый сбор) number (количество) price (цена) cost (стоимость) Рис. 2.1. Один из способов представления четырех переменных Раздел описания переменных всегда начинается со слова var и описывает элементы внутренних данных, требующиеся в ходе выполнения программы. В данном случае таких элементов четыре; их имена: postage, number, price и cost. Каждый элемент можно представить себе так, как показано на рис. 2.1, т.е. в виде клетки, в которой может быть записано некое число. Каждая клетка - это область в памяти компьютера, где хранится указанный элемент данных. Храни- мое значение обычно изменяется в ходе выполнения программы, в силу чего эти элементы данных называются переменными. Явное именование переменных в начале программы или в других ее местах (см. гл. 4) называется описанием переменных; отсюда и название этой части программы. За исключением некото- рых особых переменных, которые называются буферными и связаны с операция- ми ввода и вывода (см. гл. 3), все переменные в программе на Паскале должны быть описаны. Слово integer (целочисленный) в конце строки 5 сопоставляет тип данных переменным postage, number, price и cost. При сопоставлении типа данных эле- менту данных подразумевается, а) что значение, которое имеет элемент данных, должно находиться внут- ри определенного диапазона значений*. б) что операции, которые могут выполняться с элементом данных, могут относиться только к определенному набору операций. Для типа integer диапазон значений - это диапазон целых чисел, предста- вимых в процессоре. Так, например, конкретный процессор может допускать работу с числами в диапазоне от -32767 до +32767. Что же касается операций, то их набор для целочисленных значений в стандартном Паскале включает: + сложение вычитание * умножение div деление mod деление по модулю (остаток от деления нацело) В примерах этой главы встречаются только +, - и ♦. Менее очевидные опе- рации div и mod будут объяснены в гл. 3. Целые числа - это только один из мно- жества типов данных, которые могут быть представлены в компьютере и с кото-
28 Гл. 2. Как читать программы рыми он может иметь дело. В ходе обсуждения мы встретимся и с другими типа- ми данных. Раздел операторов содержит команды, которые описывают процесс, подле- жащий выполнению. Каждая команда называется оператором, и весь процесс описывается последовательностью операторов, начинающейся словом begin („начало”) и завершающейся словом end (’’конец”). Выполняя операторы один за другим, процессор производит требующиеся вычисления. Таким образом, программа Расчет! содержит четыре переменные, и все они целочисленного типа; процесс, который должен быть над ними выполнен, описывается последовательностью из пяти операторов. 2.2. Трассировка программ Сейчас мы покажем, как для осуществления расчета выполняются опера- торы программы Расчет!, В качестве входных данных требуются два элемента информации, а именно: количество покупаемых единиц товара - целое число, стоимость единицы товара в долларах - целое число. Если, например, должна быть рассчитана стоимость четырех единиц, по 10 долларов каждая, входные данные будут иметь вид 4 10 а выполняться программа в этом случае будет так, как показано в табл. 2.1. Здесь в табличной форме отображено, что в точности происходит при выполне- нии каждой строки из раздела операторов. Такое отображение называется трас- сировкой {прослеживанием). Таблица 2.1. Выполнение программы Расчет! Входные данные 4 10 Трассировка строка ход выполнения postage number price cost 6 7 8 9 10 11 12 вход в Расчет! вывод: 45 выход из Расчет! 5 4 10 40 45 Результаты 45 Эта и последующие таблицы трассировки имеют одну и ту же структуру: а) В самой левой колонке, озаглавленной ’’строка”, записаны номера строк, соответствующих операторам, в том порядке, в котором послед- ние выполняются.
Гл. 2. Как читать программы 29 б) Колонка ’’ход выполнения” содержит пометки о входах и выходах в разные части программы и о других факторах, влияющих на то, какой оператор будет выполняться следующим. Эти сведения называются информацией об управлении. В этой же колонке показываются порожда- емые результаты. в) Каждая из остальных колонок соответствует переменной в программе и используется для прослеживания значений этой переменной по ходу выполнения. В данном случае таких колонок четыре, потому что четыре переменные описаны в соответствующем разделе программы. Вход в Расчет! осуществляется в начале раздела операторов, т.е. в строке 6, и в этот момент четыре переменные уже созданы, но еще не имеют кон- кретных значений. Говорят, что их значения не определены - факт, который от- мечается в трассировке вопросительными знаками в колонках, соответствующих переменным. Полагать, что переменная имеет какое бы то ни было начальное значение (например, нулевое), ошибочно. Первый оператор, который должен быть выполнен, записан в строке 7. Его выполнение приводит к тому, что значение переменной postage становится равным 5. Такой оператор называется оператором присваивания, так как занесе- ние результата в переменную принято называть присваиванием значения пере- менной. Оператор делится на две части символом который является знаком операции присваивания. Та часть, которая находится справа от него, определяет значение, в данном случае равное 5, а часть, расположенная слева, определяет имя переменной, которой должно быть присвоено данное значение. Затем выполняется строка 8. Это оператор чтения (ввода), в результате выполнения которого приобретают значения переменные number и price, упомя- нутые в операторе. Значения поступают в виде входных данных. Таким образом, после выполнения этого оператора number имеет значение 4, a price - значение 10, в то время как значение cost по-прежнему не определено. Далее мы переходим к строке 9, содержащей другой оператор присваива- ния. Часть оператора, находящаяся справа от знака операции присваивания, опять определяет значение, но на этот раз оно является результатом вычисле- ний. Вычисления, которые нужно произвести, обозначены как number * price Такого рода сочетание имен переменных и знаков операций называется арифме- тическим выражением и означает, что текущие значения переменных number и price должны быть перемножены. Так как number имеет значение 4, a price - значение 10, значение выражения равно 40. Подобные выражения строятся на основе правил, весьма сходных с правилами, применяемыми в алгебре; полнос- тью они будут рассмотрены в гл. 6. Следующей должна выполняться строка 10. Это еще один оператор при- сваивания, и на этот раз справа от знака операции стоит выражение cost + postage поэтому текущее значение переменной cost, т.е. 40, нужно сложить со значени- ем переменной postage, равным 5. В результате, значением выражения становит- ся 45. Слева от знака операции присваивания тоже указана переменная cost, поэтому новое значение, заменяющее прежнее, присваивается именно этой переменной. Предыдущее значение переменной cost при этом утрачивается и не может быть использовано при последующих вычислениях; принято говорить, что новое значение затирает старое.
30 Гл. 2. Как читать программы Последним выполняется оператор из строки 11. Это оператор вывода, после выполнения которого значение указанной переменной (переменной cost) выводится в качестве результата расчета. Достижение строки 12 означает, что вычисления окончены; так как мы выходим из программы, элементы данных, которые существовали в ходе ее выполнения, разрушаются, и единственным свидетельством того, что программа была выполнена, остается результат, порожденный строкой 11. Разрушение элементов данных показано в трассировке путем завершения соответствующих колонок. Рассмотренный процесс можно было бы неформально описать так: установить почтовые расходы в размере 5; прочитать количество закупаемых единиц и цену каждой; рассчитать основную стоимость; прибавить к ней затраты на пересылку (почтовый сбор); выдать общую стоимость. Главное достоинство такой программы состоит в том, что на основе неиз- менного порядка вычислений можно производить различные расчеты, задавая разные наборы входных данных. В качестве упражнения выполните трассировку программы Расчет! с такими входными данными: 10 15 Вы обнаружите, что вычисления происходят по тому же образцу, что и раньше, но значения переменных в ходе вычислений и результат оказываются другими. 2.3. Константы Переменная postage в программе Расчет! используется иначе, нежели остальные переменные. Ее величина в начале программы становится равной 5, а затем не изменяется. Она будет одинаковой при каждом выполнении программы, независимо от входных данных. Таким образом, postage, по существу, не явля- ется переменной, ее значение постоянно. Этот факт учтен в программе Расчет2, которая представлена на листинге 2.2. 1 2 3 4 5 6 7 8 9 10 11 12 .13 14 program Account2 (input,out put)’, const postage - 5; var number, price, cost : integer, begin read (number, price)', cost number * price', costcost + postage', write (cost) end . Листинг 2.2. Текст программы Расчет2 В программе Расчет2 основных разделов - четыре: строка 1 строки 3-4 строки 6-7 строки 9-14 : заголовок программы; : раздел определения констант; : раздел описания переменных; : раздел операторов.
Гл. 2. Как читать программы 31 В программе появился новый раздел, раздел определения констант, и в нем, естественно, определяется константа. Так же, как и переменная, константа - это элемент данных, который во время выполнения программы занимает место в памяти компьютера. Различие состоит в том, что хранимое значение на протяжении выполнения программы в этом случае остается неизменным. В разделе определения констант, который начинается со слова const и который всегда должен предшествовать разделу описания переменных, в общем случае вводится несколько констант. Для каждой определяется имя и связанное с ним фиксированное значение. В данном случае константа postage является единственной, и ее значение устанавливается равным 5. Константа всегда характеризуется каким-либо связанным- с ней типом дан- ных. Тип данных определяется значением, которое придается константе. Конста- нта postage имеет тип integer, так как ее значение - целое число 5, Трассировка программы Расчет2 приведена в табл. 2.2. Она аналогична трассировке программы Расчет!, причем результаты работы обеих программ аб- солютно одинаковы. Для пользователя они неразличимы, ибо он видит лишь входные данные и выходные данные. Для программиста же программа Расчет2 немного лучше, потому что в ее тексте явно сказано, что значение postage явля- ется постоянным. Таблица 2.2. Выполнение программы Расчет2 Входные данные 4 10 Трассировка строка ход выполнения number price cost 9 вход в Расчет2 9 9 9 10 4 10 И 40 12 45 13 вывод: 45 14 выход из Расчет2 Результаты 45 В программе Расчет2 можно было обойтись и без константы, записав опе- ратор в строке 12 как cost := cost + 5 но это затруднило бы понимание программы. Благодаря использованию поиме- нованной константы postage ясно, что величина, прибавляемая к стоимости, это почтовый сбор.
32 Гл. 2. Как читать программы 2.4. Что такое структура в программировании По ходу изложения мы часто будем употреблять слово „структура”. Структура - это способ объединения составных частей, образующих нечто целое. В программировании данное понятие применяется в трех случаях: а) когда речь идет об особых структурных операторах и других средствах, с помощью которых можно организовывать команды в программе; б) когда говорят о группировке отдельных элементов данных в так называ- емые структуры данных*, в) при описании естественной структуры задачи, которая может быть отра- жена как в структуре программы, предназначенной для ее решения, так и в структурах данных этой программы. Все эти варианты употребления термина имеют отношение к структурному программированию, В следующих трех разделах рассматриваются важнейшие средства органи- зации команд внутри программы. Этими средствами являются: последовательное выполнение: несколько операторов выполняются один за другим; выбор: выполнение одного из нескольких возможных операторов; повторение, или цикл: многократное выполнение некоторого оператора. Структуры данных и естественная структура задачи будут обсуждаться позже. 2.5. Последовательное выполнение Мы уже сталкивались с последовательным выполнением в программах Расчет! и Расчет2, Работа каждой из них сводится просто к выполнению опе- раторов одного за другим. Каждый оператор описывает часть всего процесса или, другими словами, подпроцесс. Таким образом, одной из возможных структур процесса является последовательность подпроцессов. Последовательность подпро- цессов обязательно выполняется начиная с первого из них, а завершается выполнением последнего. Кроме того, выполнение каждого подпроцесса должно быть завершено до начала следующего. Языковое средство, обеспечивающее последовательное выполнение в Паскале, называется составным оператором. Составной оператор имеет вид begin 51; 52; Sn end где 51, 52, Sn - операторы. Следовательно, как в Расчете!, так и в Расчете2 раздел операторов представляет собой один составной оператор. В Паскаль-программах это вообще всегда так. Процесс, который может быть представлен как последовательность подпро- цессов, называется последовательным процессом. Существуют процессы, где необходимо или желательно, чтобы несколько подпроцессов протекали одновременно. Такие процессы относятся к числу параллельных. Паскаль - язык последовательного программирования. Он может использоваться только для программирования последовательных процессов.
Гл. 2. Как читать программы 33 2.6. Выбор Служащий, о котором шла речь в гл. 1, должен осуществлять выбор. Если на карточке встречается слово ’’психология”, он должен писать адрес на конверте. Если же слово ’’психология” не встретилось, надписывать конверт не нужно. Это простой выбор одной из двух возможностей. Чтобы проиллюстрировать выбор в программе, вернемся к примеру ’’това- ры - почтой”, но будем считать, что почтовые сборы зависят от основной стои- мости приобретенных товаров. Если основная стоимость меньше 100 дол., почтовый сбор составляет 5 дол.; если же закупленные товары стоят дороже 100 дол., почтовый сбор равен 10 дол. Теперь, когда есть два немного отличающихся способа расчета суммарных затрат, в каждом конкретном случае приходится выбирать подходящий способ. Можно было бы написать две разные программы, однако такой путь явно неудо- влетворителен, ибо решить, какую из программ применять, можно было бы то- лько после вычисления основной стоимости. Решение должен принимать процес- сор после того, как подсчитана основная стоимость. Возможное решение представлено в программе РасчетЗ (листинг 2.3). Если сравнить ее с программой РасчетЗ, видны два внесенных изменения. Во- первых, поскольку теперь возможны два значения почтовых расходов, в раздел определения констант включены две константы: lowpostage и highpostage (’’низкий почтовый сбор” и ’’высокий почтовый сбор”) со значениями 5 и 10 соответственно. Во-вторых, оператор присваивания в строке 12 программы РасчетЗ, где почтовый сбор складывается с основной стоимостью, заменен более сложным оператором, размещенным в строках 13-15 программы РасчетЗ. Он на- зывается условным оператором. Его назначение здесь - осуществлять выбор одного из двух слегка отличающихся способов расчета общих затрат. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 program Accounts (input,output); const lowpostage - 5; highpostage - 10; var number, price, cost : integer, begin read (number,price); costnumber*price; if cost < 100 then costcost + lowpostage; else costcost + highpostage; write (cost) end . Листинг 2.3. Текст программы РасчетЗ В строке 13 после слова if (’’если”) записано соотношение: cost <100 и это еще один пример выражения. Его значение не является числовым, и в отличие, скажем, от значения выражения в строке 12: number * price 2 Заказ № 1110
34 Гл. 2. Как читать программы приобретает одно из двух возможных значений: false (’’ложь”) или true („истина”). Если, например, cost равно 40, то выражение cost < 100 истинно, т.е. имеет значение true. Если же cost равно 150, то cost <100 ложно, т.е. имеет значение false. Такого рода выражения называются булевскими выражениями. или условиями, a false и true - булевскими (логическими) значениями. Знак < называется операцией сравнения. Всего таких операций шесть: « равно; < > не равно; < меньше; < = меньше или равно; > больше; > « больше или равно. Для второго, четвертого и последнего из них математики используют особые знаки /, < > , но в языке программирования это невозможно, потому что эти символы не всегда доступны на устройствах ввода и вывода компьютерной системы. В стандартном Паскале вместо них используются составные символы о, <= и >=. Строка 14 содержит слово then („то”, „тогда”), за которым следует опера- тор, а строка 15 - слово else („иначе”) тоже с оператором вслед за ним. Рассмот- ренные три строки составляют условный оператор в его полном виде, который выполняется по следующим правилам: а) вычисляется булевское выражение; б) если булевское выражение истинно (имеет значение true), то выполня- ется оператор, который следует за словом then, а оператор, который следует за словом else, пропускается; в) если, наоборот, булевское выражение ложно (false), то пропускается оператор, который следует за словом then, и выполняется оператор, за- писанный после слова else. Таким образом, условный оператор приводит к выбору одного из двух операто- ров присваивания: cost:= cost + lowpostage или cost:= cost + highpostage Трассировка программы представлена в табл. 2.3, и входные данные тако- вы, что выбирается низкий почтовый сбор. Строка 14 выполняется, а строка 15 пропускается. В качестве упражнения повторите трассировку, используя входные данные 10 15 и вы обнаружите, что булевское выражение в строке 13 будет ложным, и поэтому строка 14 будет пропущена, а строка 15 - выполнена. Условный оператор в программе РасчетЗ имеет следующую структуру: if е then 51 else 52 где е - булевское выражение, а 51 и 52 - операторы („если е то 51 иначе 52”). Так же, как составной оператор, этот оператор структурен, потому что содержит другие операторы.
Гл. 2. Как читать программы 35 Таблица 2.3. Выполнение программы РасчетЗ Входные данные 4 10 Трассировка строка ход выполнения number price cost 10 вход в РасчетЗ 2 9 9 11 4 10 12 40 13 (cost<AOO)-true 14 45 16 вывод: 45 17 выход из РасчетЗ Результаты 45 Есть и более простой вид выбора: выбор между действием и его отсутст- вием. Он может быть выражен посредством упрощенного условного оператора, в котором последняя часть опущена. Общая форма такого оператора: if е then 51 где е - булевское выражение, а 51 - оператор (’’если е то 51”). Если, например, предприятие ’’товары - почтой” решает не взимать почтовых сборов с высоко- оплачиваемых поставок, то вместо одного из двух способов расчета общих затрат нужно будет просто решать, прибавлять к стоимости почтовый сбор или пропус- кать эту операцию. Такую программу можно получить, заменив строку 12 в Расчете2 (с. 30) следующим оператором: if cost <100 then cost:= cost + postage*, Условный оператор можно использовать и в ситуациях, когда возможнос- тей больше, чем две. Это достигается за счет вложения одних условных операто- ров в другие. Рассмотрим пример: if cost < 100 then cost:- cost + lowpostage else if cost < 200 then cost:- cost+highpostage else cost:= cost+veryhighpostage где veryhighpostage (’’очень высокий почтовый сбор”) - еще одна константа. Это один условный оператор, однако оператор, который стоит в нем за словом else, в свою очередь является условным. Конструкция в целом позволяет осуществить выбор одного из трех операторов присваивания по правилам: когда cost < 100, то выбирается cost:= cost + lowpostage*, когда cost >= 100 и cost < 200, то выбирается cost:= cost + lowpostage*, когда cost >- 200, то выбирается cost:- cost + veryhighpostage.
36 Гл. 2. Как читать программы Итак, условный оператор обеспечивает возможность выбора из двух или более возможностей, причем одна из них может состоять в отсутствии действия. 2.7. Повторение Мы отмечали в гл. 1, что для выполнения полученного задания служаще- му нужно осуществить ’’длинную повторяющуюся последовательность действий”. После того как выяснен предмет нового каталога, оставшаяся часть процесса состоит в многократном повторении одного и того же подпроцесса до тех пор, пока не будут исчерпаны все карточки со сведениями о клиентах. Повторение характеризуется двумя основными аспектами: а) подпроцессом, который должен повторяться; б) условием, при котором повторение (цикл) прекращается. Программа Расчет4, приведенная на листинге 2.4, служит для иллюстра- ции повторения. Конструкция повторения размещена в строках 13-18 и представляет собой единый оператор цикла. Он предназначен для многократ- ного, циклического выполнения операторов, записанных в строках 15-17. Использованный в программе оператор цикла имеет следующую общую структуру: while е do 51 где е - булевское выражение, а 51 - некоторый оператор, и означает: ’’пока е выполнять 51”. Здесь мы имеем дело с оператором цикла "пока”. Оператор 51 часто называют телом оператора ’’пока”. Тело этого оператора - всегда единст- венный оператор, но, как в нашем примере, он может быть составным. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 program Account4 (input, output); const lowpostage - 5; highpostage - 10; var items, totalcost, number, price: integer; begin read (items); totalcost0; while items > 0 do begin items items - 1; read (number, price); totalcosttotalcost + number ♦ price end; if totalcost < 100 then totalcosttotalcost + lowpostage; else totalcost totalcost + highpostage; write (totalcost) end . Листинг 2.4. Текст программы Расчет4 Следующие правила полностью определяют порядок выполнения операто- ра цикла, который отвечает смыслу слова ’’пока”: а) вычисляется булевское выражение;
Гл 2. Как читать программы 37 б) если значением булевского выражения является ’’ложь”, выполнение оператора цикла заканчивается; в) если же значением булевского выражения является ’’истина”, то проис- ходит однократное выполнение тела оператора и затем переход к шагу (а). Итак, определенное действие должно повторяться до тех пор, пока условие остается истинным. В программе Расчет4 снова подсчитываются общие затраты, но теперь продается несколько разных видов товаров, и цены их различны. Возможный набор входных данных приведен в табл. 2.4. Первое число показывает, сколько имеется видов товара, а в каждой из остальных строк содержатся сведения об одном виде товара, а именно: сколько ’’штук” покупается и цена одной штуки. Таблица 2.4. Выполнение программы Расчет4 Входные данные 3 4 50 3 25 6 30 Трассировка строка ход выполнения items totalcost number price 10 вход в Расчет4 о *> 2 11 3 12 0 13 (items > 0) - true 15 2 16 4 50 17 ' 200 13 (items > 0) = true 15 1 16 3 25 17 275 13 (items > 0) - true 15 0 16 6 30 17 455 13 (items > 0) - false 19 (totalcost < 100) -‘false 21 465 22 вывод: 465 23 выход из Расчет4 Результаты 465 Трассировка выполнения программы Расчет4 приведена в той же табл. 2.4. Из нее видно, как под управлением булевского выражения items > О
38 Гл. 2. Как читать программы происходит циклическое выполнение операторов, находящихся в строках 15-17. После выполнения оператора ввода read в строке 11 значение переменной items равно трем, а поскольку оно уменьшается на единицу всякий раз, когда выпол- няется строка 15, то первые три вычисления указанного булевского выражения дают значение ’’истина”. При четвертом выполнении строки 13 переменная items равна нулю, и булевское выражение обращается в ложь. Выполнение опе- ратора ’’пока” тем самым заканчивается, и выполняется следующий за ним опе- ратор в строке 19. Дальнейшие операторы, как и раньше, добавляют величину почтового сбора и выводят результат. Из анализа выполнения программы Расчет4 можно вынести еще два сооб- ражения. Во-первых, здесь многократно выполняется оператор ввода read. В строке 11 считывается первое значение входных данных, равное 3. Для выпол- нения следующего оператора read (в строке 16) требуются два целых числа, и вводятся числа 4 и 50. Повторное выполнение того же оператора приводит к считыванию очередной пары значений и т.д. Другими словами, выполнение каждого оператора ввода возобновляется с того места во входных данных, где завершилось выполнение предыдущего. Во-вторых, в строке 17 записано арифметическое выражение totalcost + number * price Читателю, который помнит школьную алгебру, нетрудно будет вычислить это выражение, если, например: totalcost = 200, number = 3, price = 25 (значения переменных при втором выполнении строки 17). Результат, очевидно, равен 275. Он получается после вычислений: 200+3*25 = 200+75 = 275 Ответ верен, но лишь при условии, что достигнута договоренность о том, что умножение должно производиться прежде сложения. Мы говорим, что умноже- ние обладает старшинством по отношению к сложению. Если нужен другой порядок действий, то для отмены соглашения можно использовать скобки. Если бы выражение было записано в виде {totalcost + number} * price то вычисление производилось бы иначе: (200+3) *25 = 203*25 = 5075 Итак, правила вычисления арифметический выражений в Паскале те же, что в математике. Так же обстоит дело во многих, хотя и не во всех, языках высокого уровня. Наконец, в большинстве случаев применения цикла требуется лишь ко- нечное число повторений. Чтобы избежать бесконечного повторения, необходимо хотя бы одну переменную, входящую в булевское выражение, изменять в теле оператора цикла. Более того, эти изменения должны быть таковы, чтобы булев- ское выражение рано или поздно обратилось в ’’ложь”. Если же булевское выра- жение первоначально истинно и ни при каких обстоятельствах не становится ложным, то выполнение оператора цикла никогда не закончится. В этом случае мы говорим, что процесс ’’зацикливается”. Вы встретитесь с такой ошибкой в упражнении 2.5 и, вероятно, будете часто сталкиваться с ней, когда начнете писать программы.
Гл. 2. Как читать программы 39 2.8. Сочетание программных конструкций Мы рассмотрели три фундаментальных средства структурирования про- грамм: последовательное выполнение - обеспечивается составными операторами; выбор - обеспечивается условными операторами; повторение - обеспечивается операторами цикла ’’пока”. Составные операторы, условные операторы и операторы цикла являются структурными в том смысле, что для построения более сложных программных структур их можно легко комбинировать, вкладывая друг в друга. В терминах Паскаля это равносильно следующему. Для каждого вида структурных операторов: а) begin б) if е в) if е г) while e do S1; then S1 then SI SI S2; else S2 Sn end объекты, обозначенные как SI, S2, ..., Sn, могут быть как простыми неструктур- ными операторами, скажем, ввода, вывода или присваивания, так и в свою очередь структурными операторами. Мы уже несколько раз сталкивались со структурными операторами, вло- женными друг в друга. Один из примеров такого вложения есть в программе Расчет4 (с. 36), где составной оператор образует весь раздел операторов и содержит: оператор ввода, оператор присваивания, оператор цикла ’’пока”, условный оператор, оператор вывода, причем телом оператора цикла опять-таки является составной оператор. Другим прймером с аналогичной, но немного отличающейся структурой является программа РасчетЗ (листинг 2.5) - еще одна вариация на тему ’’товары -почтой”. На этот раз в составной оператор входят компоненты: оператор ввода, оператор присваивания, оператор цикла ’’пока”, оператор вывода, причем телом оператора цикла опять является составной оператор. Этот составной оператор образован: оператором присваивания, оператором ввода, оператором присваивания, условным оператором, оператором присваивания. Таким образом, в Расчете4 условный оператор размещается после опера- тора цикла и выполняется один раз после того, как завершено выполнение опе- ратора цикла. В РасчетеЗ условный оператор размещается внутри оператора ци- кла и выполняется всякий раз, когда выполняется тело оператора цикла.
40 Гл. 2. Как читать программы 1 2 3 4 5 6 7 8 9 10 II 12 13 14 15 16 17 18 19 20 21 22 23 24 program Accounts (input,output); const lowpostage - 5; highpostage - 10; var items, totalcost, number, price, cost: integer, begin read (items); totalcost0; while items > 0 do begin items items - 1; read (number, price); cost number*price if cost < 100 then costcost + lowpostage; else costcost + highpostage; totalcosttotalcost + cost end; write (totalcost) end . Листинг 2.5. Текст программы РасчетЗ Подробная трассировка программы Расчет5 приведена в табл. 2.5. Резуль- татом опять-таки является полная стоимость товаров, приобретенных в фирме ’’товары - почтой”, но на этот раз для каждого вида товара почтовый сбор вычисляется отдельно. Внимательно разберите эту трассировку. Возможность сколь угодно сложного включения друг в друга трех фунда- ментальных программных структур является настолько мощным средством, что позволяет написать любую последовательную программу. Все остальные типы операторов и средства структурирования программ, используемые в языках пос- ледовательного программирования, применяются лишь потому, что они удобнее в конкретных обстоятельствах, но не потому, что без них нельзя обойтись. 2.9. Расположение текста программы и пунктуация Пришло время заострить ваше внимание на том, что тексты рассматривае- мых программ располагаются определенным образом, и это весьма существенно. В частности, пустые строки использовались для отделения основных разделов программы друг от друга, а отступы применялись в разных случаях, но всегда для того, чтобы ясно показать, где одна структура включается в другую. Боль- шинство опытных программистов, пишущих на Паскале, считают, что проду- манное расположение текстов программ является важным аспектом квалифици- рованного программирования. Единого утвержденного стиля расположения текстов программ не сущест- вует, и на практике приходится сталкиваться с несколькими различными сти- лями. В данном руководстве стиль выбран таким, чтобы сделать программы мак- симально четкими и ясными. Мы настоятельно рекомендуем вам избрать этот или какой-нибудь другой определенный стиль расположения текстов Паскаль- программ.
Гл. 2. Как читать программы 41 Таблица 2.5. Выполнение программы РасчетЗ Входные данные 3 4 50 3 25 6 30 Трассировка строка ход выполнения items totalcost number price cost 10 вход в РасчетЗ ? 9 9 9 9 11 3 12 0 13 (items > 0) - true 15 2 16 4 50 17 200 18 (cost < 100) - false 20 210 21 210 13 (items > 0) - true 15 1 16 3 25 17 75 18 (cost < 100) - true 19 80 21 290 13 (items > 0) - true 15 0 16 6 30 17 180 18 (cost < 100) - false 20 190 21 480 13 (items > 0) - false 23 вывод: 480 24 выход из РасчетЗ Результаты 480 Пора также обратить ваше внимание на пунктуацию. В программах на Паскале встречаются четыре знака препинания, а именно: запятая, точка с за- пятой, двоеточие и точка - причем все они должны употребляться строго по наз- начению. Так что, читая программу, будьте наблюдательны. Таким путем вы очень многое сможете узнать о языке программирования и его правилах. В этой главе мы ввели вас в широкий круг основополагающих понятий программирования вообще и программирования на языке Паскаль в частности. Теперь вы можете утверждать, что прочли несколько компьютерных программ. Было бы замечательно, если бы к этому вы могли добавить, что поняли их.
42 Гл 2. Как читать программы Упражнения 2.1. Известно, что first, second, third и fourth - четыре переменные, значения которых равны соответ- ственно 5, 3, 7 и 2. Вычислите арифметические выражения: a) first - second б) first ♦ second + third в) first ♦ {second + third) r) first ♦ third - second ♦ fourth и булевские выражения: д) first >- second e) third <- second ж) fourth a {third - first) з) {first*fourth) <> {second+third) 2.2. Выполните трассировку программы Присваивания (листинг 2.6) и определите результаты при входных данных 7 11 1 program Assignments {input,out put)’, 2 3 const 4 n - 10; 5 var 6 a, b, c : integer, 7 begin 8 read {a,b)‘, 9 c a - b + n’, 10 с :- c + c; 11 b :- a + b - c; 12 a :- a + b - c; 13 a :- n ♦ a; 14 write {a) 15 end . Листинг 2.6. Текст программы Присваивания 2.3. Выполните трассировку программы Расчет4 (с. 36), если входными данными являются числа 4 10 5 6 8 150 200 80 120 Выпишите последовательность значений, которые принимает переменная totalcost на протяжении выполнения программы и результат, который будет выведен. 2.4. С помощью трассировки программы Степени (листинг 2.7) определите результат, который будет выведен для каждого из следующих наборов входных данных: а) 2 3 б) 5 О Что вы думаете о соотношении между входными данными и ожидаемым результатом? Всегда ли программа работает верно? 2.5. Рассмотрите программу Делитель (листинг 2.8). а) С помощью трассировки определите результат при входных данных 49 70
Гл 2. Как читать программы 43 б) Проделайте первые десять шагов трассировки этой программы при входных данных Что, по-вашему, произойдет, если продолжить трассировку? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 program Powers; var number, power, answer, integer; begin read (number,power); answer 1; while power > 0 do begin power := power - 1; answer := answer * number end; write (answer) end . Листинг 2.7. Текст программы Степени 1 2 3 4 5 6 7 8 9 10 11 12 program Divisor (input,output) ; var first, second : integer; begin read (first, second); while first <> second do if first > second then first first - second else second := second - first; write (first) end . Листинг 2.8. Текст программы Делитель 2.6. Измените программу РасчетЗ (с. 40) так, чтобы сложение основной стоимости товара каждой группы с почтовым сбором осуществлялось в соответствии с табл. 2.6. Таблица 2.6 стоимость товара почтовый сбор менее 100 8 100-200 12 более 200 бесплатно
з простейший ввод и вывод Во второй главе мы уже встретились с операторами ввода и вывода. В этой главе мы более подробно рассмотрим операции ввода и вывода и попутно развеем тайны, связанные с операциями div и mod. Сообщаемые здесь сведения, равно как и примеры, которые встречаются на протяжении всей книги, подобраны в предположении, что вводимые данные отделены от результатов и что система ввода-вывода соответствует стандартному Паскалю. Если, однако, программы выполняются в интерактивном режиме1, то данные, вводимые пользователем, и результаты, выдаваемые процессором, на экране (или другом устройстве вывода) обычно чередуются. В этом случае выполнение программ, рассматриваемых в качестве примеров, может привести к непредвиденным результатам. 3.1. Символы Основным элементом информации в операциях простого ввода и вывода является символ. Вероятно, проще всего объяснить это понятие, сказав, что один символ порождается при нажатии одной клавиши пишущей машинки. Таким об- разом, примером символа служит отдельная буква или отдельная цифра. Пробел, который порождается при нажатии на соответствующую клавишу пишущей машинки, тоже считается символом. Более того, можно утверждать, что пробел - это самый важный символ. У вас возникли бы определенные труд- ности при попытке прочитать эту страницу, если бы наборщик не позаботился о внесении в нее пробелов. Не менее важно, чтобы правильно обращался с пробелами и процессор. Набор символов, которые могут употребляться в конкретной вычислитель- ной системе, зависит от ее устройств ввода и вывода. Каждое устройство может иметь собственный набор символов, а универсального соглашения о том, каким должен быть этот набор, к сожалению, нет. В данном руководстве мы будем опираться на один из наиболее широко употребимых наборов символов, предста- вленный в табл. 3.1. Это так называемый Американский стандартный код для обмена информацией (American Standard Code for Information Interchange); обозначаемый аббревиатурой ASCII (произносится ”аски”). Возможно, он не совпадает с тем набором символов, с которым вы имеете дело2, но для большин- ства программ это несущественно. В ряде программ, приведенных в следующих главах, мы встретимся с этим фактом и уделим ему внимание. 1 Т.е. во взаимодействии с пользователем. - Примеч. пер. 2 На ЭВМ, используемых у нас в стране, почти всегда имеются символы русского алфавита; иногда это расширение набора символов ASCII. - Примеч. ред.
Гл. 3. Простой ввод и вывод 45 Каждый символ в наборе имеет связанный с ним порядковый номер. В табл. 3.1 порядковые номера символов ASCII приведены в колонках, которые озаглавлены символом ”п”. Так как полный набор включает 128 символов, порядковые номера лежат в диапазоне от 0 до 127. Таблица 3.1. Американский стандартный код для обмена информацией (ASCII) ASCII N ASCII N ASCII N ASCII N NUL 0 пробел 32 @ 64 ‘ 96 SOH 1 ! 33 A 65 a 97 STX 2 ” 34 В 66 b 98 ЕТХ 3 # 35 C 67 c 99 EOT 4 $ 36 D 68 d 100 ENQ 5 % 37 E 69 e 101 АСК 6 & 38 F 70 f 102 BEL 7 ’ 39 G 71 g 103 BS 8 ( 40 H 72 h 104 НТ 9 ) 41 I 73 i 105 LF 10 * 42 J 74 j Ю6 VT И + 43 К 75 k 107 FF 12 , 44 L 76 1 108 CR 13 - 45 M 77 m 109 SO 14 . 46 N 78 n 110 SI 15 / 47 0 79 о 111 DLE 16 0 48 P 80 p 112 DC1 17 1 49 Q 81 q 113 DC2 18 2 50 R 82 r 114 DC3 19 3 51 S 83 s 115 DC4 20 4 52 T 84 t 116 NAK 21 5 53 U 85 u 117 SYN 22 6 54 V 86 v 118 ETB 23 7 55 W 87 w 119 CAN 24 8 56 X 88 x 120 EM 25 9 57 Y 89 У 121 SUB 26 : 58 Z 90 z 122 ESC 27 ; 59 [ 91 { 123 FS 28 < 60 \ 92 | 124 GS 29 = 61 ] 93 } 125 RS 30 > 62 - 94 " 126 US 31 ? 63 95 del 127 В общем случае набор содержит две группы символов. Первая группа - это изображаемые символы. К ним относятся: символ пробела; десять цифр - 0123456789; двадцать шесть латинских букв, каждая из которых может быть представлена в одной из форм, прописной: ABCDEFGHIJKLMNOPQRSTUVWXYZ,
46 Гл. 3. Простой ввод и вывод строчной: abcdefghijklmnopqrstuvwxyz или же в обеих формах; знаки препинания, арифметические символы, скобки и другие знаки, обычно встречающиеся в печатных текстах. Изображаемые символы ASCII имеют порядковые номера от 32 до 126. Вторую группу составляют управляющие символы, которые используются в осно- вном для управления оборудованием при передаче данных. Управляющие симво- лы в ASCII имеют порядковые номера 0-31 и 127. Мы не будем касаться их в этой книге, но вам будет полезно знать, что они существуют. 3.2. Текстовые файлы inputs output Во многих прикладных программах вводимые данные и результаты могут рассматриваться как последовательности символов, организованные построчно. В табл. 2.5 (с. 41) входные данные для программы Расчет5 были представлены в виде 3 4 50 3 25 6 30 Предположив, что в начале каждой строки есть три пробела, и считая, что пос- ледняя цифра завершает строку, можно представить эти данные более точно: ™3# 50# 25# 30# Теперь пробелы представлены явно с помощью символа \ и конец каждой стро- ки обозначен символом #. Это то, что в Паскале называется текстовым файлом. Его точное определение: текстовый файл - последовательность, состоящая из нуля или более строк, где каждая строка - последовательность, состоящая из нуля или более символов и заканчивающаяся маркером конца строки. Текстовый файл, нс содержащий ни одной строки, называется пустым, а строка, не содержащая ни одного символа, называется пустой строкой. Заметим, что маркер конца строки, представленный символом #, занимает столько же места, сколько любой другой символ. Во многих случаях полезно представлять себе текстовый файл просто как одну цепочку символов, которая составлена из образующих его строк и включает маркеры конца каждой из них. Например, приведенные выше входные данные могут быть изображены так: —3#—4—50#™3—25#—6—30# В этом примере между последним отличным от пробела символом в строке и завершающим маркером нет других пробелов. Однако так бывает не всегда. Иногда дополнительные пробелы появляются перед маркером конца строки. Их туда может поместить по своему усмотрению пользователь, или же устройство ввода может сделать это автоматически. Процесс извлечения информации из текстового файла называется вводом (или чтением) из файла. Ввод может осуществляться только последовательно, с начала файла. При предложенном способе изображения файлов это означает, что файл читается слева направо. Таким образом, в любой момент времени та
Гл. 3. Простой ввод и вывод 47 (возможно, пустая) часть, которая находится слева, уже прочитана, а ту (возмо- жно, пустую) часть, которая расположена справа, еще остается прочитать. Что- бы изобразить состояние файла, из которого происходит ввод, мы будем исполь- зовать маленькую направленную вверх стрелку, указывающую на текущую пози- цию в файле. Она всегда будет указывать на первый символ той части файла, которую остается прочитать. Так, например, на рис. 3.1 изображен текстовый файл (а) перед началом чтения символов; (б) в ситуации, когда прочитаны сим- волы вплоть до цифры 0 в числе 50, включая эту цифру; (в) когда достигнут ко- нец файла. а) —3#—50#—3—25#—30# I б) -4- 5О#~'~3"~"25# 6~'"~30# I в) 3# 4—~-'5О#~~3-'-"25# 6~ 30# Рис.3.1. Ввод из текстового файла понедельник# понедельник#вторник# понедельник#вторник#среда# понедельник#вторник#среда#четверг# Рис.3.2. Вывод в текстовый файл Процесс выдачи информации в текстовый файл называется выводом (или записью} в файл. Текстовый файл, в который производится вывод, первоначаль- но всегда пуст, и символы выводятся в него тогда и по мере того, как оказыва- ются в наличии. Новые символы, выводимые в текстовый файл, всегда добав- ляются вслед за той информацией, которая в нем уже содержится. На рис. 3.2 показано увеличение текстового файла, в который выводятся названия четы- рех дней недели, каждое - в отдельной строке. На всех этапах текущая позиция, т.е. место, с которого будет добавлена новая информация (если таковая поя- вится), - представлена направленной вверх стрелкой. Если распечатать или выдать на экран файл в его окончательном состоянии, то получится
48 Гл. 3. Простой ввод и вывод понедельник вторник среда четверг Вспомним, что слова input и output встречались в первой строке каждой программы как часть ее заголовка и что они были связаны с источником вводи- мых данных и назначением получаемых результатов. На самом деле input и output - это имена двух предопределенных текстовых файлов, называемых обыч- но входным файлом и выходным файлом. Процессор читает входные данные из входного файла и записывает результаты в выходной файл. 3.3. Ввод и вывод символов Давайте используем для обсуждения ввода и вывода символов в Паскале программу ПовторСлова, представленную на листинге 3.1. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 program Copy word (input, output)-, const space - ’ ’; var ch : char; begin while inpur “ space do read (ch)’, while inpur <> space do begin read (ch)’, write (ch) end end . Листинг 3.1. Текст программы ПовторСлова Кроме входного и выходного файлов с этой программой связаны три эле- мента данных. Первые два - это константа space, определенная в строке 4, и переменная ch, описанная в строке 7. Так же, как и в ранее рассмотренных программах, каждая из них может хранить значение, но только не число, а оди- ночный символ. Фиксированное значение константы space - это пробел, обозна- чаемый в программе как ’ ’. Тот факт, что ch - символьная переменная, обозна- чается с помощью слова char\ следующего за двоеточием во фрагменте ch : char Мы говорим, что space и ch - переменные символьного типа (имеют тип char) так же, как говорили, что элементы данных в наших прежних программах были целочисленными (имели тип integer). Третьим элементом данных является переменная input", которая встреча- ется в строках 10 и 12. Ее имя образуется из имени входного файла и символа л и произносится: ”инпут-стрелка-вверх”. Эта переменная действует как посред- ник между входным файлом и программой и поэтому называется буферной переменной. Буферные переменные составляют тот единственный тип перемен- Сокращенное character - символ. - Примеч. пер.
Гл. 3. Простой ввод и вывод 49 ных в Паскале, которые не нужно описывать; такая переменная автоматически становится доступной для каждого используемого файла. Буферная переменная inpuC имеет тип char. В любой момент выполнения программы значение буферной переменной inpuC зависит от состояния входного файла. Содержащийся в ней символ опре- деляется по следующим правилам: а) когда входной файл пуст или полностью прочитан, значение inpuC не определено; б) когда в текущей позиции находится маркер конца строки, inpuC имеет значение ’ ’ (пробел); в) во всех остальных случаях inpuC содержит копию символа, находящего- ся в текущей позиции, т.е. копию очередного символа, который будет введен из входного файла. По существу, inpuC - единственный символ во входном файле, который доступен процессору. Теперь мы готовы к тому, чтобы проследить за выполнением программы ПовторСлова, используя трассировку в табл. 3.2. В общем случае в начале выполнения программы ситуация такова*. а) входной файл содержит входные данные для. программы; б) переменная inpuC содержит копию первого символа из входного файла; в) выходной файл пуст. Таким образом, при входе в программу ПовторСлова в строке 9 состояние файлов и других переменных (при заданных значениях входных данных) таково, как показано в первой строке таблицы. Теперь, прежде чем отслеживать действие двух операторов цикла, образующих тело программы, мы должны рассмотреть два входящих в них простых оператора. Первый простой оператор: read (ch) встречается в строке 11, а затем в строке 14. Так как ch - переменная символь- ного типа, выполнение этого оператора приводит к чтению из входного файла одного символа. Переменная ch называется параметром оператора ввода, и когда параметр имеет тип char, процесс чтения сводится в точности к следующему: а) текущее значение буферной переменной inpuC присваивается перемен- ной, заданной в качестве параметра оператора; б) текущая позиция во входном файле передвигается на один символ, что приводит к изменению значения inpuC. Второй оператор, который мы должны рассмотреть, записан в строке 15: write (ch) Так как переменная ch имеет тип char, выполнение этого оператора вызывает запись в выходной файл одного символа (текущего значения ch). Теперь вы сможете понять всю программу. Первым оператором цикла ’’пока” (строки 10-11) управляет булевское выражение inpuC = space которое истинно (true), когда текущее значение inpuC есть пробел, и ложно (false), когда это не так. Таким образом, в результате выполнения этого опера- тора цикла будут пропущены пробелы в начале строки данных. Вторым операто- ром цикла (строки 12-16) управляет выражение inpuC <> space
50 Гл. 3. Простой ввод и вывод поэтому тело цикла выполняется до тех пор, пока inpuC содержит отличающий- ся от пробела символ. В ходе каждого повторения один символ вводится из вход- ного файла и выводится в выходной. Таблица 3.2. Выполнение программы ПовторСлова Входные данные ~TEST--------------------# Трассировка строка ход выполнения входной файл выходной файл inpur ch 9 вход в ПовторСлова ~TEST # * ’ 19 (inpuC^space)-true 11 "TEST # J 5 10 (inpur-space)=true 11 “TEST # T » J 10 (inpur=space) -false 12 (inpur <>s pace) ~true 14 “TEST # ’E’ T 15 T 12 (inpur <>space)=-true 14 "TEST # ’S’ ’E’ 15 ТЕ 12 СприГ <> s pace)rue 14 “TEST # T ’S’ 15 TES 12 (inpur <>space)-true 14 "TEST # T 15 TEST 12 (inpur <>space)~false 17 выход из ПовторСлова “TEST # TEST Результаты TEST
Гл. 3. Простой ввод и вывод 51 Следует упомянуть также о конечном состоянии входного и выходного файлов. В общем случае, когда выполнение программы заканчивается, ситуация такова: а) непрочитанная часть входного файла содержит входные данные, ко- торые не были введены программой; как правило, если программа ра- ботает верно, таких неиспользованных данных нет либо они пред- ставляют собой просто последовательность пробелов; б) выходной файл содержит результаты, порожденные программой. В нашем примере непрочитанными из входного файла остались шесть пробелов и маркер конца строки, тогда как выходной файл содержит четыре символа: TEST которые являются результатом вычисления. Несмотря на то, что символы в вы- ходном файле не образуют полной строки (т.е. отсутствует маркер конца стро- ки), они будут выведены на экран или напечатаны таким образом, как будто маркер есть. Посмотрим теперь, что произошло бы, если бы программа выполнялась с тремя другими наборами входных данных. Использование разных наборов дан- ных всегда является хорошим средством проверки программы, потому что она может правильно работать с одними наборами и неправильно с другими. Для начала предположим, что входные данные имеют вид ОДИН....# В этом случае значение input' при входе в программу отличается от пробела и при первом же вычислении выражение input' = space в строке 10 оказывается ложным. Поэтому управление передается строке 12, и процесс продолжается так же, как и раньше, давая в результате ОДИН Рассмотрим теперь ситуацию, когда входные данные имеют вид ....#—ДВА—# Проблем нет и в этом случае. Маркер в конце первой строки данных рассма- тривается как пробел (см. описание input' на с. 48-49), так что результат будет таким же, как если бы входные данные состояли из единственной строки ........ДВА~# Программа, таким образом, срабатывает правильно, давая в качестве результата ДВА Наконец, посмотрим, что произойдет, если входными данными является последовательность символов ....#.....# Так же, как и раньше, первые пять символов и маркер конца строки будут про- читаны как шесть пробелов, а затем процессор перейдет к обработке пяти пробе- лов во второй строке. Частичная трассировка, начиная с той точки, где пять символов второй строки уже введены, представлена в табл. 3.3. При выполнении оператора в строке 11 стрелка во входном файле указывает на завершающий маркер и input' принимает значение ’ ’. Значит, выражение input' = space ис- тинно и строка 11 должна быть выполнена еще раз. Теперь ch становится рав- ным ’ ’, и так как достигнут конец файла, значение input' становится неопреде-
52 Гл. 3. Простой ввод и вывод ленным (см. с. 49). Именно здесь возникает проблема! Когда процессор снова попытается вычислить булевское выражение inpuC - space в строке 10, это ему не удастся, потому что значение inpuC не определено. Выполнение программы завершится. Такая ситуация называется ошибкой на этапе выполнения. Мы по- требовали, чтобы процессор выполнил нечто невозможное, а именно: решил, яв- ляется неопределенное значение пробелом или нет, - и вполне естественно, что процессор останавливается, как бы отвечая тем самым: ”не могу”. Это первая из многочисленных видов ошибок на этапе выполнения, с которыми вам еще пред- стоит столкнуться. Когда возникает такая ошибка, говорят, что произошел отказ (сбой) пр. граммы. Таблица 3.3. Выполнение программы ПовторСлова (при неправильных данных) Входные данные # # Частичная трассировка строка ход выполнения входной файл выходной inpur ch файл 11 # # 10 (input^space) •true 11 # # ,?, Ю значение (inpur•space) не определено, поэтому выполнение прекращается. Результаты отсутствуют В данном случае отказ вызван неверными данными. Пользователь был не- внимателен. Но на самом деле ошибся программист. Мы говорили в первой гла- ве, что программист должен иметь в виду высокую вероятность ошибок во вход- ных данных. Желательно, чтобы программа выявляла эти ошибки, выдавала соответствующие сообщения пользователю и нормально завершалась. Решением этой задачи вам предстоит вплотную заняться после того, как вы больше узна- ете о программировании на Паскале (см. упр. 12.1). 3.4. Вывод на новую строку Наша следующая программа, см. листинг 3.2, называется СписокИмен*, она приведена для иллюстрации операторов, заставляющих процессор перехо- дить на новую строку во входном или выходном файле. Простой набор входных данных, трассировка и соответствующие результаты представлены в табл. 3.4. Каждая строка данных содержит имя, завершающееся дробной чертой, и, возмо-
Гл. 3. Простой ввод и вывод 53 ж но, несколько дополнительных пробелов. Точное количество дополнительных пробелов несущественно, но во избежание неопределенности в трассировке мы будем полагать, что их ровно два. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 program ListNames (input, output); const numberofnames - 2; space - ’ terminator - 7’; var count’, integer; ch: char; begin count:- numberofnames; while count > 0 do begin count:- count - 1; while inpur - space do read (ch); while inpuK <> terminator do begin read (ch); write (ch) end; writein; writein; readln end end . Листинг 3.2. Текст программы СписокИмен Пожалуйста, прочитайте программу внимательно. Вы обнаружите, что по- нимаете все, кроме, может быть, строк 24-26. Для того чтобы выяснить, что же в точности происходит при выполнении этих строк, рассмотрим соответствую- щую часть трассировки. Непосредственно перед выполнением строки 24 выходной файл имеет вид J0 что представляет собой часть строки. Теперь выполняется оператор в строке 24: writein который означает: вывести один маркер конца строки в выходной файл. Таблица 3.4. Выполнение программы СписокИмен Входные данные J0/ VI/
54 Гл. 3. Простой ввод и вывод Трассировка строка ход выполнения вход выход inpur count ch 12 вход в СписокИмен JO/“#VI/“# ’J’ •) *> 13 2 14 (count>0)-true 16 1 17 (inpur-space)-false 19 (inpur <>terminator) ’-‘true 21 JO/"#VI/~~# ’O’ ’J’ 22 J 19 (inpuC <>terminator)-true 21 JO/“#VI/“# 7’ ’O’ 22 JO 19 (inpur <> termi nator) -false 24 JO# 25 . JO## 26 JO/"#VI/"# ’V’ 14 (count>0)-true 16 0 17 (inpur-space) -false 19 (inpur <>terminator)-true 21 JO/“#VI/“# T ’V’ 22 JO##V 19 (inpur oterminator)-true 21 JO/“#VI/"# 7’ T 22 JO##VI 19 (inpur <>ter initiator) -false 24 JO##VI# 25 JO##VI## 26 JO/"#VI/'“'# ? 14 (count>0)-false 28 выход из СписокИмен JO/"#VI/~~# JO##VI##
Гл. 3. Простой ввод и вывод 55 Результаты J0 VI После выполнения строки 24 выходной файл, следовательно, приобретает вид J0# Строка теперь завершена маркером конца строки, и любые символы, которые после этого будут выводиться в файл, окажутся в новой строке. Однако следующий оператор (строка 25) тоже является оператором writein, поэтому после его выполнения выходной файл становится таким: J0## что представляет собой две строки текста, вторая из которых является пустой. Последний оператор, нуждающийся в объяснении, содержится в строке 26. Он имеет вид readin и означает: пропустить остаток текущей строки во входном файле, включая маркер конца строки. Пропущенные символы могут быть, а могут и не быть пробелами. При выполне- нии этого оператора текущая позиция устанавливается либо на первый символ новой строки, и inpuf становится равным этому символу, либо, если в файле строк больше нет, сразу за концом файла, причем значение inpuf оказывается неопределенным. Согласно трассировке непосредственно перед первым выполнением строки 26 входной файл находится в состоянии JO/~~#VI/~# Выполнение строки 26 приводит к пропуску дробной черты, двух пробелов и маркера конца строки; файл приобретает вид jo/“#vi/~~# причем input* равно ’V’. Второе выполнение строки 26 в конце трассировки приводит файл в конечное состояние JO/~~#VI/“# в котором значение inpuf нс определено. Таким образом, суммарное действие программы СписокИмен состоит в чтении двух последовательностей символов и их записи друг под другом, причем за каждой последовательностью помещается пустая строка.
56 Гл. 3. Простой ввод и вывод 3.5. Ввод и вывод целых чисел Теперь пора более подробно рассмотреть операции ввода и вывода, упоми- навшиеся во второй главе. В каждой из этих операций числа вводились и выво- дились целиком, и нам нужно понять, как такие операции соотносятся с вводом и выводом отдельных символов. В частности, нужно выяснить, какие именно символы извлекаются при вводе из входного файла и какие выдаются при выво- де в выходной файл. Чтобы ответить на эти вопросы, давайте очень внимательно разберем опе- рации ввода и вывода в программе Расчетб (листинг 3.3). За исключением од- ного небольшого исправления в строке 23, она полностью совпадает с програм- мой Расчет5. В качестве входных данных выберем текстовый файл 3#—4- 50#"Л~3™Л25#~~~6™~30# который содержит данные, использованные при трассировке программы Расчет5 в табл. 2.5 (с. 41). Направленная вверх стрелка соответствует текущей позиции в файле при входе в программу. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 program Account6 (input, output) -, const low post age = 5; high postage =10; var items, totalcost, number, price, cost : integer; begin read (items) -, totalcost 0; while items > 0 do begin items items - 1; read (number,price); cost := number*price; if cost < 100 then cost cost + lowpostage else costcost + highpostage’, totalcost totalcost + cost end; write (totalcost : 8) end . Листинг 3.3. Текст программы Расчетб Первый оператор ввода в программе находится в строке 11 read (items) где items - имя переменной типа integer. Какие символы будут введены операто- ром read, определяется типом данных его параметра. Мы уже знаем, что для переменных типа char процессор считывает ровно один символ. Когда же пара- метр имеет тип integer, процессор пытается прочесть одно полное целое число и присвоить соответствующее значение указанной переменной. Это достигается за счет следующей организации чтения символов из входного файла: а) все пробелы и маркеры конца строки, предшествующие числу, пропус- каются;
Гл. 3. Простой ввод и вывод 57 б) читается целое число, и процесс ввода завершается, когда input содер- жит символ, который не может быть частью целого числа. Таким образом, при первом выполнении строки 11 вводятся три пробела и цифра 3, и входной файл приобретает вид —3#——50#—г—25#—г—30# причем input содержит пробел (см. с. 49), а переменной items присваивается значение 3. Есть два типа ситуаций, в которых процессор не сумеет ввести из файла целочисленную величину: а) когда значение input не определено; б) когда последовательность отличных от пробела символов во входном файле не представляет собой целого числа. В обоих случаях процессор не может выполнить того, что требуется в програм- ме, и останавливается. Это еще два типа ошибок на этапе выполнения. Знак чи- сла, если он присутствует, должен стоять непосредственно перед первой цифрой. Следующий оператор ввода, который должен быть выполнен, находится в строке 16 read (number, price) где number и price - еще две переменные типа integer. Оператор ввода с несколькими параметрами эквивалентен последовательности операторов ввода, каждый из которых содержит по одному параметру. Наш оператор, следовательно, эквивалентен двум: read (number)* read (price) и, таким образом, при его выполнении number приобретает значение 4, price - значение 50, а входной файл остается в виде 3# 50# 3~ 25#ллл6~ 30# Этот оператор выполняется еще два раза, и каждый раз читается по два числа. Так что окончательный вид входного файла таков: —3#—4-—5о#—з~—25#—6~ 30# т.е. файл прочитан полностью, за исключением последнего маркера конца строки. В программе Расчетб есть только один оператор вывода: write (totalcost : 8) Он заменил более простой оператор write (totalcost) входивший в программу Расчет5. Оба оператора являются допустимыми, и в ре- зультате их выполнения процессор пересылает последовательность символов, образующих значение totalcost, в выходной файл. Различие состоит в количестве пересылаемых символов или, если выражаться точнее, - в количестве пробелов, которые записываются перед соответствующими цифрами. В операторе
58 Гл. 3. Простой ввод и вывод write (totalcost : 8) параметр указывает на то, что значение переменной totalcost должно быть выве- дено в выходной файл в поле шириной 8. Ширина поля вывода - это предлагае- мая программистом величина, указывающая, сколько символов должно зани- мать выводимое значение. Когда значение имеет тип integer, последовательность символов, порождаемых процессором в соответствии со значением этого пара- метра, определяется так: а) ноль или более начальных пробелов; б) знак минус, если значение отрицательное; в) одна или более цифр; где число начальных пробелов определяется так, чтобы было получено поле требуемой ширины. При рассматриваемых входных данных конечное значение totalcost равно 480, и поэтому будут напечатаны следующие восемь символов: -----480 Когда ширина поля не задана параметром, как это было в исходном опера- торе write (totalcost) процессор использует предопределенную ширину поля. Эта величина встроена в систему Паскаля и может быть различной для разных систем. Она называется значением ширины поля по умолчанию. Например, процессор, у которого для переменных типа integer ширина поля по умолчанию равна 12, породит в нашем случае следующий набор символов: ~—-------480 В связи с тем, что программа, возможно, будет выполняться разными про- цессорами, хорошей практикой является явное задание ширины поля вывода. Ширина поля, указанная в вышеприведенном операторе, была определена как число символов, которое программист ’’предлагает” использовать. Значение по умолчанию тоже является лишь предложением для процессора. В том случае, если выводимое значение не может быть представлено заданным числом симво- лов, процессор игнорирует предложенную ширину поля и выводит минимальное чцело символов, необходимое для полного представления соответствующего зна- чения. Таким образом, если бы в строке 23 стоял оператор write (totalcost : 2) в выходной файл была бы занесена последовательность символов 480 Итак, различие форм параметра в программах Расчетб и Расчетб приво- дит к незначительному различию в представлении результата. Однако в прог- рамме, которая порождает не одно-единственное число, а довольно много чис- ловой и нечисловой информации вперемежку, от правильного выбора ширины поля зависит удобочитаемость результатов. Наглядное размещение результатов так же важно для пользователя, как наглядное расположение текста про- граммы для программиста. 3.6. Вывод символьных строк Программы, которые мы рассматривали до сих пор, осуществляли ввод и вывод отдельных символов или последовательностей символов, представляющих
Гл. 3. Простой ввод и вывод 59 собой целочисленные значения. Программа ВремяЕды (см,, листинг 3.4) иллюстрирует возможность формирования более длинных последовательностей символов, предназначенных для образования заголовков и других элементов оформления результатов. Здесь встречаются также операции div и mod. Давайте поработаем с этой программой, опираясь на входные данные, которые показаны в верхней части табл. 3.5, и будем обсуждать интересные моменты но мере их возникновения. 1 2 3 4 5 6 7 8 9 10 И 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 program Mealtime (input, output)-, var hours, minutes, cookingtime : integer, ch : char, begin read (hours,ch,minutes)-, write ('Еда, приготовление которой было начато в')’, write (hours : 3, if minutes < 10 then write ('O',minutes : 1) else write (minutes : 2); write ln\ read In (cookingtime)-, writein ('и заняло', cookingtime : 4,’ минут,')-, minutes minutes + cookingtime’, hours := hours + minutes div 60; minutes minutes mod 60; hours := hours mod 24; write ('будет готова в')-, write (hours : 3,’.’); if minutes < 10 then write ('O',minutes : 1) else write (minutes : 2); writein ('.') end . Листинг 3.4. Текст программы ВремяЕды При входе в программу в строке 7 переменные hours, minutes, cookingtime и ch не определены, input* имеет значение ’Г (это первый символ входного файла); выходной файл пуст. Оператором read в строке 8 вводятся три величины: целочисленная, затем символьная и, наконец, еще одна целочисленная, так что выполнение оператора имеет тот же результат, что присваивания hours := 12; ch := minutes := 45 а входной файл после этого приобретает вид 12.45----20—# причем input* = ’ Следующая группа операторов (строки 9-14) приводит к построению одной полной выходной строки и иллюстрирует необходимость внимательного отноше- ния к операторам вывода и к программированию в целом. Выполнение первого оператора этой группы (строка 9) связано с понятием символьной строки, или литерала. Литерал представляет собой последователь-
60 Гл. 3. Простой ввод и вывод ность изображаемых символов, заключенную в апострофы? Когда литерал встречается в операторе вывода, процессор выдает данную последовательность символов в выходной файл. Таким образом, выполнением строки 9 программа приступает к формированию выходной строки, начиная ее с символов: Еда,"приготовление"которой"было"начато"в Продолжает строить выходную строку оператор write {hours : 3) (строка 10 программы). Оператор вывода с несколькими параметрами эквива- лентен последовательности операторов вывода, с одним параметром каждый. Таблица 3.5. Выполнение программы ВремяЕды Входные данные 12.45----20—# Трассировка строка ход выполнения input" hours minutes cookingtime ch 7 вход в программу ’Г *> 9 9 9 8 5 5 12 45 > > 9 вывод: Еда,"приготовление"которой"было"начато"в 10 вывод: "12. 11 (minutes < 10) - false 13 вывод: 45 14 вывод: # 15 ? 20 16 вывод: и"заняло""20"минут, # 17 65 18 13 19 5 20 13 21 вывод: будет"готова"в 22 вывод: "13. 23 (minutes < 10) = true 24 вывод: 05 26 вывод: . # 27 выход из программы Результаты Еда, приготовление которой было начато в 12.45 и заняло 20 минут, будет готова в 13.05. Поэтому наш оператор действует так же, как операторы write (hours : 3); write (’.’) и его выполнение приводит к пересылке четырех символов: 1 Не следует путать символьные строки со строками программы и строками текстовых файлов. См. Предисловие к русскому изданию. - Примеч. ред.
Гл. 3. Простой ввод и вывод 61 ~12. в выходной файл, поскольку значение hours равно 12. Вывод в файл числа минут не столь очевиден. Он описывается оператора- ми в строках 11-13. Значение переменной minutes нужно вывести одним из двух способов: если оно меньше, чем 10, то перед цифрой минут нужно вывести нуль. Если же значение minutes больше или равно 10, то оно выводится в поле шириной 2. Различать эти два случая необходимо, чтобы правильно выводить такую величину, как ’’пять часов четыре минуты”: 5.04 поскольку запись 5. 4 неприемлема. Последний оператор в группе (строка 14) завершает вывод строки. Сум- марным результатом выполнения строк 9-14 является, таким образом, занесение в выходной файл следующей последовательности, состоящей из 36 символов и маркера конца строки: Еда,~при готовление~которой~было~начатоЛв~12.45# В следующей части программы (строки 15 и 16) используются операторы readin и writein с параметрами. В общем случае выполнение оператора readin с параметрами эквивалентно выполнению оператора read с теми же парамет- рами, а затем оператора readin без параметров. Оператор readin (cookingtime) следовательно, действует так же, как пара операторов read (cookingtime); readln и при его выполнении процессор вводит целое число из входного файла, присва- ивает это число переменной cookingtime и пропускает оставшуюся часть строки. Входной файл содержит единственную строку, поэтому данных для чтения не остается и значение inpuC становится неопределенным. Аналогично, оператор writein с параметрами действует так же, как опе- ратор write с теми же параметрами и последующий оператор writein без параметров. Оператор writein (’и заняло’, cookingtime : 4,’ минут,’) эквивалентен поэтому четырем операторам: write (’и заняло’); write (cookingtime : 4); write (’ минут,’); writein и при его выполнении в выходной файл выводится другая полная строка: илзанялол~2(Гминут,# Дальше в программе идет последовательность операторов присваивания, в которых путем добавления числа минут, записанного в cookingtime, к времени, представленному переменными hours и minutes, вычисляется новое значение времени. Для того чтобы понять эти операторы, нам потребуется определить операции div и mod.
62 Гл. 3. Простой ввод и вывод Предположим, что z и j обозначают два целых числа. Мы можем разделить нацело z на /, получая частное и остаток, также целые. Например, 14, деленное на 4, дает частное 3 и остаток 2. Для положительных операндов операции div и mod можно определить следующим образом: z div j - частное от деления z на /; z mod j - остаток от деления z на у. Например: 14 div 4 = 3 5 div 7 = О 14 mod 4=2 5 mod 7=5 Деление на нуль невозможно, поэтому любая попытка вычисления z div j или z mod /, когда j равно нулю, является ошибкой. Использования этих операций с отрицательными операндами следует избегать. Другой существенный факт состоит в том, что операции div и mod имеют то же старшинство, что и операция умножения. Следовательно, если нет скобок, изменяющих порядок вычислений, операции div и mod так же, как и умноже- ние, выполняются до сложения и вычитания. Поэтому выражение hours + minutes div 60 в строке 18 программы ВремяЕды эквивалентно выражению hours + (minutes div 60) Теперь мы можем понять, как происходят вычисления в строках 17-20 программы. После выполнения строки 17 minutes равно 65, поэтому в строке 18 вычисляется выражение 12 + 65 div 60 Так как 65, деленное на 60, дает 1 с остатком равным 5, в результате получается 12 + 1 Значение, присваиваемое переменной hours, ргьно, таким образом, 13. В следу- ющем операторе (строка 19) используется остаток от деления 65 на 60, поэтому новое значение, присваиваемое переменной minutes, равно 5. И наконец, так как 13, деленное на 24, равно нулю (остаток равен 13), после выполнения строки 20 значение переменной hours не изменяется. Оставшаяся часть программы аналогична строкам 9-14. Здесь выдается вы- численное значение времени и сопровождающий его текст. Итак, действие всей программы сводится к следующему: копирование времени начала приготовления еды; копирование времени, необходимого для приготовления; расчет времени еды; выдача времени еды. Работа программы при конкретных данных описывается табл. 3.5. Так же, как и некоторые прежние наши программы, ВремяЕды дает верные результаты только для содержательных входных данных. Ее работа при иных входных набо- рах является предметом у пр. 3.4. Прежде чем закончить эту главу, посвященную вводу и выводу, нужно сделать еще два замечания об управлении шириной поля в операторах вывода. Во-первых, во всех приведенных примерах ширина поля задавалась цело- численной константой. В общем же случае она может задаваться выражением
Гл. 3. Простой ввод и вывод 63 при условии, что значение выражения имеет тип integer и оказывается не мень- ше единицы. Если, например, width - переменная типа integer и ее значение рав- но шести, запись hours : width - 2 равносильна записи hours : 4 Во-вторых, программист может задавать ширину поля и тогда, когда выво- димым значением является символ или литерал. Тогда при выводе вставляется столько начальных пробелов, чтобы в результате получилось поле требуемой ширины. Если, например, символьная переменная имеет значение ’А’, то при выполнении оператора write (ch : 6) в выходной файл будет выведена последовательность символов -----А а оператор writein (’таблица налогов’ : 25) даст ..........ТАБЛИЦА^НАЛОГОВ# Это подсказывает удобный способ для вывода большого количества пробелов. Оператор write (’ ’ : 35) выводит 34 пробела, вслед за которыми еще один! Такая запись удобнее, чем write (’ ’) где приходится высчитывать 35 пробелов между апострофами. Все, что здесь говорилось, может показаться сложным, особенно в част- ностях. Но если вы собираетесь стать хорошим программистом, вам придется уделять внимание частностям и научиться ими пользоваться! Упражнения 3.1. Переменные а, b и с имеют тип char, х и у - тип integer. Входные данные: "32—64“128# 256~~512~1024# 2048"4096"8192# Запишите значения этих переменных после выполнения каждой из следующих последователь- ностей операторов, если ввод данных для каждой последовательности начинается с начала файла: a) read (a)', read (/>); read (с); read (х); read (у); read (z); б) readin (а)\ readin <b)\ read (c); read (x,y,z) в) read (a,b,c); readln (x); readin (y); readin (z) r) readln (a,x)\ readln (b,y)\ readln (c,z) 3.2. Даны переменные и их значения: age - 45, day - 13, month - 6, year - 1936, comma -
64 Гл. 3. Простой ввод и вывод Запишите результаты, получающиеся при выполнении каждой из следующих последовательнос- тей операторов: a) write (day : 6); write (month : 6); write (year : 6) 6) write (’Результат:’, age : 4) в) writein (’Результат:*); writein; writein (day : 2, month : 2) r) writein (Тод рождения:’, year : 5, comma); writein (’Возраст:’ : 16, age : 5, ’.’) 3.3. Измените программу СписокИмен (с. 53) так, чтобы она обрабатывала пять имен вместо преду- смотренных двух, и передайте ее вашему Паскаль-процессору на исполнение со следующими входными данными: Мария Каллас/ Чарльз Спенсер Чаплин/ Бинг Кросби/ Питер Финч/ Милтон Маркс/ 3.4. Какие результаты, по-вашему, даст программа ВремяЕды (с. 59), если будут обрабатываться следующие наборы входных данных: а) 18.25 35 г) 12 . 45 20 б) 12/45 20 д) 2 р.го. 30 в) 12 45 20 е) 90.100 110 Введите программу в компьютер и проверьте ваши предположения. 3.5. Разберитесь в программе Расстановка (листинг 3.5). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 program SpaceWords (input, output); const terminator - space - ’ ’; var ch: char; begin while inpur oterminator do begin while inpur - space do read (ch); read (ch); write (ch); while inpur <> space do begin read (ch); write (’-’, ch); end; writein; read (ch) end; writein end . Листинг 3.5. Текст программы Расстановка а) Произведите трассировку выполнения программы для входных данных —А-ГНУЛ..............# и точно определите содержимое выходного файла в момент прекращения ее работы; опишите порядок появления результатов при их печати или выдаче на дисплей.
Гл. 3. Простой ввод и вывод 65 б) Что произойдет, если входными данными будет последовательность символов ~А~ГОНУШКА~ГНУ.-# 3.6. Разберитесь в программе Треугольник (листинг 3.6). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 program WordTriangle (input, output)', var line, spaces, letters : integer, ch : char, begin line 0; spaces 21; while inpuC <> ’ ’ do begin line line + 1; spaces spaces - 1; write (line : 2, ’ ’ : spaces); read (ch); letters 2* line - 1; while letters > 0 do begin letters letters - 1; write (ch) end; writein end end . Листинг 3.6. Текст программы Треугольник а) Выполните трассировку программы при входных данных XYZ-------------# и точно определите содержимое выходного файла в момент прекращения ее работы. б) Какие результаты будут напечатаны или выведены на дисплей, если, в отличие от предыдущего случая, входные данные имеют вид PASCAL------------# в) Повлияет ли в случае (б) на результат пробел перед буквой Р? Если да, то в чем это проявится? 3.7. Программа Баллы (листинг 3.7) рассчитывает средний балл, набранный студентом на двух экзаменах. 1 2 3 4 5 6 7 8 9 program Marks (input,output); var english, maths, average, sum : integer; begin read (maths, english); sum maths + english; average (sum + 1) div 2; writein ('СРЕДНЕЕ.', average : 1) end . Листинг 3.7. Текст программы Баллы 3 Заказ № 1110
66 Гл. 3. Простой ввод и вывод Усовершенствуйте ее таким образом, чтобы она печатала набранные баллы, средний балл и степень успеваемости студента в следующей форме: МАТЕМ.: 50 АНГЛ.: 59 СРЕДНЕЕ: 55 УСПЕВ.: В Зависимость степени успеваемости от среднего балла определяется таблицей: А 60-100 В 40-59 С 0-39 3.8. Программа Счетчик (листинг 3.8) предназначена для подсчета количества пробелов и количества отличных от пробела символов во входной строке, который заканчивается точкой. Правильно ли работает эта программа? Если нет, исправьте ее. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 program Counter (input, output); const blank - ’ var blankcount, othercount: integer; ch : char; begin while inpur <> do begin read (ch); if ch - blank then blankcount:- blankcount + 1 else othercount:- othercount + 1 end; writein (blankcount : 10, othercount : 10) end . Листинг 3.8. Текст программы Счетчик. 3.9. Измените программу Расчетб (с. 56) так, чтобы она выдавала результаты в форме табл. 3.6. Выполните полученную программу на компьютере, используя разные наборы входных данных. Таблица 3.6 КОЛИЧЕСТВО ЦЕНА ЕД-ЦЫ ТОВАРА СТОИМОСТЬ (С ПОЧТ. СБОРОМ) 4 75 310 3 110 340 6 120 730 ИТОГО 1380
4 КОММЕНТАРИИ И ПРОЦЕДУРЫ Невозможно преувеличить важность написания программы в ясной, удоб- ной для чтения форме. Один из возможных путей - разбиение программы на не- сколько блоков, каждый из которых описывает часть вычислений. В этой главе мы изучим два способа подобного разбиения. 4.1. Комментарии Для начала рассмотрим программу ВремяЕды2 (листинг 4.1). Она почти совпадает с программой ВремяЕды, которая обсуждалась в гл. 3, но ее легче по- нять благодаря тому, что она содержит пять фрагментов текста, заключенных между символами { и }. Такие фрагменты текста называются комментариями. В текст программы включены также дополнительные пустые строки. Первый комментарий, записанный сразу же после заголовка, является кратким определением назначения программы. В сущности, здесь указаны резу- льтаты, которые должны быть рассчитаны на основании входных данных, но ни- чего не сказано о методах расчета. Включение комментария в начало програм- мы - первый разумный шаг, облегчающий другим ее понимание. Остальные четыре комментария в программе ВремяЕды2 помещены в раз- дел операторов и разбивают всю последовательность операторов на четыре груп- пы. Если извлечь эти комментарии из программы и прочитать их отдельно, получится: копирование времени начала приготовления еды; копирование времени приготовления еды; расчет времени еды; выдача времени еды; что совпадает с кратким описанием программы, приведенным на с. 62. Можно считать также, что весь процесс разделен на подпроцессы, причем последова- тельность операторов Паскаля, следующая за комментарием, в точности опре- деляет содержание соответствующего подпроцесса. Использование комментариев для подразделения и пояснения длинной последовательности операторов позво- ляет лучше понимать программу и быстрее находить операторы, описывающие отдельный фрагмент вычислений. Нужно подчеркнуть, однако, что для процессора комментарии ровным счетом ничего не значат. Полный комментарий к программе на Паскале ока- зывает такое же действие, как и единственный пробел. Трассировка выполнения ВремяЕды.2 при тех же входных данных будет, за исключением номеров строк, полностью идентична трассировке ВремяЕды! (табл. 3.5 на с. 60). 3*
68 Гл. 4. Комментарии и процедуры Данная программа иллюстрирует только два возможных способа сделать текст программы понятнее. Ряд других способов мы рассмотрим по ходу изложения. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 program Mealtime2 {input, output)’, { рассчитывает время готовности еды по моменту начала ее приготовления и времени (в минутах), необходимому для приготовления } var hours, minutes, cookingtime : integer, ch : char, begin { копирование времени начала приготовления еды } read (hours, ch, minutes)’, write (’Еда, приготовление которой было начато в’); write (hours : 3, ’.’); if minutes < 10 then write (’O’, minutes : 1) else write (minutes : 2); write ln\ { копирование времени приготовления еды } readln (cookingtime); writein (’и заняло’, cookingtime : 4, ’ минут,’)', { расчет времени еды } minutes minutes + cookingtime’, hours hours + minutes div 60; minutes minutes mod 60; hours hours mod 24; { выдача времени еды } write (’будет готова в’); write (hours : 3, ’.’); if minutes < 10 then write (’O’, minutes : 1) else write (minutes : 2); writein (’’) end . Листинг 4.1. Текст программы ВремяЕды2 В стандартном Паскале комментарий помещается между символами { и }, однако использование составных символов (* и *) тоже является допустимым. Например, комментарий { копирование времени начала } можно переписать в виде (* копирование времени начала *) Более того, можно начинать комментарий скобкой одного типа, а заканчивать скобкой другого!
Гл. 4. Комментарии и процедуры 69 4.2. Процедуры Второй способ разбиения программы на части показан в еще одной версии программы ВремяЕды - в программе ВремяЕдыЗ (листинг 4.2). Теперь каждый из четырех образующих процесс подпроцессов описывается подпрограммой. В стандартном Паскале есть подпрограммы двух видов: процедуры и функции. С функциями мы встретимся в гл. 12. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 program Mealtime3 (input, output)', { расчет времени готовности еды по моменту начала ее приготовления и времени (в минутах), необходимому для приготовления } var hours, minutes, cookingtime : integer, ch : char\ procedure copystartingtime-, begin read (hours, ch, minutes)', write Г Еда, приготовление которой было начато в9)', write (hours : 3, ’.’); if minutes < 10 then write (’O’, minutes : 1) else write (minutes : 2); writeln end { copystartingtime }; procedure copycookingtime', begin readln (cookingtime); writeln (’u заняло9, cookingtime : 4, ’ минут,9) end { copycookingtime }; procedure calculatemealtime', begin minutes minutes + cookingtime', hours hours + minutes div 60; minutes minutes mod 60; hours hours mod 24 end { calculatemealtime }; procedure writemealtime', begin write Сбудет готова в9)', write (hours : 3, if minutes < 10 then write (’O’, minutes : 1) else write (minutes : 2); writeln (’.’) end { writemealtime }; * * * * * * begin copystartingtime', copycookingtime', calculatemealtime’, writemealtime end { Mealtime3 } Листинг 4.2. Текст программы ВремяЕдыЗ
70 Гл. 4. Комментарии и процедуры Рассматривая программу ВремяЕдыЗ, отметим прежде всего ее общую структуру: строка 1 строки 7-9 строки 11-44 строки 46-51 : заголовок программы; : раздел описания переменных; : раздел описания процедур и функций; : раздел операторов. Это первая из рассмотренных нами программ, имеющая раздел описания процедур и функций, и, как подсказывает название раздела, в нем описывается одна или более процедур и функций. Раздел описания процедур и функций все- гда располагается после раздела описания переменных и перед разделом опера- торов. В данном случае описываются четыре процедуры. Как программа описы- вает процесс в целом, так описание процедуры описывает отдельный подпроцесс и дает подпроцессу имя. Имя в описании процедуры записывается сразу после слова procedure. Описание процедуры выглядит почти так же, как программа. Единствен- ное отличие состоит в том, что процедура начинается с заголовка процедуры, а не с заголовка программы. Именно из заголовка процедуры и из раздела опера- торов состоит каждая из процедур в программе ВремяЕдыЗ. Так, например, в процедуре copystartingtime строка 11 : заголовок процедуры; строки 12-20 : раздел операторов. Раздел операторов процедуры иногда называют ее телом. Чтобы увидеть, как используются процедуры, проследим за работой про- граммы ВремяЕдыЗ (табл. 4.1). Как обычно, для выполнения программы процес- сор должен выполнить последовательность операторов из ее раздела операторов. Таким образом, вход в программу происходит в строке 46, и первым выполняет- ся оператор в строке 47. , Строка 47 содержит имя процедуры copystartingtime. Такой оператор назы- вается оператором процедуры, о нем говорят, что он вызывает процедуру (или обращается к ней). Чтобы выполнить этот оператор, процессор должен выпол- нить последовательность операторов, составляющих тело вызываемой процедуры. Этот факт отражен в трассировке: происходит вход в процедуру в начале ее раздела операторов, и выполняются операторы в строках 13-19. Их действие полностью совпадает с действием строк 8-14 исходной программы. После строки 20, последней в процедуре, процессор возвращается в то место основной про- граммы, где он ее покинул. Мы говорим, что управление возвращается в точку, из которой была вызвана процедура. Строки 48-50 содержат операторы процедуры, поэтому одна за другой вы- полняются процедуры copycookingtime, calculatemealtime и writemealtime, и общее действие, производимое программой, полностью совпадает с действием ее преды- дущих версий. Если писать программы таким образом, т.е. давая процедурам имена, от- ражающие выполняемые ими действия, раздел операторов головной программы приобретет вид ее краткого описания1, в целом аналогичного комментариям в программе ВремяЕды2. Программы ВремяЕды2 и ВремяЕдыЗ иллюстрируют две крайности в сти- ле программирования. Разбиение на блоки в программе ВремяЕды2 осуществле- 1 На английском языке. - Примеч. пер.
Гл. 4. Комментарии и процедуры 71 но исключительно за счет комментариев. В программе ВремяЕдыЗ оно полнос- тью достигается за счет процедур. На практике эти методы обычно сочетаются. Таблица 4.1. Выполнение программы ВремяЕдыЗ Входные данные 12.45 20 Трассировка строка ход выполнения hours minutes cooking ch time 46 47 вход в программу ВремяЕдыЗ 2 2 2 2 вызов copystartingtime 12 13 14 15 16 18 19 20 вход в copystartingtime 12 45 вывод: Еда,~приготовление~которой~было"начато"в вывод: "12. (minutes < 10) - false вывод: 45 вывод: # выход из copystartingtime 48 возврат в программу вызов copycookingtime 23 24 вход в copycookingtime 20 25 26 вывод: и"заняло"20"минут,# выход из copycookingtime 49 возврат в программу вызов calculatetime 29 30 31 32 33 34 вход в calculatetime 65 13 5 13 выход из calculatetime 50 37 38 39 40 41 42 44 возврат в программу вызов writemealtime вход в writemealtime вывод: будет"готова"в вывод: "13. (minutes < 10) - true вывод: 05 вывод: .# выход из writemealtime 51 возврат в программу выход из программы ВремяЕдыЗ
72 Гл. 4. Комментарии и процедуры Результаты Еда, приготовление которой было начато в 12.45 и заняло 20 минут, будет готова в 13.05. 4.3. Многократный вызов процедур В каждой версии программы ВремяЕды мы найдем по две одинаковых по- следовательности операторов. В программе ВремяЕдыЗ это строки 15-18 и 39-42. В обоих этих фрагментах выводятся значения переменных hours и minutes в виде шести символов, обозначающих время. Такие повторения часто встречают- ся в программах. Для того, чтобы избежать многократной записи одной и той же после- довательности команд в программе, можно использовать процедуру. Вероятно, именно для этого и были первоначально изобретены подпрограммы. Применение процедуры с этой целью иллюстрируется в окончательной версии рассматривае- мой программы - ВремяЕды4 (листинг 4.3). Новая процедура, названная writethetime, описана в строках 11-17. Она вызывается дважды: в строке 23, в процедуре copystartingtime, и в строке 44, в процедуре writemealtime. Если в программе много подпрограмм, полезно бывает знать, как они вы- зывают друг друга. Пользуясь именами подпрограмм, эти сведения для програм- мы ВремяЕды4 можно представить так: ВремяЕды4 copystartingtim е writethetime copycookingtim е calculat etime writemealtime writethetime Под именем программы, со сдвигом по отношению к нему, одна под другой за- писаны имена вызываемых из программы подпрограмм. Аналогично под каждым из имен подпрограмм, со сдвигом по отношению к нему, записаны имена под- программ, вызываемых из этой подпрограммы (если они есть). Мы будем назы- вать такого рода список схемой вызовов программы. Вызов одной процедуры из другой выполняется точно так же, как и вызов из головной программы. После входа в процедуру выполняются операторы, обра- зующие ее тело, а затем управление передается в точку вызова. В качестве уп- ражнения предлагаем вам самостоятельно произвести трассировку программы ВремяЕды4. 4.4. Локальные константы и переменные в процедурах Изучая программы ВремяЕдыЗ и ВремяЕды4, можно обратить внимание еще на один факт: из четырех переменных, hours, minutes, cookingtime и ch, первые три используются в нескольких процедурах каждая, тогда как четвертая, ch, - только в одной, copystartingtime. Переменная, которая используется только в пределах некоторой под- программы, может быть описана в этой подпрограмме и называется в этом случае локальной переменной. Например, для того чтобы ch стала локальной пе- ременной процедуры copystartingtime в программе ВремяЕды4, нужно просто
Гл. 4. Комментарии и процедуры 73 удалить строку 9, а между строками 19 и 20 вставить новый раздел описания переменных: var ch : char, 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 program Mealtime4 (input, output); { расчет времени готовности еды по моменту начала ее приготовления и времени (в минутах), необходимому для приготовления } var hours, minutes, cookingtime : integer; ch : char; procedure writethetime; begin write (hours : 3, if minutes < 10 then write (’O’, minutes : 1) else write (minutes : 2) end { writethetime }; procedure copystartingtime; begin read (hours, ch, minutes); write ('Еда, приготовление которой было начато в'); writethetime; writein end { copystartingtime }; procedure copycookingtime begin readin (cookingtime); writein Си заняло', cookingtime : 4, ’ минут,') end { copycookingtime }; procedure calculatemealtime; begin minutes minutes + cookingtime; hours hours + minutes div 60; minutes minutes mod 60; hours hours mod 24 end { calculatemealtime }; procedure writemealtime; begin write Сбудет готова в'); writethetime; writein end { writemealtime }; begin co pystart i ngti me; copycookingti me; calculatemealtime; writemealtime end { Mealtime }. Листинг 4.3. Текст программы ВремяЕды4
74 Гл. 4. Комментарии и процедуры Переменные hours, minutes и cookingtime остаются при этом в разделе описания переменных головной программы. Переменные, описанные в головной програм- ме, называются глобальными переменными. Аналогично константы, описанные в головной программе, называются глобальными константами, а описанные в подпрограммах - локальными константами. Оставим серию видоизменений программы ВремяЕды: исследуем поведе- ние локальной переменной, рассматривая вариант программы СписокИмен, которая впервые появилась в гл. 3. В новом варианте программа называется СписокИмен2 (листинг 4.4). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 program ListNames2 (input,output)', { читает два имени и выводит их в отдельных строках, выдает пустую строку после каждого имени } const numberofnames = 2; var count : integer, procedure copyonename-, const space - ’ ’; terminator ~'Г\ var ch : char, begin while input* = space do read (ch)\ while input* <> terminator do begin read (chY, write (ch) end', writeln’, writeln', readln end { copyonename }; begin count := numberofnames', while count > 0 do begin count := count - 1; copyonename end end { ListNames2 }. Листинг 4.4. Текст программы СписокИмен2 Программа написана с помощью процедуры copyonename. Основные части программы: строка 1 строки 6-7 строки 8-9 строки 11-28 строки 30-37 : заголовок программы; : раздел определения констант; : раздел описания переменных; : раздел описания процедур и функций (он содержит опи- сание единственной процедуры); : раздел операторов.
Гл. 4. Комментарии и процедуры 75 В программе есть, таким образом, одна глобальная константа numberofnames и одна глобальная переменная count. Процедура соруопепате подразделяется на следующие части: строка 11 строки 12-14 строки 15-16 строки 17-28 : заголовок процедуры; : раздел определения констант; : раздел описания переменных; : раздел операторов. Итак, в соруопепате есть две локальные константы, space и terminator, и одна локальная переменная ch. Из прежних примеров мы знаем, что переменные, описанные в головной программе, создаются при входе в программу, существуют в течение выполнения программы и разрушаются при выходе из нее. Локальные переменные ведут себя сходным образом. Локальная переменная, описанная в подпрограмме А, начина- ет существовать в момент вызова А, существует в течение выполнения А и всех вызываемых из А подпрограмм и разрушается при выходе из А. Таблица 4.2. Трассировка программы Список11мен2 Входные данные J0/ VI/ Трассировка строка ход выполнения inpuC count 30 вход в Список11мен2 'Г 2 31 2 32 (count > 0) - true 34 1 35 вызов соруопепате сА / 17 вход в соруопепате 9 18 (inpuC * space) - false 20 ИприГ <> terminator) - true 22 ’О’ т 23 вывод: J 20 (inpuC <> terminator) - true 22 7’ ’О’ 23 вывод: 0 20 (inpuC <> terminator) = false 25 вывод: # 26 вывод: # 27 ’V’ 29 выход из соруопепате возврат в СписокИмсн2 32 (count > 0) - true 34 0 35 вызов соруопепате
76 Гл. 4. Комментарии и процедуры ch 17 18 20 22 23 20 22 23 20 25 26 27 29 32 37 вход в соруопепате (input* - space) - false (input* <> terminator) - true ’Г вывод: V (input* <> terminator) - true 7’ вывод: I (input* <> terminator) - false вывод: # вывод: # 9 выход из соруопепате возврат в СписокИмен2 (count > 0) - false выход из СписокИмен2 Результаты J0 VI Чтобы сделать эти утверждения более понятными, обратимся к трассиров- ке программы СписокИмен2 (табл. 4.2). Входные данные, вычисления и резуль- таты здесь такие же, как и в исходной программе (с. 53-55), однако из-за наличия процедуры соруопепате характер вычислений немного изменяется. Нас особенно интересует все то, что связано с существованием переменных на каждой стадии процесса. В этом смысле порядок событий таков: 1. Единственными переменными, существующими за пределами програм- мы, являются входной и выходной файлы. Входной файл содержит дан- ные, выходной файл пуст. 2. При входе в головную программу в строке 30 создается буферная пере- менная input*. и начальным значением этой переменной становится первый символ входного файла. Кроме того, создается глобальная пере- менная count, значение которой не определено. 3. При входе в процедуру соруопепате в строке 17 создается локальная переменная ch. значение которой тоже не определено. 4. При выходе из процедуры соруопепате в строке 28 локальная перемен- ная ch разрушается. 5. При повторном входе в процедуру соруопепате локальная переменная ch создается снова, как при первом вызове. Свою вторую жизнь она на- чинает с неопределенного значения - так же, как начинала первую. 6. При втором выходе из процедуры соруопепате локальная переменная ch снова разрушается. 7. При выходе из головной программы разрушаются глобальная перемен- ная count и буферная переменная input*. Остается входной файл, кото- рый можно использовать снова, и выходной файл, содержащий резуль- таты.
Гл. 4. Комментарии и процедуры 77 Таким образом, в программе, имеющей одну или несколько подпрограмм с локальными переменными, общее число действующих переменных изменяется по ходу выполнения. 4.5. Доступ к локальным константам и переменным Последняя программа в этой главе является очередным вариантом прог- раммы СписокИмен и называется СписокИменЗ (листинг 4.5). На этот раз го- ловная программа состоит из следующих частей: строка 1 строки 6-37 строки 39-41 : заголовок программы; : раздел описания процедур и функций (содержит описания двух процедур); : раздел операторов. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 program ListNames3 (input, output); { читает два имени и выводит их в отдельных строках, выдает пустую строку после каждого имени } procedure copyonename; const space - ’ terminator - 7’; var ch : char, begin while inpur - space do read (ch); while inpur <> terminator do begin read (ch); write (ch) end; writeln; writeln; readln end { copyonename }; procedure processallnames; const numberofnames - 2; var count: integer; begin countnumberofnames; while count > 0 do begin countcount - 1; copyonename end end { processallnames }; begin processallnames end { ListNames3 }. Листинг 4.5. Текст программы СписокИменЗ
78 Гл. 4. Комментарии и процедуры Так как в программе нет ни раздела определения констант, ни раздела описания переменных, то нет ни глобальных констант, ни глобальных переменных. Первая процедура - соруопепате - точно та же, что в программе СписокИмен!. Вторая, названная processallnam.es, содержит константу, перемен- ную и операторы, которые входили в головную программу в СписокИмен2. Раздел операторов головной программы состоит теперь ровно из единственного оператора процедуры, который обращается к processallnam.es. Схема вызовов всей программы имеет вид СписокИменЗ processalln am es соруопепате Чтобы увидеть, как такая реорганизация программы влияет на ее выпол- нение, обратимся к трассировке (табл. 4.3). Входные данные, вычисления и ре- зультаты остаются теми же, что и раньше, а количество переменных, действую- щих на каждом этапе процесса, регулируется правилами, которые были рассмот- рены выше. Изменилась, однако, одна важная деталь. Колонка, в которой отра- жаются значения переменной count, в двух местах заштрихована. Это способ по- казать, что переменная иногда бывает недоступной. Когда переменная недо- ступна, ее значение не может быть ни использовано, ни изменено, несмотря на то, что переменная существует. Заметьте, что в табл. 4.3 заштрихованы области, на которые приходится выполнение процедуры соруопепате. При входе в нес переменная count стано- вится недоступной, а при выходе делается доступной снова. Это верно и для константы numberofnames, нс показанной в трассировке. Она тоже оказывается недоступной в течение выполнения процедуры соруопепате. В общем случае действует правило: когда из подпрограммы А вызывается любая подпрограмма В, не описанная внутри А, константы и переменные, которые являются локальными для А, становятся недоступными на время выполнения В. Таблица 4.3. Трассировка программы СписокИменЗ Входные данные J0/ VI/ Трассировка строка ход выполнения inpuC 39 вход в СписокИменЗ ’J’ 40 вызов processallnatnes count 30 вход в processallnames 9 31 2 32 (count > 0) - true 34 1 35 вызов соруопепате
Гл. 4. Комментарии и процедуры 79 count ch 12 вход в соруопепате WWW 9 13 ИприГ - space) - false WWW 15 (inpur <> terminator) = true WWW 17 ’O’ WWW т 18 ВЫВОД' J WWW 15 (inpur <> terminator) = true WWW 17 ’/’ WWW ’О’ 18 вывод: 0 WWW 15 (inpur <> terminator) = false WWW 20 вывод: # WWW 21 вывод: # WWW 22 ’V’ WWW 23 выход из соруопепате возврат в processallnames 1 32 (count > 0) - true 34 0 35 вызов соруопепате WWW ch 12 вход в соруопепате WWW 9 13 (inpur - space) - false WWW 15 (inpur <> terminator) = true WWW 17 т WWW ’V’ 18 вывод: V WWW 15 (inpur <> terminator) - true WWW 17 7’ WWW ’Г 18 вывод: I WWW 15 (inpur <> terminator) = false WWW 20 вывод: # WWW 21 вывод; # WWW 22 ? WWW 23 выход из соруопепате возврат в processallnames 0 32 (count > 0) = false 37 выход из processallnames возврат в СписокИменЗ 41 выход из СписокИменЗ Результаты J0 VI Итак, в отношении существования элементов данных и доступа к ним события в течение выполнения СпискаИменЗ наступают в следующем порядке:
80 Гл 4. Комментарии и процедуры 1. Входной и выходной файлы существуют за пределами программы. 2. При входе в головную программу создается переменная inpuC. 3. При входе в processallnames создается переменная count, 4. При входе в copyonename переменные count и numberofnames становятся недоступными и создается переменная ch, 5. При выходе из copyonename ch разрушается, a count и numberofnames снова делаются доступными. 6. При повторном входе в copyonename переменные count и numberofnames опять становятся недоступными и снова создается ch. 7. При втором выходе из copyonename опять разрушается ch и становятся доступными count и numberofnames. 8. При выходе из processallnames count разрушается. 9. При выходе из программы переменная inpuC разрушается и остаются только входной и выходной файлы. На первый взгляд временная недоступность некоторых элементов данных в программе СписокИменЗ может показаться недостатком по сравнению с прог- раммой СписокИмен2, где существующие элементы данных доступны всегда. Но на самом деле все наоборот. Идеальной была бы ситуация, когда каждая часть программы была бы максимально независимой от остальных и имела бы дос- туп только к тем элементам данных, которые нужны в этой части и не нуж- ны в остальных. Из двух программ, СписокИмен2 и СписокИменЗ, последняя предпочтительнее. В ней требуемый процесс описан двумя почти независимыми процедурами. Каждая из процедур имеет доступ к нужным ей константам и пе- ременным и не имеет доступа к тем из них, которые ей не нужны. Кроме того, программу СписокИменЗ удобнее читать, так как логическая независимость двух подпроцессов доведена в ней до полной ясности за счет их физического разнесе- ния в отдельные подпрограммы. Когда мы дойдем до написания программ с ис- пользованием процедур, мы примем стиль программы СписокИменЗ за образец, которому нужно следовать. Упражнения 4.1 . Рассмотрите программу Множители (листинг 4.6). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 program Factors (input, output); { выдает список множителей для каждого числа из заданной последовательности } var number : integer; procedure findandwritefactors; var factor : integer; begin write (number : 6); factor number div 2; while factor <> 0 do begin while number mod factor <> 0 do factor factor - 1; write (factor : 6); factor - factor - 1 end end { find and write factors };
Гл. 4. Комментарии и процедуры 81 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 procedure processallnumbers; begin writein Г число' : 8, 'множители' : 15); writein; read (number); while number > 0 do begin write (number : 8, ’ ’ : 8); findand write factors; writein; read (number) end end { processallnumbers }; begin processallnumbers end { Factors }. Листинг 4.6. Текст программы Множители а) Определите, в каких строках программы размещаются следующие ее части: заголовок про- граммы, раздел описания переменных, раздел описания процедур и функций, раздел операторов. б) Сколько операторов присваивания, составных операторов, операторов ’’пока”, заголовков про- цедур и частей операторов вы можете насчитать в программе? в) Произведите трассировку программы с входными данными 9 14 О Подробно опишите, в каком порядке результаты будут появляться на экране дисплея или на печатающем устройстве. 4.2 . Рассмотрите программу РезулыпатыО проса (листинг 4.7). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 program Survey Analysis (input, output); { анализ результатов опроса, перемежающихся комментариями } const length - 3; { число задаваемых вопросов } procedure analyzeoneline; var interview, count, yescount : integer; ch : char; begin read (interview); write (interview : 7, ’ ’ : 5); count := length; yescount 0; read (ch); while count <> 0 do begin read (ch); write (ch : 2); count count - 1; if ch = 'Y then yescount yescount+l end; writein (yescount : 8, length - yescount : 8) end { analyzeoneline }; proced u re processe very line; const
82 Гл 4. Комментарии и процедуры 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 commentindicator = ’С’; dataindicator - ’ D ’; terminator - ’Е’; var code : char, begin write (’ОТВЕТЫ : 20, ’ДА’ : 2*length, 'HET : 8); read (code); while code <> terminator do begin if code - dataindicator then analyzeoneline else if code <> commentindicator then writeln ('НЕВЕРНАЯ СТРОКА ДАННЫХ'), readbv, read (code) end; readln end { processeveryline }; begin processeveryline end { Survey Analysis }. Листинг 4.7. Текст программы РезулыпатыОпроса а) Перечислите части головной программы и процедур, определяя, в каких строках содержится каждая из частей. б) Ответьте на следующие вопросы: сколько здесь глобальных переменных? сколько здесь глобальных констант? сколько здесь локальных констант и к какой подпрограмме относится каждая из них? сколько здесь глобальных переменных и к какой подпрограмме относится каждая из них? в) Выполните трассировку программы при следующих входных данных: TEST DATA D 21632 YNY D 21633 NNY С КОНЕЦ TECTA E Подробно опишите, в каком порядке результаты будут появляться на экране дисплея или на печатающем устройстве. г) Покажите схему вызовов для этой программы. д) Опишите и объясните последствия удаления раздела определения констант (строк 5 и 6) и вставки такого же раздела между строками 8 и 9. е) Измените программу так, чтобы она обрабатывала по 10 ответов, а затем, не прибегая к под- робной трассировке, определите результаты ее работы при следущих входных данных:
Гл. 4. Комментарии и процедуры 83 С ОПРОС НА ГЛАВНОЙ УЛИЦЕ D 22179 YNYYNNYNYY D 22180 YNNNYYYYNY D 22181 YYYYNYNNYY С ОПРОС НА ХАЙ-СТРИТ D 23126 YNYNNYYNYY D 23127 NNNNNNNNNN D 23128 YNNNYYYYYN Е ж) Измените программу так, чтобы число, указывающее количество входных строк с результатами опроса (включая строки с кодом С), считывалось бы из входного файла в качестве первого элемента данных, за которым будут идти строки с данными опроса. Последняя строка входных данных с символом завершения Е больше нс понадобится. Выполните программу на вашем компьютере. з) Программу в се исходном виде или в вашем варианте, созданном при выполнении пункта (ж), измените так, чтобы она допускала ответы ”не знаю”, обозначаемые во входных данных символом ’?’.
5 СИНТАКСИС Чтобы понять текст, на каком бы языке он ни был написан, необходимы два условия: наличие в нем структуры и смысла. Когда мы пишем на своем родном языке, мы из букв составляем слова, из слов и знаков препинания обра- зуем предложения, из предложений - абзацы, и т.д., причем существуют пра- вила, регулирующие построение каждой части текста из более простых частей. Правила, определяющие структуру текста, называют грамматикой, или син- таксисом языка. Нам, однако, не удастся создать понятный фрагмент текста, ес- ли мы употребим слова, которых нет в словаре, если мы не построим предложе- ний, имеющих смысл, если мы не создадим абзацев из соотносимых друг с дру- гом предложений, и т.д. Короче говоря, мы должны строить текст, имеющий смысл. Правила, управляющие смыслом текста, называются семантикой языка. До сих пор мы сосредоточивали внимание на чтении и понимании про- грамм, следовательно, в предыдущих главах большая часть объяснений касалась семантики стандартного Паскаля. Прежде чем перейти к конструированию и написанию программ, желательно обсудить синтаксис языка. Каждая из изучен- ных нами программ является фрагментом текста, который понятен процессору, ’’читающему на стандартном Паскале”. Писать такой текст - почти то же самое, что писать на естественном языке, но так как обычно в роли процессора компь- ютер, не терпящий ошибок, все правила должны соблюдаться абсолютно точно. Потому так важно, чтобы программист знал, что считается правильным в языке, на котором он пишет программы. Это раз. Вторая причина для изучения синтак- сиса языка программирования состоит в том, что методы синтаксического описания языков могут пригодиться и в других случаях, например, для описания входных данных программы. В этой главе мы обсудим два широко применяемых метода описания син- таксиса, а именно: синтаксические диаграммы и расширенную форму Бэкуса- Наура, а также процесс, называемый синтаксическим анализом, который можно использовать для проверки синтаксической правильности фрагмента текста. Для иллюстрации мы прибегнем к крайне ограниченному языку, не имеющему ниче- го общего с программированием. Он, в сущности, позволяет лишь писать предло- жения о кошках и мышках. Наш крохотный язык включает всего десять слов: ’’ЧЕРНАЯ” ’’КОШКА” ”ЕСТ” ’’УПИТАННАЯ” ’’МЫШКА” ’’БЫСТРО” "БЕЖИТ” "МЕДЛЕННО” ’’ТОЩАЯ” ’’БЕЛАЯ” и два знака препинания: 55 55 55 55 Эти слова и знаки препинания называются лексемами языка.
Гл. 5. Синтаксис 85 Предложение на нашем языке состоит из последовательности лексем, разделенных пробелами, например: ТОЩАЯ , БЕЛАЯ МЫШКА БЕЖИТ БЫСТРО . Назовем наш язык Показушкой'. 5.1. Синтаксические диаграммы На рис. 5.1 показаны три синтаксические диаграммы. Каждая диаграмма определяет синтаксический класс рассматриваемого языка, в данном случае - Показушки. Верхняя диаграмма определяет синтаксический класс ’’предложе- ние”, для чего привлекается другой класс, называемый ’’именная группа”. Именная группа определяется в средней диаграмме, и для этого, в свою очередь, используется ’’прилагательное”, определяемое на нижней диаграмме. предложение ---- именная-группа *-- ”ЕСТ”----Г""Л" именная-группа -у- --- I---’’БЕЖИТ”-' -’’МЕДЛЕННО”— —’’БЫСТРО”- именная-группа ’’КОШКА” ’’МЫШКА” прилагательное ЧЕРНАЯ’ БЕЛАЯ ТОЩАЯ УПИТАННАЯ”--/ Рис. 5.1. Синтаксическая диаграмма, определяющая язык Показушка Чтобы построить отрывок текста по синтаксической диаграмме, нужно проделать следующее: 1 Как убедится читатель, синтаксис этого миниязыка отличается от русского синтаксиса. Для того чтобы, с одной стороны, Показушка оставалась подмножеством русского языка и, с другой сто- роны, чтобы не слишком перегружать диаграмму, будем считать, что падежные окончания и нали- чие или отсутствие предлога ”за” неразличимы при синтаксическом анализе (разд. 5.3). Напри- мер: мышка - за мышкой - мышку и т.д. Мы всякий раз станем выбирать ту форму имени, кото- рая соответствует русским грамматическим правилам. - Примеч. пер.
86 Гл 5. Синтаксис а) Двигаясь слева направо, пройти по линиям диаграммы от начала до ко- нца, помещая в текст все встретившиеся элементы. Какой бы путь ни был выбран на диаграмме, порожденный фрагмент текста будет син- таксически правильным. б) Когда в определении упомянуто название другого синтаксического класса, вместо него можно подставить любой фрагмент, построенный по соответствующей синтаксической диаграмме. В этом процессе имя синтаксического класса в определении другого син- таксического класса действует аналогично оператору процедуры в программе. В обоих случаях текущий процесс прерывается, выполняется подпроцесс, а затем исходный процесс возобновляется с той точки, где он был прерван. Пользуясь синтаксической диаграммой, мы можем получить, например, следующие конструкции: именная-группа ЕСТ именная-группа именная-группа ЕСТ БЫСТРО именная-группа ЕСТ МЕДЛЕННО именная-группа БЕЖИТ ЗА именная-группа именная-группа БЕЖИТ БЫСТРО именная-группа БЕЖИТ МЕДЛЕННО где каждая именная группа должна быть заменена каким-либо фрагментом текс- та, порождаемым при прохождении диаграммы именная-группа. Обратившись к ней, мы обнаружим бесконечное многообразие именных групп, которые можно построить благодаря тому, что диаграмма содержит цикл - путь, замкнутый на себя через лексему Этот путь можно проходить сколько угодно раз, всякий раз порождая одну из четырех лексем диаграммы прилагательных. Вот некото- рые имена из тех, что можно сконструировать: КОШКА МЫШКА УПИТАННАЯ, ЧЕРНАЯ КОШКА ЧЕРНАЯ, ЧЕРНАЯ КОШКА БЕЛАЯ, ЧЕРПАЯ, УПИТАННАЯ, ТОЩАЯ, ТОЩАЯ, БЕЛАЯ КОШКА Они позволяют образовать предложения типа: КОШКА ЕСТ МЫШКУ . ЧЕРНАЯ КОШКА ЕСТ МЕДЛЕННО . УПИТАННАЯ, ЧЕРПАЯ КОШКА ЕСТ ТОЩУЮ, БЕЛУЮ МЫШКУ . ЧЕРНАЯ, ЧЕРНАЯ КОШКА БЕЖИТ БЫСТРО . ЧЕРНАЯ, БЕЛАЯ, ТОЩАЯ, БЕЛАЯ, УПИТАННАЯ, ЧЕРПАЯ КОШКА БЕЖИТ ЗА БЕЛОЙ, ЧЕРНОЙ, УПИТАННОЙ, ЧЕРНОЙ, ТОЩЕЙ, БЕЛОЙ МЫШКОЙ . Все эти предложения синтаксически правильны в Показушке. Если предположить, что слова и словосочетания в Показушке имеют обыч- ные значения, то окажется, что одни предложения, сконструированные по синтаксическим диаграммам, имеют смысл, а другие, типа последнего в приве- денном примере, бессмысленны. Таким образом, следование синтаксическим правилам не гарантирует получения осмысленных предложений. Названия синтаксических классов, встречающиеся в формальном опреде- лении, важны сами по себе. Каждое из них является эффективным рабочим тер- мином, который точно определен своей синтаксической диаграммой, и поэтому может быть использован при обсуждении языка. Теперь мы можем порассуждать о предложениях, именах и прилагательных из Показушки и выяснить, чем же они в точности являются.
Гл. 5. Синтаксис 87 5.2. Расширенная форма Бэкуса-Наура Наш второй метод определения синтаксиса состоит в применении особых обозначений, называемых расширенной формой Бэкуса-Наура (РБНФ). РБНФ - язык для определения других языков! Он является результатом усовершенство- вания прежних обозначений - формы Бэкуса-Наура (БНФ) - и широко использу- ется в документации по языкам программирования. Таблица 5.1. Определение языка Показушка в расширенной форме Бэкуса-Наура 1. предложение 2. подлежащее 3. предикат 4. именная-группа 5. список-прилагательных 6. существительное 7. прилагательное 8. глагол 9. наречие - подлежащее предикат . в именная-группа. - глагол (именная-группа | наречие) . - [ список-прилагат ] существительное . ' в прилагательное { прилагательное } . = ’’КОШКА” | ’’МЫШКА” . = ’’ЧЕРНАЯ” | ’’БЕЛАЯ” | ’’ТОЩАЯ” | ’’УПИТАННАЯ” . - ’’БЕЖИТ” | ”ЕСТ” . - ’’БЫСТРО” | ’’МЕДЛЕННО” . Табл. 5.1 содержит девять РБНФ-определений. Каждое из них определяет один синтаксический класс в Показушке, а все вместе они полностью определя- ют синтаксический класс предложение. Приглядевшись к таблице, вы обна- ружите, что в определения входят: а) записанные в кавычках лексемы (т.е. слова и знаки препинания, пере- численные на с. 84); б) названия синтаксических классов; в) записанные без кавычек дополнительные знаки: -.!<}[]<) Чтобы научиться понимать и использовать РБНФ, давайте рассмотрим оп- ределения, но не в том порядке, в котором они представлены, а в том, который соответствует логике введения обозначений. Знак равенства можно читать: ’’определяется как...”, а точка обозначает конец определения. Например, определение 2 означает лишь следующее: подлежащее определяется как именная группа. Справа от знака равенства стоят названия синтаксических классов, разде- ленные пробелами, и этим указывается, что объекты данных классов следуют друг за другом. Поэтому определение 1 эквивалентно утверждению предложение определяется как подлежащее, за которым следует предикат, за которым, в свою очередь, следует лексема точка. В этом определении важным является различие между значением точки, завершающей определение, и точки, заключенной в кавычки. Первая указывает на конец определения, тогда как вторая является лексемой, которая должна записываться в конце синтаксически правильного предложения языка. Вертикальная черта означает выбор, альтернативу. Поэтому определение 8 можно на обычном языке выразить так: глагол определяется как лексема ’’бежит” или как лексема ”ест”. В сущности, здесь просто сообщается, что слова ’’бежит” и ”ест” являются гла- голами. Точно так же определения 6, 7 и 9 относят каждое из слов к существи- тельным, прилагательным или наречиям.
88 Гл. 5. Синтаксис Фигурные скобки означают возможность повторения, что соответствует замкнутому циклу в синтаксической диаграмме. Например, в определении 5 утверждается: список-прилагательных определяется как прилагательное, вслед за кото- рым может многократно повторяться часть, состоящая из лексемы и прилагательного. Это значит, что список-прилагательных должен содержать по крайней мере одно прилагательное, но что за ним могут идти и другие прилагательные при усло- вии, что каждому из них предшествует запятая. Поэтому следующие списки прилагательных являются допустимыми: ЧЕРНАЯ УПИТАННАЯ, ЧЕРНАЯ ТОЩАЯ, ТОЩАЯ, БЕЛАЯ ЧЕРНАЯ, БЕЛАЯ, ТОЩАЯ, БЕЛАЯ, УПИТАННАЯ, ЧЕРНАЯ Квадратные скобки обозначают необязательную часть синтаксического класса. Они встречаются в определении 4, которое можно прочитать так: именная-группа определяется как существительное, перед которым следует, но не обязательно, список-прилагательных . Другими словами, правильным является как отсутствие списка прилагательных, так и его наличие, однако присутствие двух или более списков прилагательных будет неправильным. Круглые скобки, используемые вместе с вертикальной чертой, обозна- чают альтернативы внутри определения. Они есть в определении предиката, которое читается так: предикат определяется как глагол, за которым следует либо именная группа, либо наречие. Это определение равносильно более длинному: предикат = глагол именная-группа | глагол наречие Следующие предикаты являются правильными: ест мышку БЕЖИТ МЕДЛЕННО БЕЖИТ ЗА ЧЕРНОЙ, БЕЛОЙ КОШКОЙ РБНФ-обозначения сведены в табл. 5.2. Таблица 5.2. Обозначения РБНФ обозначение смысл обозначения — определяется как конец определения выбор {х} IX] (X | у | ... | Z) ”х" отсутствие х или одно или больше вхождений х отсутствие х или ровно одно вхождение х вхождение х, или у, или ... или z лексема х X фрагмент текста, относящийся к синтаксическому классу х
Гл. 5. Синтаксис 89 5.3. Синтаксический анализ До сих пор мы занимались построением синтаксически правильных фраг- ментов текста с помощью синтаксических диаграмм или РБНФ-определений. Обратный процесс состоит в том, чтобы выяснить, принадлежит ли конкретный фрагмент текста данному синтаксическому классу. Этот процесс называется син- таксическим анализом, а одним из методов его проведения является построение синтаксического дерева. Для наглядности давайте выясним, будут ли следующие предложения син- таксически правильны в Показушке. КОШКА ЕСТ МЫШКУ . УПИТАННАЯ, ЧЕРНАЯ, ЧЕРНАЯ КОШКА БЕЖИТ БЫСТРО . БЫСТРО БЕЖИТ МЫШКА . На первом этапе текст нужно расчленить, превратив в последователь- ность лексем. В Показушке это тривиально, и в первом из наших примеров получается "КОШКА” ”ЕСТ” ’’МЫШКУ” Второй этап анализа состоит в том, чтобы пройти по тексту слева направо и найти последовательно составные части, образующие объект дан- ного синтаксического класса. Так как сейчас мы пытаемся доказать, что текст - это предложение, обратимся к синтаксической диаграмме предложения. Диагра- мма показывает, что всякое предложение начинается с именной группы. Поэто- му в начале текста мы должны найти именную группу. Именная-группа - другой синтаксический класс, поэтому в рамках основ- ной процедуры анализа нужно произвести еще один разбор, чтобы найти имен- ную группу. Согласно соответствующей синтаксической диаграмме, лексема ’’кошка” допустима в качестве именной группы. Поэтому мы возвращаемся к основной процедуре и заменяем первое слово в предложении названием класса именная-группа: I "КОШКА” "ЕСТ" ’’МЫШКУ" именная-группа "ЕСТ” "МЫШКУ” Согласно синтаксической диаграмме, именная группа, найденная в начале предложения, заставляет непосредственно за ней искать лексемы ”ест” или ’’бежит”. Это просто. Следующей в частично уже проанализированном тексте идет лексема ”ест”, что синтаксически правильно. Затем мы должны найти либо другую именную группу, либо лексему ’’быстро”, либо лексему ’’медленно”. Так как ’’мышка” - действительно именная группа, наш анализ продолжается так: ’’КОШКА” ”ЕСТ” именная-группа ”ЕСТ” именная-группа "ЕСТ” Наконец, требуется найти лексему и она в тексте обнаруживается. Итак, дойдя до правого конца синтаксической диаграммы и исчерпав весь текст, мы можем заключить, что первый фрагмент в нашем примере является правиль- ным предложением. Существенно, что синтаксический анализ не является простым разгляды- ванием текста с целью отнесения его к какому-нибудь из классов. Нужно знать, МЫШКУ” мышку
90 Гл. 5. Синтаксис какой класс мы ищем, и определить, принадлежит ли текст этому классу. В нашем примере, начиная анализ, мы искали предложение, и это побудило ис- кать вначале именную группу. На каждом из следующих этапов мы тоже точно знали, что именно мы ищем. Можно не выписывать каждый из этапов анализа отдельной строкой, а со- кратить запись и показывать в одной строке несколько этапов. Полный анализ приведенного примера можно представить так, как это сделано на рис. 5.2, а если привнести в изображение толику артистизма, - так, как на рис. 5.3. Понят- но теперь, почему этот вид анализа называется построением синтаксического дерева. Еще раз отметим, что обработка всегда должна проходить слева направо. ’’КОШКА” именная-группа ЕСТ” ’’МЫШКУ” именная-группа предложение Рис. 5.2. Синтаксический анализ фрагмента ”КОШКА ЕСТ МЫШКУ” Анализ второго примера проводится так, как показано на рис. 5.4, и де- монстрирует синтаксическую правильность другого предложения в Показушке. ’’УПИТАННАЯ” ’’ЧЕРНАЯ” ’’ЧЕРНАЯ”’’КОШКА” ’’БЕЖИТ”’’БЫСТРО” ”.” прилагат. прилагат. прилагат. ’’КОШКА” ’’БЕЖИТ” ’’БЫСТРО” ”.” ’-------------------------,------------------------------> список-прилагательных существит. ’’БЕЖИТ” ’’БЫСТРО” именная-группа ’’БЕЖИТ” ’’БЫСТРО” J предложение Рис. 5.4. Синтаксический анализ фрагмента "УПИТАННАЯ, ЧЕРНАЯ, ЧЕРНАЯ КОШКА БЕЖИТ БЫСТРО” В этих двух примерах анализ привел к положительным результатам, но если на любом из этапов требующийся элемент найти не удается, значит, ана-
Гл. 5. Синтаксис 91 лиз потерпел неудачу. В третьем примере мы сразу сталкиваемся с трудностями, так как первая же лексема, ’’быстро”, не может быть ни именной группой, ни ее частью. Поэтому можно сделать мгновенный вывод, что предложение БЫСТРО БЕЖИТ МЫШКА не является синтаксически правильным в Показушке. С помощью РБНФ-определений синтаксический анализ выполняется почти так же. На каждом этапе последовательность лексем и названий синтаксичес- ких классов, удовлетворяющая правой части определения, заменяется в текс- те названием синтаксического класса из левой части того же определения. В общем случае язык, определенный с помощью РБНФ, содержит больше син- таксических классов, чем тот же язык, определенный синтаксической диаграм- мой. Это значит, что синтаксическое дерево будет содержать больше уровней. Попробуем произвести РБНФ-анализ, используя те же примеры, что и раньше. Анализ первого из них показан на рис. 5.5. Выводы о синтаксической пра- вильности предложения согласуются с выводами, сделанными раньше. ’’КОШКА” существительное^ именная-группа ”ЕСТ” глагол глагол К______ ’’МЫШКУ” существительное именная-группа подлежащее предикат предложение Рис. 5.5. Синтаксический анализ фрагмента ’’КОШКА ЕСТ МЫШКУ”, выполненный с помощью РБНФ ’’УПИТАННАЯ” ”,” ’’ЧЕРНАЯ” ’’ЧЕРПАЯ” ’’КОШКА” ’’БЕЖИТ” ’’БЫСТРО” ” п рил агат. < прилагат. прилагат. существительное глагол наречие список-прилагательных существительное глагол наречие v v— / именная-группа предикат 1 подлежащее предикат предложение Рис. 5.6. Синтаксический анализ фрагмента ’’УПИТАННАЯ, ЧЕРНАЯ, ЧЕРНАЯ КОШКА БЕЖИТ БЫСТРО”, выполненный с помощью РБНФ Анализ второго примера показан на рис. 5.6 правильно, но на одном из этапов здесь легко ошибиться. Важно, чтобы на каждом шаге последователь- ность, замещаемая в результате подстановки, была максимально длинной из
92 Гл. 5. Синтаксис всех возможных последовательностей, принадлежащих данному синтаксичес- кому классу. В этом примере можно выделить три списка прилагательных: "УПИТАННАЯ” "УПИТАННАЯ” "ЧЕРНАЯ” "УПИТАННАЯ” "ЧЕРНАЯ” ”ЧЕРНАЯ” и если не выбрать самую длинную из них, анализ будет неверным. Если в каче- стве списка прилагательных мы возьмем, скажем, фрагмент ’’упитанная” ”,” ’’черная”, то анализ получится таким, как показано на рис. 5.7. Он приводит к отрицательному результату, так как за списком прилагательных в именной группе должно следовать существительное, а лексема ”,” таковой не является. Для того чтобы анализ был выполнен правильно, список прилательных нужно брать целиком. ’’УПИТАННАЯ” ”," ’’ЧЕРНАЯ” ”,” ’’ЧЕРНАЯ” ’’КОШКА” ’’БЕЖИТ” "БЫСТРО” прилагат. прилагат. прилагат. существительное глагол наречие список-прилагательных прилагат. существительное глагол наречие Рис. 5.7. Неверный синтаксический анализ В третьем примере: "БЫСТРО” "БЕЖИТ” "МЫШКА” закономерно получается отрицательный результат, так как "БЫСТРО” есть наречие и не может быть частью именной группы. Следовательно, фрагмент начинается не с подлежащего и не может быть синтаксически правильным предложением. Итак, процедура синтаксического анализа, проводимого как с помощью синтаксических диаграмм, так и с помощью РБНФ, сводится к следующему: а) Разбить текст на последовательность лексем. б) Разбирая текст слева направо, найти последовательность элементов, со- ставляющую фрагмент текста, принадлежащий к заданному синтакси- ческому классу, выполняя при необходимости дополнительный анализ в рамках основного. Последовательность, заменяемая на каждом этапе, должна быть максимально длинной из всех возможных. Если на каком- либо этапе требуемый элемент обнаружить не удается, значит, текст не разобран, анализ был безуспешен. Упражнения 5.1. С помощью синтаксической диаграммы, определяющей Показушку (рис. 5.1), проведите подроб- ный синтаксический анализ следующих фрагментов текста с тем, чтобы выяснить, являются ли они синтаксически правильными в этом языке: а) МЫШКА БЕЖИТ БЫСТРО . б) БЕЛАЯ КОШКА ЕСТ ЧЕРНУЮ КОШКУ . в) УПИТАННАЯ, ТОЩАЯ, ЧЕРНАЯ, БЕЛАЯ МЫШКА ЕСТ МЕДЛЕННО . г) КОШКА БЕЖИТ ЗА ОЧЕНЬ ТОЩЕЙ МЫШКОЙ . 5.2. Вот синтаксическое описание другого крайне ограниченного языка, который называется Лепет'.
Гл. 5. Синтаксис 93 предложение предикат подлежащее переходный-глаеол дополнение наречие непереходный-глагол - подлежащее предикат - переходный-глагол дополнение | непереходный-глагол { наречие } . - ”МАМА” | ”БУБА” . - "КОРМИТ” | "КУПАЕТ” . - ”ПАПА” | ”БУБА” . - ’’МЕДЛЕННО” | ’БЫСТРО” . - "ШАЛИТ” . а) Начертите синтаксические диаграммы для синтаксических классов предложение и предикат, включив в них определения других синтаксических классов. б) Определите с помощью синтаксического анализа, какие из следующих предложений являют- ся синтаксически правильными в языке Лепет'. МАМА КОРМИТ БУБУ МЕДЛЕННО . БЫСТРО МАМА КУПАЕТ БУБУ . БУБА ШАЛИТ МЕДЛЕННО БЫСТРО . МАМА МЕДЛЕННО КОРМИТ ПАПУ . МАМА ШАЛИТ БУБУ . в) Выпишите все предложения на Лепете, состоящие из двух слов. г) Сколько предложений на Лепете состоят ровно из трех слов. 5.3. Рассмотрите следующие РБНФ-определения: фио “ имя фамилия . имя - имя-собственное ” ” . фамилия - имя-собственное { ”-” имя-собственное } . имя-собственное - болыиая-буква { малая-буква } . где синтаксические классы большая-буква и малая-буква содержат соответствующие русские буквы. а) Выразите эти синтаксические определения 1) с помощью синтаксических диаграмм - по одной диаграмме на каждый из классов: фио, имя, фамилия и имя-собственное; 2) с помощью единственной синтаксической диаграммы для класса фио, включающей все остальные определения. б) Полагая, что не допускаются никакие пробелы, кроме тех, что включены в вышеприведенные определения, установите, являются ли следующие фрагменты синтаксически правильными фио, и если нет, то почему: 1) Валерий Волин 2) Давид Михайлович Райский 3) Антон Петров-Давыдов 4) Антонина Петрова-Давыдова 4) Эрик П.Д.Кузнецов 5) Дж Кротер 6) Др Льюис 5.4. Рассмотрите синтаксическую диаграмму на рис. 5.8, в которой синтаксические классы цифра, большая-буква и малая-буква содержат соответственно 10 цифр, 33 прописные и 33 строчные русские буквы. а) Выразите эту синтаксическую диаграмму с помощью РБНФ. б) Полагая, что между лексемами может быть один или несколько пробелов, выясните, являют- ся ли следующие фрагменты синтаксически правильными адресами, и если нет, то почему: 1) Солнцеград, улица Варлама, 49 . 2) Ростов-на-Дону, переулок Правды, ’Главрембыттехника’ 3) Вашингтон, улица Пенсильвании, 121 . 4) Голливуд, бульвар Заката, 4395 . 5) Урюпинск, улица Нижняя, 10 .
94 Гл. 5. Синтаксис номер --------цифра -------------------------------------------? ........ '----- цифра название -----большая-буква-малая-буква ----__ адрес ----- название- — улица ———название--г----номер- 4— шоссе —название '— переулок--' Рис. 5.8. Правильно пишите адрес
6 НЕКОТОРЫЕ СВЕДЕНИЯ О СТАНДАРТНОМ ПАСКАЛЕ Эта книга посвящена программированию, а не языку программирования, но очевидно, что желательно разобраться в некоторых деталях языка, на кото- рый мы опираемся, т.е. стандартного Паскаля. Кое-что вы должны были узнать, разбирая программы в предыдущих главах, и если бы вам пришлось взяться за программу, то, вероятно, в основном, вы написали бы ее правильно. Но как бы- вает у большинства, впервые программирующих на некотором языке, скорее всего вы сделали бы ряд синтаксических и семантических ошибок. Цель этой главы - привлечь ваше внимание к отдельным сторонам применения стандартно- го Паскаля, знание которых поможет вам избегать ошибок. Она поможет вам также понять, как ваша Паскаль-система реагирует на те или иные виды оши- бок, которые вы допускаете. 6.1. Лексемы Паскаля Проанализируйте, пожалуйста, программу РаспространениеЗвука (лис- тинг 6.1). На самом нижнем уровне она образована лексемами языка, записан- ными построчно. Пробелы и пустые строки вставлены в нее для того, чтобы вне- шне программа стала более ясной, для той же цели в нее включены небольшие комментарии. program SoundTravel (input, output)', { чтение времени в секундах и выдача расстояния, на которое звук распространяется за это время } const velocityofsound = 344; var time, distance : integer, begin read (time); distance velocityofsound ♦ time; writeln ('3a ', time : 1, ’ сек. звук проходит ', distance : 1, ’ м') end { SoundTravel}. Листинг 6.1. Текст программы РаспространениеЗвука, как его видит читатель
96 Гл. 6. Процессы, процессоры и программы Вспомните: язык Показушка строился из базисных элементов, которыми были десять слов и два знака препинания, называвшиеся лексемами языка. Если мы хотим понять, какой видит программу РаспространениеЗвука Паскаль-про- цессор, то мы должны посмотреть на нее как на последовательность лексем Паскаля. Такой ’’взгляд” приведен на рис. 6.1, где в каждом прямоугольничке содержится по одной лексеме языка. Заметьте, что комментарии и большинство пробелов исчезли. writein ( ’За time time сек. звук проходит ’ d i stance end Рис. 6.1. Текст программы РаспространениеЗвука, как его ’’видит” процессор Паскаля В качестве первого шага на пути к определению лексем Паскаля удобнее всего определить синтаксические классы цифра и буква так, как показано на рис.6.21. В классе буква показаны только строчные буквы, однако могут употре- бляться и прописные. В Паскале неважно, записана буква как прописная или как строчная, за исключением тех букв, которые входят в состав литералов. Например, три слова: cost COST Cost эквивалентны, тогда как литералы ’ЗАГОЛОВОК’ ’заголовок’ ’Заголовок’ различны. Теперь мы готовы к тому, чтобы определить нужные нам типы лексем Паскаля, а именно: специальные символы, идентификаторы, числа и литералы. "а” "к” "и” буква цифра "Ь" | ”с” | ”rf” | ”е" | "Г | ”g” | ”h" | "i" | | । ,,n|„ । । „0„ । ,,p,. । .y. । ,,r„ । >v> । >7„ | "v" I ’’И'” I "x" I "y" I ”z” . "0” | ”1” | ”2” | ”3” | ”4” | ”5” | ”6” | ”7” | ”8” | ”9”. Рис. 6.2. Буквы и цифры 1 В комментариях и литералах допускается, кроме того, использование букв русского алфавита. Примеч. пер.
Гл. 6. Процессы, процессоры и программы 97 Соответствующие определения представлены на рис. 6.3. Этот рисунок (так же, как и все последующие рисунки, содержащие синтаксические определе- ния) состоит из двух частей. В части (а) приводится синтаксическая диаграмма, а в части (б) дается РБНФ-определение. В одних случаях вы захотите взглянуть на диаграмму, в других удобнее окажется РБНФ. идентификатор ------ буква — ---------у буква цифра число число-без-знака--------- число-без-знака литерал один-из-символов-допускаемых-оборудованием Рис. 6.3а. Лексемы Паскаля (кроме специальных символов, определенных в 6.36) Множество специальных символов (рис. 6.36) распадается на две группы. Первая группа содержит: знаки препинания . , : ; скобки ( ) [ ] знаки арифметических операций + - * / знаки отношений =<>/><=>= оператор присваивания := вертикальная стрелка (или надчерк) индикатор диапазона Большинство этих символов уже использовалось. 4 Заказ № 1110
98 Гл. 6. Процессы, процессоры и программы Все остальные специальные символы - это слова, вроде program, begin или while, каждое из которых играет в языке особую роль. Всего их 35, и значения этих слов в стандартном Паскале неизменны: их нельзя употреблять ни в каких других целях. Чаще всего их называют служебными словами. Если вы случайно употребляете одно из них не по назначению, например в качестве имени пере- менной, то во время компиляции программы будет выявлена ошибка. В данной книге так же, как и во многих документах, касающихся Паскаля, служебные слова напечатаны жирным шрифтом. Так вам легче будет их запомнить. Но эти слова никак не выделяются ни в программе, подготовленной для процессора, ни в машинной распечатке программы. специальный-символ идентификатор число число-без-знака целое-без-знака действительное-без-знака порядок ~ знак цепочка-цифр литерал элемент-литерала изображение-апострофа строковый-символ “ | п.п | п*„ | п/п | | „<м I | пр | Г | п п | | п.п | п.п | п.п | „|п | W<>n [ „<в„ | | п.^п | | "and” | ’’array” | ’’begin” | ’’case” I ’’const” | ”div” | ”do” | ”downto” | ”else” | ’’end” | ”file” | ”for” | "function” | ”goto” | ”if” I ”in” | ’’label” | ”mod” | ’’nil” | ’’not” | ”of” | ”or”| ”packed” | "procedure” | ’’program” | ’’record” | ’’repeat” | ”set” | ’’then” | ”to” | ’’type” | ’’until” | ”var” | ’’while” | ’’with” . - буква { буква | цифра } . - [ знак ] число-без-знака . - целое-без-знака | действительное-без-знака . - цепочка-цифр . - цепочка-цифр цепочка-цифр | цепочка-цифр [ ”.” цепочка-цифр ] | ”Е” порядок . - [ знак ] целое-без-знака . Ш ” j.W | п_п - цифра { цифра } . - ”’” элемент-литерала { элемент-литерала } ”’” . - изображение-апострофа | строковый-символ . мни - любой-символ-допускаемый-оборудованием-кроме- апострофа . Рис. 6.36. Лексемы Паскаля Слова, используемые для именования любых объектов в программе на Паскале, принадлежат синтаксическому классу идентификатор. Каждый иденти- фикатор в любой программе относится к одной из двух групп: к предопределенным идентификаторам или к идентификаторам, определенным программистом. Предопределенный идентификатор имеет стандартный смысл, который вхо- дит в описание языка1. Стандартный Паскаль требует по крайней мере 40 предо- пределенных идентификаторов (некоторыми из которых мы уже пользовались): abs cos false maxi nt pack readln sin true arctan dispose get new page real sqr trunc boolean eof input odd pred reset sqrt unpack 1 Предопределенные идентификаторы называют также встроенными в язык. - Примеч. ред.
Гл. 6. Процессы, процессоры и программы 99 char chr eoln exp integer word put In output read rewrite succ write round text writein Предопределенные идентификаторы не являются служебными словами, и хотя каждый из них имеет стандартный смысл, программист по своему желанию может его изменить. Как правило, это неразумно. Мы привели список полнос- тью, потому что вы случайно можете употребить один из этих идентификаторов, не описав его1. Паскаль-система решит, что имеется в виду предопределенный, и уже от того, каким образом вы его употребили, будет зависеть, появится сооб- щение об ошибке или нет. Идентификатор, определенный программистом, - это идентификатор, смысл которого определен непосредственно в программе. В нашем примере (лис- тинг 6.1) было четыре таких идентификатора: SoundTravel velocityofsound time, distance - имя программы РаспространениеЗвука, - имя константы (’’скорость звука”), - имена переменных (’’время”, ’’расстояние”). Подробнее о том, как задается значение идентификатора и в какой части про- граммы оно действует, мы поговорим в этой главе чуть позже. При выборе идентификаторов нужно помнить две вещи. Первое и самое важное - всегда выбирать идентификаторы, имеющие смысл для тех, кому при- дется читать программу, включая и самого программиста. Правда, для этого часто приходится использовать довольно длинные идентификаторы, однако стан- дартный Паскаль допускает идентификаторы любой длины2. Второе: синтаксис класса идентификатор требует, чтобы: а) идентификатор начинался с буквы, б) идентификатор состоял только из букв и цифр. Таким образом, недопустимы никакие идентификаторы, начинающиеся с цифры. Следующие идентификаторы, допустимые в некоторых языках програм- мирования, в стандартном Паскале недопустимы: process one word advance- to-pall- m all word-length - так как содержит пробелы, - так как содержит дефисы, -так как содержит символ подчеркивания. К третьему типу лексем относятся числа. Они фигурируют в тексте Пас- каль-программы, во входных данных и результатах. Это могут быть уже знако- мые нам целые числа или числа с дробной частью, называемые вещественными. Пока все внимание мы отдадим целым числам, а вещественные не будем упо- треблять вплоть до гл. 13. Синтаксис целых чисел (см. рис. 6.3) в основном соответствует их привыч- ному написанию. Все следующие целые числа правильны: О 23 708 007 +52 -179 но заметьте, что никакие символы, кроме цифр, внутри целых чисел синтакси- чески недопустимы. Поэтому некоторые обозначения, иногда встречающиеся, 1 И не подозревая о его существовании. - Примеч. ред. 2 Если вы не владете ни английским, ни каким-либо другим иностранным языком, в основе которого лежит латинский алфавит, лучше всего использовать обозначения, принятые в конкрет- ной предметной области. Например, в физике: скорость v, расстояние - $, в экономике: прибыль - р, норма прибыли - пр, и т.д. В разделах определения констант и описания переменных рекомен- дуется в этом случае описывать смысл всех идентификаторов в комментариях. - Примеч. пер. 4*
100 Гл. 6. Процессы, процессоры и программы скажем, при записи больших чисел, употреблять нельзя. Следующие целые чис- ла недопустимы по указанным справа причинам: 4.396.217 - так как внутри целого числа не может быть точек, 734 683 - так как внутри целого числа не может быть пробелов, 1Е6 - так как внутри целого числа не может быть символа ”Е”. Вероятно, самый важный факт относительно целых чисел - то, что суще- ствует предельное значение, которого не могут превосходить употребляемые целые числа. Это ограничение не является чем-то присущим только стандартно- му Паскалю. Оно является следствием физических пределов в представлении чи- сел в процессоре. Чтобы понять природу этой проблемы, можно рассмотреть цифровой индикатор вроде тех, что встроены в карманные калькуляторы. Пред- положим, что на индикаторе есть место для четырех цифр и дополнительная позиция для знака. Тогда с его помощью можно представить числа в диапазоне -9999.Л9999 но число 10000 представить уже нельзя. Физические параметры индикатора не позволяют показать вообще никакого числа, состоящего более, чем из четырех цифр. В компьютере целое число обычно представляется как последовательность двоичных цифр, т.е. единиц и нулей, и общее количество двоичных цифр огра- ничено. Тем самым определяется наибольшее число, которое можно естественно представить и обработать в процессоре. В каждой Паскаль-системе это число - предопределенная константа, которая называется maxint, и ее значение постоянно для программной среды конкретного процессора. Диапазоном целых чисел всегда является -maxint..maxint. Последний вид лексем - это литерал, три раза употребленный в программе РаспространениеЗвука'. ’За ’ ’ сек. звук проходит ’ ’ м’ С литералами вы уже сталкивались в предыдущих главах, но здесь уместно подчеркнуть два важных момента. Во-первых, весь литерал считается одной лексемой языка. Это важно потому, что если программист по невнимательности пропустит закрывающий апостроф, процессор может воспринять весь текст вплоть до следующего открывающего апострофа как часть литерала. Такая ситу- ация обычно приводит к ошибке на этапе компиляции, которая диагностируется довольно далеко от того места, где реально пропущен апостроф. Если бы, напри- мер, оператор writein в программе РаспространениеЗвука был записан в виде: writein (’ За ’, time : 1, ’ сек. звук проходит , distance : 1, ’ м’) то в лучшем случае на этапе компиляции было бы выдано сообщение о том, что м - символ, не ожидаемый в данном месте программы. Действительная же ошиб- ка - пропуск апострофа после слова ’’проходит”. Во-вторых, нужно отметить, что апостроф включается в литерал особым образом. Процессор, который строит каждую лексему, читая букву за буквой, воспримет одиночный апостроф внутри литерала как его завершение. Проблема решается за счет представления апострофа в литерале с помощью изображения апострофа. Изображением апострофа служат просто два апострофа, расположен- ных рядом. Таким образом, ’об”ездчик осликов’ это не два литерала, а один, представляющий фрагмент текста: об’ездчик осликов
Гл. б. Процессы, процессоры и программы 101 Литерал, состоящий из единственного апострофа, должен обозначаться четырьмя апострофами, следующими непосредственно друг за другом. 6.2. "Пустоты” в программе В программе на Паскале лексемы следует отделять друг от друга, и есть два соображения, которые при этом стоит принимать во внимание. Во-первых, способ разделения должен удовлетворять процессор, во-вторых, что более важно, он должен быть удобен для человека. Что касается процессора, то с его точки зрения лексемы в программе отде- ляются друг от друга разделителями^ каковыми являются: пробел, комментарий, маркер конца строки. При этом действуют правила: а) разделители не могут появляться внутри лексем; исключение составля- ют пробелы, которые могут входить в литералы; б) слову program может предшествовать сколько угодно разделителей, но может не предшествовать ни одного; в) между двумя соседними лексемами может стоять сколько угодно разде- лителей, и, в частности, может не быть ни одного; г) по крайней мере один разделитель должен стоять между двумя соседни- ми лексемами, если они являются идентификаторами, служебными сло- вами или целыми числами без знака1. Если вы нарушите какое-нибудь из этих правил, то, скорее всего правило (а). Оно подразумевает, в частности, как о том говорилось выше, что пробелы не могут стоять ни в идентификаторах, ни в записи числа. Не могут они присут- ствовать и в специальных символах. Так что в следующих парных сочетаниях два знака должны идти непосредственно друг за другом: о <= >= := .. Это относится и к изображению апострофа в литерале >> и к парам символов (* *) когда в них заключен комментарий. Так как конец строки - тоже разделитель, то ни специальные символы, ни идентификаторы, ни числа нельзя переносить с одной строки на другую. Оборудование, на котором вы работаете, может до- пускать переносы литералов, но такие переносы нежелательны просто потому, что в литералах при этом могут появиться непредусмотренные пробелы. program soundtraveKinput,output) ;const velocityofsound~344;var time,distance'. integer,begin read (time) ,distance:-velocityofsound*time; writeln C3a ',timeA,' сек. звук проходит ’,distance:!3t’)end. Листинг 6.2. Текст программы РаспространениеЗвука с минимальным количеством разделителей Остальные правила практически означают, что разделители редко бывают абсолютно необходимы. Разделитель между двумя лексемами нужен только в тех случаях, когда, будучи написаны непосредственно друг за другом, они могут быть ошибочно приняты за одну лексему. Таким образом, для процессора вари- 1 Более точно: если назвать липкими лексемами идентификаторы, служебные слова и целые без знака, то между двумя липкими лексемами должен стоять разделитель. - Примеч. ред.
102 Гл. 6. Процессы, процессоры и программы ант программы РаспространениеЗвука, приведенный на листинге 6.2, полностью идентичен предыдущему варианту, содержавшему достаточные пустоты. Программист, который пишет программы вроде той, что показана на лис- тинге 6.2, совершенно не осознал, насколько важен внешний вид программы. Наглядность - существенный аспект хорошего программирования на Паскале, и если вы так расположили текст программы, что она легко читается и сразу просматривается ее структура, то уж процессор и подавно удовольствуется таким количеством пустот. 6.3. Выражения Определив лексемы стандартного Паскаля, можно выяснить, как они соче- таются друг с другом, образуя другие синтаксические классы языка. Мы начнем с того, что рассмотрим выражения; синтаксис класса выражение и нескольких связанных с ним классов показан на рис. 6.4. В построении выражений участвуют три других класса: множители, термы и простые выражения - и применяются знаки трех типов: операции умножения, операции сложения и операции сравнения (знаки отношений). Последние три термина вы найдете только в РБНФ-определении, в синтаксической диаграмме их нет. Найдя их, вы заметите, что класс ’’операция сложения” содержит не то- лько знак ’’плюс”, и что привычный нам знак ’’умножить” лишь одна из пяти операций умножения. Пусть вас также не смущает, что ”+” и являются и обозначениями операций, и знаками (чисел). Операции сложения они обознача- ют, когда стоят внутри простого выражения, а в роли знаков стоят в его начале. Процесс конструирования выражений имеет иерархическую природу и сво- дится к следующему: а) множители сочетаются между собой посредством операций умножения и образуют термы; б) термы сочетаются между собой посредством операций сложения и обра- зуют простые выражения; в) простые выражения сочетаются между собой посредством операций сра- внения (знаков отношений) и образуют выражения. Если вы посмотрите на определение множителя, то увидите, что первые два типа множителей - это константа без знака и переменная. Они-то и дают ключ к пониманию природы множителей, потому что обычно множитель являет- ся или константой, или переменной. Есть три вида констант без знака, и все они нам знакомы. К ним относят- ся числа без знака, имена констант и литералы, причем имя константы - это тот самый идентификатор, который фигурирует в разделе определения констант. Синтаксис переменной немного сложнее, особенно в РБНФ. В сущности, в это понятие включено все, что позволяет процессору в ходе выполнения програ- ммы обратиться к переменной, чтобы получить ее текущее значение либо его изменить. На данном этапе нам понадобятся только два вида переменных. В РБНФ-определении они названы полной переменной и буферной переменной. Полная переменная - это просто имя переменной, описанной в соответст- вующем разделе программы, а единственной допустимой буферной переменной является inpuf. Мы уже отмечали в гл. 5, что названия синтаксических классов - полез- ные рабочие термины, и будучи однажды определены, они могут в дальнейшем использоваться при обсуждении языка. РБНФ содержит большее количество синтаксических классов и поэтому, как правило, позволяет работать с более широким диапазоном терминов. Несколько понятий, употребленных в последних абзацах, определены только в РБНФ. Некоторые из этих дополнительных син-
Гл. 6. Процессы, процессоры и программы 103 таксических классов, однако, нужны нам здесь для других целей. Рассмотрим в качестве примера определение: выражение ----простое-выражение простое-выражение----' простое-выражение терм множитель L”*” ”div” "mod” "and” ________________1_______I_______I________I___________> -----число -без-знака —r------- <---имя-константы —— ---литерал------------' Примечание. Тот синтаксический класс "константа”, который используется в разделе описания констант, определен на рис.6.15. переменная Рис. 6.4а. Выражения, константы и переменные
104 Гл. 6. Процессы, процессоры и программы выражение - простое-выражение [ операция-сравнения простое-выражение ) . простое-выражение терм множитель - [ знак ] терм { операция-сложения терм } . - множитель { операция-умножения множитель } . - константа-без-знака | переменная | ”(” выражение ”)” "not” множитель | обращение-к-функции . операция-сравнения операция-сложения операция-умножения знак константа-без-знака имя-константы переменная - | ”<>” | ”<” | ”>” | | . - ”+” 1 1 ”ог” . - ”♦” 1 "/" 1 "div” 1 "mod” | ”and” . - w+” | ”_n . - число-без-знака | имя-константы | литерал. - идентификатор. - полная-переменная | буферная-переменная | индексированная-переменная | выборка-поля | . полная-переменная имя-переменной буферная-переменная файловая-переменная индексированная-переменная - имя-переменной . - идентификатор . - файловая-переменная ”Л” . - переменная . - переменная-массива ”(” индекс { индекс} . переменная-массива индекс выборка-поля переменная-записи имя-поля обращение-к-функции - переменная . - выражение. - [ переменная-записи ”." ] имя-поля . - переменная . - идентификатор. - имя-функции [ список-фактических-параметров ] . Рис. 6.46. Выражения, константы и переменные константа-без-знака - число-без-знака | имя-константы | литерал . имя-константы - идентификатор. Можно обойтись без синтаксического класса имя-константы, если сделать подста- новку и получить константа-без-знака - число-без-знака | идентификатор | литерал . Будучи эквивалентным исходному с точки зрения синтаксиса, это определение далеко не так полезно. Исходное определение напоминает о том, что идентифи- катор, используемый в качестве константы без знака, будет иметь смысл только тогда, когда он предварительно определен как имя константы. Этим в синтаксис, по существу, включается доля семантических сведений. А теперь попробуем убедиться в том, что описанный синтаксис позволяет конструировать некоторые из выражений, встречавшихся в предыдущих главах. Для этого проанализируем фрагменты a) cost г) number * price б) 50 д) cost + lowpostage в) ’ и заняло ’ е) cost <100 где cost, number и price - имена переменных, a lowpostage - имя константы. Мы применим тот же метод анализа, что и при синтаксической проверке предложений в Показушке; однако на сей раз нас интересует, являются ли данные фрагменты текста правильными выражениями в стандартном Паскале.
Гл. 6. Процессы, процессоры и программы 105 (а) cost (б) 50 (в) ’ и заняло ’ имя-переменной число-без-знака литерал переменная константа-без-знака константа-без-знака множитель множитель множитель терм терм терм простое-выражение простое-выражение простое-выражение выражение выражение выражение (г) number * price имя-переменной Нф V имя-переменной переменная МфП переменная множитель Ч ПфП множитель / терм простое-выражение выражение (д) cost + lowpostage имя-переменной имя-константы переменная константа-без-знака множитель множитель терм терм / простое-выражение выражение (е) cost < 100 имя-переменной число-без-знака переменная константа-без-знака множитель множитель терм терм простое-выражение V- простое-выражение выражение Рис. 6.5. Синтаксический анализ нескольких выражений Ход анализа и его результаты, полученные благодаря привлечению синтаксичес- ких диаграмм, показаны на рис. 6.5. Каждый из фрагментов действительно ока-
106 Гл. 6. Процессы, процессоры и программы зывается правильным выражением. Прежде чем продолжить чтение, тщательно разберите ход анализа и попытайтесь выполнить упр. 6.3. 6.3.1. Старшинство операций и скобки в выражениях Обсуждая вычисление выражений в гл. 2, мы выяснили, что умножение должно выполняться раньше сложения, если этот порядок не изменен с помо- щью скобок. Кроме того, операции, имеющие одинаковое старшинство, выполня- ются последовательно, слева направо. Эти факты в значительной мере отражены в синтаксисе выражений, в чем можно будет убедиться, анализируя четыре сле- дующих примера. Рассмотрим схему анализа на рис. 6.6. Исследуемое выражение содержит операции сложения и умножения. Фрагмент множитель множитель в ходе анализа должен быть заменен на терм прежде, чем фрагмент терм ”+” терм будет заменен на простое выражение. Здесь отражен тот факт, что при вычисле- нии выражений процессор сначала должен выполнять умножение и лишь потом - сложение. Это верно в отношении любых операций умножения и сложения. cost number имя-переменной ”+” переменная ”+” множитель ”+” имя-переменной переменная множитель price имя-переменной переменная множитель терм простое-выражение выражение Рис. 6.6. Анализ выражения, демонстрирующий старшинство операций умножения над операциями сложения В схеме на рис. 6.7 анализируемое выражение содержит знаки операций умножения, сложения и сравнения, причем процессор выполнит сначала умно- жение в конце выражения и лишь после этого - сложение в начале. Здесь же по- казано, что сравнение двух результатов (операцией ”<”) выполняется в послед- нюю очередь. Операции сравнения имеют, таким образом, самое низкое стар- шинство. Что касается скобок, то благодаря наличию фрагмента (” выражение "У" в определении синтаксиса множителя на рис. 6.4а, изменяется обычное старшин- ство операций. Этот факт иллюстрируется на рис. 6.8, где порядок выполнения подстановок таков, что первым вычисляется подвыражение cost + lowpostage
Гл. 6. Процессы, процессоры и программы 107 а его значение затем умножается на результат. cost + lowpostage имя-перем. имя-конст. переменная конст-без-знака множитель п | п множитель терм П | W терм у J простое-выражение К___________________ значение number, что дает итоговый < number * price имя-перем. ПфП имя-перем. переменная ПфП переменная множитель множитель терм простое-выражение У выражение Рис. 6.7. Анализ выражения, показывающий, что операции сравнения имеют наименьшее старшинство cost + lowpostage number выражение ') имя-переменной выражение ’) переменная множитель множитель терм простое-выражение ( ) выражение Рис. 6.8. Анализ выражения, содержащего выражение в скобках И наконец, на рис. 6.9 показан анализ выражения, содержащего три опе- рации умножения (вы, вероятно, помните, что ”div” тоже является таковой). Последовательность множитель множитель ”div” множитель множитель заменяется на терм в результате единственной подстановки, и может показать- ся, что все три операции выполняются одновременно. Однако на практике это обычно невозможно - такие операции должны выполняться последовательно, од- на за другой. Поэтому нам нужно дополнительное правило: операции с одинако- вым старшинством выполняются слева направо.
108 Гл. 6. Процессы, процессоры и программы number price div 2 ♦ lowpostage имя-перем. имя-перем. "div” число-без-знака ПфЦ имя-константы переменная ПфП переменная ”div” конст-без-знака ПфМ конст-без-знака множитель множитель "div” множитель ПфП множитель к / терм простое-выражение выражение Рис. 6.9. Анализ выражения, содержащего последовательность операций с одинаковым старшинством 6.4. Операторы Теперь мы переходим к синтаксису операторов. Нужные нам определения представлены на рис. 6.10. Здесь определены две основные категории операторов: простые операторы и структурные операторы. Есть здесь и несколько видов опе- раторов, с которыми мы еще не сталкивались. Все они, однако, будут встреча- ться в дальнейшем. Приведенный здесь синтаксис обладает одним важным свойством. Он пост- роен так, что лексема, с которой начинается оператор, однозначно определяет тип этого оператора. Для известных нам операторов соотношение такое: начальная лексема в операторе тип оператора имя переменной оператор присваивания имя процедуры оператор процедуры begin составной оператор if условный оператор while оператор цикла ’’пока” Это свойство существенно для выполнения рассмотренного нами процесса син- таксического анализа. Давайте проведем анализ нескольких типичных операторов для того, что- бы соотнести данный синтаксис с теми операторами, которыми мы уже пользова- лись, и заострить внимание на некоторых мелочах, часто бывающих источником тривиальных, но весьма досадных ошибок. Предположим, что processonename - имя процедуры, а все остальные идентификаторы являются именами перемен- ных. Чтобы анализ был менее громоздким, будем прибегать к сокращениям для тех словосочетаний, подобные которым уже встречались раньше. Начнем с примера на рис. 6.11. Если данный фрагмент является операто- ром, то он может быть только оператором присваивания, потому что лексема, с которой он начинается, есть имя переменной. Весьма краткий анализ фрагмента позволяет убедиться в том, что он действительно является синтаксически прави- льным оператором присваивания, а значит, и простым оператором, и опера- тором. Еще один разбор приведен на рис. 6.12. Он начинается со служебного сло- ва while, поэтому следует, очевидно, ожидать оператора ’’пока”, и анализ это подтверждает. Отсюда вытекает, что это структурный оператор, а значит, и one-
Гл. 6. Процессы, процессоры и программы 109 оператор — простой-оператор — структурный-оператор простой оператор *--- переменная-----__ --------выражение <--- имя-функции----J Примечание. Различные виды списков параметров определены на рис. 11.1. список-фактических-параметров —> 4— список-параметров-оператора-read —> список-параметров-оператора-readln—> 4— список-параметров-оператора-write —* список-параметров-оператора-writeln —) ’— структурный оператор выражение "then”------ оператор Lrelse”— оператор---- Awhile”-------- выражение--------”do”-------- оператор выражение оператор Рис.6.10а. Операторы ратор. Возможно, больше всего нуждается в комментариях переход выражения в булевское выражение под фрагментом count > 0. Синтаксический класс ’’булев- ское выражение” в определении оператора цикла это еще один пример семанти-
110 Гл. 6. Процессы, процессоры и программы ческих сведений, как бы встроенных в синтаксис. Так как булевское выражение является выражением, класс оператор-пока мог быть определен в РБНФ так: оператор-цикла-пока = ’’while” выражение ”do” оператор. Но тогда определение не напоминало бы нам о том, что оператор осмыслен, лишь когда используемое в нем выражение имеет булевское значение. Вы, вероятно, уже заметили, что точка с запятой-это чертенок, доставля- ющий массу хлопот при программировании на Паскале. Возникает, например, вопрос, нужно ли ставить точку с запятой непосредственно перед словом end, завершающим составной оператор? На него позволяет ответить анализ двух фрагментов, приведенный на рис. 6.13. Фрагменты идентичны, за исключением единственной точки с запятой. Согласно синтаксису составного оператора в РБНФ-определении: ’’begin” оператор { оператор } ’’end” оператор простой-оператор пустой-оператор оператор-присваивания оператор-процедуры структурный оператор составной-оператор условный оператор булевское-выражение оператор-цикла- "пока" оператор-цикла- "пока-не" оператор-цикла-с-шагом переменная-цикла начальное-значение конечное-значение оператор-присоединения переменная-записи - простой-оператор | структурный-оператор . - пустой-оператор | оператор-присваивания | оператор-процедуры. - ( переменная | имя-функции ) выражение . - имя-процедуры ( [ список-фактических-параметров | список-параметров-оператора-read | список-параметров-оператора-readln | список-параметров-оператора-write | список-параметров-оператора-writebi ] ) . - составной-оператор | условный-оператор | оператор-цикла-"пока" | оператор-цикла-"пока-не" | оператор-цикла-с-шагом | оператор-присоединения . - ’’begin” оператор { оператор } ’’end” . - ”if” булевское-выражение ’’then” оператор [ ’’else” оператор ] . - выражение. - ’’while” булевское-выражение ”do” оператор . - ’’repeat” оператор { оператор } ’’until” булевское-выражение. - ’’for” переменная-цикла начальное-значение (”to” | ” down to”) конечное-значение ”do” оператор . - полная-переменная. - выражение. - выражение. - ’tyith” переменная-записи { переменная-записи } ”do” оператор. - переменная. Рис.6.106. Операторы
Гл. 6. Процессы, процессоры и программы 111 cost cost + number * price имя-переменной выражение переменная выражение ;---------------v_________________________/ оператор-присваивания простой-оператор оператор Рис.6.11. Синтаксический анализ оператора присваивания while count > О do begin countcount - 1 processonename v- J end "while” выражение ”do” "begin” оператор-присваивания оператор-процедуры "end” ’’while” булево-выражение ”do” ”begin” простой-оператор простой-оператор ’’end” ”while” булево-выражение ”do” "begin” оператор”;”оператор”end”y ”while” булево-выражение ”do” составной-оператор ’’while” булево-выражение ”do” структурный-оператор ’’while” булево-выражение ”do” оператор V-------------------------------______________________________ J while-оператор структурный-оператор оператор Рис.6.12. Синтаксический анализ оператора цикла ”пока” точки с запятой должны ставиться только между отдельными операторами. По- явление точки с запятой непосредственно перед лексемой ’’end” выглядит ошиб- кой. Однако анализ в обоих случаях приводит к положительным результатам! Все дело в том, что разработчики языка предусмотрительно позаботились о до- пустимости оператора, который ничего не содержит и называется пустым опера- тором. Вы могли уже заметить его на рис. 6.10, где он определен как один из видов простого оператора. Таким образом, лишняя точка с запятой в составном операторе рассматривается как включение в него пустого оператора. Поэтому вы можете по своему желанию ставить лишнюю точку с. запятой или не ставить ее! В нашем последнем примере (рис. 6.14) представлены две схемы анализа одного и того же фрагмента. Они вскрывают гораздо более важную проблему в синтаксическом анализе Паскаля, состоящую в потенциальной неоднозначности
112 Гл. 6. Процессы, процессоры и программы некоторых условных операторов. Проблему иллюстрируют два разных способа размещения анализируемого фрагмента’: if cost < 200 if cost < 200 then if cost < 100 then if cost < 100 then low :e low + 1 then low := low + 1 else high := high + 1 else high := high + 1 Левый вариант соответствует верхней части рисунка: там ’’иначе” соотнесено со вторым ”то”. Правый же вариант соответствует нижней части рисунка. В этом случае ’’иначе” соотносится с первым ”то”, и, как показывает анализ, фрагмент опять оказывается синтаксически правильным! а) begin jermj- "begin” оператор-присваивания оператор-присваивания b term end оператор-присваивания "end” а b ”begin” простой-оператор простой-оператор простой-оператор "end” составной-оператор структурный-оператор оператор б) begin jerm a J "begin" оператор-присваив. ’’begin” простой-оператор ”begin” оператор а b ; оператор-присваив. ";" простой-оператор ”;” оператор b term ; оператор-присваив. простой-оператор оператор end пустой-оператор "end” npocTOft-onepaTOp”end” оператор "end” составной-оператор структурный-оператор оператор Рис. 6.13. Анализ двух правильных составных операторов Различия были бы несущественны, если бы обе интерпретации имели один и тот же смысл. Однако это не так. Оба варианта совпадают, только когда зна- чение cost меньше 100, значит, условный оператор неоднозначен. Но очевидно, что неоднозначность в языке программирования недопустима. Когда програм- мист записывает оператор, он должен точно знать, какой смысл придаст этому оператору процессор. ’’Проблема” возникает из-за того, что в нижней схеме анализа нарушено одно из правил, сформулированных при анализе предложений из Показушки 1 В переводе с Паскаля на русский: ”если стоимость ниже 200 то если она ниже 100 то добавить в мелкие расходы иначе в крупные”. В этой фразе два ”то” и лишь одно ’’иначе”, что и приводит к видимой неоднозначности. - Примеч. ред.
Гл. 6. Процессы, процессоры и программы 113 (с. 91-92), гласящее: последовательность, которая замещается на каждом из этапов, должна быть максимально длинной последовательностью, принадлежа- щей данному синтаксическому классу. Таким образом, обнаружив фрагмент ”if” булевское-выражение ’’then” мы вслед за этим ищем оператор, причем нужно выбрать самую длинную после- довательность лексем, образующую этот оператор. Поэтому верна верхняя схема на рис. 6.14, а нижняя неверна, и условный оператор в конечном итоге одно- значен! а) if cost < 200 then if cost <100 then low low+1 else high high+1 / v----v---' 4----v----7 V----------*-----* ”if” булево-выражение "then” ”if” булево-выражение "then” оператор ”else” оператор ”if” булево-выражение "then” if-оператор ”if” булево-выражение ’’then” структурный-оператор ”if” булево-выражение ”then” оператор if-оператор структурный-оператор оператор б) if cost<200 then if cost<100 then V - _ > low:-low+l else high:~high-4 ”if” булево-выражение ’’then” ”if” ' V* булево-выражение ’’then” V"' оператор j ”else” оператор ”if” булево-выражение ’’then” if-оператор ”else” оператор ”if” булево-выражение ”then” структурный-оператор ”else” оператор ”if” булево-выражение ”then” оператор ”else” оператор if-оператор структурный-оператор оператор Рис.6.14. Правильный и ошибочный анализ одного и того же условного оператора Но проблема все-таки остается. Хотя у процессора и не будет ’’сомнений” относительно смысла оператора, запутаться может программист. Хороший прак- тический метод, позволяющий избегать ошибок, заключается в том, чтобы записывать сложный условный оператор, располагая лексему else непосредст-
114 Гл. 6. Процессы, процессоры и программы венно под последней свободной лексемой then. Левый способ записи следует этому правилу, тогда как правый вводит в заблуждение. Если требуется по-другому сопоставить ”то” и ’’иначе” в условном опера- торе, часть условного оператора можно превратить в составной. Так, в следую- щем примере: if cost < 200 then begin if cost < 100 then low := low + 1 end else high := high + 1 else соответствует первому then, потому что фрагмент begin if cost < 100 then low:= low + 1 end является составным оператором. 6.5. Структура программы Последний вопрос, обсуждаемый в этой главе, это структура подпрограмм и программы в целом и следствия, вытекающие из определения этой структуры. Описание синтаксиса программ, подпрограмм и блоков представлено на рис. 6.15. Хотя мы пока не сталкивались с функциями и еще некоторое время будем обходиться без них, сейчас нам представляется удачная возможность убедиться в существовании следующих тесных взаимосвязей между программами, процеду- рами и функциями: а) программа состоит из заголовка программы и блока программы; б) описание процедуры состоит из заголовка процедуры и блока процедуры; в) описание функции состоит из заголовка функции и блока функции; г) и блок программы, и блок процедуры, и блок функции синтаксически соответствуют блоку. Таким образом, структура, именуемая блоком, играет центральную роль в пост- роении программ на Паскале. По этой причине Паскаль называют языком с блочной структурой. Нас будут интересовать сейчас два аспекта использования блоков: а) способ определения идентификаторов внутри блока; б) область программы, в которой действует каждый из определенных иден- тификаторов. Сюда входит и вопрос о том, какие константы, переменные и подпрограммы до- ступны из различных частей программы. 6.5.1. Определение идентификатора и область его действия Работа компилятора Паскаля при трансляции исходной программы в объ- ектную напоминает чтение фрагмента текста человеком. Человек понимает текст, если знает смысл всех встречающихся слов. Новые слова и специальные технические термины должны объясняться в самом тексте. В этой книге такие объяснения встречаются повсеместно. Например, в начале вы прочитали, что ’’процессор - это человек или машина, выполняющие вычислительный процесс”. Такого рабочего определения вполне достаточно для понимания технического термина ’’процесс”, и с тех пор он многократно использовался именно в этом смысле. Конечно же, человек знает обычно значения очень многих слов, и по-
Гл. 6. Процессы, процессоры и программы 115 этому определять приходится лишь немногие новые слова или новые значения слов, которые уже известны. программа ------"program”----идентификатор ” (”—-г— идентификатор 99 П блок---------- описание-процедуры procedure”— идентификатор список-формальных-параметров блок описание-функции -’’function идентификатор список-формальных-параметров ---имя-типа— ---блок блок Рис.6.15а. Программы, подпрограммы и блоки
116 Гл. 6. Процессы, процессоры и программы программа - заголовок-программы блок-программы . заголовок-программы - "program” идентификатор [ ”(” параметры-программы ”)" ] . параметры-программы блок-программы описание-процедуры заголовок-процедуры - идентификатор { идентификатор } . - блок . - заголовок-процедуры блок-процедуры . - "procedure” идентификатор [ список-формальных-параметров ] . блок-процедуры описание-функции заголовок-функции - блок . - заголовок-функции блок-функции . - "function” идентификатор [список-формальных-параметров] тип-результата . тип-результата - простое-имя-типа. простое-имя-типа блок-функции блок - имя-типа. - блок . - раздел-определения-констант раздел-определения-типов раздел-описания-переменных раздел-описания-процедур-и-функций раздел-операторов . раздел-определения-констант - [ "const” определение-константы { определение-константы ”;” } ] . определение-константы - идентификатор константа . константа - [знак] ( число-без-знака | имя-константы ) | литерал . раздел-определения-типов определение-типа ” ["type” определение-типа { определение-типа }] . - идентификатор (имя-типа | новый-тип). имя-типа - идентификатор . раздел-описания-переменных - [ ”var” описание-переменной { описание-переменной } ]. описание-переменной - идентификатор { идентификатор } ( имя-типа | новый-тип ) . раздел-описания- процедур-и-функций - { ( описание-процедуры | описание-функции ) } . раздел-операторов - составной-операпюр . Рис.6 Л 56. Программы, подпрограммы и блоки В программе на Паскале есть много слов и лексем, известных процессору Паскаля заранее. К ним относятся, например, служебные слова, значение которых постоянно, и предопределенные идентификаторы, тоже имеющие смысл. Но процессор Паскаля не может понять тех слов, которые включены в програм- му по решению программиста, т.е. определяемых программистом идентифика- торов. Давайте же выясним, где именно в программе можно определить иденти- фикатор и какой смысл он имеет впоследствии. Место, где программист вводит новый идентификатор, называется точкой определения. В левой части диаграммы на рис. 6.15а синтаксический класс идентифика- тор встречается шесть раз. Соответственно на рис. 6.156 он входит в определе- ния синтаксических классов:
Гл. 6. Процессы, процессоры и программы 117 заголовок-программы, определение-константы, заголовок-процедуры, определение-типа, заголовок-функции, описание-переменной. Эти элементы в описании синтаксиса соответствуют некоторым из точек опре- деления в Паскаль-программе, а именно тем, где определяются идентификаторы следующих видов: имя программы, имя константы, имя процедуры, имя типа, имя функции, имя переменной. До сих пор мы не сталкивались с именами функций и типов, определяемых про- граммистом, но в дальнейшем мы с ними познакомимся и начнем их применять. Заголовок программы записывается только в ее начале, а имя программы никогда не используется внутри нее, поэтому о нем можно забыть. Другие виды идентификаторов могут встречаться внутри блока. Так, например, в левой части рис. 6.16 показан блок, в котором определяются четыре идентификатора: Р, Q, R и 5: Р - имя константы, Q - имя переменной, R и 5 - имена процедур. О каждом из них можно сказать, что он принадлежит тому блоку, где определен. const Р- ... ; var Q: ; procedure Я; procedure 5; begin end Обозначения: * - точка определения идентификатора; : - область, в которой идентификатор не имеет смысла; | - область, в которой идентификатор имеет смысл и может употребляться. Рис.6.16. Область действия определений в блоке Мы уже знаем, как определяется идентификатор, т.е. как он приобретает смысл. Но нужно знать также, в какой части программы этот смысл сохраня- ется. В терминологии стандартного Паскаля ответ на этот вопрос дается с помо- щью понятия области действия определения. Правда, идентификатор имеет смысл лишь в части соответствующей области действия. Здесь соблюдаются следующие правила.
118 Гл. 6. Процессы, процессоры и программы Правило 1 Для идентификатора, определенного в блоке Б1, областью действия определения являются блок Б1 и все входящие в него блоки, за исключением тех, где тот же идентификатор переопределяется. Правило 2 Идентификатор не имеет смысла вне своей области дей- ствия, а также между началом области действия и точкой определения. Правило 3 Идентификатор имеет смысл в любой точке своей области действия, расположенной за точкой его определения. Употребление идентификатора там, где он неизвестен процессору, явля- ется ошибкой, так что эти правила эффективно определяют область возможного использования любого идентификатора. В данном контексте предопределенные идентификаторы (с. 98) можно считать определенными в воображаемом блоке, который содержит в себе всю программу. Правая часть рис. 6.16 построена по указанным правилам. Каждому из идентификаторов, Р, Q, R и S, соответствует отрезок, ограниченный по краям штрихами. На каждом из отрезков точка определения идентификатора обозначе- на звездочкой. Часть отрезка, расположенная выше звездочки, соответствует об- ласти, где идентификатор не имеет смысла. Та же часть отрезка, которая нахо- дится ниже звездочки, соответствует области, где идентификатор имеет смысл и где его можно употреблять. Рис. 6.16 выполнен в предположении, что ни один идентификатор не пере- определяется ни в процедуре R, ни в процедуре 5, и, таким образом, исключе- ния, упомянутые в правиле 1, не имеют силы. Если же, например, идентифика- тор Р заново определяется в процедуре S, скажем, как переменная, значение Р в разных частях программы будет различным. В процедуре Айв головной про- грамме Р будет константой, а в процедуре 5 - переменной. Это не помешает процессору, но программист запутается почти наверняка! Поэтому употреблять идентификатор в разном смысле так же, как и переопределять предопределен- ный идентификатор, о чем говорилось раньше, крайне неразумно. В некоторых языках программирования не требуется определять каждый идентификатор до его первого использования. В частности, не обязательно явно описывать переменные. Вместо этого первое появление переменной, допустим, в операторе присваивания рассматривается как ее неявное определение. Для начи- нающего программиста такой путь проще, но в то же время и опаснее, так как если он неверно записывает ранее встречавшийся идентификатор, в программе появляется новая, непредусмотренная переменная. Паскаль требует, чтобы про- граммист определял каждый идентификатор до того, как им воспользоваться. Это добавляет нам забот, но зато помогает компилятору выявлять некоторые наши ошибки. Так что идя на дополнительные хлопоты, мы кое-что получаем взамен. 6.5.2. Доступ к константам, переменным и подпрограммам Область программы, в которой осмыслен идентификатор, показывает так- же, доступен или нет обозначаемый им объект на разных этапах выполнения программы. Вопрос о доступности переменных и констант мы разбирали в гл.4, выясняя в точности, что происходит при выполнении программ СписокИмен2 (с. 74) и СписокИменЗ (с. 77). Теперь мы можем связать эти знания с прави- лами области действия.
Гл. 6. Процессы» процессоры и программы 119 Рассмотрим сначала рис. 6.17, где показаны каркас программы СписокИмен2, точки определения и области действия всех введенных идентифи- каторов и область действия предопределенных идентификаторов. Одного взгляда на такую диаграмму достаточно, чтобы определить, какие константы, перемен- ные и подпрограммы доступны в любой из точек программы. Если такая точка проецируется на вертикальную линию ниже точки определения некоторого объ- екта, то этот объект доступен. Для констант и переменных это означает возмож- ность их употребления, для подпрограмм - возможность вызова. Таким образом, рис. 6.17 подтверждает в отношении программы СписокИменЗ следующее: а) в теле головной программы доступны все предопределенные идентифи- каторы и значения numberofnames, count и copyonename; б) в теле процедуры copyonename остается доступным все, что было досту- значения пно за ее пределами, и, кроме того, становятся доступными space, terminator и ch. program ListNamcs2...; const numberofnames - 2; var count integer» procedure copyonename; const space - * terminator - 7’; var ch: char; begin c h end { copyonename }; begin end { ListNames } Рис. 6.17. Области действия идентификаторов в программе СписокИмен2 Аналогичная схема для программы СписокИменЗ представлена на рис. 6.18. Согласно этой схеме: а) в теле головной программы доступны только предопределенные иденти- фикаторы и две процедуры; б) в теле процедуры processallnames остается доступным все то, что было доступно в головной программе, и, кроме того, становятся доступными значения numberofnames и count; в) в теле процедуры copyonename остается доступным все то, что было до- ступно в головной программе, кроме процедуры processallnames, но ста- новятся доступными значения space, terminator и ch. Тот факт, что процедура processallnames недоступна в copyonename, не имеет большого значения, так как нет никакой необходимости оттуда ее вызывать. И тем не менее это иллюстрация крайне важного момента, а именно: в программе на стандартном Паскале возможность вызова одних подпрограмм из других определяется порядком их описания. Если бы в программе СписокИменЗ про- цедура processallnames была описана до copyonename, copyonename нельзя было вызвать из processallnames, и это было бы уже достаточно важно. При написа-
120 Гл. 6. Процессы, процессоры и программы нии на Паскале программ с подпрограммами порядку грамм нужно уделять особенно пристальное внимание. ♦п расположения подпро- program ListNames3...', procedure соруопепате', const space - ’ terminator var ch: char, begin P e : c : p Д *0 :r о p :o п у :c p о :e e n :s end { соруопепате }; procedure processallnames', const numberofnames - 2; var count: integer, begin Д e :s e n :a л a :1 e m : I н e ; n end { processallnames }; begin end { ListNames } и д e н т и Ф и к a : s : t : с ♦p :e : h a *r c m e i ♦ n a t .. . 1° .. r :n :c *u :o m : u b *n e t r о J n a m e s Рис. 6.18. Области действия идентификаторов в программе СписокИменЗ В этой главе была поставлена цель привлечь ваше внимание к различным аспектам языка Паскаль и тем самым помочь вам избегать синтаксических и се- мантических ошибок. Это не самые серьезные типы ошибок в программе, так как большинство из них будет выявлено процессором на этапе компиляции. Не- приятно, однако, тратить время на повторную компиляцию ради устранения пу- стяковых ошибок. Вам скоро станет ясно, к какого рода ошибкам вы наиболее склонны, и лучше тратить время на то, чтобы учиться избегать именно их. Теперь, научившись читать программы, мы знаем о языке достаточно для того, чтобы начать программы писать. Этим мы будем заниматься на протяже- нии оставшейся части книги! Упражнения 6.1. Может ли каждое из следующих чисел быть введено в переменную типа integer в программе на стандартном Паскале, и если нет, то почему? а) 747 б) -1906 в) 326,117 г) 17 219 6.2. Допустимы ли следующие идентификаторы в программе на стандартном Паскале? Для тех, ко- торые недопустимы, ответьте - почему? a) findnewtime б) case в) Process-One-Person г) TestValue д) 2overX е) grand total 6.3. Пусть width - имя константы и count, ch, sidel и side2 - имена переменных. С помощью подроб- ного синтаксического анализа покажите, что следующие выражения синтаксически правильны в стандартном Паскале:
Гл. 6. Процессы, процессоры и программы 121 a) count - 1 г) sidel + side2 + width б) count mod width д) sidel + side2 * width в) ch о ’a’ e) {sidel + side2) * width 6.4. Вычислите значения следующих выражений, если а, b и с равны соответственно 4, 6 и 7: а) а * b - 30 div с в) a* b mod 3-0 6) a + 4*b-2*c div 5 r)a + b*c-a*b + c 6.5. х, у и z - имена переменных. Не анализируя выражений, покажите с помощью синтаксического анализа, что следующие фрагменты являются операторами стандартного Паскаля: a) while х > 0 do begin х х - Г, у :- у * z; end б) while х <> у do if х > у then х х-у else у:- у-х 6.6. Рассмотрите программу РезулыпатыОпроса в упр. 4.2 (с. 82). а) Сколько раз лексемы перечисленных ниже видов встречаются в строках 25-44 этой про- граммы? 1) специальные символы (не считая служебных слов); 2) служебные слова; 3) идентификаторы; 4) числа; 5) литералы. б) Перечислите все предопределенные идентификаторы, использованные в программе. в) Перечислите все идентификаторы, определенные программистом, которые использованы в программе, и укажите номера строк, содержащих их точки определения. г) Какие строки образуют область действия каждого из следующих идентификаторов, и в какой части этой области может использоваться каждый из них? 1) length 3) processeveryline 2) interview 4) terminator 6.7. а) Полагая, что area, fO, flvif2- имена переменных и width - имя константы, покажите с помо- щью синтаксического анализа, что фрагмент fO + 4 * fl + f2 принадлежит к синтаксическому классу выражение в стандартном Паскале. Продолжая син- таксический анализ, покажите, что фрагмент area width ♦ (JO + 4 * fl + f2) div 3 принадлежит к синтаксическому классу оператор в стандартном Паскале. б) Предположим, что из стандартного Паскаля исключены определения множителя, терма, простого-выражения и выражения, и вместо них введены классы: операнд - целое-без-знака | переменная | имя-константы . операция - ”+” | | | ”div” . выражение - операнд операция операнд . Представьте оператор из части (а) как последовательность операторов модифицированного языка.
7 ПОСТРОЕНИЕ ПРОГРАММ Прежде чем переходить к главному предмету этой главы, построению про- грамм, давайте рассмотрим еще раз соотношение между программами и процес- сорами. 7.1. Программы и процессоры В главе 1 мы уже рассматривали схему, представленную на рис. 7.1. Она отражает тот факт, что процессор получает набор команд (программу) и инфор- мацию, которую нужно обработать (входные данные), и выполняет программу, т.е. обрабатывает входные данные, порождая результаты. Это совершенно точное описание происходящего. Процессор является активной частью системы, тогда как программа и данные играют пассивную роль. программа > процессор входные данные — > > результаты Рис. 7.1. Общая схема обработки информации (фактически) Однако программисты предпочитают говорить о программе как об актив- ной части системы, полностью игнорируя процессор. Этот способ мышления со- ответствует схеме на рис. 7.2. Если бы нас попросили объяснить действие программы, например, программы СписокИмен (с. 53), мы могли бы сказать: Программа СписокИмен заставляет процессор ввести два имени и вы- вести их отдельными строками с добавлением пустой строки вслед за каждым из них. входные данные программа > результаты Рис. 7.2. Общая схема обработки информации (как представляется программисту) Но большинство опытных программистов выразилось бы проще: Программа СписокИмен вводит два имени и выводит их отдельными строками с добавлением пустой строки вслед за каждым из них.
Гл. 7. Построение программ 123 Наделять программу способностью обрабатывать данные так же ошибочно, как полагать, что кулинарный рецепт может печь пирожные. Но это крайне распро- страненная практика, и похоже, что она никогда не приводит к путанице или, во всяком случае, приводит к ней очень редко. Поэтому и мы будем придержи- ваться ее в оставшейся части книги. 7.2. Поэтапное уточнение Для того чтобы начать писать программу, программисту нужно знать: а) какую информацию программа будет получать в качестве входных данных; б) какую информацию программа должна вырабатывать в качестве результатов. Эти сведения составляют спецификацию программы и полностью определяют, что она должна делать. Обычно программисты не пишут спецификаций прог- рамм, но прежде, чем начинать программировать, нужно быть уверенным, что относительно спецификации достигнута полная договоренность. Если в основу программного проекта положена плохая спецификация, которую впоследствии приходится изменять, это приводит к множеству дополнительных трудностей. Таблица 7.1. Спецификация программы СловоВРамке Напишите программу на Паскале под названием WordlnBox (СловоВРамке), которая читала бы восьмибуквенное слово и выдавала бы его, поместив в рамку из звездочек. Входные данные Одно восьмибуквенное слово, представленное первыми восемью символами входного файла. Результат Пять строк, первые двенадцать позиций каждой из которых заняты символами следующим образом: ХХХХХХХХ где хххххххх - восемь символов, введенных в качестве входных данных. Программисту нужно знать также, на каком языке должна быть написана программа. В этом руководстве все программы будут написаны на стандартном Паскале, но методы построения, которые мы будем использовать, применимы к любому языку последовательного программирования. При параллельном про- граммировании нужны и другие средства, но их описание в книгу не вошло. Разница между спецификацией программы и самой программой не так ве- лика, как может показаться на первый взгляд. В определенном смысле специфи- кация, без сомнения, является программой - программой, понятной программи- сту. Его работа состоит в том, чтобы преобразовать спецификацию в программу, которую сможет понять и выполнить конкретный процессор. Рассмотрим простую программистскую задачу, сформулированную в табл. 7.1, с тем чтобы проиллюстрировать эти соображения. Спецификацию можно сделать более похожей на программу, переписав ее так:
124 Гл. 7. Построение программ begin ввести восьмибуквенное слово из входного файла и вывести пять строк, первые двенадцать позиций каждой из которых заняты символами следующим образом: * хххххххх * * * ************ где хххххххх - восемь символов, введенных в качестве входных данных end . В каком смысле это программа? В том смысле, что эту программу вы, про- граммист, можете понять и выполнить. Взяв восьмибуквенное слово в качестве входных данных, вы можете породить необходимые результаты. Будущая про- грамма по ее завершении должна быть полностью эквивалентна данной, но выражена она будет в терминах операций, выполнимых реальным процессором, и на языке, понятном ему. Преобразование спецификации в программу в принципе сводится к более подробному перечислению необходимых вычислений. Метод создания программ, который мы начинаем обсуждать, и состоит в такой постепенной детализации, на каждом этапе которой более развернутый вид приобретает определенная часть программы. Мы будем называть такой этап уточнением, а сам метод извес- тен как поэтапное (пошаговое) уточнение, или нисходящее проектирование. Оставшаяся часть этой главы будет посвящена созданию нескольких простых программ путем поэтапного уточнения. 7.3. Программы без процедур В дальнейшем мы будем поощрять вас широко пользоваться процедурами, но начнем мы все-таки с двух коротких программ, в которых процедур нет. 7.3.1. Простая программа обработки текста Наш первый пример заключается в том, чтобы полностью построить про- грамму СловоВРамке, специфицированную в табл. 7.1. У нас уже есть началь- ный вариант программы, состоящий из одного предложения и основанный непосредственно на спецификации, а на первом шаге уточнения программу можно представить в виде begin выдать символы ’************’ и перейти к новой строке; выдать символы ’* *’ и перейти к новой строке; ввести первые восемь символов из входного файла и выдать их в таком виде, чтобы им предшествовали символы ’* ’ и за ними следовали символы ’ а затем перейти к новой строке; выдать символы ’* *’ и перейти к новой строке; выдать символы ’************’ и перейти к новой строке; end . Структура этого варианта программы повторяет структуру результатов. Резуль- татами являются пять последовательно выводимых строк, поэтому и программа выражена в виде пяти последовательных предложений. Это простой пример реа- лизации принципа, который широко применяется при создании программ. Он
Гл. 7. Построение программ 125 формулируется так: часто можно получить хорошую структуру программы или ее части, беря за образец структуру обрабатываемых данных. Мы рассма- триваем программу, структура которой основана на результатах, но при других обстоятельствах она может базироваться на входных данных или данных, возни- кающих в ходе вычислений. Эта схема программы учитывает то, что входные данные используются при выводе третьей строки, и поэтому команда ввода сим- волов включена в третье предложение. Теперь мы продолжим работу с предложениями, выбирая один из двух путей: а) те предложения, которые можно выразить единственным оператором Паскаля, мы пока оставим без изменений; б) те предложения, которые выразить одним оператором Паскаля нельзя, мы подвергнем дальнейшему уточнению. Сейчас в уточнении нуждается только третье предложение; этот шаг напо- минает построение еще одной, более простой программы. При уточнении можно опереться на то, что выводимая строка отчетливо делится на три части: begin выдача ’* ’; пересылка восьми символов; выдача ’ ♦’ и переход к новой строке end причем символы пересылаются из входного файла в выходной. Далее, подставив этот фрагмент в исходную версию и заменив максимально возможное количест- во предложений операторами Паскаля, получаем begin writein (’*******♦****’); writein С* *’); write (’* ’); пересылка восьми символов; writein (’ *’); writein (’* *’); writein (’*********♦**’); end . Теперь это немного больше напоминает законченную программу. Отме- тим, что при подстановке были удалены лексемы begin и end, обрамлявшие три уточняющих оператора. Если бы мы оставили их, это не было бы ошибкой, но они не обязательны, поэтому мы просто объединили две последовательности, образовав более длинный составной оператор. Здесь так же, как и во всех других программах, выстраиваемых в данном руководстве, фрагменты, которые нуждаются в дальнейшем уточнении , напеча- таны обычным шрифтом. Это поможет вам отличать их от тех частей, которые выражены уже на Паскале. В настоящий момент развернуть требуется только четвертое предложение: пересылка восьми символов В ходе разработки мы до сих пор выражали каждое предложение в виде последо- вательности более подробных предложений. Можно было бы и теперь поступить так же, записав восемь предложений, каждое из которых имело бы дело только с одним символом. Но мы не ограничены одними лишь последовательностями, да нам и не удалось бы далеко продвинуться в создании большинства программ, если бы такое ограничение имело место. В нашем распоряжении три основопола-
126 Гл. 7. Построение программ гающих средства структурирования, рассмотренные в гл. 2: последовательная организация, выбор и повторение. Они будут обозначаться в разрабатываемых программах с помощью слов, отражающих их семантику1. В данном случае, очевидно, требуется повторение. Вместо восьми предло- жений можно написать пока переслано меньше восьми символов выполнять переслать один символ что принимает непосредственно форму оператора цикла ’’пока” после замены выделенных слов служебными словами Паскаля2: while переслано меньше восьми символов do переслать один символ Оператором цикла управляет булевское выражение, записанное обычными словами, однако сразу выразить его на Паскале затруднительно. Мы вынуждены прибегнуть к уточнению другого рода. Для получения ин- формации, необходимой в основном вычислении, часто бывает нужно организо- вать вычисления вспомогательные. В данном случае нужно найти способ опре- делить, были скопированы восемь символов или нет. Решение состоит в том, чтобы их сосчитать. Есть два метода организации счета. Первый: начать подсчет с нуля, прибавлять по единице каждый раз, когда насту- пает интересующее нас событие, и прекратить процесс повторения, ко- гда достигнуто требуемое значение. При использовании этого метода наше предложение приобретает вид установить счетчик на ноль; пока значение счетчика не достигло восьми выполнять begin добавить к счетчику единицу; переслать один символ end Второй метод таков: начать подсчет с требуемого значения, вычитать по единице каждый раз, когда наступает интересующее нас событие, и прекратить процесс повторения, когда мы досчитаем до нуля. В этом случае уточнение будет выглядеть так: установить счетчик на восемь; пока значение счетчика не достигло нуля выполнять begin уменьшить значение счетчика на единицу; переслать один символ end Снова мы столкнулись с типичной ситуацией, возникающей при разработ- ке программ. Если есть несколько способов уточнения, программист должен выбрать наилучший. К сожалению, ситуация не всегда, как мы убедимся в дальнейшем, позволяет принять правильное решение, но в данном случае выбор не подлежит сомнению. Хотя ни один из вариантов не имеет значительных пре- 1 На английском это непосредственно служебные слова языка. - Примеч. ред. 2 В дальнейшем эта довольно очевидная замена будет производиться без специальных оговорок. - Примеч. ред.
Гл. 7. Построение программ 127 имуществ, компьютеру намного проще сравнивать значение счетчика с нулем, нежели с восемью, и поэтому мы выбираем второй метод. Введя вспомогательное вычисление, мы тем самым обязали себя запоми- нать значение счетчика. Всякий раз, когда требуется что-либо запоминать в ходе вычислений, для хранения значения необходимо определять переменную. Давайте введем такую переменную и назовем ее count (’’счетчик”), а так как значение счетчика - целое число, переменная получит тип integer. Если это сделано, то предложение пересылка восьми символов можно развернуть таким образом: count := 8; while count <> 0 do begin count := count - 1; переслать один символ end Остается раскрыть лишь предложение переслать один символ Нетрудно выразить его так: begin ввод одного символа; вывод одного символа end или, на Паскале: begin read (ch); write (ch) end В окончательном варианте появилась новая переменная типа char, так как для того, чтобы выдать прочитанный символ, процессор должен сначала его запо- мнить. Теперь уже все части программы записаны на Паскале, и остается лишь собрать их вместе, добавив: а) заголовок программы; б) комментарии, кратко описывающие назначение программы; в) раздел описания переменных, в котором будут описаны нужные нам переменные. В результате мы получаем законченную программу СловоВРамке (лис- тинг 7.1). Мы преобразовали спецификацию программы, понятную программи- сту, в программу, которую может понять процессор. Более сжатая запись разработки программы представлена в табл. 7.2. При ее составлении были приняты следующие правила и обозначения: а) каждый раздел таблицы соответствует одному из этапов уточнения; б) символ -> означает ’’превращается в”; в) если какое-либо предложение или выражение можно сразу записать на Паскале, оно так и записывается, без промежуточной формулировки;
128 Гл. 7. Построение программ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 program WordlnBox (input,output); { читает восемь символов и вписывает их в рамку, образованную звездочками } var count: integer; ch: character, begin writein (’♦***********»); writein (’* *’); write (’* count :-8 while count <> 0 do begin count count - 1; read (ch); write (ch) end; writein (’ *’); writein (’* *’); writein (’************») end { WordlnBox }. Листинг 7.1. Текст программы СловоВРамке г) предложения и выражения, если они записываются обычными словами, формулируются кратко и не повторяют того, что читателю известно из спецификации или из предыдущих этапов разработки; д) если вводятся новые переменные (или константы), они перечисляются в графе ’’Примечания”. Такого рода таблицы мы будем называть таблицами разработки программ. Мы долго бились над программой, и в конце концов все-таки закончили ее. Увы, не всегда нам будет так везти! Используя метод поэтапного уточне- ния, программист всегда должен быть готов к тому, что придется частично или полностью отказаться от сделанного, если выяснится, что программу построить не удается. Если бы вы самостоятельно взялись за эту задачу, не имея еще никакого опыта программирования, то вполне могли бы спроектировать ее так: begin нарисовать рамку; прочитать восемь символов и вписать их в рамку end • Это гораздо более естественный способ решения задачи, чем тот, что был предложен выше. Более естественный, уточним, для ”процессора”-человека. Мы можем теперь развернуть предложение нарисовать рамку и получить:
Гл. 7. Построение программ 129 begin writelnC***** **♦****’); writein (’* *’); writelnC* *’); writelnV* *’); writelnC* ***********») end но, дойдя до предложения прочитать восемь символов и вписать их в рамку мы сталкиваемся с проблемой. Как заставить процессор Паскаля вернуться и за- писать несколько дополнительных символов в уже выведенную строку? Сделать этого нельзя. Так что у нас не остается другого выбора, кроме как отказаться от этого решения. Таблица 7.2. Таблица разработки программы СловоВРамке Шаги разработки Примечания слово в рамке -> begin writein (»************’); writein (’* *’); обработка третьей строки; writein (’* *’); writein (’************») end обработка третьей строки -> begin write (’* ’); пересылка восьми символов; writein (’ *’) end пересылка восьми символов -> пока переслано меньше восьми символов выполнять переслать один символ; -> begin count 8; while count <> 0 do begin count := count - 1; переслать один символ end end локальная переменная count : integer переслать один символ -> begin read (ch)-, write (ch) end локальная переменная ch : char 5 Заказ № НК)
130 Гл. 7. Построение программ 7.3.2. Программа численного расчета В качестве второго примера поэтапного уточнения мы рассмотрим про- грамму Прогрессия, специфицированную в табл. 7.3. Это простой численный пример. Во всяком случае он выглядит простым, хотя, как мы выясним в даль- нейшем, он более содержателен, чем может показаться. Таблица 7.3. Спецификация программы Прогрессия Если даны целые числа а и Ь, то последовательность а а + b а + 2b а + ЗЬ ... называется арифметической прогрессией, число а - начальным членом, а число b - разностью прогрессии. Постройте Паскаль-программу под названием Progression (Прогрессия), которая записывала бы арифметическую прогрессию на основании следующих данных: начального члена, разности прогрессии, требующегося количества членов. Например, при входных данных 3 5 6 результаты должны быть следующими: 3 5 13 18 23 28 Программа построена в табл. 7.4, из второго раздела которой мы сразу же можем извлечь урок. В первом разделе процесс записан в виде begin ввод данных; выдача результатов end но второй раздел представляет собой уточнение ’’выдачи результатов”, а не ’’ввода данных”. Причина в том, что часто выгодно бывает вести уточнение с наиболее сложной части программы. Если возникнет проблема, заставляющая Таблица 7.4. Таблица разработки программы Прогрессия Шаги разработки Примечания Прогрессия -> begin ввод входных данных; выдача результатов end выдача результатов -> begin выдать начальный элемент; посчитать и выдать оставшиеся члены end
Гл. 7. Построение программ 131 посчитать и выдать оставшиеся члены -> пока не все члены выданы выполнить посчитать и выдать один член Прогрессия -> begin ввод входных данных; выдать первый член; пока не все члены выданы выполнять посчитать и выдать один член end локальные переменные term, difference, numberofterms : integer Считать число элементов можно по убыванию до нуля. ввод данных -> read (term, difference, numberofterms) выдать первый член -> begin write (term : 8); numberofterms numberofterms - 1 end не все члены выданы -> numberofterms о 0 посчитать и выдать один член -> begin term term + difference', write (term : 8); numberofterms numberofterms - 1 end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 program Progression (input, output)', { выдает арифметической прогрессии по известному начальному члену, разности и требующемуся количеству членов X var term, difference, numberofterms : integer, begin { ввод данных } read (term, difference, numberofterms) ', { выдача первого члена } write (term : 8); numberofterms numberofterms - 1; { вычисление и выдача оставшихся членов } while numberofterms <> 0 do begin term term + difference', write (term : 8); numberofterms numberofterms - 1 end end { Progression } . Листинг 7.2. Текст программы Прогрессия пересмотреть всю работу, то скорее всего она возникнет в более сложной части и, таким образом, будет обнаружена раньше.
132 Гл 7. Построение программ Развернем ’’выдачу результатов”, опираясь на их структуру. Результатами являются: первый член, который уже известен, и все остальные члены, каждый из которых рассчитывается на основе предыдущего. Таким образом, расчет оста- льных членов - это повторяющийся процесс, и программируется он с помощью оператора цикла ’’пока”, как показано в третьем разделе табл. 7.4. В четвертом разделе таблицы результаты предыдущих этапов разработки собираются вместе и в итоге выстраивается пересмотренный и более подробный вариант компоновки всей структуры. В графе ’’Примечания” отражено решение о введении трех целочисленных переменных: term, difference и numberofterms. Переменная term первоначально употребляется для запоминания первого члена, а затем - для запоминания всех следующих членов по мере их вычисле- ния. В переменной difference фиксируется разность прогрессии. Значение numberofterms сначала равно требующемуся числу членов; потом эта переменная используется для счета членов, которые предстоит рассчитать и вывести. В конце таблицы разработки показаны два последних этапа построения программы. Программа, полученная в результате, представлена на листинге 7.2. При входных данных, приведенных в спецификации, она даст правильные резу- льтаты, но всегда ли она будет работать правильно? Это вопрос, который вам предлагается исследовать в упр. 7.2 и к которому мы еще вернемся в гл. 8. 7.4. Программы с процедурами Мы уже убедились в гл. 4 в том, что применение процедур - мощное сред- ство, позволяющее разделить программу на небольшие независимые части, каж- дая из которых выполняет отдельный подпроцесс. Благодаря процедурам струк- туру программы можно сделать более понятной и ясной. Надо согласиться с тем, что большинство программ в этой книге настолько просты, что их не стоит делить на меньшие части, однако этого никак нельзя сказать про большинство прикладных программ. В большой программе может быть несколько десятков или даже сотен тысяч строк, и построение такой программы из меньших частей - важнейший момент. Поэтому одна из целей этой книги - убедить вас как можно свободнее применять процедуры и функции (о них речь пойдет позже) при написании даже простейших программ. Это по- служит хорошей подготовкой к созданию программ более сложных. Преимущества использования процедур естественно вытекают из двух ме- тодов поэтапного уточнения: а) любое предложение, требующее дальнейшего уточнения, можно заме- нить вызовом процедуры; таким образом, процедура становится способом уточнения программы на очередном этапе; б) любую последовательность операторов, встречающуюся в программе не- сколько раз, можно представить как процедуру и заменить вызовами процедуры все места, где встречается эта последовательность. Для того чтобы проиллюстрировать сейчас первый из этих методов, мы вернемся к программе СловоВРамке, второй метод будет реализован в следующем раз- деле, в программе СведенияОКниге. Вы, вероятно, помните, о чем мы договорились в гл. 4: при написании про- грамм с процедурами мы будем принимать за образец стиль, в котором написана программа СписокИменЗ (с. 77). Главные характеристики этого стиля таковы: а) головная программа очень проста; б) главный вычислительный процесс описывается в процедуре, вызываемой из головной программы, и называется головной процедурой*, в) каждая процедура содержит необходимые ей локальные элементы дан- ных, и все процедуры почти не зависят друг от друга.
Гл. 7. Построение программ 133 Таблица 7.5. Четыре процедуры, построенные на основании табл. 7.2 procedure processoneword', { обработать одно слово } begin writeln (’** ********* *,). writein (’* *’); processthethirdline', writeln С* *’); writeln (’************’) end { processoneword }; procedure processthethirdline\ { обработка третьей строки } begin write (’ * ’); copyeightcharacters; writeln (’ * ’) end { processthethirdline }; procedure copyeightcharacters', { пересылка восьми символов } var count: integer, begin count 8; while count <> 0 do begin count count - 1; copyonecharacter end end { copyeightcharacters }; procedure copyonecharacter, { переслать один символ } var ch: char, begin read (ch)', write (ch) end { copyonecharacter }; Переписать программу СловоВРамке относительно просто. Обратившись к таблице разработки (с. 129), можно представить каждое уточнение в виде проце- дуры. Результат показан в табл. 7.5. Первая процедура названа processoneword, а не wordinbox для того, чтобы не путать ее с головной программой. Имена всех остальных процедур получены путем ’’склеивания” английских слов, описываю- щих соответствующий этап разработки. Можно было выбрать и более короткие названия, включая содержательное описание в виде комментария в процедуру вслед за ее заголовком1. Элементы данных, которые появлялись при том или ином уточнении, становятся локальными в соответствующей процедуре. При данном наборе процедур тело головной программы можно было бы представить в простейшем виде 1 Мы сделали и то и другое: длинные названия сохранены, а их перевод дан в комментариях. Вообще, учитывая англоязычную основу Паскаля, для отечественного программиста хороший комментарий в программе даже более важен. - Примеч. пер.
134 Гл. 7. Построение программ begin processoneword { обработка одного слова } end { WordlnBox}. но мы запишем его немного пространнее: begin writein (’♦**♦ РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ СЛОВО В РАМКЕ ♦♦♦*’); writein', processoneword\ { обработка одного слова } writeln\ writein (’**** конец выдачи результатов ****’) end { WordlnBox}. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 program WordInBox2 (input, output), { читает восьмибуквенное слово и вписывает его в рамку, образованную звездочками } . procedure copyonecharacter, var ch: char; begin read (ch): write (ch) end { copyonecharacter }; procedure copyeightcharacters', var count: integer: begin count 8; while count <> 0 do begin count := count - 1; copyonecharacter { переслать один символ } end end { copyeightcharacters }; procedure processthethirdline: begin write (’* ’); copyeightcharacters: { пересылка восьми символов } writein (’ *’) end { processthethirdline }; procedure processoneword; begin writein (’************»); writein (’* *’); processthethirdline: { обработка третьей строки } writein (’* *’); writein (’************’> end { processoneword }; begin writein (’*♦** РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ СЛОВО В РАМКЕ ♦***’); write In: processoneword: { обработка одного слова } writein; writein (’♦*** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ****’) end { WordlnBox }. Листинг 7.3. Текст программы СловоВРамке2
Гл. 7. Построение программ 135 Такая форма головной программы будет использоваться и в дальнейшем. Ее пре- имущество состоит в том, что результаты всегда размещаются между двумя со- общениями, первое из которых информирует пользователя о программе, которая выдает эти результаты, а второе подтверждает, что выдача закончена. Остается решить, в каком порядке нужно описать головную процедуру и процедуры processthethirdline, copyeightcharacters и copyonecharacter. Сделать это можно, записав схему вызовов: СловоВРамке processoneword processthethirdline copyeightcharacters copyonecharacter Нужно позаботиться о том, чтобы процедура вызывалась только после того, как описана. Это достигается при единственном порядке описания: copyonecharacter, copyeightcharacters, processthethirdline, processoneword. Именно в этом порядке процедуры размещены в окончательной программе СловоВРамке2 (листинг 7.3). Он прямо противоположен порядку создания про- цедур, причем так бывает довольно часто. Большинство программ на Паскале в какой-то мере выстроены задом наперед, но вы скоро привыкнете читать их снизу вверх. И последнее. Заключительный вариант программы СловоВРамке и ее пре- дыдущий вариант (см. с. 128) представляют собой две крайности в употреблении процедур: прибегать к ним при каждой возможности или не использовать вооб- ще. Как и в жизни, лучше всего обычно держаться золотой середины. В упр. 7.1 вам предлагается создать третий вариант программы, содержащий ровно две процедуры. 7.5. Альтернативы при разработке Мы уже столкнулись с тем, что программисту порой приходится рассмат- ривать несколько вариантов построения программы и выбирать из них наилуч- ший. В этом разделе мы займемся задачей, в решении которой можно придержи- ваться двух совершенно различных структур программы, и обсудим последствия, к которым приводит неверный выбор. Задача состоит в построении программы СведенияОКниге, специфициро- ванной в табл. 7.6. В спецификации точно описано представление входных данных и располо- жение результатов, так что об этом заботиться нам не нужно. Первым делом мы должны очертить общую структуру главной процедуры. Структура входных данных в своей основе такова: Каждый прямоугольник здесь соответствует одному полю, а их последова- тельность заканчивается дробной чертой. Будем называть этот ограничитель ’’маркером данных”. Структура самих полей на этом этапе несущественна. Стрелка под первым прямоугольником изображает текущую позицию входного
136 Гл. 7. Построение программ файла. Как вы помните, первый вводимый символ хранится в предопределенной буферной переменной input". Таблица 7.6. Спецификация программы СведенияОКниге Напишите на Паскале программу BookDetails (СведенияОКниге), которая читает библиографические сведения о книге, представленные в упакованном виде, и выводит их в удобной для человека форме, снабжая соответствукуцими пояснениями. Входные данные Все сведения о книге содержатся во входном файле, представлены последовательностями символов и отделяются друг от друга ограничителем Одна книга описывается тремя такими полями или более. За ними следует дополнительный символ Между символом ”/”, завершающим поле, и началом другого поля могут быть пробелы. В первых трех полях всегда содержится название книги, имя автора, издательство. Ни одно из полей не содержит более 50 символов. Пример типичного набора данных: УЧИТЕСЬ ПРОГРАММИРОВАТЬ/ДЖОНСТОН/ФИНАНСЫ И СТАТИСТИКА/ ПЕРЕВОД С АНГЛИЙСКОГО/МОСКВА, 1989 ГОД// Результаты Нужный формат иллюстрируется следующим примером, который соответствует приведенным входным данным: ЗАГЛАВИЕ АВТОР(Ы) ИЗДАТЕЛЬСТВО ДРУГИЕ СВЕДЕНИЯ УЧИТЕСЬ ПРОГРАММИРОВАТЬ ДЖОНСТОН ФИНАНСЫ И СТАТИСТИКА ПЕРЕВОД С АНГЛИЙСКОГО МОСКВА, 1989 Если во входных данных ровно три поля, строка ’’другие сведения” выводиться не должна Таким образом, головная процедура, в основу которой положена структура входных данных, будет выглядеть так: begin пока input" не есть маркер данных выполнять обработка одного поля; чтение маркера данных end причем состояние входного файла по выходе из оператора ’’пока” будет сле- дующим: ' I т. е. останется прочитать лишь маркер данных. Он не влияет на результаты, и мы могли бы не читать его. Но лучше все-таки это сделать: тогда будет прочитан весь входной файл, как и положено во всякой хорошей программе. Пока все идет прекрасно! Теперь требуется развернуть предложение обработка одного поля и тут-то у нас возникают трудности, потому что разные поля нужно обрабаты- вать по-разному. В частности, должны меняться пояснения, сопутствующие вы-
Гл. 7. Построение программ 137 ходным данным. Это наводит на мысль, что структура программы, исходящая из структуры результатов, видимо, предпочтительнее. Исследуем эту возможность, а позже вернемся к начатому построению. Результаты обычно будут иметь такую структуру: ЗАГЛАВИЕ АВТОР(Ы) ИЗДАТЕЛЬСТВО ДРУГИЕ ДАННЫЕ где каждый прямоугольник соответствует последовательности символов из входного файла, и так как, согласно спецификации, любое поле содержит не более пятидесяти символов, мы будем полагать, что каждое из них можно записать на отдельной строке. Мы должны также допускать возможность того, что полей будет только три и выходной формат будет таким: заглавие IZ22ZZ2Z2Z222ZZZ2ZZZ222ZZI автор(ы) : IZZZZZZZZZZZZZZZZZZI издательство IZZZZZZZZZZZZI Головная процедура, основанная на этой структуре, может быть записана в виде begin обработать поля, которые присутствуют всегда; если есть другие поля то обработать другие поля; чтение маркера данных end Условный оператор используется для того, чтобы выяснить, нужно ли обрабатывать какие-нибудь ’’другие поля”, а последнее предложение включено на основании прежних соображений. Поэтапное уточнение этого варианта пред- ставлено в табл. 7.7. Оно приводит к построению процедуры processonebook, головной процедуры в окончательной программе на листинге 7.4. Ряд моментов нуждается в комментариях. Предложение обработать поля, которые присутствуют всегда естественно распадается на три части, соответствующие обработке данных о за- главии, авторе и издательстве. Каждая включает в себя вывод пояснения, чтение и выдачу значения поля и переход к новой строке. Части отличаются друг от друга только текстом пояснения. Раскрывая булевское выражение есть другие поля
138 Гл. 7. Построение программ Таблица 7.7. Таблица разработки программы СведенияОКниге (частично) Шаги разработки Примечания обработка одной книги -> begin обработать поля, которые присутствуют всегда; если есть другие поля то обработать другие поля; read (ch) { чтение маркера данных } end локальная переменная ch: char, обработать поля, которые присутствуют всегда -> begin обработать поле ’’заглавие” обработать поле ”автор(ы)” обработать поле ’’издательство” end аналогично предыдущему аналогично предыдущему обработать поле ’’заглавие” -> begin write (’заглавие :’); чтение и вывод значения поля; переход к новой строке end модифицированная процедура copyonename из программы СписокИменЗ есть другие поля -> триГ <> bookterminator локальная константа bookterminator - 7’ обработать другие поля -> begin обработать первое из оставшихся пока есть другие поля выполнять обработать очередное из оставшихся end аналогично обработке заглавия аналогично обработке заглавия мы опираемся на ранее отмеченный факт: когда все поля прочитаны, input* со- держит заключительную дробную черту. Таким образом, другие поля есть, когда input* не содержит маркер данных. Для удобства чтения программы введена кон- станта bookterminator, значение которой равно ”/”. Дальше введена другая кон- станта - detailterminator, имеющая такое же значение. Это поможет отличать дробь, завершающую все сведения о книге (значение bookterminator), от дроби, завершающей поле (значение detailterminator). При уточнении предложения обработка других полей учтено, что только для первого из ’’других” полей нужно писать рядом поясне- ние. Поэтому первое из других полей обрабатывается отдельно, а все остальные (если они есть) - с помощью оператора цикла. Если же ’’других полей” нет, то обработка другого поля являющаяся телом оператора цикла, будет просто прекращена.
Гл. 7. Построение программ 139 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 program BookDetails (input, output)', { ввод сведений о книге, представленных в упакованном виде, и их выдача в удобной для человека форме, сопровождаемая пояснениями } procedure processonedetail', { чтение и вывод поля данных и переход к новой строке } const space - ’ ’; detailterminator = 7’; var ch: char, begin while inpuC - space do read (ch)', while inpuC <> detailterminator do begin read (ch)\ write (ch) end; read (ch); { чтение маркера, завершающего поле } writein end { processonedetail}; procedure processonebook; { ввод и выдача всех сведений о книге } const bookterminator = 7’; var ch: char\ begin write ('ЗАГЛАВИЕ : processonedetail', write САВТОР(Ы) : processonedetail', write ('ИЗДАТЕЛЬСТВО : '); processonedetail', if inpuC <> bookterminator then begin write ('ДРУГИЕ ДАННЫЕ : processonedetail', while inpuC <> bookterminator do begin write C ’ : 17); processonedetail end end; read (ch) { чтение маркера, завершающего книгу } end { processonebook }; begin writein (’** РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ СВЕДЕНИЯ О КНИГЕ *♦’); writebv, processonebook', { обработка одной книги } writebv, writein (’**** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ****’) end { BookDetails }. Листинг 7.4. Текст программы СведенияОКниге Не попало в таблицу разработки уточнение нескольких предложений типа обработка поля ’’заглавие” каждое из которых, в свою очередь, содержит последовательность предложений:
140 Гл. 7. Построение программ ввод и выдача значения поля; переход к новой строке. Это позволяет прибегнуть ко второму методу применения процедур из тех, что были упомянуты на с. 132. Именно, процедурой мы можем выразить после- довательность операторов, которая встречается в программе несколько раз. Про- цедура должна пропускать начальные пробелы, читать завершающуюся дробью последовательность символов, выдавать их без этого ограничителя и переходить к новой строке. Но нечто похожее делает процедура соруопепате в программе СписокИменЗ (с. 77). Поэтому, вместо того, чтобы создавать новую процедуру с самого начала, мы взяли процедуру соруопепате и, слегка изменив ее, включи- ли в строки 6-24 нашей программы под именем processonedetaiL Кроме переиме- нования были сделаны, в частности, следующие изменения: а) константа terminator была переименована в detailterminator, б) оператор writeln был исключен; в) readin был заменен на read (ch), так как читать нужно только ограни- читель, завершающий поле, а нс весь остаток текущей входной строки. В программировании часто можно сэкономить усилия, используя сущест- вующие программы и подпрограммы - иногда оставляя их неизменными, иногда, как в данном случае, незначительно их изменяя. procedure processonebook', { ввод и выдача всех сведений о книге } const bookterminator ~ 7’; var detailnumber : integer, ch : char, begin detailnumber := 0; while input* <> hookterminator do begin detailnumber := detailnumber + 1; if detailnumber - 1 then write (’ЗАГЛАВИЕ : ') else if detailnumber « 2 then write (’ABTOP(Ы) : ’) else if detailnumber ~ 3 then write (’ИЗДАТЕЛЬСТВО : ’) else if detailnumber - 4 then write СДРУГИЕ ДАННЫЕ : ’) else write (’ ’ : 17); processonedetail end; read (ch) { чтение маркера книги } end { обработка одной книги ); Листинг 7.5. Альтернативный вариант процедуры processonebook для программы СведенияОКниге (листинг 7.4) Вернемся, наконец, к наброску программы begin пока input' нс содержит маркер данных выполнять обработка одного поля; чтение маркера данных end ранее отвергнутому из-за того, что предложение
Гл 7. Построение программ 141 обработка одного поля должно обрабатывать пять разных видов полей. Процедура processonebook (лис- тинг 7.5) построена на основании именно этого подхода. Ею можно заменить головную процедуру программы СведенияОКниге в листинге 7.4, сохранив все остальные части этой программы в прежнем виде. Трудности удалось преодолеть за счет введения целочисленной переменной detailnumber, в которой хранится номер и соответственно способ обработки текущего поля. Ее значение использу- ется в последовательности вложенных друг в друга условных операторов для того, чтобы определить, какое пояснение должно быть напечатано перед содер- жимым данного поля. После того как пояснение напечатано, содержимое поля читается и выводится, как и раньше, с помощью процедуры processonedetail. Итак, придерживаясь любого из двух подходов, можно получить закончен- ную программу. Но какая из программ лучше? С точки зрения критериев прави- льности и ясности ни одна нс является безусловно лучшей, однако эффектив- ность первого решения несомненно выше. Вы легко можете убедиться в этом, выполнив трассировку обоих вариантов процедуры processonebook с входными данными из спецификации программы (табл. 7.6). Трассировка вызовов проце- дуры processonedetail для этого не нужна. Трассировка второго варианта вчетве- ро длиннее, чем трассировка первого, и этого достаточно, чтобы заключить, что решение, основанное на структуре входных данных, является гораздо более не- уклюжим. Такого рода неэффективные программы часто получаются в результа- те ошибки программиста, допущенной при выборе общей структуры программы или отдельной ее части. Умение угадать наилучшую структуру для каждой час- ти программы - один из важнейших аспектов программистского мастерства, 7.6. Влияние языка В заключение главы, посвященной процессу создания программ, уместны несколько замечаний о том, как влияет на этот процесс объектный язык. Объектный язык - это язык, на котором должна быть записана окончательная программа1. Наиболее очевидный момент состоит в том, что объектный язык опреде- ляет необходимую степень уточнения программы. Для получения программы на языке сравнительно более низкого уровня, т.е. на языке с более примитив- ными средствами, понадобится дополнительная детализация. Представим себе для иллюстрации язык типа Паскаля, в котором нет про- цедур write и writeln. Символы можно выдавать в выходной файл только по одно- му с помощью процедуры out, а для перехода к новой строке нужно вызывать процедуру newline. На этом более примитивном языке предложение выдать символы ’************’ и перейти к новой строке; которое на Паскале можно выразить единственным оператором writeln С *********** *’) потребует дополнительного уточняющего этапа наподобие следующего: count := 12; while count <> 0 do 1 Объектным принято называть язык, на котором записывается программа, полученная в результате работы некоторого процессора (являющаяся объектом работы процессора). В случае компилятора таким языком является обычно машинный код. В данном случае в роли процессора выступает программист, и объектным языком является Паскаль. - Примеч. ред.
142 Гл. 7. Построение программ begin count := count - 1; out (’*’) end; newline Если, кроме того, в этом языке более низкого уровня нет оператора цикла, повторение нам придется выражать какими-нибудь другими средствами. Во всех языках програмирования должны быть возможности для выражения трех фунда- ментальных структур: последовательного выполнения, выбора и повторения. Наоборот, когда мы располагаем языком, в котором некоторые операции можно выразить проще, чем в Паскале, этапов уточнения потребуется меньше. Если, например, для чтения символа из входного файла и его вывода в выходной файл можно просто написать copychar, то предложение пересылка одного символа из программы СловоВРамке не потребует дальнейшей детализации. Нужно сказать, что объектный язык влияет на сам характер мышления программиста. Ясно, что программист все время ищет такие пути детализации программы, чтобы как можно больше предложений непосредственно выразить на объектном языке. Через некоторое время это начинает влиять на стиль програм- мирования. Если язык устроен плохо, под его влиянием вырабатываются дурные программистсткие привычки. С другой стороны, общепризнано, что обучение программированию с помощью хорошо сконструированного языка типа Паскаля способствует выработке правильного стиля программирования. Упражнения 7.1. Разработайте третий вариант программы СловоВРамке, в котором использовались бы ровно две процедуры processoneword (обработка одного слова) и copyeightcharacters (пересылка восьми символов). Они должны быть видоизменениями соответствующих процедур из листинга 7.3. 7.2. Выясните, будет ли программа Прогрессия (с. 131) правильно работать с каждым из следующих наборов входных данных: а) 3 ' -5 6 б) 3 5 0 в) 3 5 1 г) 3 5 -1 д) 3 5 60 е) 1000000 2000000 6 7.3. В файле данных хранятся сведения о рейсах самолетов из Лондона в другие города Европы. В каждой строке содержится пункт назначения - отдельное слово, заканчивающееся пробелом, - время отправления и время посадки. Время задано как чч.мм. Например: AMSTERDAM 18.15 19.35 OSLO 10.05 00.45 Файл данных заканчивается строкой, первым и единственным символом которой является звездочка. Файл предварительно проверен и содержит правильные данные. Напишите программу под названием FlightTimes (ВремяВылета), которая читает данные и выводит их в таблицу с указанием пунктов назначения, времени отправления, времени посадки и длительности полета. Полет может начинаться до полуночи и заканчиваться после полуночи, но никогда не длится больше 24 часов. 7.4. Напишите программу Display (Показ), которая вводила бы три слова и выдавала их так, как показано в столбике справа.
Гл 7. Построение программ 143 Входные данные Три слова записаны в одной строке данных. Слово - * О * это последовательность отличных от пробелов символов, за которой * Р * следует пробел. Перед первым словом и между словами могут быть * У * дополнительные пробелы. * . Т * Результаты Слова, расположенные нужным образом, должны быть * напечатаны в центре страницы или экрана. Считайте, что * 5 * выходная строка содержит 70 символов. * * * * * * р * * т * * о * * в ‘ * * 7.5. Напишите про1рамму Адреса, которая читала бы из файла, содержащего адреса и фамилии. Входные данные Входные данные - это 10 адресов и фамилий, представленных в упакованном виде. Каждый адрес и фамилия образованы несколькими цепочками, а каждая цепочка хра- нится во входном файле в виде последовательности символов, завершающейся ограничите- лем ’@’. После адреса и фамилии записан еще один символ следующий адрес может начинаться в той же строке. Цепочка может начинаться с пробелов. Первые два адреса с фамилиями могут быть представлены во входном файле, например, так: Москва@Эльдорадовский переулок, дом 23, корп.2@Курсы программирования® Руководителю курсов Грустнову Н.Е.®@ The Personal Manager,©Software Systems Inc.,@ 413, Killysorell Road,©Dunamanagh,@USA@© Входной файл предварительно проверен и содержит правильные данные. Результаты Адреса и фамилия должны быть напечатаны друг под другом в следующем виде: Москва Эльдорадовский переулок, дом 23, корпус 2 Курсы программирования Руководителю курсов Грустнову Н.Е. За каждым адресом и фамилией должны следовать две пустые строки.
8 ОШИБКИ ПРОГРАММИРОВАНИЯ Когда программа написана и в ней исправлены все синтаксические ошибки, мы рассчитываем, что при ее исполнении на процессоре с некоторым набором входных данных мы получим правильные результаты. Мы надеемся, что программа будет давать правильные результаты в течение всего времени ее эксплуатации. К сожалению, дело далеко не всегда обстоит таким образом. В предыдущей главе мы рассмотрели программу Прогрессия (с. 131) ив у пр. 7.2 предложили протестировать ее с помощью различных наборов входных данных. Если вы это проделали, то, вероятно, обнаружили, что программа не вполне удовлетворительна. Возникшие проблемы довольно характерны для этапа выполнения программы. Их можно разделить на три группы: логические ошибки, некорректные данные, ограничения, накладываемые оборудованием. Каждой из этих групп ошибок посвящены следующие три раздела. В конце главы мы пересмотрим программу Прогрессия и избавим ее от недочетов. 8.1. Логические ошибки Логическая ошибка это такой изъян программы, который мешает ей во всех случаях давать правильные результаты. Время от времени результаты могут быть правильными; они могут даже быть правильными в большинстве случаев. Но программа, которая хотя бы изредка дает неверный результат, непрйгодна для использования. Задача всякого хорошего программиста - писать программы без логических ошибок. В программе Прогрессия есть два изъяна, которые можно отнести к логи- ческим ошибкам. Первый из них обнаруживается на следующих входных данных: 3 5 0 Эти числа - соответственно начальный член арифметической прогрессии, ее разность и число элементов, значит, мы имеем дело с арифметической прогрессией без единого элемента. Тем не менее программа на процессоре, доступном автору, дает следующие результаты: 3 8 13 18 23 28 33 38 43 48 53 58 63 68 73 (числа расставлены по ширине 120-символьной строки). Запуск программы за- вершается сообщением об ошибке в ходе исполнения, гласящим, что печатать в текущей строке больше невозможно. Мы еще вернемся к этой ошибке, когда бу- дем говорить об ограничениях оборудования; сейчас наша задача - объяснить,
Гл.8. Ошибки программирования 145 почему вообще печатаются какие-то элементы последовательности. Чтобы разо- браться, что происходит, посмотрим на след работы программы в табл. 8.1. Поскольку переменная numberofterms вначале равна нулю, а в строке 14 про- граммы (с. 131) из нее вычитается 1, то когда впервые исполнение доходит до строки 17, переменная уже меньше нуля. Таким образом, при выполнении стро- ки 17 логическое условие всякий раз истинно и выполнение программы, по-ви- димому, никогда не завершится. Мы говорим, что программа зациклилась. По- скольку, однако, на каждом шаге цикла в выходной файл посылается 8 симво- лов, первая строка скоро переполняется и (на авторском процессоре) исполнение прерывается. Таблица 8.1. Частичный след программы Прогрессия (листинг 7.2) Входные данные 3 Трассировка 5 О строка ход выполнения term difference numberofterms 8 вход в программу ? *> О 10 3 5 0 13 результат: ~~~~ ~3 14 -1 17 (numberofterms< >0) =true 19 8 20 результат: ~ 8 21 -2 17 (numberofterms< >0) -true 19 13 20 результат: ~13 21 - -3 17 (numberofterms<>Q)-true и так далее до бесконечности Можно попытаться оправдать ошибку тем, что некорректны входные дан- ные: никому не придет в голову получать последовательность, в которой нет ни одного элемента. В данном случае это, возможно, и так, однако на удивление часто от программы в определенных ситуациях требуется ”ничего не делать”. Один из анекдотов о промахах компьютеров рассказывает о системе, высылав- шей чек на 0 долларов 00 центов и периодически напоминавшей об этом с угро- зой преследования по закону в случае неуплаты. Наконец, измученный ’’непла- тельщик” решил выслать чек на сумму 0.00, но компьютер в банке отклонил его и чек остался неоплаченным. Сложностей не возникло, если бы каждая из про- грамм умела обращаться с нулевыми денежными суммами. Вообще стоит всегда убедиться, возможна ли ситуация, когда от программы не требуется никаких действий. Этот случай мы называем нулевым вариантом. Если возможен нулевой вариант, то программист обязан позаботиться о его правильной трактовке. Неспособность программы Прогрессия работать с пустой последовательностью, т.е. либо не выдавать ни одного элемента, либо объявлять данные некорректны- ми, несомненно, является логической ошибкой. Второй изъян программы обнаруживается на наборе данных
146 Гл.8. Ошибки программирования 3 5 60 задающем последовательность длиной 60 элементов. На авторском процессоре печатается всего 15 членов прогрессии, вслед за чем выдается вышеприведенная ошибка. Ваш процессор может давать другие результаты. Изъян программы состоит в том, что все выходные данные в количестве 480 символов она пытается поместить в одну строку. Снова мы сталкиваемся с логической ошибкой. Ошибки в программе Прогрессия позволяют понять, какого рода просчеты часто допускает программист. Это просчеты, возникающие не вследствие не- брежного кодирования, а оттого, что на ранней стадии проектирования программы упускаются существенные особенности задачи. Как только предложение вывести результаты было заменено на begin вывести первый элемент; подсчитать и вывести остальные элементы end (с. 131), мы сделали первую ошибку, предположив, что последовательность всегда имеет первый элемент. Если бы мы допустили возможность появления пустой, без единого элемента последовательности, то, вероятно, на этом шаге развернули бы предложение так: if numberofterms > 0 then begin вывести первый элемент; подсчитать и вывести остальные элементы end Аналогично обстоит дело со второй ошибкой: на этапе проектирования про- граммы нужно было правильно спланировать расположение выходных данных. Не все логические ошибки происходят из-за просчетов на стадии проекти- рования. Многие из них могут закрасться в процессе уточнения программы, которая была логически правильной на предыдущем этапе. Поразительно легко написать условие, обратное тому, которое требуется. Иногда пропускаются це- лые операторы. На заключительных этапах подготовки текста программы знак плюс может превратиться в умножение, а <> (не равно) - измениться на < (ме- ньше). Приобретая опыт программирования, вы не раз будете поражены своими ошибками, порой самыми глупейшими. 8.2. Некорректные данные Один из наборов данных для программы Прогрессия в упр. 7.2 был таким: 3 5-1 В нем задается последовательность из -1 элемента. Тот же процессор на этом наборе повторяет результаты, полученные для нулевого варианта. Снова можно было бы ’’снять вину” с программы и все свалить на некорректность данных. Это так, но входные данные обычно вводятся людьми (прямо или при посредстве оборудования), а значит, некорректность данных это нормальное явление и следует его учитывать. Как же должна поступать с некорректными данными правильно написан- ная программа? Прежде всего она должна давать результаты, отличные от результатов, получаемых на правильных данных, чтобы не вводить пользователя
Гл.8. Ошибки программирования 147 в заблуждение. Хорошо известен яркий пример неадекватного поведения программы в системе противовоздушной обороны, когда птичья стая была приня- та за вражеские ракеты. Такие ошибки могут дорого стоить. Если программа сталкивается с некорректными данными, то лучшее, что она может сделать, это: а) для каждого некорректного элемента данных дать сообщение об ошибке, указывающее какой элемент и почему неправилен; б) произвести как можно больше требуемых вычислений. Так, например, если при расчете зарплаты 1000 служащих для трех из них данные не- корректны, то это не должно мешать произвести расчет для остальных 997; в) встретив некорректный элемент, не позволяющий произвести вычисле- ния во всей полноте, проверить оставшуюся часть исходных данных. Например, транслятор с Паскаля может найти за один проход компиля- ции все синтаксические ошибки, не останавливаясь на первой же из них1. Таким образом, проверять данные чрезвычайно важно, и если только за- ранее не известно, что входные данные заведомо корректны, всякая програм- ма должна производить их надлежащую проверку. С этой целью спецификация программы должна указывать, насколько это возможно, точные границы, в пре- делах которых данные считаются корректными. Более того, целесообразно, что- бы способ представления входных данных отражал их содержательный смысл. Это облегчит обнаружение основной массы ошибок, возникающих при подготов- ке данных. 8.3. Ограничения, накладываемые оборудованием Мы уже отметили, что на некоторых процессорах сообщается об ошибке и исполнение программы завершается, если во время ее работы в одну выходную строку выдается слишком много символов. Это следствие логической ошибки в программе, связанной с ограничением выводного устройства: в программе не уч- тено предельное количество символов, которое может быть напечатано принте- ром в одной строке. В других случаях ограничивающим фактором может быть размер экрана дисплея, размеры страницы графопостроителя или длина магнит- ной ленты. Иногда в случаях, когда процессор управляет другими устройствами, воз- никают ограничения на операции ввода-вывода из-за необходимости согласова- ния их во времени. Если эти временные ограничения не принять во внимание, программа может работать неправильно. Программирование такого рода, обычно называемое программированием систем реального времени, в книге не обсуж- дается. Имеются и аппаратные ограничения, связанные с процессором. Примером могут служить результаты, полученные из Прогрессии на двух разных Паскаль- процессорах для следующих наборов входных данных: 5000 10000 6 1000000 2000000 6 1 С этим можно спорить: иногда первая ошибка служит причиной всех дальнейших, поскольку "сбивает” формат входных данных. Зачастую именно так случается при компиляции: единственная ошибка вызывает видимость неправильности оставшейся части текста. Предвидя такую ситуацию, программа может поступать обратно рекомендованному: прекращать работу/ на первой же встреченной ошибке. - Примеч. пер.
148 Гл. 8. Ошибки программирования На первом процессоре (большая ЭВМ) результаты программы для первого набора данных правильны: 5000 15000 25000 35000 45000 55000 но второй набор данных дает 1000000 3000000 5000000 7000000 после чего исполнение завершается сообщением об ошибке. На втором процессоре (микрокомпьютер) первая группа результатов такова: 5000 15000 25000 -30536 -20536 -10536 Эти результаты уже неверны, а вторая группа результатов: 16960 -14656 19264 -12352 21568 -10048 - полная бессмыслица! Проблемы в этих вычислениях вытекают из ограничений оборудования, которые ставят предел возможности процессора представлять большие числа. В гл. 6 (с. 100) эти ограничения отражены на программном уровне * виде диапа- зона -maxint..maxint, в котором могут лежать допустимые значения целых чисел в Паскале, включая числа в тексте программы и входных данных, а также вели- чины, порождаемые в ходе вычислений. На большой ЭВМ, где получены приведенные выше результаты, величина maxint равна 8388607, так что в первой последовательности все 6 элементов попадают в установленный интервал и вычисление завершается успешно. Во второй последовательности камнем преткновения становится оператор term := term + difference когда он выполняется над значениями term == 7000000 и difference = 2000000 потому что сумма становится слишком большой. Говорят, что произошло цело- численное переполнение. В данной Паскаль-системе оно обнаруживается, ошибка диагностируется и программа прерывается. На микрокомпьютере максимальное значение целого числа maxint равно 32767 и четвертый член последовательности, таким образом, выходит за уста- новленные пределы. Это происходит, когда term = 25000 и difference = 10000 и делается попытка выполнить сложение. Вместо получения ожидаемой суммы 35000 происходит целочисленное переполнение, порождающее результат term = -30536 В Паскаль-системе микрокомпьютера ситуация целочисленного переполнения не обнаруживается. Исполнение продолжается как ни в чем не бывало и далее приводит к неверным результатам. Со второй последовательностью происходят похожие события. Во-первых, оба числа, 1000000 и 2000000, слишком велики для представления. Они вводятся и запоминаются как term = 16960 и difference = -31616
Гл.8. Ошибки программирования 149 соответственно. Вычисления затем продолжаются, несколько раз переполнение игнорируется, а итогом является бессмысленная последовательность чисел. Совершенно ясно, что программы, ведущие себя таким образом, неудовле- творительны. Как же тут быть? Прежде всего во многих случаях мы можем га- рантировать, что в вычислениях не встретятся недопустимо большие числа, и тогда проблемы нет. Если, однако, есть опасность появления больших чисел, необходимо предупредить целочисленное переполнение, которое привело бы к неверным результатам. В таких системах, как первая из вышеописанных, про- граммист может полагаться на то, что системное сообщение об ошибке прервет исполнение его программы, но если система ошибки не обнаруживает и сообще- ния не выдает, то это обязан сделать программист. Как именно, мы увидим да- лее в этой главе, когда подвергнем переработке программу Прогрессия. Предельные значения целых чисел - не единственное ограничение, связан- ное с процессором. Другие ограничения могут проявляться в точности представ- ления вещественных чисел, общем объеме памяти, отводимом под хранение про- граммы и данных, быстродействии, с которым процессор может проводить вычи- сления. Некоторые из ограничений программист обязан принимать во внимание, когда он проектирует программу. Совершенно недопустимо, чтобы программа из-за них неправильно работала. Сообщения системы об ошибках во время ис- полнения программы иногда приемлемы, но неверные результаты - никогда! 8.4. Устойчивые программы Когда мы называем нечто устойчивым, то имеем в виду его способность устоять под ’’грубым” воздействием. Говоря об устойчивости программы, мы подразумеваем два вида такого воздействия: а) неподходящие для программы данные, б) внесение в программу изменений. Выше мы убедились, что при некоторых обстоятельствах от программы не- льзя ожидать правильных результатов. Стало ясно, что некорректные или час- тично некорректные входные данные, а также ограничения оборудования могут препятствовать ее нормальному завершению. В таких условиях правильное поведение программы состоит в том, чтобы продвинуться в вычислениях как мо- жно дальше и дать понятные сообщения, указывающие на ошибки в оставшихся данных. Устойчивая программа порождает правильные результаты во всех случаях, когда это возможно, а когда невозможно - всегда указывает почему. Неопытные программисты, завершив в один прекрасный день программу, склонны считать, что все трудности позади и их детище останется неизменным на весь период своего существования. Но это определенно не так в отношении прикладного программирования. Можно считать установленным фактом, что всякая сколько-нибудь полезная программа неоднократно меняется на протя- жении своей жизни. Изменение включает в себя, во-первых, исправление логи- ческих ошибок после того, как программа начала практически применяться, и, во-вторых, модификацию, которая учла бы возникающие новые требования. На эту деятельность, называемую сопровождением программы, профессиональные программисты по некоторым оценкам затрачивают вдвое больше времени, чем на создание новых программ. С сопровождением программы связаны две проблемы. Во-первых, програм- ма, на какой-то стадии совершенно правильная, в результате внесения измене- ний может перестать быть таковой. Во избежание этой опасности программу ну- жно писать так, чтобы она была предельно ясной и понятной тому, кто занима- ется ее сопровождением. Во-вторых, каждая вносимая поправка обычно ослабляет программу подоб- но заплате на одежде. Если программист исключительно предусмотрителен, то
150 Гл. 8. Ошибки программирования создаваемой им программе он придаст такую структуру, что в нее будут легко вписываться все последующие изменения. Но подобным предвидением обладают немногие, и в результате каждое изменение чуть нарушает общую структуру, делает ее труднее для понимания и для очередной поправки. В конце концов программа становится столь залатанной, что легче переписать ее заново, чем пытаться модифицировать дальше. Устойчивая программа спланирована так хорошо и написана так ясно, что ее можно сопровождать, не лишая устойчи- вости. Обсуждение в этой главе показало, что программа Прогрессия (с. 131) определенно не является устойчивой, поэтому в завершение главы рассмотрим ее вариант, свободный от обнаруженных изъянов и дополненный некоторыми украшениями. Для начала более точно определим входные данные, а именно: первый элемент может лежать в диапазоне -maxint..maxint, разность прогрессии должна быть неотрицательна, число элементов прогрессии должно быть больше нуля. Новую, пересмотренную программу назовем Прогрессия!; ее текст приве- ден на листинге 8.1. Она состоит из основной программы и двух процедур, processoneprogression и writetheprogression. Заслуживают интереса три момента, связанные с главной процедурой, processoneprogression. Во-первых, оформление результатов прежней нашей прог- раммой в виде чисел без всяких пояснений было слишком бедным. В отношении выходных данных наглядное расположение, ясное представление не менее важ- ны, чем в отношении текста программы. Результаты работы программы в но- вой версии показаны в табл. 8.2. Они гораздо более информативны для пользо- вателя, чем набор голых чисел в верху пустой страницы. Дополнительная ин- формация частично выдается основной программой, частично - каждой из про- цедур. Второй момент, достойный упоминания, это то, что три переменные - term, difference, numberofterms - определяются не как локальные в процедуре processoneprogression, а как глобальные в строке 7 программы. Если бы они были объявлены внутри одной из процедур, другая бы не имела к ним доступа. Если их объявить в каждой из двух процедур, это были бы различные, несогласован- ные пары переменных, и значения процедуры processoneprogression нельзя было бы передать в процедуру writetheprogression. Как глобальные переменные они доступны обеим процедурам. Вместе с тем именно здесь уместно подчеркнуть, что глобальные перемен- ные не следует употреблять без явной необходимости. Большие программы, использующие глобальные переменные, часто трудны для понимания, и мы возь- мем за правило вводить глобальную переменную, только когда она в самом деле необходима. В свете того, что говорится в гл. 11 о подпрограммах с пара- метрами, прибегать к глобальным переменным нам придется редко. Наконец, третье соображение касается процедуры processoneprogression, которая в строках 55-59 определяет корректность входных данных и вызывает процедуру writetheprogression. Это делается только тогда, когда данные заведомо правильны. Поскольку значение переменной term не может быть неверным, проверяются только переменные difference и numberofterms. Здесь нужно сделать два замечания: Сообщения об ошибках оформлены в начале процедуры как строковые константы error 1 и еггог2. Этим подчеркивается, что процедура определяет два вида ошибочных данных. Проверки построены так, что numberofterms проверяется всегда, даже если неверно задана difference. Таким образом, если неверны значения как difference, так и numberofterms, выдаются оба сообщения об ошибках.
Гл. 8. Ошибки программирования 151 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 program Progression! (input, output)’, { Выдает арифметическую прогрессию по первому элементу, разности и общему количеству членов прогрессии } var term, difference, numberofterms : integer, procedure writetheprogression', const termsperline - 8; fieldwidth, - 8; var termswritten : integer; begin { выдать заглавие и первый элемент } writein ('ИСКОМАЯ АРИФМЕТИЧЕСКАЯ ПРОГРЕССИЯ.')’, write (term : fieldwidth); termswritten 1; { посчитать и выдать остальные элементы } while termswritten о numberofterms do begin { если нужно, перейти на новую строку } if termswritten mod termsperline - 0 then writein', { посчитать и выдать очередной элемент, если это возможно } if term <- maxint - difference then begin term term + difference’, write (term : fieldwidth.)', termswritten termswritten + 1; end; else begin writein (’????’ : fieldwidth)’, writein', writein ('ОЧЕРЕДНОЙ ЭЛЕМЕНТ ДАЕТ ПЕРЕПОЛНЕНИЕ.')’, termswritten numberofterms { выйти из цикла } end end; writein end { writetheprogression }; procedure processoneprogression; const error!-'НЕКОРРЕКТНЫЕ ДАННЫЕ:РАЗНОСТЬ ДОЛЖНА БЫТЬ >- 0.’; error2-'НЕКОРРЕКТНЫЕ ДАННЫЕ. ЧЛЕНОВ ДОЛЖНО БЫТЬ > 0.’; begin { чтение и вывод данных } read (term, difference, numberofterms)’, writein ('ПЕРВЫЙ ЭЛЕМЕНТ :', term : 8); writein ('РАЗНОСТЬ ПРОГРЕССИИ :', difference : 8); writein ('ЧИСЛО ЭЛЕМЕНТОВ :', numberofterms : 8); write In’, { выдать прогрессию, если данные корректны } if difference < 0 then writein (error!)', if numberofterms < 0 then writein (error!)', if difference >- 0 then if numberofterms > 0 then writetheprogression end { processoneprogression };
152 Гл.8. Ошибки программирования 62 63 64 65 66 67 begin writeln (’**** РЕЗУЛЬТАТЫ ПРОГРАММЫ ПРОГРЕССИЯ ♦♦♦♦’); writeln', processoneprogression; writeln; writeln (’**** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ *♦*♦’); end { Progression }. Листинг 8.1. Текст программы Прогрессия2 В процедуре writetheprogression также есть три момента, заслуживающие обсуждения. Во-первых, способ представления выходных данных. В первоначаль- ном варианте программа просто печатает элементы один за другим и оканчивает работу, когда величина, равная numberofterms, уменьшается до нуля. Удобнее, однако, считать от нуля в положительную сторону, чтобы следить за числом элементов, выводимых на одну строку. С этой целью в строке 14 определена переменная termswritten. Ей присваивается 1 в строке 19, сразу после вывода первого элемента, а в строке 32 после вывода очередного элемента она увеличи- вается на 1. Процесс нужно остановить, когда termswritten - numberofterms поэтому условие выхода из цикла while, проверяемое в 22-й строке программы, выглядит как termswritten <> numberofterms Внутри цикла, в строке 25, переход на новую строку при выводе результатов производится в зависимости от значения логического выражения termswritten mod termsperline - О в котором termsperline есть константа, равная 8. Это выражение истинно при termswritten, равном 8, 16, 24 и т.д., и вызывает перевод строки после каждого восьмого элемента. Таблица 8.2. Образец результатов программы Прогрессия?. **** РЕЗУЛЬТАТЫ ПРОГРАММЫ ПРОГРЕССИЯ **** ПЕРВЫЙ ЭЛЕМЕНТ : 0 РАЗНОСТЬ ПРОГРЕССИИ : 3 ЧИСЛО ЭЛЕМЕНТОВ 20 ИСКОМАЯ АРИФМЕТИЧЕСКАЯ ПРОГРЕССИЯ: 0 3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 **** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ **** Посмотрим теперь, как процедура writetheprogression следит за целочис- ленным переполнением. Используется соображение о наличии ’’зоны опасности”, попадание в которую очередного элемента сигнализирует о том, что следующий элемент вызовет переполнение. Каждый следующий элемент, если только разность прогрессии difference не равна 0, больше предыдущего, так что опасная
Гл.8. Ошибки программирования 153 зона лежит возле величины maxint. Перед вычислением очередного элемента наибольшее ’’безопасное” значение для переменной term равно maxint - difference На рис. 8.1 эти рассуждения изображены графически. Соответствующая часть процедуры writetheprogression расположена в строках 28-38. Здесь, вычисляя выражение term <= maxint - difference процессор определяет, ’’опасно” или нет вычислять очередной член прогрессии. Такой способ предупредить переполнение на первый взгляд чересчур сложен. Более естественным кажется определять допустимость элемента, просто вычисляя его значение и проверяя, не слишком ли оно велико. Рассуждая так, мы могли бы поставить в программу term :- term + difference*, if term > maxint then Но это решение совершенно ошибочно! По этой логике можно было дать следу- ющие инструкции для достижения края "обрыва: закройте глаза; шагайте вперед пока не почувствуете, что падаете; затем сделайте шаг назад. Поступая согласно такой инструкции, вы попадете не на край обрыва, а к его подножию! Точно так же в нашей программе переполнение случится уже в мо- мент сложения, а попытка узнать о событии post factum обречена на неудачу. Край пропасти нужно распознать прежде, чем мы рухнем в нее. Элементы возрастают Зона ------------> опасности -maxint »+ maxint maxint-aifference Рис. 8.1. Иллюстрация опасности целочисленного переполнения Наконец, обратите внимание на строку 37 программы, где сказано: termswritten := numberofterms {выход из цикла} Этот оператор выполняется после того, как сообщено об ошибке переполнения, и основной цикл должен быть прекращен. Обеспечить выход из цикла ’’пока” (оператора while) всегда можно, обратив в ложь условное выражение, управля- ющее повторением цикла. Именно этому служит вышеприведенный оператор. В гл. 10 мы встретимся с другим способом ’’аварийного” выхода из цикла. Впро- чем, и приведенный здесь способ вполне надежен и эффективен. Является ли Прогрессия! устойчивой программой? Во многом да. Она на- писана прозрачно, с комментариями, удобными именами переменных, структура ее разумна, наглядно расположен текст. Она снабжена константами, облегчаю- щими понимание сообщений об ошибках и изменение таких параметров, как чи- сло элементов в строке и ширина поля для каждого элемента. Программа всегда
154 Гл.8. Ошибки программирования дает правильный результат, если наличный процессор в состоянии выполнить вычисления. Она всегда сообщает об ошибках, если данные некорректны или происходит целочисленное переполнение во время вычисления последовательно- сти. И однако же она дает фантастические результаты, если переполнение про- исходит в процедуре ввода read. На упоминавшемся выше микрокомпьютере вы- ходные данные, получаемые из набора данных 1000000 2000000 6 будут выглядеть так, как показано в табл. 8.3. Эти результаты не столь чудо- вищны, как в прошлом примере, но все же далеки от истины. Чтобы устранить этот изъян, вам потребуется написать собственную процедуру чтения1. Таблица 8.3. Образец результатов программы Прогрессия2, которая исполнена на микрокомпьютере, имею- щем maxint - 32767, и обрабатывает числовые данные, превышающие maxint. **** РЕЗУЛЬТАТЫ ПРОГРАММЫ ПРОГРЕССИЯ **** ПЕРВЫЙ ЭЛЕМЕНТ : 16960 РАЗНОСТЬ ПРОГРЕССИИ : -31616 ЧИСЛО ЭЛЕМЕНТОВ 6 НЕКОРРЕКТНЫЕ ДАННЫЕ : РАЗНОСТЬ ДОЛЖНА БЫТЬ >= 0. **** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ **** Сказанное помогает понять, почему разработка надежных больших прог- рамм для ЭВМ считается сложной интеллектуальной деятельностью. Если так непросто написать устойчивую программу для вычисления арифметической про- грессии, то удивительно ли, что среди больших программ абсолютно надежных совсем немного? Упражнения 8.1. Напишите простые программы для печати значения встроенной константы maxint в вашей Паскаль-системе и для определения того, что происходит, когда (а) значение, читаемое из входного файла процедурой read, превосходит maxint и (б) результат арифметической операции превосходит maxint. 8.2. Напишите программу Факториал, которая выдает таблицу следующего вида: N факториал N 0 1 1 1 2 2 3 би т.д. причем размер таблицы настолько велик, насколько позволяет наличный процессор (в предположении, что для хранения величины NI используется переменная типа integer). 1 Похожую, несколько более сложную программу можно найти в: Вирт Н., Алгорит- мы + структуры данных - программы. - М.: Наука, 1985; программа 1.3. - Примеч. пер.
Гл. 8. Ошибки программирования 155 Программа должна без изменения работать на любом Паскаль-процессоре, так что не предполагайте никакого конкретного значения для maxint. Замечание. Факториал числа N по определению равен 1 для ЛЮ и равен произведению чисел от 1 до N при NX). 8.3. Напишите программу ПрогрессияЗ, которая столь же устойчива, что и Прогрессия2, и имеет те же спецификации за исключением следующего условия на входные данные: первый элемент может лежать в диапазоне -maxint..maxint, разность может лежать в диапазоне -maxint..maxint, число членов прогрессии должно лежать в диапазоне 1..200. Проверьте программу на следующих наборах данных перед тем, как ввести ее в компьютер: а) -х х 3 б) х -х 3 где х равно значению maxint в вашей Паскаль-системе. 8.4. Если заданы начальный элемент а и знаменатель г, то последовательность а аг аг2, аг3 . . . называется геометрической прогрессией. Напишите программу ГеометрическаяПрогрессия, которая при входных данных: начальный элемент : целое в диапазоне -maxint..maxint знаменатель : целое в диапазоне -maxint..maxint число элементов : целое в диапазоне 1..200 выдает соответствующую геометрическую последовательность или максимально возможное число ее элементов.
9 ПРЕДОТВРАЩЕНИЕ, ОБНАРУЖЕНИЕ И ИСПРАВЛЕНИЕ ОШИБОК Рассуждения в гл. 8 и ваш собственный опыт (если вы проделали упраж- нения в гл. 7 и 8), вероятно, показали, что ошибки программирования - серьез- ная проблема; с этой проблемой нужно познакомиться поглубже, чтобы научить- ся писать устойчивые программы. Данная глава в основном касается того, как предотвращать ошибки в программах, обнаруживать уже имеющиеся и, наконец, исправлять их, не внося при этом новые. Прежде всего обсудим, кто отвечает за ошибки в программах. 9.1. Ответственность программиста Первая строка в некоем телефонном справочнике выглядела так: Окунев и сын вслед за чем следовал телефонный номер. Остальные Окуневы располагались спустя 400 страниц. Через несколько дней после выхода в свет справочника представитель телефонной компании причиной опечатки назвал компьютер. Ве- роятно, более справедливым было искать виновника среди сотрудников компа- нии, однако даже и тогда истинный виновник скорее всего был бы упущен из виду. Ошибка была простой. Во время подготовки входных данных в фамилии Окунева первой буквой был набран нуль. Поэтому непосредственную ошибку внес, по-видимому, оператор, сидевший за клавиатурой в отделе подготовки дан- ных компании, или клерк в отделе распространения, небрежно заполнивший бланк. Такого рода ошибка рано или поздно должна была случиться. Посмотрим теперь, не была ли эта почти неизбежная ошибка, так сказать, допущена в программу? Похоже, что компьютерная программа, обрабатывающая очередной элемент справочника, была написана так, что пропускала подобные ошибки данных. Едва ли имя в справочнике может начинаться с цифры нуль. Итак, представителю компании следовало бы бросить упрек программисту, раз- работавшему и написавшему программу. Ошибка в справочнике, несомненно, затруднила клиентам Окунева поиск его телефона. В данном случае проблема легко разрешима внесением исправле- ния в телефонную книгу. Ныне, однако, компьютеры часто применяются в та- ких областях, где ошибки могут стоить очень дорого. Известны случаи, едва не повлекшие авиакатастрофы из-за погрешностей в компьютерных системах управления полетами. Известен по крайней мере один случай, когда в резуль- тате ошибки в программе было сорвано дорогостоящее космическое исследова-
Гл.9. Предотвращение, обнаружение и исправление ошибок 157 ние. Не однажды по тем же причинам мы были близки к ядерной войне. Все эти факты возлагают серьезную ответственность за будущие разработки на тех, кто связан с компьютерной индустрией. А поскольку вы учитесь программированию, то речь, возможно, идет именно о вас! 9.2. Предотвращение ошибок Если не вносить ошибок в программу, то не нужно будет ни искать, ни исправлять их. Потому так важно предотвращать ошибки. В предыдущих главах мы несколько раз касались основных способов предотвращения ошибок, а теперь удобно свести их воедино и поговорить об этом еще раз. В разделе 7.2 мы говорили о важности точных спецификаций программы. Впоследствии, в разд. 8.2, отмечалось, что спецификации должны содержать указание диапазонов, в которых лежат данные, чтобы можно было проверить их правильность в программе. Программисту требуется также знать, в какой форме нужно представлять результаты и при каких обстоятельствах программа должна выдавать сообщения об ошибках по ходу исполнения программы. Если в специ- фикации это не указано, то принять определенное решение нужно уже на ран- ней стадии разработки программы. Раннее уточнение спецификаций связано с предотвращением ошибок, так как неопределенность и вызванные ею перемены решений всегда ведут к дополнительным ошибкам. Методичность разработки программы также помогает предупредить ошиб- ки. В гл. 7 в качестве способа составления программы, начиная от специфи- каций вплоть до завершения, рекомендуется поэтапное уточнение. Там же было отмечено, что не всегда верное решение приходит первым и нужно быть готовым к тому, чтобы частично или полностью отказаться от уже сделанного и проде- лать работу заново. Существенная часть этого процесса - упорядоченный перенос ваших мыслей в программу, пусть не столь основательный, как в табли- цах разработки, применяемых в книге, но, несомненно, в такой степени деталь- ный, чтобы ясно видеть, насколько вы продвинулись в составлении программы. Если вы не придаете значения методичной разработке своих программ или же записываете свои решения небрежно, то у вас наверняка будут получаться программы, содержащие массу ошибок, которых вполне можно было избежать. Ошибки чаще всего возникают, помимо случаев простой беспечности, в об- становке путаницы, а что может быть более путаным, чем плохо написанная программа? Потому-то мы и подчеркиваем необходимость разбиения программ на части с тем, чтобы каждая из них была по возможности независима от других и чтобы в совокупности все части образовывали законченную программу. Это, по-видимому, наиболее трудный этап разработки большой программы, поскольку программист, как отмечалось в разд. 8.4, должен выбрать структуру, подхо- дящую не только для первоначальной программы, но и учитывающую вероятные изменения ее в будущем. Если структура программы выбрана удачно, то ее будет легче понять и вам, и тем, кто будет сопровождать программу. Итак, хорошая структура программы это гарантия ее ясности,. надежный способ избежать путаницы и предупредить ошибки. Далее, доля ошибок будет ниже, если программы, с которыми вы рабо- таете, удобочитаемы. Добиться этого, как говорилось, можно следующими спо- собами: а) использовать осмысленные идентификаторы; б) писать комментарии; в) наглядно располагать текст программы. Можно надеяться, что вы осознали преимущества этих приемов и ввели их в свою практику. Если нет, то вам при- дется усвоить их более тяжким способом, когда вы попытаетесь прочесть и по- нять свою собственную программу спустя несколько месяцев после ее написания. Тут-то и выяснится, насколько понятные программы вы пишете.
158 Гл.9. Предотвращение, обнаружение и исправление ошибок Систематически применяя рассмотренные приемы, вы сможете предотвра- щать многие из ошибок, которые иначе неминуемо допустили бы. Всех ошибок, к сожалению, не избежать: людям свойственно ошибаться. При первых опытах программирования ошибок, вероятно, будет более чем достаточно; даже много- летняя практика от них не застраховывает. Поэтому необходимы и методы обна- ружения ошибок. 9.3. Подбор тестовых данных Есть два основных метода обнаружения ошибок: ручная проверка и тес- тирование программы. Ручная проверка состоит в выполнении различных про- верок в ходе разработки программы, а тестирование программы заключается в прогонах полностью или частично завершенной программы на компьютере. Оба этих метода требуют тестовых данных, подбор которых, следовательно, является важной частью работы программиста. Таблица 9.1. Спецификация программы СчетчикСлов Напишите Паскаль-программу под названием СчетчикСлов, которая читала бы и снова выдавала предложение, указывая общее число слов в предложении и длину самого длинного из них. Более точно: Входные данные На входе программы одно предложение, которому, возможно, предшествует некоторое количество пробелов. Предложение есть последовательность слов, оканчивающаяся точкой. Слово есть последовательность букв, оканчивающаяся одним или несколькими пробелами. Пример входных данных: Как облако брожу я одинок . Замечание Из приведенных определений следует, что между последним словом и точкой имеется по крайней мере один пробел. Результаты Предложение нужно выдать на выходе в том же виде, как оно получено на входе, снабдив его дополнительной информацией в последующих строках. Возможный выходной формат, иллюстрирующий вышеприведенные входные данные: Получено предложение: Как облако брожу я одинок . Предложение состоит из 5 слов. Самое длинное из них состоит из 6 букв. Для упрощения задачи предположите, что: данные на входе всегда корректны всякий символ, отличный от ’’пробела” и ’’точки”, является буквой точка не может встретиться иначе, чем в конце предложения предложение помещается в одной строке. Для иллюстрации данной темы и последующих, рассмотрим процесс разра- ботки по спецификациям в табл. 9.1 программы, которую мы назовем СчетчикСлов. Программа должна читать предложение и выводить его в том же виде, указывая общее число слов в нем и размер самого длинного из них. Обра- батываемые предложения необычны в одном отношении: между последним ело-
Гл.9. Предотвращение, обнаружение и исправление ошибок 159 вом предложения и заключительной точкой должен находиться по крайней мере один пробел. Это допущение, как и те, что приведены в завершение специфика- ции, сделаны, чтобы облегчить задачу. Может показаться, что думать о тестовых данных не придется до тех пор, пока программа не будет готова к тестированию. В действительности же разумно подобрать хотя бы некоторые из тестов в самом начале, еще до разработки от- дельных частей программы. Само размышление о тестовых данных часто об- наруживает такие аспекты проблемы, которые иначе можно было упустить. Их можно учесть при написании программы и избежать тем самым переделки каких-то ее частей в дальнейшем. Тестовые данные, разумеется, должны быть пополнены, если программисту становится ясно, что нужны дополнительные проверки. Заманчиво выбрать в качестве теста первое, что придет в голову. Так, для нашей программы мы могли бы взять предложение, которое приведено в специ- фикации: Как облако брожу я одинок . Однако это было бы недальновидно по двум причинам. Прежде всего предложе- ние это чересчур длинно, во всяком случае для ручной проверки, когда работа программы будет прослеживаться на бумаге. Во-вторых, что более важно, вряд ли можно ограничиться одним тестом. Гораздо разумнее выбрать несколько на- боров тестовых данных, как можно более простых, но наиболее типичных сре- ди всех разновидностей данных, которые могут встретиться. Как правило, в тестовые наборы включаются и корректные, и некорректные данные. Таблица 9.2. Три простых набора тестовых данных для программы СчетчикСлов Тест Входные данные Примечание 1 АВЧГ.# Нет начальных пробелов и самое длинное слово - первое. 2 лтвсл.# Имеются начальные пробелы и самое длинное слово - не первое. 3 .# Ни одного слова в предложении. Замечание. Символ ~ - обозначает пробел, символ # - маркер конца строки. Для программы СчетчикСлов мы можем отобрать предложения, показан- ные в табл. 9.2. Все предложения очень короткие, и ’’слова” в них не являются словами в обычном смысле. В то же время предложения тщательно подобраны с тем, чтобы охватывались следующие случаи: а) предложение без начальных пробелов; б) предложение с несколькими начальными пробелами; в) предложение, в котором самое длинное слово - первое; г) предложение, в котором самое длинное слово - не первое; д) предложение, в котором самое длинное слово - последнее; е) предложение, не содержащее ни одного слова. В окончательном виде наша программа должна правильно обрабатывать все такие предложения. Тестовые данные не включают ни одного случая некор-
160 Гл.9. Предотвращение, обнаружение и исправление ошибок ректных данных, поскольку в спецификации разрешено ограничиться коррект- ными входными данными. Среди тестов, однако, имеется один, который можно назвать ’’предельным случаем”. Предельные случаи, проверяющие программу на предельно маленьком или предельно большом наборе данных, особенно важны при обнаружении ошибок. На стадии тестирования программы, когда объем вычислений не столь су- ществен, поскольку работу выполняет компьютер, можно использовать более близкие к реальным предложения, сохраняя все разнообразие их особенностей. Уже из этого простого примера ясно, что подбирать тестовые данные сле- дует продуманно. Чтобы не опасаться упустить из виду что-то важное, полезно предпринять еще ряд шагов. Во-первых, программист может получить реальные данные у потенциаль- ного пользователя программы. Это не всегда возможно, когда вы только осваи- ваете программирование, однако в других ситуациях такое вполне реально. Получаемые данные большей частью бывают тривиальны, но порой содержат нечто не предусмотренное программистом. Во-вторых, можно каким-либо способом породить случайный набор вход- ных данных. В численной задаче можно прибегнуть к генератору случайных чисел. Для задачи текстовой обработки уместно подобрать порции текста из газет, технической документации или из книг. Цель прежняя - получить дан- ные, выявляющие ошибку. Наконец, ”ум хорошо, а два - лучше”, и существенную пользу может принести совет коллеги по поводу того, как сформировать тестовые данные, особенно если это опытный программист. Ясно одно: время, затраченное на обдумывание вероятных проблем до написания программы, с лихвой окупится позднее. В то же время изменение завершенной программы с целью добавить не- что ранее упущенное может вылиться в крайне трудоемкую процедуру. 9.4. Обнаружение ошибок: ручная проверка Первый принцип в обнаружении ошибок: чем раньше ошибка найдена, тем меньше она принесет неприятностей, тем дешевле она обойдется. Исправленная в программе на ранней стадии разработки, она достаточно безобидна. Если же ошибка сохраняется к началу использования программы, ее устранение может дорого стоить, особенно когда речь идет о промышленной или коммерческой сфере, где затрагиваются интересы тысяч пользователей. В вашем случае, в условиях обучения программированию, быстрое обнаружение ошибки экономит ваше время и силы. Обсуждая программу Прогрессия в гл.8, мы отмечали, что несколько ошибок присутствовали в ней уже в начале разработки. Найди мы их вовремя, нам не пришлось бы, видимо, писать программу дважды. Обнаружение ошибок, таким образом, должно быть почти непрерывным процессом, начинающимся задолго до момента запуска программы. Как только вы сели за стол и приступили к созданию программы, началось и обнаружение ошибок с помощью карандаша и бумаги. Этот процесс и называется ручной проверкой. Чтобы получить о нем представление, посмотрим, как проектируется программа СчетчикСлов, описанная в предыдущем разделе (табл. 9.1). Первый вариант основной процедуры, названной processonesentence, показан в табл. 9.3. Изучите ее внимательно. В табл. 9.3 отражено одно из решений - ввести 3 переменные: wordlength totalwords longestsofar - длина очередного слова; - общее число слов; - длина самого длинного слова
Гл.9. Предотвращение, обнаружение и исправление ошибок 161 Таблица 9.3. Проектирование в программе СчетчикСлов основной процедуры processonesentence Шаги разработки Примечания processonesentence -> begin вывести заголовок; ввести и разобрать предложение; выдать результаты; end ввести и разобрать предложение -> пока не конец предложения выполнять begin ввести очередное слово и найти его длину; скорректировать общее число слов;: integer, если слово к этому моменту самое длинное, учесть это; ввести пробелы после слова end var wordlength, totalwords, longestsofar не конец предложения -> inpuT о *.* ввести очередное слово и найти его длину оформить как отдельную процедуру processoneword, которая длину слова посылает в wordlength скорректировать общее число слов -> totalwords totalwords+l если слово к этому моменту самое длинное, учесть это -> if wordlength > longestsofar then longestsofarwordlength ввести'пробелы после слова оформить как отдельную процедуру copyspaces выдать результаты -> begin вывести значение totalwords с пояснением; вывести значение longestsofar с пояснением end Кроме того, решено оформить части основной процедуры как две отдельные про- цедуры следующего назначения: processoneword - ввести и передать на выход очередное слово, подсчитать его длину и послать в переменную wordlength, завер- шить процедуру, когда input' содержит пробел, идущий сразу после слова; copyspaces - ввести и передать на выход последовательность пробе- лов, завершить процедуру, когда input' содержит пер- вый не-пробел после пробелов. Исходя из данной таблицы разработки сделан первый набросок процедуры processonesentence (табл. 9.4). 6 Заказ № ПК)
162 Гл.9. Предотвращение, обнаружение и исправление ошибок Теперь пора приступить к ручной проверке. В первом наброске могут быть незначительные просчеты, которые легко исправить. Могут, однако, встретиться и более серьезные огрехи, делающие необходимым существенное переосмысле- ние программы. Таблица 9.4. Первый вариант процедуры processonesentence procedure processonesentence; { Первый вариант } var wordlength, totalwords, longestsofar : integer, begin writein (’Очередное предложение :’); writein; while input* <> *.* do begin processoneword; totalwords totalwords + 1; if wordlength > longestsofar then longestsofar wordlength; copyspaces end; writein СВ предложении слов : totalwords : 1) ; writein С Самое длинное содержит longestsofar : 1, ’ букв.') end; Для каждого проектируемого кусочка программы нужно сделать три следующие вещи. Ручная проверка 1 : проверить инициализацию всех переменных. Ручная проверка 2 : проверить завершение всех циклов. Ручная проверка 3 : проследить, как работает вариант программы на тестовых данных. Поскольку при объявлении переменной ее значение не определено, каждая переменная должна получить нужное значение до того, как она будет использована в выражении. Это называется инициализацией переменной. Инициализацию можно произвести посредством: а) оператора read или б) оператора присваивания, в котором переменная фигурирует в левой ча- сти и отсутствует в правой. Забыть инициализировать переменную - очень распространенная ошибка, и, к сожалению, большинство систем программирования не обнаруживает ее ни во время компиляции программы, ни во время ее исполнения. Столь же прискорбно и, пожалуй, удивительно то, что наличие такой ошибки в программе не всегда очевидно. Поэтому будьте внимательны! Следите за инициализацией перемен- ных в ваших программах. Снова взглянем на табл. 9.4 и проверим, все ли в порядке в процедуре processonesentence. По-видимому, не все: переменная wordlength не инициализируется, причем логически правиль- нее сделать это не здесь, а в процедуре processoneword', переменная totalwords впервые появляется в операторе totalwords :« totalwords + 1 и в момент его первого исполнения не будет определена; ей нужно присвоить нулевое начальное значение до входа в цикл;
Гл.9. Предотвращение, обнаружение и исправление ошибок 163 переменная longestsofar впервые появляется в выражении wordlength > longestsofar и в момент его первого исполнения не будет определена; ей также нужно присвоить нулевое начальное значение снаружи цикла. Неплохой улов ошибок для одной простой проверки! Чтобы исправить их, нужно вставить операторы totalwords :в 0; longestsofar :» 0; непосредственно перед оператором цикла. Второй вариант программы показан в верхней части табл. 9.5. Обнаруженные сейчас ошибки, вероятно, не возникли, если бы первый на- бросок программы в верхнем отделе табл. 9.3 выглядел так: begin вывести заголовок; инициализировать нужные переменные; ввести и разобрать предложение; выдать результаты; end Хотя это пишется на стадии, когда программист очень слабо представляет себе, какие переменные ’’нужные”, ему во всяком случае будет ясно, что нечто долж- но быть инициализировано. Поэтому совсем нелишне вставлять оператор инициализировать нужные переменные; в начале всякого отдельного кусочка вашей программы. Инициализация может и не потребоваться, но чаще всего она нужна. Проверив инициализацию переменных, перейдем к проверке циклов. Наша цель - установить правильное завершение всех циклов. Ошибка здесь может привести к зацикливанию - бесконечному выполнению цикла. Другая частая ошибка состоит в том, что тело цикла выполняется на один раз больше или на один раз меньше, чем требуется. На этапе ручной проверки эти нелады, как правило, устраняются при детальном разборе каждого цикла. В процедуре processonesentence только один цикл - оператор ’’пока”, выполняемый под условием: input* <> ’.’ поэтому выполнение цикла завершится, когда это логическое выражение станет ложным, т.е. при input* = ’.’ Можем ли мы быть абсолютно уверены, что рано или поздно в начале оче- редного цикла input* будет содержать точку? Для нашего третьего набора тесто- вых данных (табл. 9.2) положительный ответ очевиден, поскольку input* = ’.’ при входе в процедуру. Для других наборов данных проблема упрощена благода- ря допущению о корректности входных данных, и мы во всяком случае можем быть уверены, что точка в конце концов встретится и к тому же на нужном мес- те. Точка, однако, может быть случайно обработана как буква или пробел в од- ной из процедур, вызываемых внутри цикла ’’пока”. Поскольку точке, когда она идет за одним или несколькими словами, всегда предшествует пробел, то ключе- вой является процедура copyspaces. Если написать ее так, чтобы в ней заведомо читались лишь пробелы, точка будет оставаться в переменной input* и цикл завершится правильно. 6*
164 Гл.9. Предотвращение, обнаружение и исправление ошибок Таблица 9.5. Ручная проверка процедуры processonesentence (во втором варианте) на первом наборе тестовых данных из табл. 9.2 procedure processonesentence., { Второй вариант } var wordlength, totalwords, longestsofar : integer, begin writeln ('Очередное предложение writeln', totalwords 0; longestsofar 0; while inpur <> do begin processoneword', totalwords totalwords + 1; if wordlength > longestsofar then longestsofarwordlength', copyspaces end; writeln ('В предложении слов : totalwords : 1); writeln ('Самое длинное содержит букв : longestsofar : 1) end; Входные данные ав;С2а# I 1111 Переменные wordlength totalwords longestsofar •) •> ? 0 0 2 1 2 1 2 Результаты Очередное предложение : АВ С В предложении слов : 2 Самое длинное содержит букв : 2 Замечания 1. Считывать точку и игнорировать остаток входной строки. 2. Выводить точку. 3. Перейти на новую строку в конце предложения и еще одну строку пропустить. Третья проверка - проследить работу процедуры на отобранных тестовых данных (табл. 9.2). Это можно сделать при условии, что нам точно известно, как работают ее отсутствующие пока части. В данном случае мы имеем уже точные определения отсутствующих частей - процедур processoneword и copyspaces. Вообще говоря, какой бы простой программа ни казалась, трассировку не следует проводить ”в уме”, без записи результатов. Только что спроектировав
Гл.9. Предотвращение, обнаружение и исправление ошибок 165 кусок программы, вы, естественно, уверены в его правильности, и эта уверен- ность, если вы ограничиваетесь только проверкой в уме, может помешать вам увидеть ошибки. Вы склонны мысленно работать за программу так, как вам это представляется, а не так, как она действительно работает. Можно, впрочем, записывать ход исполнения не столь детально, как это делалось в предыдущих главах. Достаточно: а) отмечать входные данные (если они есть) по ходу их чтения; б) записывать последовательные значения переменных; в) записывать результаты по мере их формирования (когда они есть); г) фиксировать обнаруженные ошибки. В нижней половине табл. 9.5 показан пример записи, которой ограничился бы автор. Входные данные приведены сразу за текстом процедуры, а несколько стрелочек под ними служат для указания на процесс чтения данных по мере ее исполнения. Вслед за входными данными выписаны последовательные значения различных переменных в форме, похожей на ранее рассмотренные таблицы трассировки с той разницей, что управляющая информация не записывается. Отдельно ведется запись результатов, причем именно в той форме, в какой они будут воспроизведены при печати и на дисплее. Огрехи, обнаруженные при трассировке, невелики. Позиция последней стрелочки под входным предложением сигнализирует о том, что точка и маркер конца строки не прочитаны. Выходной результат показывает, что предложение выводится без заключительной точки, а информация о числе слов в предложе- нии выводится вслед за ним в той же строке. Эти ошибки отмечены в нижней части таблицы. С помощью процедуры copyonecharacter, читающей один символ и выда- ющей его на выход, эти ошибки легко исправляются, что отражено в третьем варианте процедуры processonesentence, показанном в табл.9.6а. Таблица 9.6а. Третий вариант процедуры processonesentence procedure processonesentence, { Третий вариант } var wordlength, totalwords, longestsofar : integer, begin writein (’Очередное предложение writein; totalwords 0; longestsofar 0; while inpuC <> ’ do begin processoneword; totalwords totalwords + 1; if wordlength > longestsofar then longestsofar wordlength; copyspaces end; copyonecharacter; { точка } readin; writein; writein; writein (’В предложении слов : totalwords : 1); writein С Самое длинное содержит букв : longestsofar : 1) end { processonesentence }; Теперь мы имеем процедуру, успешно обрабатывающую первый набор данных. А что с остальными? В табл.9.66 отражен ход проверки исправленной процедуры на втором множестве данных из табл.9.2, и при этом обнаружена
166 Гл.9. Предотвращение, обнаружение и исправление ошибок более серьезная ошибка. Когда процедура processoneword вызывается в первый раз, текущий символ во входном файле не является первой буквой слова: это пробел, обычно служащий признаком конца слова. Таблица 9.6б. Ручная проверка процедуры processonesentence (в третьем варианте) на втором наборе тестовых данных из табл. 9.2 Входные данные ГОГ-# I ИИ Переменные wordlength totalwords longestsofar ? •> 0 6 0 1 1 2 1 2 3 2 Результаты Очередное предложение : А ВС В предложении слов : 3 Самое длинное содержит букв : 2 Замечания 1. Процедура processoneword вызывается, когда в input* пробел. 2. Если допустить, что processoneword правильно обрабатывает "пустое слово” (т.е. ничего не копирует и присваивает word length нуль), то получается, что значение totalwords увеличивается и в том случае, когда не должно бы. Действия, выполняемые процедурой processoneword, можно выразить такими словами: прочесть на входе и выдать на выход одно слово, вычислить его длину в переменной wordlength и завершить работу, когда inpuC содержит символ пробела, непосредственно следующий за словом. Что же должна делать процедура, когда ни одного слова нет? Хорошо составлен- ная процедура должна трактовать это как ’’нулевой вариант”. Она не должна ни считывать, ни выводить символы, а переменной wordlength должна присваивать нуль. Исходя из этого процедура processonesentence начинается со считывания ’’пустого слова”, после чего она готова правильно обработать остаток предло- жения. Общее число слов, однако, подсчитывается неверно. Оно на 1 больше, чем должно быть, потому что и пустое слово посчитано.
Гл.9. Предотвращение, обнаружение и исправление ошибок 167 Исправить эту ошибку можно несколькими способами, из которых неко- торые более предпочтительны. Рассмотрим три таких способа. Если идти от результата, то проблему можно усмотреть в том, что когда предложение начинается с пробелов, значение переменной totalwords больше, чем нужно. Может быть на этом и построить исправление, инициировав пере- менную totalwords следующим образом: if inpur - ’ ’ then totalwords :» -1 else totalwords :« 0; С этим изменением программа, конечно, даст на нашем тесте правильное число слов, однако такое исправление таит в себе два просчета. Прежде всего оно затемняет суть дела. Что значит ’’минус одно слово”? Одно то, что это бессмысленно, должно подсказать нам, что такой способ исправления не самый лучший. Мы скорее латаем прореху в программе, чем проникаем в суть вопроса. Второй просчет заключен в том, что в одном специальном случае программа даст все же неверный результат. Попробуйте указать, в каком. Вторая попытка исправления могла бы исходить из того, что ошибочно учитывается слово, не содержащее букв, чего можно избежать, заменив оператор totalwords :« totalwords + 1; на if wordlength > 0 then totalwords :» totalwords + 1; Такое исправление снимает проблемы во всех случаях, но это опять-таки довольно грубая заплата. Она заставляет проверять длину каждого слова только для того, чтобы избежать нулевого слова, встречающегося однажды в начале предложения, т.е. приводит к лишней работе. Действительный корень проблемы заключен в том, что процедура не учи- тывает должным образом начальные пробелы, несмотря на то, что они упомяну- ты в спецификации программы! Обе возможные структуры входных данных, с которыми мы имеем дело: а) слово пробелы слово пробелы ... слово пробелы точка б) пробелы слово пробелы слово ... слово пробелы точка можно правильно обработать следующим образом: учесть пробелы; пока не точка выполнять begin обработать слово; учесть пробелы; end при условии, что процедура ’’учесть пробелы” правильно работает в нулевом варианте, т.е. когда пробелы отсутствуют. Но похожая процедура copyspaces у нас уже есть. Если она будет правильно обрабатывать отсутствие пробелов, то ’’починить” процедуру processonesentence (табл. 9.6а) будет очень легко, добавив вызов copyspaces непосредственно перед оператором ’’пока” в дополнение к ее вызову в теле цикла. В качестве упражнения читателю предлагается проверить исправленную таким образом процедуру processonesentence на всех трех наборах тестовых дан- ных из табл. 9.2. Нелад в программе мы исправили, не прибегая ни к засылке
168 Гл.9. Предотвращение, обнаружение и исправление ошибок бессмысленного значения в totalwords, ни к неоправданной проверке длины каж- дого слова. Мораль сей басни: старайтесь избегать латания ошибок; пытай- тесь вникнуть в суть проблемы и должным образом разрешить ее. Проверив вручную основную процедуру, мы теперь можем перейти к со- ставлению трех отсутствующих пока процедур: processoneword, copyspaces и copyonespace. Все они весьма просты, и даже не очень опытный программист в состоянии написать их на Паскале без долгого предварительного планирования. Первые наброски каждой из них приведены в табл. 9.7. Простота этих процедур может вызвать искушение обойтись без их тестирования. Хорошая практика, однако, состоит в том, чтобы подвергать проверке даже самые очевидные куски программы; иногда в них встречаются ’’глупые” ошибки. Таблица 9.7. Первые варианты процедур processoneword, copyspaces и copyonecharacter для программы СчетчикСлов procedure processoneword; begin wordlength 0; while input* <> ” do begin copyonecharacter; wordlength wordlength + 1 end end { processoneword }; procedure copyspaces; begin . while input* - ” do copyonecharacter end { copyspaces }; procedure copyonecharacter; var ch: char; begin read (ch); write (ch) end { copyonecharacter }; Хотя процедура processoneword не имеет собственных переменных, мы уже отмечали, что она должна отвечать за инициализацию переменной wordlength. Она это и делает при условии, что переменная ей доступна. А здесь-то и лежит проблема! Wordlength пока что является локальной переменной процедуры processonesentence (табл. 9.6а) и потому недоступна процедуре processoneword. Есть несколько путей решения этой проблемы, но единственный нам пока известный, заключается в том, чтобы сделать переменную wordlength глобаль- ной. В окончательном варианте программы мы так и поступим. Если входной файл еще не пуст в тот момент, когда вызывается processoneword, цикл внутри процедуры должен завершиться. Это гарантируется тем фактом, что последний символ в любом текстовом файле, а именно маркер конца строки eol, читается как пробел. Простым прослеживанием, которое мы
Гл.9. Предотвращение, обнаружение и исправление ошибок 169 оставляем читателю, можно обнаружить, что процедура целиком работает пра- вильно как (а) при наличии хотя бы одного слова, так и (б) при отсутствии слов. Других возможностей, очевидно, нет. Copyspaces не имеет своих переменных, поэтому не нужно беспокоиться о присваивании начальных значений. Всегда ли правильно завершается ее цикл? Выход из цикла происходит, когда inpur <> ' ’ и таким образом цикл работает правильно и в случае, когда (a) input содержит не-пробел при входе в copyspaces, и в случае, когда (б) input при входе в copy spaces содержит пробел, но есть хотя бы один не-пробел, подлежащий счи- тыванию. Внимательное изучение структуры тех видов данных, которые должны об- рабатываться, и тех мест в процедуре processonesentence, где вызывается copyspaces, показывает, что одно или другое из приведенных условий будет выполнено, если только входные данные корректны. Без допущения корректно- сти данных правильная работа copyspaces не гарантирована, например, в случае, если пользователь забывает поставить точку в конце предложения. Другой вопрос, связанный с copyspacesz может ли эта процедура перено- сить в выходной файл также и точку? Выше мы отметили, что это существенно. Из текста процедуры видно теперь, что этого не происходит. Тело оператора цикла начинает выполняться, только когда input содержит пробел, и значит каждый копируемый символ обязательно является пробелом. Процедура copyonecharacter настолько проста, что поистине трудно напи- сать ее неверно. Однако много лет наблюдая за программистами-новичками, автор уверяет вас, что и здесь можно ошибиться! Наиболее вероятная ошибка - забыть объявить переменную ch. Наконец, мы готовы составить программу целиком. На Паскале, вследст- вие его блочной структуры и правил видимости, нужно делать это аккуратно, а затем дополнительно проверить. Как мы заметили в гл. 7 (с. 134), порядок, в котором подпрограммы должны быть описаны, выводится из схемы их взаимных обращений, в нашем случае следующей: СчетчикСлов processonesen tence copyspaces copyonecharacter processoneword copyon echaracter copyon echaracter Отсюда вытекает, что один возможный порядок описания процедур - тот, что показан в завершенной программе (листинг 9.1). В другом варианте могли быть переставлены copy spaces и processoneword, поскольку они друг друга не вызыва- ют. Переменная wordlength теперь является глобальной, как мы этого и потребо- вали выше, с тем, чтобы processonesentence и processoneword обе могли иметь к ней доступ. Очень легко ошибиться, соединяя вместе различные компоненты програм- мы, поэтому стоит сделать дополнительную ручную проверку. Уместны следую- щие проверки.
170 Гл.9. Предотвращение, обнаружение и исправление ошибок 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 program WordCounter (input, output); { Считает слова в предложении и находит длину самого длинного } var wordlength : integer; procedure copyonecharacter, var ch : char, begin read (ch); write (ch) end { copyonecharacter}; procedure copyspaces; begin while input* - ’ ’ do copyonecharacter end { copyspaces }; procedure processoneword; begin wordlength 0; while input* o’’ do begin copyonecharacter, wordlength wordlength + 1 end end { processoneword }; procedure processonesentence; { Третий вариант } var totalwords, longestsofar : integer; begin writeln (’Очередное предложение :’); writeln; totalwords 0; longestsofar 0; copyspaces; { если нет пробелов, ничего не делает } while inpur <> ’ . ’ do begin processoneword; totalwords totalwords + 1; if wordlength > longestsofar then longestsofar wordlength^ copyspaces end; copyonecharacter; { точка } readin; writeln; writeln; writeln СВ предложении слов : ’, totalwords : 1); writeln С Самое длинное содержит букв : ’, longestsofar : 1) end { processonesentence }; begin writeln (’♦*♦♦ РЕЗУЛЬТАТ РАБОТЫ СЧЕТЧИКА СЛОВ ***★')-, writeln; processonesentence; writeln; writeln (’♦♦*♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ end { WordCounter }. Листинг 9.1. Окончательный вариант программы СчетчикСлов
Гл.9. Предотвращение, обнаружение и исправление ошибок 171 Ручная проверка 4 : проверить, всегда ли операторы процедуры встречаются после описания процедуры. Ручная проверка 5 : проверить, что все остальные идентификаторы (напри- мер, констант и переменных) употребляются только в их области действия и после места определения. Ручная проверка 6 : проверить, нет ли синтаксических ошибок. В нашем случае программа ’’собрана” правильно, но не всегда это удается сделать сразу. Указанные проверки в каком-то смысле менее существенны, чем логичес- кие, обсуждавшиеся выше, поскольку синтаксические ошибки и неверное расположение описаний почти всегда обнаруживаются компилятором. Тем не менее есть свои плюсы в том, чтобы найти такие ошибки до того, как процедура будет введена в компьютер: а) вы экономите время, затрачиваемое на компиляцию программы; б) вы экономите время, необходимое для редактирования программы и удаления ошибок; в) вы избегаете опасности внесения новых ошибок при исправлении старых. Несколько минут, потраченных на проверки, едва ли можно рассматривать как пустую трату времени, даже если в вашем распоряжении эффективный компи- лятор и мощная система редактирования. Итак, программа СчетчикСлов получена в окончательном виде (лис- тинг 9.1). Она была обстоятельно разработана и тщательно проверена. Но совер- шенно ли свободна она от ошибок? Смелым будет программист, решительно отвечающий ”да”. Итак, мы переходим к тестированию программ. 9.5. Обнаружение ошибок: тестирование программы Тестирование программы включает в себя запуск программы на реальном процессоре с теми или иными входными данными. Мы обсудим два вида тестиро- вания: разрушающее и диагностическое. Разрушающее тестирование произво- дится над программой, которая считается правильной, с целью заставить ее дать сбой, отказ. Диагностическое тестирование имеет место в тех случаях, когда о программе известно, что она содержит ошибки, с целью их локализации. Разрушающее тестирование может показаться чисто негативным подходом. Гораздо более конструктивным - исходя из цели создания правильной програм- мы - выглядело бы обсуждение такой формы тестирования, которая могла бы по- служить для доказательства полной правильности программы без всяких огово- рок. К сожалению, такие тесты можно разработать разве что для самых простых программ. Классическое замечание на этот счет принадлежит профессору Эдсге- ру Дейкстре: ’’Тестирование доказывает наличие ошибок, но не их отсутствие”. Если мы добились сбоя программы, то тем самым доказано, что в ней есть хотя бы одна ошибка. Если сбоя добиться не удается, то еще ничего не доказано. Это говорит о том, что тестирование программ - процесс довольно неопределенный. Программы, применяемые в промышленной или коммерческой сфере, как правило, очень велики, и произвести сразу полное тестирование программы нереально. Поэтому сначала проверяются отдельные части, а затем тестируется программа в целом. Программа, написанная для замены существующей, может тестироваться путем параллельного использования со старой. Результаты, полу- ченные двумя способами, затем сопоставляются и все расхождения тщательно изучаются. При чтении этой книги вы, по-видимому, будете составлять только
172 Гл.9. Предотвращение, обнаружение и исправление ошибок короткие программы, и данный метод не подойдет. В большинстве случаев вы просто будете писать программу целиком, а затем проверять ее на нескольких наборах тестовых данных. Разрушающее тестирование в значительной степени похоже на трассиров- ку при ручной проверке за исключением того, что исполнение программы возла- гается на компьютер, а не на программиста. Так же, как и при ручной провер- ке, выбор тестовых данных очень важен. Обычно они должны включать кор- ректные и некорректные наборы данных для демонстрации правильной работы программы на каждом из них, и, кроме того, они должны внимательно отбирать- ся с тем, чтобы гарантировать проверку всех частей программы. Поскольку на этой стадии необязательно, чтобы вычисления были максимально короткими, можно брать данные, близкие к реальным. Выполнение тестов само по себе - без анализа результатов - бесцельно. Может появиться искушение отнестись к результатам тестирования поверхност- но в расчете на то, что все ошибочное всплывет само собой. Это не всегда так; ошибка в результатах может с не меньшей легкостью ускользнуть от вашего внимания, что и ошибка в программе. Где только возможно, следует в точности установить, каких результатов вы ожидаете перед запуском программы, а затем внимательно сопоставить их с реальными. К сожалению, для многих программ это требует очень кропотливой работы, если вообще возможно. Иногда лучшее, что можно сделать, это тщательно проверить некоторые ’’небольшие” наборы тестовых данных, для которых могут быть посчитаны конечные результаты, а затем, если на них программа работает правильно, предположить, что результа- ты из ’’большого” набора также будут верными. В таких допущениях есть своя опасность, но иногда они неизбежны. Если тест показывает наличие ошибки, то следующий шаг - найти ее. Это не всегда просто. Порой результаты тестирования уводят программиста в поис- ках ошибки в другую часть программы. Иногда ошибка ’’кричит” вам в лицо, а вы ее просто не замечаете. Поразительно, насколько трудно бывает выследить проявившуюся ошибку и насколько очевидна она, когда обнаружена! Здесь-то и пригодится нам диагностическое тестирование. В какой мере необходимо диагностическое тестирование, зависит от того, сколько информации об исполнении программы можно получить от вычислите- льной системы. Многие языки высокого уровня обладают средствами разработки программ, которые обеспечивают выдачу такой информации в большом объеме. Следующие услуги являются типичными: а) средства трассировки, выдающие при выполнении программы особой формы листинг, в чем-то похожий на ручную трассировку; б) "посмертные выдачи”, подробно описывающие состояние программы и ее переменных после завершения исполнения и особенно полезные, если программа закончилась ошибкой во время исполнения; в) средства профилирования, которые показывают на листинге программы, сколько раз исполнялся каждый оператор, и порой могут дать наиболее содержательные сведения о том, что же делается в программе. Конкретная форма и степень доступности этих средств может сильно варьи- роваться, поэтому они не обсуждаются здесь в деталях. Советуем просто обращать внимание на наличные средства разработки программ в вашей вы- числительной системе и пользоваться ими. Если системные средства, дающие дополнительную информацию, отсутст- вуют или когда полученных данных недостаточно, чтобы найти ошибочное место в программе, то можно прибегнуть к наиболее простой и распространенной методике отслеживания ошибки, которая состоит в том, что в программу временно вставляются операторы вывода (write), так что она сама по ходу исполнения выдает дополнительную информацию. Такие операторы в начале и
Гл.9. Предотвращение, обнаружение и исправление ошибок 173 конце процедуры или внутри цикла могут открыть вам глаза на происходящее в программе. При этом могут обнаружиться не только логические ошибки. Иногда вы вдруг находите, что старина-процессор трудится гораздо больше, чем вы ожидали, и это заставит подумать, как получше организовать вычисления. И еще один совет. Во многих языках программирования оператор можно "удалить" из программы, превратив его в комментарий. Поэтому если уж вы потрудились вставить дополнительные диагностические операторы, то можно оставить их в программе на случай, если они снова понадобятся, переведя их в пассивное состояние. В Паскале для этого нужно просто заключить оператор в скобки { } или (* *). В этом обсуждении неоднократно говорилось о неопределенности, о не слишком высокой надежности, присущей процессу тестирования программы. Программисты постоянно основываются на результатах тестирования, когда су- дят о правильности программ, но вновь и вновь их заключения оказываются не- верными. Несмотря на это, многие продолжают свято верить в тестирование как средство обнаружения изъянов в программе. На практике кропотливая ручная проверка, пусть она и не позволяет с гарантией найти все ошибки, является гораздо более надежным методом. Без сомнения, вашей целью должна быть тщательная разработка программы и кропотливая ручная проверка ее с тем, чтобы программа работала правильно с первого раза. При такой установке на процесс программирования вас может приятно удивить, как быстро вы прибли- зитесь к желанной цели. Отсюда не следует, что тестировать программы не надо. Тестируйте их ради бога как можно тщательнее. Но самые завуалированные ошибки вы скорее всего найдете за рабочим столом, а не за компьютером. 9.6. Исправление ошибок Конец главы мы посвящаем теме исправления ошибок, важность которой больше, чем можно предположить из размеров этого раздела. Когда ошибка найдена, может показаться, что все проблемы позади, но иногда это оказывается лишь началом всех проблем. Весьма нередко исправив один нелад в программе, вы потом обнаруживаете, что "исправление” привело к новым ошибкам. Всякий опытный программист подтвердит вам, что с большей вероятностью ошибка вносится при изменении программы, а не при первоначальном составлении. Тут следует усвоить два правила. Первое: перед всяким изменением программы нужно всесторонне обду- мать, к чему оно приведет. Опасность состоит в том, что, столкнувшись с неверной работой какого-либо куска программы на одном из тестов, вы внесете изменение, которое на данном тесте действительно исправляет работу про- граммы, но вызывает ошибочную работу в других случаях или влечет ошибки в других частях программы. Опасность уменьшается, если программа имеет ’’хорошую” структуру и ясную запись, поскольку в этом случае удается лучше проследить все следствия какого-либо изменения. Итак, никогда не вносите исправления поспешно или когда сомневаетесь в их конечном эффекте. Второе правило: когда в программу внесено изменение, то все ранее про- шедшие на ней тесты нужно провести заново. Очевидно, это разумная предо- сторожность против появления новых ошибок. Эти правила лучше усвоить сразу, иначе вы придете к ним позже на своем горьком опыте!
174 Гл.9. Предотвращение, обнаружение и исправление ошибок Упражнения 9.1. Входной файл содержит целое число 7V, за которым следует еще N целых чисел. Напишите программу Мин&Макс, которая читала бы эти данные и печатала бы наибольшее и наименьшее из N целых чисел (само N сюда не включается). Проверьте работу своей программы вручную, "запустив” ее три раза на следующих наборах данных: О 1 7 4 3 9 7 9 Потом перенесите ее на компьютер и проверьте на собственных входных данных. 9.2. Спроектируйте программу ТаблицаС те пеней, которая выводит на печать таблицу целых чисел и их степеней вплоть до пятой. Несколько первых строк таблицы, например, могут быть следующими: N квадрат куб четвертая пятая 1 1 1 1 1 2 4 8 16 32 3 9 27 81 243 Программа должна работать на любой стандартной Паскаль-системе без модификации, а таблица должна заканчиваться последней полной строкой, которую допускает имеющийся процессор. Проверьте программу вручную, считая, что максимальное целое maxint равно 63. 9.3. Напишите программу ПоискСлова, которая читала бы букву алфавита, а затем предложение и печатала бы список всех слов в предложении, которые начинаются с данной буквы. Она должна также давать общее число остальных слов предложения. Точные спецификации таковы: Входные данные Буква будет появляться отдельно в первой строке входных данных. Предложение является последовательностью слов, заканчивающейся точкой. Слово - последовательность "букв”, заканчивающаяся одним или несколькими пробелами. Результаты Вслед за поясняющим заголовком должны перечисляться одно под другим слова, начинающиеся с данной буквы; в заключение должна идти фраза вида Число других слов в предложении хх. Предположите, что (а) данные корректны; (б) всякий символ, отличный от ’ ’ и является буквой; (в) точка не может встретиться иначе, чем в конце предложения (и ей всегда предшествует пробел). 9.4. Результаты опроса населения записаны в файле данных. Первая строка содержит количество учтенных лиц, а в каждой из следующих дается такая информация о людях: ИМЯ : цепочка символов, заканчивающаяся двоеточием ВОЗРАСТ : целое в интервале 1..120 СТАТУС : ’Н’ для неженатых (незамужних), ’Б’ для состоящих в браке, ’Р’ для разведенных, ’О’ для живущих отдельно друг от друга Пример правильной строки: РОБЕРТ ДЖ.БЕЛЛ : 42 Н (Заметьте, что между частями данных может быть несколько пробелов.) Принимая, что данные в файле проверены, напишите программу Survey (Отчет), читающую входной файл и выдающую следующие результаты: (а) число учтенных лиц; (г) число тех, кто состоит или состоял в браке; (б) число тех, кому меньше 21; (д) число тех, кто старше 21 и разведен; (в) число тех, кому больше 21; (е) число тех, кто моложе 21 и состоит в браке.
10 БУЛЕВЫ ВЫРАЖЕНИЯ И ПЕРЕМЕННЫЕ Проработав предыдущие главы, вы должны были уяснить себе, что цент- ральную роль играют в программах логические (или булевы, булевские) выра- жения. В определенных ключевых точках программы управление зависит от того, истинно или ложно некоторое булевское выражение. В этой главе мы об- судим, как запоминать булевы выражения для использования в дальнейшем и как строить более мощные логические выражения с помощью трех операций not, and, or, которые впервые были без пояснений употреблены в гл. 6 (с. ЮЗ-104)1. По ходу дела мы столкнемся с законом Моргана для получения обратных буле- вых выражений и научимся записывать в виде булевых выражений особого рода комментарии, называемые утверждениями. 10.1. Булевы переменные Мы уже знаем, как хранить числа и отдельные символы при помощи пере- менных типа integer и типа char. Для хранения логических выражений нам ну- жен и соответствующий тип данных, носящий в стандартном Паскале название булевского (Boolean). Этот термин, кстати говоря, происходит от имени ма- тематика Джорджа Буля, опубликовавшего в 1854 г. работу ”Исследование законов мышления”. В ней вводилась алгебра множеств и рассматривались свойства логических суждений, которые известны ныне как Булева алгебра и которые во многих отношениях являются основой как программного обеспече- ния, так и оборудования современных вычислительных систем. С каждым типом данных связывается диапазон значений. Поскольку логи- ческое выражение может иметь только два разных значения, то диапазон типа Boolean есть просто false true причем значение false (ложь) по определению меньше значения true (истина), а слова false и true являются предопределенными именами констант. Это констан- ты логического типа аналогично тому, как 1 2 3 4 и т.д. суть константы типа integer, а ’А’ ’В’ ’С’ ’D’ и т.д. 1 Значение этих операций: not - "не”, and - ”и”, or - ”или” - Примеч. пер.
176 Гл. 10. Булевы выражения и переменные - константы типа char. Таким образом, если в программе имеется переменная типа Boolean^ она в любой момент может: а) быть неопределенной; б) хранить значение false} в) хранить значение true. Разберем в качестве примера процедуру processoneprogression) приведен- ную на листинге 10.1. Это исправленная версия основной процедуры из програм- мы Прогрессия! (с. 151). Вы помните, что Прогрессия! выдает арифметическую прогрессию и что переменные term) difference) numberofterms описаны в основной программе и имеют тип integer. 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 procedure processoneprogression; const errorl - 'НЕКОРРЕКТНЫЕ ДАННЫЕ:РАЗНОСТЬ ДОЛЖНА БЫТЬ >- 0.’; error2 - 'НЕКОРРЕКТНЫЕ ДАННЫЕ:ЭЛЕМЕНТОВ ДОЛЖНО БЫТЬ > 0.’; var OKdifference, OKnumberofterms : Boolean; begin { чтение и вывод данных } read (term, difference, numberofterms); writeln ('ПЕРВЫЙ ЭЛЕМЕНТ term'd); writeln ('РАЗНОСТЬ ПРОГРЕССИИ differenced); writeln ('ЧИСЛО ЭЛЕМЕНТОВ numberoftermsd); writeln; { выдать прогрессию, если данные корректны } OKdifference difference >- 0; OKnumberofterms numberofterms >- 0; if not OKdifference < 0 then writeln (errorl); if not OKnumberofterms < 0 then writeln (error2) ; if OKdifference and OKnumberofterms then writetheprogression end { processoneprogression}; Листинг 10.1. Видоизменение процедуры processoneprogression (с. 151) с использованием переменной типа Boolean Одна из задач процедуры - определить правильность входных данных, для чего она в теперешнем варианте пользуется несколькими булевыми перемен- ными. В процедуре есть три фрагмента, на которые следует обратить особенно пристальное внимание. Первый из них, в строке 51, показывает, что булевы переменные описыва- ются как обычные переменные: OKdifferencet OKnumberofterms ; Boolean причем в качестве типа используется предопределенный тип Boolean. Наимено- вание типа не обязано, конечно, начинаться с прописной буквы. Одинаково правильными являются следующие варианты записи: Boolean boolean BOOLEAN В описании вводятся переменные OKdifference и OKnumberofterms. Далее обратите внимание на строки 60-61, где показано, как булевы пере- менные получают значения. К примеру, в строке 60 стоит оператор присваива- ния OKdifference := difference >= 0;
Гл. 10. Булевы выражения и переменные 177 Как во всяком операторе присваивания, справа от знака := стоит выражение (с. 110). При выполнении оператора это выражение вычисляется, и его значение присваивается переменной, которая стоит слева от знака :=. Поскольку выра- жение difference >= 0 является булевым, его значение либо истинно, либо ложно, и значит может быть присвоено булевой переменной OKdifference. Всякий оператор присваивания с булевой переменной в левой части является допустимым, если выражение в правой части является булевым. Наконец, в строках 62-65 показано, что булевы переменные, получив на- чальные значения, могут, как и любые другие, употребляться внутри выраже- ний. Смысл этих четырех строк программы можно, пожалуй; понять, даже не заглядывая в следующий раздел, где дается определение символов not и and. Работает этот фрагмент так же, как соответствующий фрагмент в прежнем ва- рианте программы (с. 151). 10.2. Булевы операции Булевы выражения, такие, как cost > 0 termswritten <> numberofterms многократно употреблялись и до этой главы, но они включали в себя только переменные, константы и операции сравнения. С помощью же трех логических (булевых) операций not, and, or можно строить гораздо более мощные булевы выражения. Таблица 10.1. Определение операции not р not р false true true false Булевская операция not имеет единственный операнд, который является булевым выражением. Операция используется для отрицания логического выра- жения, что похоже на употребление знака ’’минус” для отрицания арифметичес- кого выражения. Действие операции полностью описано в табл. 10.1, называе- мой таблицей истинности. Если, к примеру, переменная OKdifference - типа Boolean, то выражение not OKdifference является ложным, если OKdifference истинно, и истинным, если OKdifference ложно. Если, далее, переменная number имеет значение 25, то
178 Гл. 10. Булевы выражения и переменные not (number > 0) - not (25 > 0) - not (true) « false и вообще, not (number > 0) равносильно number <e 0. У каждой из операций and и or два булевых операнда, и применяются они в выражениях вида (number > 0) and (number < 10) или (ch = ’ ’) or (ch « ’,’) Действие этих операций показано в другой таблице истинности (табл. 10.2). В ней булевы операнды обозначаются с помощью символов р и q, которые обычно употребляются в логике (так же, как х, у и z обычно применяются в алгебре). С помощью таблицы истинности можно вычислять выражения, содержа- щие and и or. Если, например, переменная number снова равна 25, a ch содер- жит запятую, то (number > 0) and (number < 10) « (25 > 0) and (25 < 10) = true and false = false и (ch = ’ ’) or (ch = ’,’) = (’,’ = ’ ’) or (’,’ = ’,’) e false and true = true Нужно заметить, в частности, что случаи истинности р или q включают и случаи, когда истинны оба операнда. Таким образом, операция or в Паскале является, как сказал бы логик, включающим ’’или”. Таблица 10.2. Определение операций and и ог р Q p and q P or q false false false false false true false true true false false true true true true true Вспомним, какую роль операции not, and и or играют в синтаксисе выра- жений языка Паскаль (гл. 6, с. 103-104). Основные моменты здесь следующие: а) определение множителя содержит альтернативу not множитель', б) and входит в синтаксический класс операция умножения; в) or входит в синтаксический класс операция сложения. Вследствие этого булевы выражения строятся очень похоже на арифметические. Так, например, по своей природе выражения
Гл. 10. Булевы выражения и переменные 179 р or q and г и р and q or г аналогичны арифметическим выражениям а + b * с и а* Ь + с В каждом из этих выражений для определения порядка вычислений приме- няются правила старшинства операций. Теперь нужно вспомнить, что правила старшинства вытекают из синтаксических описаний. Поскольку not входит в определение множителя, and фигурирует как операция умножения в определении терма, a or является операцией сложения в определении простого выражения, то старшинство этих операций следующее: not and or иначе говоря, not имеет высший приоритет, and трактуется так же, как арифметическое умножение, or - как арифметическое сложение. Поэтому если, скажем, р = true, q = false, г = false то получим: р or q and г = true or false and false = true or false = true и p and q or r = true and false or false » false or false = false и not p or q or not r = not true or false or not false - false or false or true = false or true = true Из последнего примера видно, что операция not может идти сразу за дру- гой булевской операцией. В отличие от этого две арифметические операции в арифметическом выражении никогда не встречаются рядом. 10.3. Скобки в булевых выражениях В стандартном Паскале круглые скобки используются в логических выра- жениях для двух целей. Во-первых, если подвыражение содержит отношение, то такое подвыражение нужно заключить в скобки. Выражения (number = 0) and (number = 100) и datavalid and (ch - ' ’) являются правильными, но те же выражения без скобок:
180 Гл. 10. Булевы выражения и переменные number = 0 and number = 100 и datavalid and ch = ’ ’ недопустимы, хотя они, возможно, приемлемы в других языках программиро- вания. Во-вторых, скобки применяются, чтобы ’’пересилить” обычное старшинст- во операций not, and и or. Например: not ip or q or r) означает, что отрицанию подвергается целиком выражение р or q or г. Подобно этому в выражении (р or q) and г сначала вычисляется подвыражение р or q. 10.4. Законы де Моргана При разработке или тестировании программы часто необходимо получить отрицание логического выражения. Для этой цели есть два полезных правила, которые в нотации языка Паскаль можно выразить следующим образом: not (р or (?) « not р or not q и not (p and q} = not p or not q По существу, эти правила были известны с XIV в., но теперь связываются с английским математиком XIX в. Аугустусом де Морганом, впервые выразившем их в математической форме, и носят название законов де Моргана. Эти правила позволяют легко вычислить отрицание любого булевского вы- ражения. Приведем несколько примеров: a) not (р and q and г) = not ((р and q) and r) = not (p and q) or not r = (not p or not q) or not r = not p or not q or not r 6) not (p or q or r) = not ((p or q) or r) = not (p or q) and not r = (not p and not q) and not r = not p and not q and not r в) not (p and q or r) e not ((p and q) or r) = not (p and q) and not r = (not p or not q) and not r r) not (p or q and r) = not (p or (q and /•)) = not p and not (q and r) « not p and (not q or not r) Обратите внимание, что в заключительных выражениях примеров в) и г) нужно сохранить скобки, чтобы обеспечить выполнение операции or перед операцией and. 10.5. Ввод и вывод булевых значений В стандартном Паскале переменная типа Boolean не может использоваться как параметр в процедурах ввода read и readin. Следовательно, нельзя непосред- ственно ввести в программу булевы константы false или true. Это неудобство, но небольшое: если необходимо ввести значение типа ”истина/ложь” или ”да/нет”, можно представить его во входном файле в виде одиночного символа, например: ’F’ вместо значения false и ’Г’ вместо true
Гл. 10. Булевы выражения и переменные 181 или W’ вместо значения false и ’ У5 вместо true или ’О’ вместо значения false и ’ Г вместо true. Одиночный символ можно прочитать в переменную типа char и затем присвоить соответствующее значение логической переменной следующим образом: read (ch); р := ch « ’Г’; где ch переменная типа char, ар- упомянутая логическая переменная. Перемен- ной р будет присвоено истинное значение, если прочитанный символ есть Т, и ложное - в противном случае. Выводить значение логического выражения можно с помощью операто- ров write и writeln. Например, оператор write (х : 8, х > 0 : 8, х > 100 :8) выводит значения трех выражений: х х > 0 х > 100 каждое из которых занимает при выводе поле шириной 8 знаков. При х, равном 10, последовательность выдаваемых символов будет следующей: ------1(Г—TRUE—FALSE с той оговоркой, что в некоторых Паскаль-системах две булевы константы будут выводиться строчными буквами. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 program TruthTable (input, output)', { Выдает таблицу истинности для демонстрации ввода и вывода Булевых значений, а также реализацию логических операций эквивалентности, исключающего ИЛИ и импликации с помощью ’<>’ и } var line : integer, ch : char, p,q : Boolean', begin writeln (’♦*** РЕЗУЛЬТАТЫ ПРОГРАММЫ ТАБЛИЦА ИСТИННОСТИ ♦♦♦♦’); writeln', writeln ('p' : 5, 'q' : 9, ’p and q' : 12, ’p or q' : 9, ’p - q' : 8, ’p <> q' : 9, ’p <= q' : 9); writeln', line 4; while line <> 0 do begin line line - 1; read (ch)', p ch - *7”; read (ch)\ q ch - 'T\ writeln (p : 7, q : 9, p and q : 9, p or q : 9, P “ q - 9, p <> q : 9, p <- q : 9); readln end; writeln', writeln (’**** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ****’) end { TruthTable }. Листинг 10.2. Текст программы ТаблицаИ стинности
182 Гл. 10. Булевы выражения и переменные Программа ТаблицаИстинности (листинг 10.2) иллюстрирует эти особен- ности языка. В строках 21 и 22 вводимые значения преобразуются в логические переменные р и q. Операторы write в строках 23 и 24 выводят значения семи различных булевских выражений, содержащих переменные р и q. Программа требует ввода данных в четырех строчках по два символа в каждой. При усло- вии, что вводятся данные, приведенные в начале табл. 10.3, будут получены ре- зультаты, показанные в нижней части этой таблицы. Если вы знакомы с фор- мальной логикой и вам интересно, как в Паскаль-программе можно манипули- ровать логическими выражениями, можете убедиться, что операции сравнения Паскаля =, <>, <в в последних трех столбцах таблицы соответствуют операциям формальной логики: эквивалентности, исключающему "или" и импликации. Таблица 10.3. Входные данные и результаты программы ТаблицаИстинности Входные данные FF FT TF ТТ Результаты **** РЕЗУЛЬТАТЫ ПРОГРАММЫ ТАБЛИЦА ИСТИННОСТИ **** р q p and q P or q p = q P <> q P <= q FALSE FALSE FALSE FALSE TRUE FALSE TRUE FALSE TRUE FALSE TRUE FALSE TRUE TRUE TRUE FALSE FALSE TRUE FALSE TRUE FALSE TRUE TRUE TRUE TRUE TRUE FALSE TRUE **** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ **** 10.6. Булевы выражения и циклы Все до сих пор рассматривавшиеся нами циклы управлялись единственным условием. К примеру, while inpuC <> ’ ’ do ... или while termswritten <> numberofterms do ... Наличие булевских операций not, and и or позволяет нам записывать циклы, выполнение которых зависит от нескольких условий. В этом разделе мы увидим два примера таких циклов. В первом из них, кроме того, демонстрируется интересный ’’обратный” способ построения оператора цикла ’’пока”, а во втором дается методика выхода из цикла при возникновении ’’аварийной” ситуации. В качестве первого примера возьмем переделанную процедуру processoneword, которая входила в программу СчетчикСлов (с. 170), сняв ограничение, согласно которому за каждым словом должен идти пробел. Теперь
Гл. 10. Булевы выражения и переменные 183 мы считаем, что за словом следует пробел, запятая или точка. Новая процедура специфицирована и построена в табл. 10.4. Таблица 10.4. Измененная спецификация и разработка процедуры processoneword Разработать на Паскале процедуру под названием processoneword, которая будет переносить одно слово из входного файла в выходной и вычислять его длину в нелокальной переменной wordlength. Слово определяется как последовательность символов, завершающихся пробелом, запятой или точкой. Заключительный символ не должен копироваться. Шаги разработки Примечания processoneword -> begin инициализировать wordlength', пока не пора закончить выполнять begin скопировать один символ; скорректировать wordlength end { пора закончить } end wordlength (длина слова) - глобальная переменная пора закончить -> input" есть пробел, запятая или точка -> (input" - ’ or (input" - ’,’) or (input" - ’.’) следовательно, не пора закончить -> (input" <> * ’) and (input" <> ’,’) and (input" <> ’.’) processoneword -> begin wordlength 0; while (input" <> ’ ’) and (input" <> ’,’) and (input" <> ’.*) do begin copyonecharacter, wordlength wordlength + 1 end { (input" - ’ ’) or (input" - or (input" - } end взять процедуру copyonecharacter из программы СчетчикСлов Первоначально программа записана в структурном виде на обычном языке, и первая строка выглядит так: пока не пора закончить выполнять Это универсальная первая строка оператора цикла ’’пока”; в такой форме ее можно писать всякий раз, когда вы прибегаете к данному оператору цикла! После оператора следует комментарий { пора закончить } в котором содержится условие, которое должно быть выполнено при выходе из оператора цикла.
184 Гл. 10. Булевы выражения и переменные Теперь мы стоим перед выбором: а) развернуть условие ”не пора закон- чить” или б) развернуть условие ’’пора закончить”, а затем построить его отри- цание. Естественно мы выберем, что проще. Второй способ можно назвать пост- роением оператора цикла ”с конца”. В нашем примере идти с конца намного легче. Исходя из того, что текущий символ во входном файле доступен через буферную переменную триГ, получаем: пора закончить - > input' есть пробел, запятая или точка - > (input* = ’ ’) or (inpur = ’,’) or (inpur = ’.’) и значит: не пора закончить - > not ((inpur • ’ ’) or (inpur = ’,’) or (inpur = ’.’)) что можно упростить при помощи правила Моргана: (inpur <> ’ ’) and (inpur <> ’,’) and (input* <> ’.’) Мы получили требуемое условие, управляющее выполнением оператора ’’пока”. В таблице разработки (табл. 10.4) коротко записана эта последователь- ность шагов. В той полосе таблицы, где содержится окончательный текст проце- дуры, в виде комментария записано также условие завершения цикла. Подобный комментарий в форме булева выражения называется утверждением. В данном случае утверждается нечто о состоянии буферной переменной inpur по выходе из оператора пока. ’’Обратный” метод построения оператора пока можно теперь сформулиро- вать следующим образом: а) записать то, что вы хотите ’’утверждать” (полагать истинным) по выхо- де из цикла; б) построить отрицание этого логического выражения и взять его в качест- ве условия оператора пока. Этот прием поможет вам избегать ошибок при записи оператора пока. Очень легко запутаться, имея дело со сложными логическими выражения- ми. Отчасти это можно объяснить тем, что в обыденной речи мы порой употреб- ляем ”и” вместо ’’или” и наоборот, и предложение, в котором есть несколько этих союзов, часто выглядит неоднозначным. Отрицание, когда оно встречается, еще более усугубляет дело! Вследствие этого нужно особенно тщательно про- верять все места программы, где имеются операции not, and и or. Предположим, например, что мы не стали пользоваться обратным методом при записи оператора пока в процедуре processoneword, а попытались непосред- ственно развернуть условие ”не пора закончить”. Мой личный опыт и наблюде- ние за многими учащимися программированию подсказывает, что могло прои- зойти следующее: не пора закончить -> inpur не есть пробел, запятая или точка -> (триГ <> ’ ’) or (inpur <> ’,’) or (inpur <> ’.’) что приведет к записи while (триГ <> ’ ’) or (inpur <> ’,’) or (inpur <> ’.’) do begin end
Гл, 10. Булевы выражения и переменные 185 Мы подчеркивали, как важно вручную проверять правильность окончания цикла. Это тем более важно, когда условие имеет столь сложный вид, как в приведенном случае. Проверку легко провести с помощью правил Моргана. Взяв отрицание логического выражения в операторе цикла "пока”, получите утвер- ждение, истинное на выходе из цикла, а затем внимательно его изучите. В по- следнем случае утверждение будет следующим: { (inpuC в ’ ’) and (inpuC = ’,’) and (inpuC » ’.’) } Отсюда явствует, что при выходе из цикла переменная inpuC будет иметь одно- временно три различных значения! Иными словами, он вообще не завершится. Проверка со всей ясностью показала, что оператор цикла записан неверно. Посмотрим теперь на процедуру writetheprogression (см. листинг 10.3). Она полностью эквивалентна соответствующей процедуре в программе Прогрессия2 (с. 151), но записана чуть по-другому. При условии, что в глобальные перемен- ные term, difference, numberofterms считаны значения, процедура выдает ариф- метическую прогрессию, если только процессор выполняет вычисления без цело- численного переполнения. Те места, которых коснулись изменения, связаны со способом ’’досрочного” выхода из цикла перед грозящим переполнением. Как вы помните, в предыдущем варианте (листинг 8.1) первая строка опе- ратора цикла выглядела так: while termswritten о numberofterms do и досрочное окончание цикла достигалось благодаря оператору termswritten :e numberofterms выполняемому перед наступлением переполнения. В новом варианте тот же эффект достигается введением булевой перемен- ной overflow. Это вызывает изменения в четырех местах программы. Первые два из них: в 14-й строке программы, где описывается переменная overflow, и в 24-й, где ей присваивается начальное значение false, отражающее тот факт, что переполнение пока только предстоит обнаружить. В первой строке оператора цикла - и в 25-й строке программы - третье изменение: while (termswritten <> numberofterms) and not overflow do откуда выводится утверждение, истинное по выходе из цикла в строке 43: { (termswritten - numberofterms) or overflow } Другими словами, цикл завершится, или когда будет выдано требуемое число членов прогрессии, или когда обнаружится переполнение, в зависимости от того, что произойдет раньше. Присвоением overflow значения true в 40-й строке обес- печивается досрочное окончание цикла. Это четвертое изменение. Рассмотренная процедура иллюстрирует способ выхода из цикла, когда во- зникает некая аварийная ситуация. Его можно подытожить следующим образом: а) описать булевскую переменную с подходящим именем; б) присвоить ей начальное ложное (или истинное) значение до входа в цикл; в) составить булевское выражение, зависящее от этой переменной и конт- ролирующее выполнение цикла; г) присвоить переменной истинное (или ложное) значение внутри цикла в том месте, где обнаружено аварийное условие. Этот метод может применяться наряду с более простым рассмотренным выше. Еще одной важной деталью дополнена процедура writetheprogression*, это утверждение
186 Гл 10. Булевы выражения и переменные { (difference >- 0) and (numberofterms > 0) } появляющееся в 16-й строке программы. Дело в том, что вычисление и выдача арифметической прогрессии в виде последовательности чисел могут понадобиться в других программах. Написанная нами для этой цели процедура достаточ- но самостоятельна, и мы могли бы просто перенести ее в другую программу, рассчитывая, что она будет нормально работать. Но работать она будет не всег- да. Во-первых, другая программа должна будет содержать переменные term, difference, numberofterms, хотя это ограничение мы сможем преодолеть, узнав из следующей главы о параметрах (см. упр. 11.4). Во-вторых, процедура разработа- на для случая, когда разность прогрессии больше либо равна 0 и число ее членов больше нуля. 9 10 и 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 procedure writetheprogression', const terms perline - 8; fieldwidth - 8; var overflow : Boolean; termswritten : integer, begin { {difference > 0) and (numberofterms > 0) } { выдать заглавие и первый элемент } writeln (*ИСКОМАЯ АРИФМЕТИЧЕСКАЯ ПРОГРЕССИЯМ; write (term : fieldwidth}; termswritten 1; { посчитать и выдать остальные элементы } overflow false; while (termswritten <> numberofterms) and not overflow do begin { если нужно, перейти на новую строку } if termswritten mod termsperline - 0 then writeln; { посчитать и выдать очередной элемент, если это возможно } if term <- maxint - difference then begin term term + difference; write (term : fieldwidth); termswritten termswritten + 1; end; else begin writeln (’????’ : fieldwidth); writeln; writeln (ОЧЕРЕДНОЙ ЭЛЕМЕНТ ДАЕТ ПЕРЕПОЛНЕНИЕМ; overflow true; end end; { (termswritten - numberofterms) or overflow } writeln end { writetheprogression }; Листинг 10.3. Применение булевской переменной для выхода из цикла Если процедура будет вдруг использована в ситуации, когда одно из этих условий не выполнено, она может отказать. Об этом предупреждает нас приве- денное выше утверждение. Если теперь мы вставим-таки writetheprogression в другую программу, утверждение в 16-й строке не спасет процедуру от неправильной работы. Утвер- ждение это подобно надписи
Гл. 10. Булевы выражения и переменные 187 ТОЛЬКО ДЛЯ НАРУЖНОГО ПРИМЕНЕНИЯ на баночке с лосьоном или на упаковке с нафталином. Предупреждение не ме- шает вам проглотить содержимое, но по крайней мере сообщает о неприятных последствиях. Если утверждение, записанное в начале процедуры, при входе в нее не является истинным, то последствия для программиста могут быть непри- ятными. Такое утверждение называется предусловием, т.е. условием, которое должно быть истинно перед началом выполнения процедуры, если мы хотим, чтобы оно было успешным. Напротив, утверждение в 43-й строке процедуры мо- жет быть названо постусловием для оператора пока, так как оно должно быть истинным после того, как оператор был выполнен. Вообще, если подпрограмма гарантированно работает только при вы- полнении некоторого предварительного условия, его следует записать как пре- дусловие в начале подпрограммы. Возможно, это и не предотвратит неправиль- ного использования подпрограммы (как может остаться незамеченной предупре- дительная надпись на порошке или жидкости), но когда возникнут нежелатель- ные последствия, жертву можно будет винить в том, что она не вняла предупре- ждению. Итак, теперь вы знакомы с булевскими переменными и немного с булев- скими выражениями. Если вы научитесь правильно их применять, то вы на верном пути к программистскому мастерству. Упражнения 10.1 . Допуская, что в нижеследующих выражениях фигурируют идентификаторы переменных, проведите полный синтаксический разбор выражений (в соответствии с определениями на рис. 6.4) и выясните, являются ли они синтаксически правильными с точки зрения стандартного Паскаля. a) john and тагу or Joan and peter 6) (ch - ’.’) or successful в) number - 0 or number - 100 10.2 . Принимая значение переменной number равным 25, значение переменной ch равным значение переменной overflow равным false, вычислите следующие булевские выражения: a) (number >- 0) and (number <- 100) б) (number < 0) or (number > 20) в) (number <> 0) and not overflow r) (ch - ’.’) or (ch - ’,’) and (number - 25) д) (ch - ’.’) and (number - 25) or (ch - ’;’) 10.3. По правилам Моргана получите отрицания булевских выражений из упр. 10.2. 10.4. Вычисляя обе части тождеств при всех возможных значениях булевских переменных р, q, г, докажите, что: а) (р or г) and (q or г) - (р and q) or г б) (р and г) or (q and г) - (p or q) and r 10.5. Следующий оператор взят из неудачно написанной Паскаль-программы: if mark > 69 then grade :- ’A* else if mark > 54 and mark < 70 then grade ’B’ else if mark > 39 and mark < 55 then grade ’C else if mark <- 39 then grade:- *D’ а) Исправьте синтаксические ошибки и запишите оператор более наглядно. б) Считая, что значение переменной mark всегда находится в диапазоне 0..100, определите, ка- кой диапазон ее значений соответствует каждому из возможных значений переменной grade. в) Запишите более простой оператор, который действует так же. 10.6. Напишите программу под названием Контроль, проверяющую файл, из которого вводятся результаты экзаменов. Вот точные спецификации:
188 Гл 10. Булевы выражения и переменные Входные данные информацию: Предполагается, что каждая строка файла содержит следующую кандидат возраст оценка : целое число в диапазоне 1..9999; : целое число в диапазоне 16.. 18, : целое число в диапазоне 0..100 или число 999 (обозначающее отсутст- вие кандидата), уровень : один из символов Т, ’A’, ’S’. Файл завершается строкой, начинающейся со знака Считайте, что в каждой строке всегда есть три целых числа, за которыми следует некоторый символ, а заключительная строка всегда правильна. Результаты Каждую строку данных нужно вывести на дисплей, причем против неверных строк должно быть указано, в чем ошибка. Например: 10312 15 80 I неверно : : кандидат возраст 4276 17 53 А 4913 19 70 В неверно : : возраст уровень
11 ПРОЦЕДУРЫ С ПАРАМЕТРАМИ Основной темой этой главы является использование параметров, которые позволяют из одной части программы обращаться к информации, находящейся в другой ее части, а также делают процедуры более устойчивыми, более самостоя- тельными, более наглядными, более гибкими, более мобильными. Чтобы подойти к этой теме, поразмышляем о доступе к информации вообще. 11.1. Доступ к информации Характер доступа к информации - ключ к ее обработке. Забудем на время о компьютерах и программировании и посмотрим, в какой форме бывает доступ- на информация в повседневной жизни. Обратимся к информации, которая все время меняется. В качестве примера возьмем дневник бизнесмена. Планируя свои деловые встречи, он вносит в дневник новые записи и корректирует старые. Иногда встреча отменяется и запись вычеркивается. Словом, информация в дневнике никогда не остается постоянной сколько-нибудь долгое время. Этим она напоминает переменную в программе. Выбранный пример помогает проиллюстрировать несколько разных уров- ней доступа к информации. Прежде всего сам бизнесмен и его старший секре- тарь имеют полный доступ к дневнику. Они могут брать из него информацию и вносить любые изменения. Далее, помощникам разрешается заглядывать в днев- ник, чтобы отвечать на вопросы, но записывать что-либо в него им нельзя. Будем называть это доступом для чтения. Если представить себе, что, наоборот, запись разрешена, а чтение - нет (хотя для нашего примера это нелегко), то такую форму естественно назвать доступом для записи. Наконец, еще одна воз- можность (которая в обыденной жизни, видимо, не встречается, но нам нужна в показательных целях) - это возможность, скажем, для старшего помощника получать дубликат дневника. Сотрудник имеет полный доступ к дубликату, он может извлекать информацию и даже, если хочет, вносить в него изменения, но в оригинале эти изменения никак не отражаются. Кроме того, дубликат явля- ется "мгновенным снимком" дневника в некоторый момент времени и уже через какие-то полчаса может устареть из-за того, что исходный дневник изменился. Такой вид доступа назовем доступом к дубликату. Наконец, большинство людей вообще не имеют доступа к дневнику. Отсутствие доступа к дневнику означает, что нельзя ни читать его, ни вносить в него какие-либо записи, ни копировать дневник. Итак, имеется пять уровней доступа к информации: полный доступ : разрешены чтение и запись; доступ для чтения доступ для записи : чтение разрешено, запись запрещена; : запись разрешена, чтение запрещено;
190 Гл. 11. Процедуры с параметрами доступ к дубликату : разрешен полный доступ к ’’снимку” информации в некоторый момент времени; отсутствие доступа : чтение, запись, копирование запрещены. Нас как программистов интересует главным образом доступ к информа- ции, хранящейся в программных переменных. Разрешение ’’читать” переменную означает, что значение этой переменной можно употреблять в выражении, а разрешение ’’писать” в переменную означает возможность присваивать ей значение. Здесь возникает опасность смешения терминов, потому что, читая в Паскале переменную оператором read (х) мы не требуем доступа к переменной х для чтения. Речь в действительности идет о двух разных доступах. Сначала нам необходим доступ для чтения из входного файла, откуда мы получим значение. Затем требуется доступ для записи или полный доступ к переменной х, чтобы присвоить ей это значение. Аналогично, когда мы употребляем оператор write (х) нам нужен доступ для записи или полный доступ к переменной х и доступ для записи в выходной файл. Константы также содержат информацию, но поскольку по определению их значения постоянны, то полный доступ и доступ для чтения равносильны, а доступ для записи лишен смысла. В гл. 6 мы уже обсудили доступ к локальным и глобальным переменным и константам. В той или иной точке программы к некоторому элементу данных есть либо полный доступ, либо нет никакого. Если эта точка программы входит в область действия (видимости) элемента данных и лежит после точки его определения, то имеется полный доступ. В противном случае доступ отсутствует. Кроме этих двух крайних случаев, желательно обеспечить другие, более гибкие способы доступа из одной части программы к другой. Здесь-то и приходят на помощь параметры! 11.2. Процедуры с параметрами В большинстве языков программирования подпрограммы могут обладать параметрами. Параметр позволяет одной части программы иметь доступ к информации, относящейся к другой ее части. Два наиболее важных вида пара- метров в стандартном Паскале - параметры, передаваемые по значению и по ссылке. Если говорить кратко, отличие между ними состоит в том, что при пере- даче параметра по значению подпрограмма получает доступ к дубликату зна- чения переменной, тогда как передача параметра по ссылке дает подпрограмме полный доступ к переменной. Вообще говоря, подпрограмма может принимать параметры как по значе- нию, так и по ссылке, но мы сначала рассмотрим процедуру только с парамет- рами первого типа, а затем - только второго, чтобы различие между ними сделать как можно яснее. В этой главе мы столкнемся также с процедурой, имеющей параметры обоих типов.
Гл. 11. Процедуры с параметрами 191 11.2.1. Параметры, передаваемые по значению Чтобы проиллюстрировать этот тип параметров, рассмотрим программу под названием ПланПрогулки. Программа предназначена для тех, кто привык измерять расстояния в милях и ярдах, но вынужден пользоваться картами, где они даны в километрах. Программе сообщается, из скольких этапов состоит маршрут и какова протяженность каждого из них, после чего она вычисляет все расстояния, включая общую протяженность, в милях и ярдах. Образец входных данных и отвечающих им результатов показан в табл. 11.1. Таблица 11.1. Образец входных данных и результа- тов программы ПланПрогулки. Входные данные 2 10 15 Результаты **** РЕЗУЛЬТАТЫ ПРОГРАММЫ ПЛАН ПРОГУЛКИ **** ЭТАП В КИЛОМЕТРАХ В МИЛЯХ И ЯРДАХ 1 10 6 380 2 15 9 570 ВСЕГО 25 15 950 **** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ **** Текст программы показан в листинге 11.1. Он содержит процедуру convertandwrite, которая переводит расстояние, заданное только в ярдах, в мили и ярды и выдает числа в поле заданной ширины. Посмотрим на эту процедуру. В ее заголовке, в строке 7, после имени процедуры стоит конструкция (yards, width : integer) называемая списком формальных параметров, которая говорит нам, что парамет- ры yards и width, оба типа integer, т.е. целочисленные, передаются процедуре по значению. Когда мы имеем дело с подпрограммами, то параметр, передаваемый по значению, является новым элементом данных, почти таким же, как локаль- ная переменная. Параметры, передаваемые по значению, и локальные перемен- ные сходны в том, что они: а) создаются при входе в подпрограмму; б) могут употребляться внутри подпрограммы; в) разрушаются (исчезают) по выходе из подпрограммы. Единственное различие между ними в том, что г) параметр, передаваемый по значению, имеет начальное значение, тогда как для локальной переменной оно не определено. Начальное значение параметра равно значению выражения, копируемого при вызове подпрограммы. Говорят, что значение выражения передается в подпро- грамму. Способ передачи в подпрограмму того или иного значения при ее вызове показан в операторе процедуры в строке 38. Оператор вызывает процедуру
192 Гл. 11. Процедуры с параметрами convertandwrite, причем вслед за идентификатором процедуры идет следующая конструкция: (distance ♦ уardsperkilometer, 26) 1 2 3 4 5 6 7 8 9 10 И 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 program TripPlanner (input, output); { Читает из входного файла количество этапов прогрулки и протяженность каждого в километрах; выдает табличку общей и поэтапной протяженности в километрах, а также милях и ярдах } procedure convertandwrite (yards, width : integer); { Переводит расстояние, заданное в ярдах, в мили и ярды и выдает его в поле шириной не менее заданного числа символов } const yardspermile - 1760; var miles : integer, begin miles yards div yardspermile; yards yards mod yardspermile; if width < 10 then width 10; write ( miles : width - 9, yards : 9) end { convertandwrite }; procedure processonejourney; const yardsperkilometer - 1094; var number, stage, totaldistance, distance : integer, begin writeln ('ЭТАП В КИЛОМЕТРАХ В МИЛЯХ И ЯРДАХ'); writeln; readin (number); stage 0; totaldistance 0; while stage < number do begin stage stage + 1; read (distance) ; totaldistance totaldistance + distance; write (stage : 3, distance : 14); convertandwrite (distance * yardsperkilometer, 26); writeln end; writeln ('------------------------------------’); write ('ВСЕГО', totaldistance : 12); convertandwrite (totaldistance * yardsperkilometer, 26); writeln end { processonejourney }; begin writeln (’♦♦♦♦ РЕЗУЛЬТАТЫ ПРОГРАММЫ ПЛАН ПРОГУЛКИ ♦♦♦♦’); writeln; processonejourney; writeln; writeln (’*♦♦♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦♦’) end { TripPlanner}. Листинг 11.1. Текст программы ПланПрогулки, включающий процедуру с двумя параметрами, передаваемыми по значению
Гл.11. Процедуры с параметрами 193 которая называется списком фактических параметров. Между фактическими параметрами в операторе процедуры и формальными параметрами в заголовке процедуры имеется взаимно-однозначное соответствие. Оно определяется позицией параметров в списках, так что число фактических параметров всегда должно совпадать с числом формальных. Фактический параметр distance ♦ yardsperkilometer в строке 38 соответствует формальному параметру yards, а фактический параметр 26 соответствует формальному параметру width. При вызове процедуры каждое из выражений, задающих фактические параметры, вычисляется и полученные значения присваиваются в качестве начальных соответствующим формальным параметрам. Все происходит так, как если бы при входе в процедуру выполнялись операторы yards := distance * yardsperkilometer, width := 26; Поскольку происходят такие присваивания, тип данных выражения, явля- ющегося фактическим параметром, должен быть совместим с типом данных соответствующего формального параметра. Фактический параметр типа char, например, был бы несовместим с формальным параметром типа integer, так как символьное значение не может присваиваться целочисленной переменной. Действие параметров в процедуре convertandwrite иллюстрирует частичная трассировка программы ПланПрогулки в табл. 11.2. Трассировка демонстрирует выполнение строки 50 программы, т.е. вызов processonejourney, основной проце- дуры, и вытекающие из него вызовы convertandwrite. Таблица 11.2. Частичная трассировка выполнения программы ПланПрогулки строка ход выполнения number stage totaldist. distance 28 29 30 31 32 34 35 36 37 38 вход в processonejourney ? вывод: ЭТАП В КИЛОМЕТРАХ 2 (stage < number) - true вывод: ~~ ~*~10 вызов convertandwrite В МИЛЯХ Й ЯРДАХ## 0 0 1 10 10 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ yards width miles 16 вход в convertandwrite \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 10940 26 ? 17 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 6 18 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 380 19 (width < 10) - false \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ 20 вывод: 6 380 WWWWWWWWWWW 21 выход из convertandwrite \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ возврат в processonejourney number stage totaldist. distance 39 вывод: # 32 (stage < number) - true 34 2 35 15 36 25 37 вывод: ~~~ 15 38 вызов convertandwrite 7 Заказ № 1110
194 Гл. 11. Процедуры с параметрами WWWWWWWWWWWWWWWWX yards width miles 16 вход в convertandwrite WWWWWWWWWWWWWWWWX 16410 26 ? 17 WWWWWWWWWWWWWWWWX 9 18 WWWWWWWWWWWWWWWWX 570 19 (width < 10) - false WWWWWWWWWXWWWWWWW 20 вывод: 9 570 WWWWWWWWWWW 21 выход из convertandwrite WWXWWWWWWWWWWWWWW возврат в processonejourney 39 вывод: # 32 (stage < number) - false 41 вывод: ~ “ _ - _ - - # 42 вывод: ВСЕГ0~лл~~~л~лл25 43 вызов convertandwrite WWXWWWWWWWWWWWWWW yards width miles 16 вход в convertandwrite WWXWWWWWWWWWWWWWW 27350 26 9 17 WWXWWWWWWWWWWWWWW 15 18 WWXWWWWWWWWWWWWWW 950 19 (width < 10) - false WWXWWWWWWWWWWWWWW 20 вывод: 15 950 WWWWWWWWWWW 21 выход из convertandwrite WWXXXWXXXXWWWWWWWWWW возврат в processonejourney 44 вывод: # 45 выход из processonejourney При входе в процедуру processonejourney выполняется последовательность операторов со строки 28 до строки 38, после чего вызывается процедура convertandwrite. С этого момента становятся недоступными локальные объекты процедуры processonejourney, и создаются локальные переменные и параметры, передаваемые по значению, связанные с процедурой convertandwrite, именно: а) параметр yards с начальным значением 10940, равным значению выра- жения distance * yardsperkilometer, б) параметр width с начальным значением 26, равным значению выраже- ния 26; в) локальная переменная miles с неопределенным значением. Локальная константа yardspermile также становится доступной. После этого вы- полняется тело процедуры convertandwrite, и в выходной файл выводится последовательность символов, выражающая данное расстояние в милях и ярдах. Поскольку width - 9 равно 17, значение переменной miles представляется с помо- щью 17 знаков. Следующие 9 знаков отводятся для представления значения переменной yards, так что всего выдается 26 символов. По выходе из convertandwrite параметры, полученные по значению, и ло- кальные переменные разрушаются, и хранящаяся в них информация теряется. При возврате в processonejourney ее локальные объекты снова становятся до- ступными. При каждом последующем вызове convertandwrite происходит та же после- довательность событий. Два значения, представляющие расстояние и ширину поля, передаются процедуре, а в ответ порождается требуемое число символов, которые посылаются в выходной файл. Заметьте, кстати, что при вызове проце- дуры в строке 43 первый фактический параметр выглядит чуть по-другому. Процедуру convertandwrite безо всякого изменения можно использовать в другой программе. Ее нужно вызывать, давая любые два выражения в качестве
Гл.11. Процедуры с параметрами 195 фактических параметров при условии, что оба они целочисленные. Если, напри- мер, х равен 2000, а у равен 10, то обращение convertandwrite (х, у) приведет к выводу десяти символов: —240 а давая в качестве выражений просто константы convertandwrite (3520, 4) получим на выходе восемь символов: 2------О так как значение width будет увеличено процедурой до восьми, чтобы по крайней мере одна позиция осталась для поля, куда выводится значение miles. Таким образом, параметр, передаваемый по значению, в подпрограмме является, по существу, локальной переменной, в которую поступает значение. В подпрограмме, следовательно, содержится дубликат значения, которым она может оперировать. Операции над дубликатом не влияют ни на какие перемен- ные в той части программы, которая обращается к подпрограмме, а по выходе из подпрограммы дубликат исчезает. 11.2.2. Параметры, передаваемые по ссылке Параметр, передаваемый по ссылке, в корне отличен от параметра, пере- даваемого по значению. Ссылочный параметр дает подпрограмме полный доступ к некоторой переменной программы. Чтобы понять, как параметр передается по ссылке, рассмотрим программу под названием ПростаяСортировка. Она упорядочивает целые числа в группе, причем групп может быть несколько, каждая из них содержит три числа и на входе представлена отдельной строкой. Программа получает число строк, подле- жащих обработке, тройки целых чисел вслед за ним, читает строку за строкой и сначала выводит числа в исходном порядке, затем сортирует их и выводит по возрастанию. Не бог весть какая работа, но сложнее, чем может показаться! Образец входных данных и отвечающих им результатов показан в табл. 11.3, а полный текст программы приведен в листинге 11.2. Из трех процедур, имеющихся в программе, особенный интерес представ- ляет первая, под названием sorttwonumbers, поскольку она имеет два ссылочных параметра. Как и в нашем предыдущем примере, заголовок процедуры содержит список формальных параметров: (var small, large: integer) который отличается от списка формальных параметров процедуры convertandwrite (с. 192) наличием служебного слова var. Это слово говорит о том, что следующие за ним идентификаторы являются именами ссылочных пара- метров. В его отсутствие передача параметров происходила бы по значению. Итак, два параметра процедуры sorttwonumbers, small и large, передаются по ссылке и оба имеют тип integer. В каждом из трех операторов процедуры, обращающихся к sorttwonumbers в строках 26-28, имеется список фактических параметров. К примеру, в строке 26 он выглядит так: (first, second)
196 Гл.11. Процедуры с параметрами Как и в предыдущем случае, имеется взаимно-однозначное соответствие между фактическими параметрами в операторе процедуры и формальными параметрами в заголовке процедуры, определяемое положением в списке. Таблица 11.3. Образец входных данных и результа- тов программы ПростаяСортировка Входные данные 3 14 7 9 9 14 7 14 9 7 Результаты **** РЕЗУЛЬТАТЫ ПРОГРАММЫ ПРОСТАЯ СОРТИРОВКА **** ПОРЯДОК В НАЧАЛЕ ПОРЯДОК В КОНЦЕ 14 7 9 7 9 14 9 14 7 7 9 14 14 9 7 7 9 14 **** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ **** Так, в строке 26 параметр first соответствует small, a second соответствует large. Тип каждого фактического параметра должен в точности совпадать с типом соответствующего формального параметра. Важнейшее отличие, однако, состоит в том, что для каждого ссылочного параметра в списке формальных параметров соответствующий фактический параметр должен быть перемен- ной. Как вы помните, фактический параметр, передаваемый по значению, явля- ется выражением. Когда выполняется подпрограмма с одним или более ссылочными парамет- рами, всякая операция, определенная в теле подпрограммы и содержащая ссылочный параметр, производится над соответствующим фактическим параметром, т.е. над переменной, действующей вне подпрограммы. Это равносильно следующим действиям: а) пройти по тексту подпрограммы и заменить каждый идентификатор ссылочного параметра на идентификатор фактического параметра; б) исполнить измененную процедуру. Показанная в табл. 11.4 частичная трассировка программы ПростаяСортировка иллюстрирует сказанное. В ней прослеживается один полный вызов процедуры sortoneline и вытекающие из него три вызова sorttwonumbers. Обращение к процедуре sortoneline происходит в строке 23, и после того, как читается и сразу выводится одна строка данных, в строке 26 происходит вы- зов sorttwonumbers. Переменные first и second являются фактическими парамет- рами, так что sorttwonumbers выполняется с тем же эффектом, что конструкция if first > second then begin safe := first; first := second; second := safe; end
Гл. 11. Процедуры с параметрами 197 1 2 3 4 5 6 7 8 9 10 И 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 program SimpleSorter (input, output)', { Читает целое число, указывающее количество сортируемых наборов, за которым следуют наборы из трех целых чисел, по одному набору в строке; выдает построчно наборы в исходном виде и отсортированные в порядке возрастания } procedure sorttwonumbers (var small, large : integer); var safe : integer, begin if small > large then begin safe small', smalllarge', largesafe end end { sorttwonumbers }; procedure sortoneline; var first, second, third : integer, begin readin (first, second, third)', write (first: 6, second : 10, third : 10); sorttwonumbers (first, second)', sorttwonumbers (second, third) ', sorttwonumbers (first, second)', writeln (first: 15, second : 10, third : 10) end { sortoneline }; procedure sorteveryline; var numberoflines : integer, begin writeln ('ПОРЯДОК В НАЧАЛЕ' : 23, 'ПОРЯДОК В КОНЦЕ' : 34); writeln', readin (numberoflines); while numberoflines > 0 do begin numberoflines numberoflines - 1; sortoneline end end { sortoneline }; begin writeln (’**** РЕЗУЛЬТАТЫ ПРОГРАММЫ ПРОСТАЯ СОРТИРОВКА ♦♦♦♦’) ; writeln; sorteveryline; writeln; writeln (’♦**♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦♦’) end { SimpleSorter}. Листинг 11.2. Текст программы ПростаяСортировка, включающий процедуру с двумя ссылочными параметрами и следовательно, процедура имеет полный доступ к переменным first и second. В таблице это показано переименованием столбцов, отведенных под фактические параметры, идентификаторами формальных параметров. Так, при первом вызове small записывается в графу, отведенную под first, a large - в графу, отведенную под second. Смысл такого обращения состоит в том, чтобы использовать и изме- нить значения переменных first и second. Выполнение процедуры не задевает
198 Гл. 11. Процедуры с параметрами переменную third, поскольку это локальная переменная процедуры sortoneline, не передаваемая как параметр в sorttwonumbers. Таблица 11.4. Частичная трассировка выполнения программы ПростаяСортировка. Отслеживаемая часть входных данных 14 7 9 Трассировка строка ход выполнения first second third 23 24 25 26 вход в sortoneline ? 14 вывод: *ЛЛ~14~ ------у--- вызов sorttwonumbers (first, second) 9 7 9 11 12 14 15 16 18 вход в sorttwonumbers (small > large) - true выход из sorttwonumbers (small) 7 (first) (large) 14 (second) WWWW WWWW WWWW WWWW WWWW WWWW safe ? 14 27 возврат в sortoneline вызов sorttwonumbers (second, third) 11 12 14 15 16 18 вход в sorttwonumbers (small > large) - true выход из sorttwonumbers WWWW WWWW WWWW WWWW WWWW WWWW (small) 9 (second) (large) 14 (third) safe 9 14 28 возврат в sortoneline вызов sorttwonumbers (first, second) И 12 18 вход в sorttwonumbers (small > large) - false выход из sorttwonumbers (small) (first) (large) (second) WWWW WWWW WWWW safe ? 29 30 возврат в sortoneline вывод: ~~~ ~ ~ 7~ выход из sortoneline r 14# Результаты, выданные в отслеженной части 14 7 9 7 9 14 При очередном обращении к sorttwonumbers, в строке 27, second соответст- вует small, a third соответствует large, так что процедура имеет теперь полный доступ к другой паре переменных. Это равносильно следующему:
Гл. 11. Процедуры с параметрами 199 if second > third then begin safe := second*, second :e third*, third := safe*, end а в таблице трассировки процедура меняет содержимое колонок, отведенных под second и third. Как вы заметили, переменная safe является локальной и при каждом вы- зове процедуры возникает и исчезает. Она нужна для того, чтобы в теле услов- ного оператора обменять значения переменных small и large. Если бы в тексте процедуры было просто begin small := large-, large := small-, end то обе переменные в итоге имели бы одно значение, равное large. На первый взгляд третий вызов процедуры sorttwonumbers в строке 28 мо- жет показаться необязательным. Первый вызов обеспечивает упорядочение по возрастанию первых двух чисел. Второй вызов упорядочивает второе и третье, но после этого может снова нарушиться порядок первых двух. Третий вызов проверяет и, если нужно, обменивает их значения. Если вас такое рассуждение не убеждает, проследите работу процедуры sortoneline на входных данных 9 14 7 Подведем итоги. Всякий ссылочный параметр дает подпрограмме возмож- ность манипулировать с некоторой переменной из другой части программы. Имя этой переменной передается при вызове подпрограммы в качестве ее факти- ческого параметра. 11.3. Синтаксис параметров процедуры Формальные правила, описывающие синтаксис процедур и их параметров в стандартном Паскале, складываются из трех частей: а) оператор процедуры, определяемый на рис. 6.10 (с. 109-110), может содержать список фактических параметров; для встроенных процедур read, readin, write, writein - это список параметров особого вида; б) описание процедуры, определяемое на рис. 6.15 (с. 115-116), может содержать список формальных параметров; в) список формальных параметров, список фактических параметров и спи- ски параметров особого вида для процедур ввода-вывода определяются на рис. 11.1. Эти определения касаются только параметров, передаваемых по значению и по ссылке. Другие виды параметров, допускаемые языком, мы не рассматриваем. Обратите внимание, что список формальных параметров в заголовке про- цедуры составлен из одной или нескольких секций формальных параметров. Каждая такая секция описывает один или несколько параметров одного класса и одного типа данных. Например, процедура со следующим заголовком:
200 Гл.11. Процедуры с параметрами список-формальных-параметров имя-типа имя-типа список-фактических-параметров ( выражение переменная список-параметров-оператора-read переменная список-параметров-оператора-readln переменная список-параметров-оператора-write параметр-оператора-write список-параметров-оператора-writeln ( ( параметр-оператора-write параметр-оператора-write выражение : ----выражение : ---выражение Рис. 11.1а. Синтаксис параметров
Гл.11. Процедуры с параметрами 201 список-формальных-параметров - ”(” секция-формальных-параметров { секция-формальных-параметров } секция-формальных-параметров - описание-параметра-значения | описание-ссылочного-параметра . описание-параметра-значения - список-имен имя-типа . описание-ссылочного-параметра - ”var” список-имен имя-типа . список-имен = идентификатор { идентификатор } . список-фактических-параметров - ”(” фактический-параметр фактический-параметр } . фактический-параметр - выражение | переменная . список-параметров- onepamopa-read список-параметров- onepamopa-read bi список-параметров- = ”(” переменная { переменная } ”)” . - [ ”(” переменная { переменная } ”)” ] . onepamopa-write - ”(” параметр-оператора-write { параметр-оператора-write } ”)” . список-параметров- onepamopa-writeln - [ ”(” параметр-оператора-write { параметр-оператора-write } ”)” ] . параметр-оператора- write - выражение [ выражение [ выражение ] ] . Рис. 11.16. Синтаксис параметров procedure example (a,b: integer; c,d: char; var e,/: integer; var gjv. Boolean) имеет восемь параметров, причем a,b,c,d - параметры, передаваемые по значе- нию, a e,f,g,h - параметры, передаваемые по ссылке. Заметьте, что служебное слово var появляется дважды: первый раз при описании ссылочных параметров типа integer, а второй - при описании того же вида булевских параметров. В разделе описания переменных var употребляется совсем в другом смысле. Там это служебное слово должно встретиться лишь однажды - в начале раздела опи- сания переменных. 11.4. Чем выгодны параметры Использование параметров имеет следующие основные плюсы: а) они позволяют при необходимости ограничить доступ к информации и тем самым сделать программу более устойчивой; ( б) с помощью параметров подпрограммы становятся более самостоятельны- ми частями программы, что облегчает их понимание; в) они повышают гибкость подпрограмм и тем самым удобство от примене- ния последних и в данной, и в других программах; г) они повышают мобильность подпрограмм.
202 Гл. 11. Процедуры с параметрами 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 program WordCounter2 (input, output); { Считает слова в предложении и находит длину самого длинного } procedure copyonecharacter; var ch : char; begin read (ch); write (ch) end { copyonecharacter }; procedure copyspaces; begin while inpur - ” do copyonecharacter end { copyspaces }; procedure processoneword Гvar length : integer); begin length 0; while inpur <> ” do begin copyonecharacter; length length + 1 end end { processoneword }; procedure processonesentence; var wordlength, totalwords, longestsofar : integer; begin writeln ('Очередное предложение :'); writeln; totalwords 0; longestsofar 0; copyspaces; { если нет пробелов, ничего не делает } while inpur о ’. 'do begin processoneword (wordlength); totalwords totalwords + 1; if wordlength > longestsofar then longestsofar := wordlength; copyspaces end; copyonecharacter; / точка } 1 readln; writeln; writeln; ! writeln ('В предложении слов : ', totalwords:!); I writeln (Самое длинное содержит букв : ', longestsofar:!) i end { processonesentence }; begin writeln РЕЗУЛЬТАТ РАБОТЫ СЧЕТЧИКА СЛОВ writeln; processonesentence, writeln; writeln КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ end { WordCounter } Листин! 11.3. Вариант программы СчетчикСлов, в котором процедура processoneword имеет ссылочный параметр Для иллюстрации этих достоинств параметров вернемся к программе СчетчикСлов, разработанной нами в гл. 9 (с. 170). Ес измененная версия. СчетчикСлов2, приведена на листинге 11.3. Изменения следующие:
Гл. И. Процедуры с параметрами 203 а) переменная wordlength описана в строке 30 вместо строки 4; б) в строке 18 указано, что processoneword имеет формальный ссылочный параметр length, а в строках 20 и 24 над ним выполняются действия; в) переменная wordlength передается как фактический параметр при вызо- ве процедуры processoneword в строке 37. Что касается доступа к информации, то в программе СчетчикСлов2 дости- гнута цель, поставленная нами в гл. 4: каждая часть программы должна иметь доступ к нужным ей данным и как можно меньшему числу других объектов. Вспомним обсуждение программы СчетчикСлов (с. 169), в ходе которого мы столкнулись с проблемой, как сделать переменную wordlength доступной для двух процедур processoneword и processonesentence. Логически .эта переменная относится к processonesentence, но длину слова фактически считает процедура processoneword. Мы решили проблему, сделав wordlength глобальной переменной, но в этом был тот недостаток, что она стала доступна из любого места програм- мы и из процедур copyonecharacter и copyspaces, ни одной из которых этот дос- туп не нужен. В СчетчикеСлов2 переменная wordlength описана в процедуре processonesentence и передается в качестве параметра, что гораздо предпочтите- льнее. Каждой части программы доступны только те объекты, которые ей необ- ходимы. Это сделало программу более устойчивой, поскольку всякое обращение к переменной wordlength снаружи процедуры processonesentence будет выявлено компилятором как ошибка. Внесенные изменения сделали процедуру processoneword также более неза- висимой от остальной программы. Чтобы оценить независимость подпрограммы, посмотрим, насколько она понятна сама по себе, просто закрыв остальную часть программы. Назовем такую проверку тестом на отсечение. Применяя этот тест, мы увидим процедуру processoneword в программе СчетчикСлов такой: procedure processoneword; begin wordlength := 0; while inpur <> ’ ’ do begin copyonecharacter; wordlength :e wordlength + 1 end end { processoneword }; Процедура содержит два определенных программистом идентификатора, wordlength и copyonecharacter, про которые нужно что-то знать, чтобы полностью понять, как процедура работает, и убедиться, что она правильна. Обращение к copyonecharacter не вызывает проблем. Переменная wordlength, как видно, при- надлежит к наиболее простому, целочисленному типу, однако непонятно, ни где она описана, ни каково ее назначение, ни то, насколько серьезны могут быть последствия от изменения в процедуре ее прежнего значения. Отсечение от остальной части той же процедуры в программе СчетчикСлов2 дает нам procedure processoneword (var length : integer)*, begin length 0; while inpur <> ’ ’ do begin copyonecharacter, length length + 1 end end { processoneword };
204 Гл. 11. Процедуры с параметрами Место wordlength заняла переменная length, и вся неопределенность исчезла. Введение параметра сделало процедуру более независимой от остальной части программы и в результате более понятной. Придав процедуре processoneword параметр, мы сделали ее и более гибкой. В прежнем варианте слово копировалось и попутно его длина вычислялась в переменной wordlength. Для данной программы это приемлемо, но предположим, что мы хотим копировать два слова и их длины получать в переменных lengthl и length2. Можно было бы написать processoneword9, lengthl := wordlength; processoneword; length2 .•= wordlength; однако с параметризованной процедурой все выглядит и проще, и изящнее: processoneword (lengthl); processoneword (length2); Если вернуться к программе ПростаяСортировка (с. 197) и попробовать ее переписать без параметров в процедуре sorttwonumbers, то обнаружится, что весь смысл этой процедуры в ее гибкости. Она может работать с любой парой целых чисел. Наконец, еще одно преимущество параметров - мобильность подпрограмм. Тот факт, что параметризованные подпрограммы являются в высокой степени самостоятельными и гибкими, упрощает создание библиотеки подпрограмм, пригодных к использованию в различных программах с минимальными измене- ниями или вообще без изменений. В своем окончательном варианте процедура processoneword могла бы применяться в любой программе, лишь бы в ней имелась также процедура copyonecharacter. Единственным реальным минусом употребления параметров является то, что они удлиняют заголовок процедуры и каждый оператор процедуры. Но даже это нельзя считать в полной мере недостатком, так как параметры, особенно в операторе процедуры, значительно повышают удобочитаемость программы. Например, если применить тест отсечения к процедуре processonesentence в СчетчикеСлов2 (с. 202), то станет ясно, что оператор процедуры processoneword (wordlength) гораздо больше дает для понимания того, что делается, чем соответствующий оператор в СчетчикеСлов'. processoneword В последнем случае нет никаких свидетельств того, что процедура что-либо производит над переменной wordlength. Итак, мы назвали четыре плюса подпрограмм с параметрами и лишь один минус, откуда можно заключить, что параметры следует применять всегда. Но такой вывод будет неверным. Когда использовать параметры, а когда нет, вы начнете понимать постепенно, на практике - как, впрочем, и многое другое, чего мы здесь касаемся. 11.5. Какой вид параметра выбрать Для каждого параметра, который мы решаем ввести в программу, нужно решить, каким способом он будет передаваться. Иногда все равно, по значению или по ссылке, но чаще является правильным только одно из решений. К счас- тью, на этот счет есть довольно четкие правила, которыми можно руководство- ваться. Относительно каждого параметра мы должны спросить себя, какого рода доступ нужен подпрограмме к соответствующей информации. Имея ответ на этот вопрос, можно следовать такой схеме:
Гл. 11. Процедуры с параметрами 205 полный доступ доступ для чтения : передавать параметр по ссылке; : передавать параметр либо по ссылке, либо по значению; доступ для записи : передавать параметр по ссылке; доступ к дубликату : передавать параметр по значению. В случае доступа для чтения, когда могут использоваться оба вида пара- метров, обычно лучше передавать параметр по значению. Это дает следующие преимущества: а) поскольку фактический параметр есть выражение, то передаваться мо- жет значение константы, переменной или как угодно сложного выра- жения; б) любая информация снаружи программы полностью защищена и не мо- жет быть испорчена через данный параметр; в) параметр можно использовать в процедуре как локальную переменную. Вместе с тем оговорка ’’обычно лучше” имеет в виду исключения из этого пра- вила, которые делаются для структурных типов данных. Покажем, как применяются правила выбора параметров, на примере трех параметризованных процедур, которые мы изучаем на протяжении этой главы. Первая из них, процедура convertandwrite из программы ПланПрогулки (с. 192), служит для перевода расстояния, выраженного в ярдах, в мили и ярды и для вывода на экран полученной величины в поле заданной ширины. Инфор- мация, к которой процедура должна иметь доступ, это расстояние и ширина. Доступ нужен только для чтения, так что оба параметра передаются по значению. Вторая процедура, sorttwonumbers в программе ПростаяСортировка (с. 197), должна работать с двумя переменными, находящимися вне процедуры, с тем, чтобы упорядочить их значения по возрастанию. Поскольку при этом воз- можен обмен значений переменных, процедура должна иметь полный доступ к обеим переменным. Оба параметра, следовательно, передаются по ссылке. Третьим примером является процедура processoneword из программы СчетчикСлов2 (с. 202), которая должна вырабатывать длину слова. Она требует доступа для записи к той переменной, в которую помещается результат, поэтому параметр length должен быть ссылочным. Правильно выбрать способ передачи параметра при программировании на Паскале крайне важно. Иногда случайная вставка или пропуск словечка var может привести при выполнении программы к очень странным результатам, что вы и увидите, проделав упр. 11.2. Стоит, следовательно, включить проверку каждого параметра в перечень обязательных действий, выполняемых при ручной проверке программ. 11.6. Программа с параметрами В качестве следующего примера разработки программы и использования параметров составим программу ТаблицаСтепеней, спецификация которой содержится в табл. 11.5. Она должна прочитать положительное целое число £, а затем построить таблицу степеней хк для последовательности значений х Для простоты в спецификации допущено, что данные корректны, хотя в общем случае потребовалась бы их проверка. Основная процедура программы, названная processonetabulation, построена в табл. 11.6. В каждом разделе таблицы есть моменты, заслуживающие особого интереса. В первом разделе наиболее интересно то, что содержащийся в нем набро- сок действий приходится отвергнуть. Как первое приближение он выглядит впо- лне здраво, однако последовательность
206 Гл. 11. Процедуры с параметрами вычислить х*; выдать х и х*; предполагает, что х* всегда можно посчитать. Это целое число может, однако, быть слишком велико для наличного процессора. Но мы хотим от программы, чтобы она при чересчур большом хк завершалась нормально, а не ’’вылетала” по ошибке, поэтому нужно предупредить переполнение. Во втором разделе приведен исправленный вариант, содержащий булеву переменную successful, значение которой будет истинным, если х* удастся по- считать, и ложным в противном случае. Фактическое же вычисление степени возложено на процедуру calculatepower. В зависимости от значения successful принимается решение выводить очередную строку результатов на экран или нет. Таблица 11.5. Спецификация программы ТаблицаСтепеней Составьте Паскаль-программу под названием ТаблицаСтепеней, которая читает целое число к, а затем строит таблицу значений х* для целочисленных значений х, начиная от 1 и до 50 или пока наличный процессор сможет вести вычисления, в зависимости от того, что произойдет раньше. Считайте, что входные данные всегда правильны. Например, для Л-4 результаты на процессоре, имеющем maxint - 32767, должны быть следую- щими: X Х"4 1 1 2 16 3 81 4 256 5 625 6 1296 7 2401 8 4096 9 6561 10 10000 11 14641 12 20736 13 28561 В третьем разделе таблицы отрабатывается оператор пока. Требуемое для него условие получается обратным методом, разобранным в гл. 10. Выясняется, что выход из цикла может произойти по двум совершенно разным причинам, и обе они учитываются. В последнем разделе таблицы добавляются необходимые детали и устанав- ливается, что процедура calculatepower должна иметь четыре аргумента. Два из них нужны для задания числа и степени, в которую оно возводится, один, чтобы вернуть в него результат, и еще один для записи исхода операции: успех или неудача. Назовем их number, power, answer, OK. Перед тем как составлять процедуру calculatepower, нужно решить, как будет передаваться каждый из параметров - по значению или по ссылке. Следуя ранее сформулированным правилам, получим: а) процедуре нужен доступ для чтения или доступ к дубликату для данных, участвующих в вычислении, значит, number и power могут передаваться по значению;
Гл. 11. Процедуры с параметрами 207 б) процедуре нужен как минимум доступ для записи к тем переменным, куда помещаются результаты, значит, answer и ОК должны передавать- ся по ссылке. Таблица 11.6. Разработка процедуры processonetabulation из программы ТаблицаСтепеней Шаги разработки Примечания processonetabulation -> begin ввести значение к; выдать заголовок; инициализировать нужные переменные; пока не пора закончить выполнять begin х х + 1; посчитать хк; выдать хи? end end локальные переменные к : integer; х : integer; xpowerk : integer; Плохо! Возведение в степень может вызвать переполнение processonetabulation -> begin ввести значение к; выдать заголовок; инициализировать нужные переменные; while не пора закончить do begin х х + 1; попытаться посчитать л?; if successful then выдать х n хк end end локальные переменные к, х, xpowerk : integer; successful: Boolean; воспользоваться процедурой calculatepower пора закончить -> (х - maximum) or not successful не пора закончить -> (х о maximum) and successful локальная константа maximum - 50; processonetabulation -> begin readln (k); write (’X’ : 20, ’X"’ : 14, к : 1); writeln; x 0; successfultrue; while (x <> maximum) and successful do begin x x + 1; calculatepower (x, k, xpowerk, successful); if successful then writeln (x : 20, xpowerk : 15) end { (x - maximum) or not successful) end { processonetabulation };
208 Гл. 11. Процедуры с параметрами Учитывая, что number, power и answer - целые числа, а ОК имеет булевский тип, получаем для процедуры следующий заголовок: procedure calculatepower {number, power : integer, var answer : integer, var OK : Boolean) и поскольку параметры делают процедуру совершенно независимой от осталь- ного текста, ее составление равносильно написанию отдельной программы. Таблица 11.7. Разработка процедуры calculatepower из про- граммы ТаблицаСтепеней Шаги разработки Примечания calculatepower -> begin инициализировать нужные переменные; пока не пора закончить выполнять begin power power - 1; answer answer * number end; OK вычисление полностью завершено end пора закончить -> (power - 0) or грозит переполнение не пора закончить -> (power о 0) and переполнение не грозит переполнение не грозит -> answer <- limit где limit - maxint div number локальная переменная limit: integer; вычисление полностью завершено -> power - 0 calculatepower -> begin answer 1; limitmaxint div number, while (power <> 0) and (answer <- limit) do begin power power - 1; answer answer * number end; { (power - 0) or (answer > limit) } OK power - 0 end * В табл. 11.7 определяется логическая структура процедуры calculatepower, которая является развитием простой программы из упр. 2.4 (с. 43). Изменение коснулось оператора пока, который завершается теперь по окончании вычисле- ний или же накануне переполнения. Единственное, на что нужно обратить осо- бое внимание, это оператор, определяющий, совершилось ли вычисление.
Гл.11. Процедуры с параметрами 209 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 program TabulatePowers (input, output); { Читает k и строит таблицу степеней х~к для х - 1, 2, 3 ... } procedure calculatepower (number, power: integer, var answer : integer, var OK: Boolean) ; { посылает в answer, если возможно, значение number power и устанавливает ОК равным true; в противном случае в ОК посылает false } var limit : integer, begin { (number > 0) and (power > 0) } answer 1; limitmaxint div number, while (power <> 0) and (answer <- limit) do begin power power - 1; answer answer ♦ number end; { (power - 0) or (answer > limit) } OK power - 0 end { calculatepower }; procedure processonetabulation; const maximum - 50; var к, x, xpowerk : integer; successful: Boolean; begin readin (k); write ('X' : 20, ’2Г’: 14, к : 1); writein; x 0; successfultrue; while (x <> maximum) and successful do begin x x + 1; calculatepower (x, k, xpowerk, successful); if successful then writein (x : 20, xpowerk : 15) end { (x - maximum) or not successful} end { processonetabulation }; begin writein (’**** РЕЗУЛЬТАТЫ ПРОГРАММЫ ТАБЛИЦА СТЕПЕНЕЙ ****'); writein; processonetabulation; writein; writein (’**** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦***’) end { TabulatePowers }. Листинг 11.4. Текст программы ТаблицаСтепеней Из утверждения, стоящего вслед за оператором пока в последнем разделе таблицы разработки { (power = 0) or (answer > limit) ) можно заключить, что вычисление успешно завершено, только если конечное значение power равно нулю. Значит, оператор, устанавливающий значение ОК, записывается просто как ОК := power = 0
210 Гл. 1L Процедуры с параметрами и если ОК принимает значение ложь, то результат, получаемый в answer, каков бы он ни был, лишен смысла. Этим наш пример почти завершается. Собрав процедуры processoneword и calculatepower стандартным способом в единую программу ТаблицаСтепеней, получим ее текст в окончательном виде (листинг 11.4). В строку 14 добавлена одна важная деталь, а именно предусловие { (number > 0) and (power >= 0) } которое должно напоминать, что процедура calculatepower будет работать правильно не для всех значений number и power. Проведя ручную проверку, вы убедитесь, что правильная работа процедуры имеет место только для строго положительного number и неотрицательного power. В этой главе мы рассматривали процедуры, хотя часто называли их под- программами. В действительности все, что говорилось о процедурах, применимо в равной степени и к другому виду подпрограмм в Паскале, а именно к функ- циям. Функции мы рассмотрим в следующей главе. Упражнения 11.1. Изучите листинг 11.5. а) Сделайте трассировку программы ParameterTest (с той же степенью подробности, что и в табл. 11.4), считая, что входной файл содержит 1 1 ABCD б) Не делая подробную трассировку, запишите результаты при следующих входных параметрах: 2 3 PQRS WXYZ 11.2. Повторите упр. 11.1, произведя в программе ParameterTest следующие изменения: строку 3 замените на строку: procedure jumble (one, two, three: char)', строку 10 замените на строку: procedure processonepattern (var n: integer); 11.3. Изучите следующую процедуру: procedure writemany (ch: char, length: integer)', begin while length > 0 do begin write (ch)\ length length - 1; end; end; { writemany }; а) Если переменная ch равна ’X*, а переменная number равна 5, то каков будет результат выполнения следующих операторов процедуры: 1) writemany (ch, 4* number - 12) 2) writemany (ch, 6) 3) writemany number) 4) writemany 6) 5) writemany (’P\ -10)
Гл.11. Процедуры с параметрами 211 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 program ParameterTest (input, output)', procedure jumble (var one, two, three : char); var temp : char, begin temp := one', one :- two', two := three', three :- temp end {jumble }; procedure processonepattern (n : integer)', var chi, ch2, ch3, ch4 : char, begin readln (chi, ch2, ch3, ch4)\ writeln (chi, ch2, ch3, ch4)\ while n > 0 do begin jumble (chi, ch2, ch4)\ jumble (chi, ch2, ch3)\ write (chi, ch2, ch3, ch4)\ n :- n - 1 end; writeln end { processonepattern }; procedure processallpatterns', var patterncount, repeatcount : integer, begin readln (patterncount, repeatcount)', while patterncount > 0 do begin processonepattern (repeatcount); patterncount:- patterncount - 1 end end { processallpatterns }; begin processallpatterns end { ParameterTest }. Листинг 11.5 б) Напишите операторы обращения к процедуре writemany, которые при исполнении выводили бы следующие символы: 1) тридцать пять последовательных дефисов; 2) в шесть раз больше пробелов, чем текущее значение number, 3) текущее значение ch, повторенное четырнадцать раз. 11.4. Переделайте программу Прогрессия2 (с. 151), исключив из нее глобальные переменные за счет придания процедуре writetheprogression трех подходящих параметров. 11.5. Напишите программу ПересчетВремени, которая читает временные данные из файла и преоб- разует указанное время суток из 24-часового формата в 12-часовой1. Более точно: Входные данные Каждая строка входного файла содержит название пункта назначения (одно слово, за которым идет пробел), а также время отправления и прибытия для этого пункта, заданное в 24-часовом формате, например: 1 Принятая в некоторых странах запись времени суток, при которой часы указываются в пределах от 1 до 12, и при этом оговаривается ’’часть суток”: до или после полудня. Ср. в русском ’’восемь утра” и ’’восемь вечера’.’ - Примеч. пер.
212 Гл. 11. Процедуры с параметрами Бирмингем 11.45 14.05 Входной файл завершается строкой, первым символом которой будет звездочка. Результаты После заголовка должны следовать строки результатов, по одной для каждой пра- вильной входной строки, причем в каждой из них указывается время отправления и при- бытия в обоих форматах времени. Например, для вышеприведенной строки входных данных результирующая строка должна выглядеть так: Бирмингем 11.45 14.05 11.45 утра 2.05 дня ьсли исходное время указано некорректно, то преобразованное время должно состоять из последовательности вопросительных знаков. Например: Глазго 11.95 23.17 ???? 11.17 ночи 11.6. Биномиальный коэффициент С(п,к) определяется как n(n-l)(n-2)...(n-£+l) 7.’7- ПРИ условии, ЧТО ИЮ, Л>«0, П>~к. Ш-1)(Л-2>...(1) Если Л-О, то ни в числителе, ни в знаменателе нет ни одного члена, а результат по определению равен 1. Кроме того, С (и, к) - С (и, п-к). Например: С (9,5) - С (9,4) - ^44’1 ” 126’ а С(5’5) = С(5,0) “ 1 Напишите процедуру с заголовком procedure binomialcoef (п,к : integer, var answer : integer, var OK : Boolean)', которая, если процессор может выполнить вычисление, возвратит в answer коэффициент С(п,к) и сделает ОК истинным. В противном случае она присвоит ОК ложь, а значение answer не будет иметь смысла. Напишите также тестирующую программу, которая проверит вашу процедуру на широком диапазоне значений п и к.
12 ФУНКЦИИ Функция - это подпрограмма, вырабатывающая некоторую ’’порцию” ин- формации, которая называется результатом функции. Понятие функции извест- но из математики, но широко применимо и за рамками этой науки. В стандарте Паскаля есть два вида функций: а) функции, введенные программистом, которые описываются в разделе функций и процедур некоторого блока и могут затем вызываться внутри этого блока; б) предопределенные, или встроенные функции, которые, как и встроенные процедуры, являются частью языка и могут употребляться в программе, не будучи описанными. В этой главе будет определено несколько простых функций, рассмотрено их синтаксическое оформление, изучено несколько встроенных функций и соста- влены две программы, существенно использующие функции. 12.1. Функции, определяемые программистом Допустим, что мы пишем программу, которая ищет меньшую из пары целочисленных величин. Если бы мы исходили из ранее приобретенных знаний, то для этой цели, вероятно, попытались написать такую небольшую процедуру: procedure findsmaller (х,у: integer; var result integer); begin if x < у then result := x else result := у end {findsmaller }; Чтобы воспользоваться этой процедурой, мы должны записать оператор процедуры. Если, к примеру, переменная number равна 25, a answer - пере- менная типа integer, то оператор findsmaller (number * number, 100, answer) присвоит answer значение 100. Наиболее важно для нас сейчас то, что процедура имеет единственное предназначение - вычисление некоторой величины. В таких ситуациях с тем же успехом и часто с большим удобством могут применяться функции. Как и в случае процедур, функция, вводимая программистом, должна быть описана до того, как будет использована. Функция под названием
214 Гл. 12. Функции smaller, почти эквивалентная вышеприведенной процедуре, может быть опреде- лена так: function smaller (х,у: integer): integer, begin if x < у then smaller :« x else smaller :- у end { smaller}; Описание функции имеет два основных элемента: заголовок функции и блок функции. Помимо того очевидного отличия, что описание функции начинается со служебного слова function, тогда как описание процедуры - со слова procedure, эти описания отличаются только в двух важных отношениях. Первое отличие состоит в том, что результат функции возвращается че- рез идентификатор функции, так что с последним должен быть связан опре- деленный тип данных. Он называется типом результата и стоит в заключение заголовка функции. В нашем примере фрагмент : integer в заголовке функции (второй из двух) связывает с идентификатором smaller тип integer. Значит, функция smaller будет возвращать целочисленный результат. Второе отличие заключается в том, каким образом функция получает результат. При любом ходе вычисления внутри функции хотя бы однажды должен выполниться оператор, присваивающий значение идентификатору функции. В функции, приведенной выше, это требование удовлетворяется благодаря операторам smaller := х и smaller := у Одно из этих присваиваний обязательно будет выполнено, и поэтому неко- торое значение всегда будет присвоено идентификатору smaller. Если, однако, возможен такой ход выполнения функции, когда оператор присваивания иден- тификатору функции не встречается, то результат функции не определен и это является ошибкой программирования. С другой стороны, вполне допустимо несколько таких присваиваний, следующих друг за другом. Результатом функции в таком случае является последнее присвоенное значение. Если не считать упомянутых отличий, описание функции очень похоже на описание процедуры. В частности, точно так же, как в процедуре, могут быть указаны формальные параметры, а блок функции, как всякий блок, может содержать описания локальных констант и переменных. К процедуре мы обращаемся с помощью оператора процедуры. В отличие от этого, обращение к функции употребляется в выражениях в качестве множителя и выглядит как идентификатор функции, вслед за которым, если необходимо, следует список фактических параметров. Поскольку функция smaller имеет тип integer, то обращение к ней может стоять всюду, где допустим целочисленный множитель. Например, оператор присваивания answer := smaller (number * number, 100) равносилен оператору процедуры findsmaller (number * number, 100, answer) а в операторе total := 3 * smaller (a,b) + 5 * smaller (c,d)
Гл. 12. Функции 215 функция вызывается дважды, и ее результаты участвуют в вычислении арифме- тического выражения точно так же, как переменные или константы. Пусть, на- пример, а = 2, = 5, с-11, <7 = 7, тогда переменной total будет присвоено значение 3*2+5*7, т.е. 41. Поскольку параметры оператора write образуются из выражений, то правильным будет оператор writein (’Меньшее из а : 1, ’ и ’, b : 1, ’ равно smaller (a,b) : 1, который при тех же значениях а и b выведет строку Меньшее из 2 и 5 равно 2. Наконец, в операторе smallest :» smaller (smaller (а,Ь), с) результат функции smaller употреблен как фактический параметр при обраще- нии к ней самой. Это вполне корректно, поскольку параметр передается по зна- чению, и в соответствии с прежними значениями переменных a, Ь, с переменной smallest будет присвоен результат, равный 2. Особенно полезны функции, возвращающие булевские значения. Следую- щая функция вырабатывает истинное значение, если параметр с является симво- лом из диапазона ’О’..’9’: function digit (с: char): Boolean; begin digit (c >= ’O’) and (c <= ’9’) end {digit}; и может употребляться, например, в таких операторах while digit (inpuC) do copyonecharacter; Здесь циклически повторяется обращение к процедуре copyonecharacter до тех пор, пока в переменной inpuC не окажется символ, отличный от цифры. 12.2. Синтаксис функций Формальный синтаксис функций и их параметров в стандартном Паскале определен следующим образом: а) На рис. 6.46 (с. 104) определяется обращение к функции как один из ви- дов множителя. Это дает возможность вызывать функции внутри выра- жений. б) Рис. 6.10 утверждает, что конструкция идентификатор-функции выражение есть одно из определений оператора присваивания, что позволяет внут- ри функции присваивать значение идентификатору функции. в) На рис. 6.15 (с. 115-116) определяется описание функции, которое может содержать список формальных параметров, а также указывается, что описания функций и процедур могут в блоке перемежаться. г) На рис. 11.1 (с. 200-201) дается определение списка формальных и списка фактических параметров, которые для функций выглядят точно так же, как для процедур. Следует обратить особое внимание на два момента. Во-первых, тип резу- льтата определяется как имя простого типа. Существенное свойство простого типа - то, что значение такого типа представляет один элемент информации в противоположность структурному типу (см. гл. 16), значения которого состоят
216 Гл. 12. Функции больше, чем из одного элемента. Простые типы данных, которыми мы пользова- лись до сих пор и которые будут употребляться в следующей главе, это: integer char Boolean real Простые типы, как мы увидим в гл. 16, могут также определяться программис- том. Во-вторых, хотя функция может иметь ссылочный параметр, внутри нее никогда не следует менять значение переменной, передаваемое по ссылке, равно как и присваивать значение непосредственно глобальной переменной. И то и другое нарушает принцип, в соответствии с которым единственное назначение функции - вычисление результата1. В абзаце, открывающем эту главу, было сказано, что функция - это под- программа, вычисляющая ’’некоторую порцию информации”. Столь осторожный выбор формулировки объясняется тем, что порция информации, если брать понятие функции в самом общем виде, не сводится к таким простым элементам данных, как отдельное число. Существует важный метод, называемый функцио- нальным программированием, который почти всецело основан на функциях, причем их результатом могут быть сложные структуры данных. Эти вопросы лежат за рамками данной книги. 12.3. Некоторые встроенные функции Большинство языков программирования предоставляет определенный на- бор встроенных функций. Стандарт Паскаля требует наличия в Паскаль-системе семнадцати встроенных функций. Девять из них мы разберем в этом разделе, а остальные рассмотрим вместе с действительными числами в гл. 13. 12.3.1. Арифметические функции abs ia sqr Мы начинаем с двух встроенных функций - abs и sqr, которые предназна- чены для широко употребительных арифметических операций и определяются следующим образом: abs (х) вычисляет абсолютную величину арифметического выражения х; sqr (х) вычисляет квадрат арифметического выражения х. Для этих двух функций характерно то необычное свойство, что их параметр х может быть как типа integer, так и типа real, а результат имеет тот же тип, что и параметр. Поскольку абсолютная величина, самое большее, меняет знак исходного числа, функция abs не может завершиться неудачей. В то же время при возве- дении в квадрат может получиться число, не представимое на данном компьюте- ре, поэтому вычисление sqr (х) не всегда возможно. Обращение к функции sqr, если значение ее параметра в квадрате вычислить невозможно, является ошибкой программирования. 12.3.2. Порядковые функции ord, chr, succ, pred Следующие четыре встроенные функции, ord, chr, succ, pred, мы обсудим применительно лишь к символьной обработке, хотя они имеют более широкое употребление. 1 Часто этот принцип, принятый в структурном программировании, формулируют как отсутствие побочного эффекта. - Примеч. пер.
Гл. 12. Функции 217 Как вы, вероятно, помните, при описании в гл. 3 набора символов ASCII было отмечено, что каждому символу поставлено в соответствие десятичное чис- ло, называемое его порядковым номером. Порядковый номер, скажем, символа fa’ равен 97. Какой бы набор символов ни использовался на вашем компьютере, их можно пронумеровать, начиная с нуля, приписав каждому символу порядко- вый номер. Исходя из этого можно определить функции ord и chr следующим образом: ord (х), возвращает целое число, которое является порядковым номером символьного выражения х; chr (х) возвращает символ с порядковым номером, равным зна- чению целочисленного выражения х, если такой символ существует. Для набора ASCII, таким образом: ord ('а') равен 97, а chr (97) имеет значение ’а’ и для любого набора символов всегда справедливо соотношение chr (ord (с)) = с где с имеет тип char. Поскольку всегда существует порядковый номер данного символа, то выполнение функции ord не может кончиться неудачей, однако не каждое целое является порядковым номером некоторого символа, поэтому обращение к функции chr с параметром, значение которого выходит за пределы порядковых номеров, является ошибкой программирования. Во всяком наборе символов, в котором содержатся прописные (большие) и строчные (малые) буквы, причем те и другие расположены подряд, можно при- менить функции ord и chr для того, чтобы преобразовывать малые буквы в большие и обратно. Следующая функция возвращает для большой буквы, явля- ющейся ее параметром, соответствующую ей малую при условии, что мы имеем дело с набором ASCII (так как в этом наборе символов каждая строчная буква имеет порядковый номер на 32 больше, чем соответствующая прописная): function lowercase (с: char): char; begin { (с >= ’A’) and (c <= ’Z’) } lowercase := chr (ord (c) + 32) end { lowercase}; Поскольку символы, входящие в набор, образуют последовательность значений, полезно иметь возможность переходить от какого-то значения к соседнему. Этому и служат функции succ и pred. Для данного случая они могут быть опре- делены следующим образом: succ (х) возвращает символ, если таковой существует, порядковый номер которого на единицу больше, чем у символа х; pred (х) возвращает символ, если таковой существует, порядковый номер которого на единицу меньше, чем у символа х. Таким образом: succ С а9) равен V pred (’£’) равен ’а’. Разумеется, у последнего элемента последовательности нет следующего элемента так же, как у первого нет предыдущего. Следовательно, ошибкой программирования является вызов функции succ, когда х - последний элемент последовательности, и вызов функции pred, когда х - ее первый элемент.
218 Гл. 12. Функции 12.3.3. Булевы функции odd, eof, eoln Последняя группа встроенных функций, которые мы намерены здесь рас- смотреть, включает булевы функции odd, eof, eoln. Первая из них почти тривиальна: odd (х) истинно, когда целочисленное выражение х нечетно, и ложно, когда х является четным числом. Таким образом, эта функция равно- сильна следующей: function odd (х: integer)-. Boolean*, begin odd := abs (x) mod 2 = 1 end { odd}; Две другие встроенные функции не столь просты. Завеса таинственности приподнимется, если объяснить, что: a) eof есть сокращение от end of file, ’’конец файла”; б) eoln - сокращение от end of line, ’’конец строкй’; в) в наиболее простой форме они употребляются без параметров и в этом случае относятся к входному файлу. Вспомните гл. 3, где было сказано, что входной файл, будучи текстовым, образован из символьных строк. Данные две функции позволяют узнать текущее состояние файла. Они могут быть определены следующим образом: eof возвращает значение ” истина” (true), когда непро- читанная часть входного файла пуста, и значение ’ложь’ (false) - в противном случае; eoln возвращает значение’’истина” (true), когда текущая по- зиция входного файла находится на маркере ’конец строки’, и значение ’’ложь” (false) в том случае, если текущая позиция установлена на некотором символе внутри строки. В табл. 12.1 показаны различные состояния входного файла в процессе чтения из него данных и значения функций eof и eoln, соответствующие этим состояниям. Отметим следующие два момента. Во-первых, в части (б) можно было бы ожидать, что eof истинно, так как все содержательные входные данные уже считаны. С точки зрения процессора, однако, маркер конца строки сам требует считывания, и поэтому eof имеет значение ”ложь”. Во-вторых, в части (в) eoln является неопределенным. Следовательно, если истинно eof, то обращение к eoln есть ошибка программирования. Таблица 12.1. Образцы входных файлов, иллюстри- рующие значения функций eof и eoln а) Текстовый файл, для которого eof равно false и eoln равно false Это"простой"текстовый"файл,#состоящий"из"двух"строк"текста. # б) Два текстовых файла, для которых eof равно false, a eoln равно true Это"простой"текстовый"файл,#состоящий"из"двух"строк"текста. # Это"простой"текстовый"файл,#состоящий"из"двух"строк"текста."""# в) Текстовый файл, для которого eof равно false и eoln равно false Это"простой"текстовый"файл,#состоящий"из"двух"строк"текста. #
Гл. 12. Функции 219 12.4. Использование функций при составлении программы Мы видели, что процедуры естественно возникают в процессе разработки программы, поскольку всякое не до конца развернутое предложение представля- ет собой подпроцесс и может быть реализовано процедурой. В свою очередь, функция может применяться для реализации неразвернутого выражения. Для иллюстрации того, как применяются функции, рассмотрим построение двух программ, в которых использованы в зависимости от обстоятельств как встроенные, так и определенные программистом функции. 12.4.1. Программа, обрабатывающая текст Первым примером будет программа ДлинаТекста, спецификация и разра- ботка которой показаны в табл. 12.2. В окончательном виде программа показана на листинге 12.1. Это первая задача текстовой обработки, в которой нам приходится иметь дело более чем с одной строкой текста. В таких задачах текст всегда можно рассматривать с двух точек зрения: а) как последовательность строк, кончающихся маркером конца строки; б) как последовательность символов, в которой маркеры конца строки чи- таются точно так же, как остальные символы. Вначале надо решить, какую из этих точек зрения на текст удобнее принять. Иногда обе они приемлемы; иногда только одна служит решению задачи, другая же не работает. В нашем случае все определяется тем фактом, что вводимый текст нужно вывести обратно. Для этого нам нужно знать, где находятся концы строк, поэтому текст будем рассматривать как последовательность строк. Приняв это решение и отметив, что для подсчета количества слов необхо- дима целочисленная переменная, придем к структуре основной процедуры, показанной в первом разделе таблицы разработки (с. 220). Процедуру назовем processthetext, ее локальную переменную - numberofwords. Первая возможность употребить функцию представляется нам, когда нужно развернуть булевское выражение не в конце текста которое управляет выполнением оператора цикла. Если принять, что при обра- ботке каждой строки она читается полностью, вместе с маркером конца строки, это условие можно выразить просто как not eof и никакого уточнения больше не требуется. Следующий этап разработки программы состоит в уточнении подпроцесса обработать одну строку который мы реализуем как еще одну процедуру. Она должна переносить строку текста из входного файла в выходной, находить в ней слова и изменять numberofwords, локальную переменную процедуры processthetext. Для того чтобы иметь доступ к переменной numberofwords, наша новая процедура должна полу- чить ссылочный параметр, который назовем wordcount. Следовательно, заголо- вок процедуры будет таким: procedure processoneline (var wordcount: integer) а обращаться к ней нужно будет с помощью оператора процедуры processoneline {numberofwords)
220 Гл. 12. Функции так что numberofwords станет фактическим параметром, соответствующим формальному параметру wordcount. Всякая операция, производимая процедурой над wordcount, фактически будет производиться над numberofwords. Таблица 12.2. Спецификация и разработка программы ДлинаТекста Составьте Паскаль-программу под названием ДлинаТекста, которая читает последовательность строк текста, выводит его в том же виде и после этого сообщает общее количество слов в тексте. Слово определяется как последовательность букв. Шаги разработки Примечания обработать текст -> begin вывести заголовок; инициализировать счетчик числа слов; пока не в конце текста выполнять обработать одну строку; выдать общее число слов end локальная переменная numberofwords'. integer, обработать одну строку -> begin скопировать все до буквы или до конца строки; пока не на маркере конец-строки выполнять begin скопировать до конца слова; изменить счетчик слов; скопировать все до буквы или до конца строки end; прочитать маркер конца строки и перейти на новую строку end ссылочный параметр wordcount: integer, скопировать все до буквы или до конца строки -> begin пока не на букве и не на маркере конец-строки выполнять скопировать один символ { на букве или на маркере конец-строки } end скопировать до конца слова -> begin пока на букве выполнять скопировать один символ { на ’’пробеле” или на маркере конец-строки } end letter { функция булевского типа } -> begin letter с большая буква или с малая буква end параметр-значение с : char Проектируя логику процедуры processoneline, мы должны рассмотреть структуру строки текста и тщательно проследить за тем, чтобы все виды строк,
Гл. 12. Функции 221 которые могут встретиться, были правильно обработаны. В спецификации ска- зано, что слово есть последовательность букв; значит, все остальные символы можно считать пробелами. К примеру, цифра, дефис или знак препинания в данном контексте считаются ’’пробелами”. Таким образом, строка это последова- тельность слов, чередующихся с пробелами, которая заканчивается маркером конца строки, и всего имеется семь следующих основных видов строк: а) пробелы слово ... слово пробелы конец-строки б) пробелы слово ... пробелы слово конец-строки в) слово пробелы ... слово пробелы конец-строки г) пробелы слово ... пробелы слово конец-строки д) пробелы конец-строки е) слово конец-строки ж) конец-строки В этом есть сходство с программой СчетчикСлов, которую мы составили (с несколькими преднамеренными ошибками) в гл. 9. На этот раз, надеемся, процедура сразу будет составлена правильно. Ее схема показана во втором разделе таблицы разработки (с. 220), и прежде чем читать дальше, убедитесь, что она обрабатывает все упомянутые выше случаи при том единственном усло- вии, что подпроцесс скопировать все до буквы или до конца строки не производит никаких действий, если текущая позиция во входном файле уже содержит букву или маркер конца строки. Продолжая составление процедуры processoneline, обратимся прежде всего к неуточненному логическому выражению не на маркере конец-строки которое управляет выполнением оператора цикла. С помощью встроенной функ- ции оно записывается просто как not eoln Опасений, что функция не сработает, нет, так как отказ может произойти только в конце файла (при истинном значении eof), а у нас по крайней мере один символ, конец строки, в этот момент еще остается не считанным. Третий и четвертый разделы табл. 12.2 раскрывают подпроцессы скопировать все до буквы или до конца строки и скопировать до конца слова которые в окончательном варианте программы превращаются в процедуры copytoaletterorendofline и copytoendofword. Обе эти процедуры должны переносить символы из входного файла в выходной и, значит, обращаться к процедуре copyonecharacter, использованной нами ранее в нескольких программах. Обе они также должны уметь определять, является ли текущий символ в файле буквой. Последнее требование можно удовлетворить, вводя функцию с заголовком function letter (с: char)-. Boolean-, которая будет решать, является ли символ с буквой. С помощью этой функции и опять-таки функции eoln выражения не на букве и не на маркере конец-строки и
222 Гл. 12. Функции 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 program TextLength, {input, output) ; { Скопировать текст и подсчитать в нем количество слов } function letter (с : char) : Boolean; begin { Внимание! Здесь предполагается, что имеются последовательные наборы A’..Z’ и *a*..'z* и в обоих символы идут подряд } letter (с >- ’A’) and (с <- ’Z’) or (с >- ’a’) and (с <- ’х’) end {letter}; procedure copyonecharacter, var ch : char ; begin read (ch); write (ch) end { copyonecharacter}; procedure copytoaletterorendofline; begin while not letter (inpur) and not eoln do copyonecharacter end ( copytoaletterorendofline }; procedure copytoendofword; begin while letter (inpur) do copyonecharacter end { copytoendofword } procedure processoneline (var wordcount: integer); begin copytoaletterorendofline; while not eoln do begin { inpur - первая юва } copytoendofword; wordcountwordcount + 1, copytoaletterorendofline end; readln; writeln end { processoneline }; procedure processthetext; var numberofwords : integer; begin writeln ('Обрабатывается текст:'); writeln; numberofwords 0; while not eof do processoneline (numberofwords); writeln; writeln СВ этом тексте numberofwords : 1, ’ слов.'); end { processthetext}; begin writeln (’**♦♦ РЕЗУЛЬТАТ РАБОТЫ ПРОГРАММЫ ДЛИНА ТЕКСТА ♦♦♦♦); writeln; processthetext; writeln; writeln (’♦♦♦♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦*♦♦’) end { TextLength }. Листинг 12.1. Текст программы ДлинаТекста
Гл. 12. Функции 223 на букве можно развернуть так: not letter (inpur) and not eoln и letter (input*) К сожалению, конкретные детали функции letter зависят от набора сим- волов, предоставляемых конкретным процессором. Мы приняли, что в имеющем- ся наборе символов как большие буквы >T..Z’ так и малые ’a’./z’ следуют подряд1. При таком допущении значение функции letter можно вычислить одним оператором letter :« (с >= ’Л’) and (с <= ’Z’) or (с >= ’a’) and (с <- ’z’) однако на вашем Паскаль-процессоре такое описание может быть и неверным. Стандарт Паскаля не требует, чтобы были в наличии как большие, так и малые буквы, а кроме того, буквы хотя и должны следовать в алфавитном порядке, но не обязаны идти подряд. Предупреждающий комментарий на этот счет в оконча- тельной программе помещен в начало функции letter. Таким образом, всего имеется шесть подпрограмм, собранных в нужном порядке. Схема взаимных вызовов подпрограмм следующая: ДлинаТекста processthetext proces soneline copytoaletterorendofline letter copyonecharacter copytoendofword letter copyonecharacter и в соответствии с правилами Паскаля порядок описания их таков: a) letter и copyonecharacter должны описываться раньше остальных, в лю- бом взаимном порядке, б) copytoaletterorendofline и copytoendofword должны описываться в любом взаимном порядке вслед за ними, в) processoneline должна быть описана пятой, г) processthetext должна быть описана шестой. Именно в таком порядке мы находим эти подпрограммы в окончательном тексте программы ДлинаТекста (листинг 12.1). 12.4.2. Программа численного расчета Вторая наша программа является расчетной, однако самое сложное, что она содержит, это произведение положительных чисел, и поэтому она будет по- нятна даже наименее технически ориентированному читателю. Программа назы- вается СовершенныеЧисла. Ее спецификация и разработка показаны в табл. 12.3, а окончательный вид - на листинге 12.2. Программа предназначена для поиска совершенных чисел в заданных пре- делах, причем совершенным называется число, равное сумме своих 1 Автор, естественно, рассматривает лишь тексты на основе латинского алфавита, и мы сохраняем это допущение. Чтобы обрабатывать русскоязычные тексты, функцию letter потребуется усложнить. - Примеч. пер.
224 Гл. 12. Функции сомножителей, не считая его самого. Примером служит число 6, сомножителями которого являются 1, 2 и 3, и сумма 1 + 2 + 3 в 6. Таблица 12.3. Спецификация и частичная разработ- ка программы СовершенныеЧисла Разработать Паскаль-программу под названием СовершенныеЧисла, которая читает положительное целое число limit и выдает совершенные числа в диапазоне 1..limit. Совершенным является положительное число, равное сумме своих множителей, за исключением самого числа. Шаги разработки Примечания найти совершенные числа -> begin ввести предельное число; вывести заголовок; number 0; пока number < limit выполнять begin number number + 1; если number совершенное число то выдать его end локальные переменные limit, number, integer, perfect { функция типа Boolean } -> begin if п - 1 then perfectfalse else begin sum n - 1; вычислить первую пару возможных сомножителей; посчитать шаг между возможными сомножителями; while все неодинаковые пары не проверены and сумма sum еще положительна do begin if текущая пара - пара сомножителей then вычесть их из суммы; вычислить следующую пару сомножителей end; if сомножители равны then вычесть один из них из суммы; perfectsum - 0 end end параметр i: integer, передается по значению локальные переменные sum, low, high, step : integer; Как обычно, основная логика программы сосредоточена в процедуре. Про- цедура названа findperfectnumbers и устроена весьма просто: проверяет подряд все числа с единицы до заданного и выдает те из них, которые являются совер- шенными. По-видимому, понятно, что, реализуя выражение число является совершенным мы прибегнем к функции. Если нам удастся написать функцию с заголовком: function perfect (и: integer): Boolean;
Гл. 12. Функции 225 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 >0 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 program PerfectN umbers (input, output); { Ищет все совершенные числа, не превосходящие заданного числа. Совершенным называется число, равное сумме своих сомножителей, не считая его самого } function perfect (п : integer) : Boolean; var sum, low, high, step : integer; begin if n - 1 then perfectfalse else begin sum n - 1; { вычислить первые возможные сомножители и величину шага } if odd (п) then low 3 else low 2; high n div low; if odd (n) then step 2 else step Г, { пробовать сделать сумму отрицательной^вычитая сомножители } while (low < high) and (sum >- 0) do begin if low * high “ n then sum sum - low - high; low low + step; high n div low end; { low >- high or sum < 0 } if (low - high) and (low * high - n) then sum sum - low; { проверить,равна ли сумма в точности нулю } perfectsum - 0 end end { perfect}; procedure findperfectnumbers; var limit, number : integer; begin readln (limit); writein ('Следующие числа, не превосходящие limit: 1, являются совершенными:'); writein; number 0; while number < limit do begin number number + 1; if perfect (number) then writein (number : 10) end { number >- limit} end {findperfectnumbers }; begin writein (’♦♦♦♦ РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ ****’); writein; findperfectnumbers; writein; writein (’***♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦*♦*’) end { PerfectNumbers }. Листинг 12.2. Текст программы СовершенныеЧисла 8 Заказ № 1110
226 Гл. 12. Функции которая для данного целого числа п возвращает истину, если п совершенно, и ложь в противном случае, то вышеприведенное выражение можно развернуть так: perfect [number) а всю процедуру findperfectnumbers записать на Паскале, как показано в строках 37-51 листинга 12.2. Решение использовать функцию на этой стадии процесса разработки не может быть окончательным. Функцию следует употреблять лишь тогда, когда очевидно, что ее результат может быть посчитан для всех уместных значе- ний параметра или параметров, ибо функция, которая дает отказ в разгар своей работы, не может быть частью устойчивой программы. Для функции не- возможен результат типа ’’нельзя вычислить”. Способны ли мы составить функ- цию perfect, правильно работающую для всех подходящих значений п? Чтобы обеспечить наибольшие возможности при пользовании программой, попробуем составить ее так, чтобы она работала для любого п из диапазона 1 ..maxint. Прежде чем продолжить чтение этого раздела, советуем вам несколько ми- нут самостоятельно поразмышлять над задачей. Это позволит вам легче воспри- нять последующий разбор. Первое, что могло бы прийти в голову относительно структуры функции perfect, это простая мысль искать один за другим все сомножители числа и складывать их. Возможно, что вы решили проверить все сомножители от 2 до п div 2. Такой подход неудовлетворителен по двум причинам. Прежде всего, он подразумевает большой объем вычислений. Чтобы определить, к примеру, является ли миллион совершенным числом, потребовалось бы 499999 проверок для возможных сомножителей. Кроме того, сумма сомножителей п может пре- восходить п, и, значит, при больших п может возникнуть переполнение. Обе эти проблемы можно легко обойти. Сократить объем вычислений поможет то наблюдение, что сомножители всегда возникают парами. Например, сомножителями числа 42 являются 12 3 6 42 21 14 7 а сомножителями числа 225 - 1 3 5 15 225 75 45 15 В последней паре сомножители совпадают и считать нужно будет только один. По этому методу количество пар сомножителей, которые придется проверять, равно приближенно квадратному корню из п. Проверка числа 1 000 000 на совершенность потребует рассмотреть самое большее 999 пар возможных сомножителей. Отметим, кроме того, что нечетные числа могут иметь лишь нечетные сомножители, что иллюстрируется примером с числом 225. Избежать опасности переполнения, как увидим, тоже нетрудно. В нашем обиходе мы привыкли считать ”от начала”, так же, как слева направо читать, и кажется неестественным делать наоборот. При составлении программы часто стоит поразмышлять, не сделать ли что-нибудь ”с конца”, в непривычном направлении или порядке. В нашем примере вместо того, чтобы считать сумму сомножителей, можно взять за отправную точку сумму, которую мы желаем получить, и вычитать из нее сомножители. Принимая во внимание, что единица всегда является сомно- жителем (кроме случая п = 1, который мы будем трактовать особым образом), будем вычитать сомножители из величины п - 1 и следить, когда она станет меньше нуля. Если это произойдет, то число не является совершенным. Не будет оно таковым и в случае, если сумма останется положительной. Если же ’’сумму”
Гл. 12. Функции 227 не удалось сделать отрицательной, но она стала равной нулю, когда обнаружены все сомножители, то п является совершенным. Такая организация подсчета не только устраняет возможность переполнения, но и освобождает от поиска всех сомножителей, потому что сумма для некоторых значений п становится отрица- тельной задолго до того, как найдены все сомножители. Таковы основные идеи, стоящие за схемой функции perfect, показанной в табл. 12.3. Функция в ее итоговом виде, в строках 7-35 окончательного текста программы, дает надежный результат для любого п в диапазоне L.maxint С по- мощью данной программы вы сможете найти совершенных чисел не меньше, чем найдено Евклидом больше 2000 лет назад. Он обнаружил четыре: 6, 28, 496, 8128. Наконец, что бы мы делали, если бы нам не удалось написать функцию, заведомо работающую для всех подходящих значений параметра? Вообще гово- ря, если не удается написать вполне удовлетворительную функцию, лучше всего написать процедуру, которая среди других параметров имела бы булевский, который будет указывать на успех или неудачу вычисления. Такого рода проце- дуру мы уже строили в программе ТаблицаСтепеней в гл. 11 (с. 209). Упражнения 12.1. В гл. 3 обнаружилось, что программа ПовторСлова (с. 48) дает отказ, когда все лексемы входного файла являются пробелами или символами конца строки. Переделайте ее на процедуру с заголовком procedure copyword fvar OK' Boolean) которая в том случае, если во входном файле остаются только пробелы и символы конца строки, будет пропускать их и устанавливать параметр ОК равным false (ложь). В противном случае она будет переносить из входною файла в выходной одну последовательность не-пробелов и делать ОК истинным. Проверьте процедуру с помощью простой тестирующей программы. 12.2. В файле данных хранится набор целых чисел, по нескольку чисел в строке (может и не быть ни одного). Общее количество целых чисел не записано в файле и никаким специальным числом конец набора не помечен. Напишите программу СписокЧисел для чтения файла и вывода чисел одного под другим. Подсказка: воспользуйтесь встроенными функциями eoln и eof. 12.3. Напишите программу ЧистыйТекст, которая будет читать текст и снова выдавать его, не меняя переносы строк, но удаляя из текста все, кроме '’слов*’, и заменяя все большие буквы на соот- ветствующие им малые. ’’Словом” считается последовательность букв и/или цифр, кончающая- ся любым символом, который есть не буква и не цифра. Каждая пара слов в результате должна быть разделена единственным пробелом. Например, если входные данные содержат строки program TripPlanner (input, output); writeln (’TOTAL’, totaldistance:10); то соответствующие строки результата должны быть program tripplanner input output writeln total totaldistance 10 12.4. Напишите программу СчетчикОишбок, которая будет читать текст и снова выдавать его, встав- ляя две звездочки после каждого слова длиной более чем в 15 символов и после каждого числа (целого), имеющего болер 6 цифр, не считая начальных нулей. Слова и числа, отмеченные та- ким образом, являются ’’ошибками”. В заключение текста нужно выдать общее число "ошибок”. Слово определяется как последовательность букв, кончающаяся цифрой или терминатором. Чис- ло определяется как последовательность цифр, кончающаяся буквой или терминатором. Терми- натор есть пробел, знак препинания, скобка или кавычка. Все символы, не являющиеся буквой, цифрой или терминатором (например, дефис, апостроф, знак числа), нужно игнорировать. 8*
228 Гл. 12. Функции 12.5. Счет за электричество содержит информацию о категории потребителя, его кодовом номере, старое и новое показания счетчика, а также размер скидки. Корректные данные должны удов- летворять следующим условиям: а) категория потребителя должна обозначаться одной из букв ’А’..’С’; б) кодовый номер должен быть целым числом в диапазоне 0..99999; в) оба показания счетчика должны быть целыми числами в диапазоне 0..999999; г) израсходованное количество электроэнергии не должно превышать 100000; д) процент скидки должен отвечать категории потребителя и израсходованному количеству энергии следующим образом: категория расход скидка А < 5000 5000 0 5 В < 1000 0 >— 1000 10 С < 10000 0 > 30000 20 иначе 10 Считая, что в каждой строке входного файла содержатся символ и четыре целых числа, образующие информацию для подготовки одного счета, напишите программу проверки данных под названием ПроверкаДанных, которая будет читать файл и выводить данные в табличном виде, причем все неверные строки будут отмечены четырьмя звездочками. Примером могут служить следующие выходные строки: В 45161 4086 5192 10 А 45162 7312 8176 5 12.6. Два целых числа называются взаимными, если каждое из них равно сумме всех точных сомно- жителей другого, не считая самого числа. Например, 220 и 284 взаимные числа, поскольку со- множителями числа 220 являются 1,2, 4, 5, 10, 11, 20, 22, 44, 55 и 110, в сумме составляющие 284, а сомножители 284 - 1, 2, 4, 71 и 142 - в сумме дают 220. Напишите программу ВзаимныеЧисла, которая будет читать два числа типи находить все па- ры взаимных чисел, находящихся в диапазоне nt.n. Проверьте программу при m - 200 и п - 300, а затем при m - 5000 и и - 7000.
13 ВЕЩЕСТВЕННЫЕ ЧИСЛА До сих пор мы имели дело только с целыми числами. Теперь пора перейти к вещественной арифметике. Число, имеющее дробную часть, называют вещест- венным числом, а соответствующий тип данных в Паскале называется real. Компьютеры долгое время считались именно вычисляющими устрой- ствами, но если быть точным, компьютер, выполняющий вычисления над вещественными числами, почти всегда дает неверный результат. Насколько неверный - зависит от природы вычислений, и большей частью результаты бывают достаточно правильными, чтобы пользоваться ими. Иногда же логически безошибочная программа дает ответ настолько неточный, что он совершенно бесполезен! На протяжении почти всей этой главы мы забудем о компьютерах и будем рассматривать вещественные числа на уровне карманного калькулятора. Все факты и все ограничения, которые мы обнаружим, помогут позднее понять проб- лемы вещественной арифметики применительно к компьютеру. Мы рассмотрим также конкретные детали использования вещественных чисел в стандартном Паскале. 13.1. Представление вещественных чисел Первая проблема: как представлять числа из широкого диапазона, от очень больших до очень маленьких, при ограничении на количество знаков. Допустим, наш карманный калькулятор выводит числа на дисплей шириной 14 знаков, и обратимся к задаче изображения следующих чисел: -6392400000000.0 -639.24 -0.000000063924 Очевидно, если числа представлены именно в такой форме, то изобразить можно будет только второе число. Задача, может, однако, быть решена, если числа представляются несколько иначе. Искомый способ записи чисел таков: -6.3924Е+12 -6.3924Е+02 -6.3924Е-08 где знак Е читается как ” умножить на десять в степени”. Каждое число записа- но теперь ровно одиннадцатью знаками. Эта запись известна как экспоненциаль- ный формат, или представление с плавающей запятой. Первая часть представле- ния числа с плавающей запятой называется мантиссой, а вторая, следующая за
230 Гл. 13. Вещественные числа знаком Е, порядком. Более привычное представление, которое мы употребляли поначалу, называется представлением с фиксированной запятой 1. В представлении с плавающей запятой количество цифр в мантиссе опре- деляет точность, с которой представлено число. В приведенном примере мантисса имеет пять цифр, так что можно записать только пять значащих цифр каждого числа. Значит, всякое число не более чем с пятью значащими цифрами можно представить точно, а остальные - лишь приближенно. Так, +248.513786 превращается в +2.4851Е+02 но снова переводя в представление с фиксированной запятой, получим +248.51 с погрешностью против исходного в 0.003786. Если бы мантисса имела больше цифр, погрешность была бы меньше. Вторым свойством представления с плавающей запятой является то, что количество цифр порядка определяет наибольшее и наименьшее представимое число. В вышеприведенном формате, имеющем две цифры порядка, наибольшее число, которое может быть представлено - +9.9999Е+99 Если бы нам пришлось выводить его в формате с фиксированной запятой, оно имело бы ровно 100 цифр до запятой, т.е. и в самом деле это очень большое число. Аналогично, если не считать нуля, который изображается как 0.0000Е+00 самое маленькое положительное число будет 1.0000Е-99 В формате с фиксированной запятой оно имеет 99 десятичных знаков, а именно 98 нулей и единицу. Это очень маленькое число. Таким образом, выражая поря- док двумя десятичными цифрами, можно записывать числа очень широкого диа- пазона. 13.2. Усечение и округление Часто бывает нужно уменьшить количество цифр вещественного числа. Такая необходимость возникает, когда число преобразуется из одного представ- ления в другое, когда выполняются арифметические операции над веществен- ными числами или же когда вещественное число преобразуется в целое. В таких ситуациях можно прибегать к двум действиям: усечению и округлению. Самый простой способ уменьшить количество цифр числа - отбросить лишние, никак не затрагивая остальных. Это действие называется усечением; оно приводит к ошибке представления числа, так называемой погрешности усечения. В следующих примерах демонстрируется усечение до пяти значащих цифр и возникающие при этом погрешности: ’ У нас принято десятичную дробь отделять от целой части запятой, и термин ’’запятая” мы сохраняем, несмотря на то, что фактически в языках программирования используется другой ч зол, ’’десятичная точка” - Примеч. пер.
Гл. 13. Вещественные числа 231 число усеченное число погрешность усечения 3.14159265 3.1415 0.00009265 5.99999999 5.9999 0.00009999 -3.14159265 -3.1415 -0.00009265 Большую точность обеспечивает другой процесс, когда число усекается, а затем результат корректируется в зависимости от того, какие цифры отброшены. При этом можно действовать, например, по следующим правилам: а) когда отбрасываемая последовательность начинается с цифры в диапа- зоне 0..4, тогда усеченное число остается неизменным; б) когда отбрасываемая последовательность начинается с цифры в диапа- зоне 5..9, в последний разряд усеченного числа добавляется единица < что может повлиять и на другие цифры. Этот процесс называется округлением; ошибка, вызываемая им, называется погрешностью округления. Округление до пяти значащих цифр чисел, приведенных в примере выше, даст число округленное число погрешность округления 3.14159265 3.1416 0.00000735 5.99999999 6.0000 0.00000001 -3.14159265 -3.1416 -0.00000735 Вообще говоря, погрешность округления меньше, чем соответствующая погрешность усечения; таким образом, округление предпочтительнее усечения. 13.3. Опасные свойства вещественной арифметики; У вещественной арифметики есть несколько потенциально опасных свойств. Все они имеют общее происхождение, а именно тот факт, что мантисса и порядок в представлении с плавающей запятой занимают фиксированное число разрядов. Попробуем выявить некоторые из этих свойств, внимательно всматриваясь в результаты простых вычислений, выполняемых на нашем калькуляторе. Как вы помните, калькулятор показывает каждое число в формате с плавающей запятой с пятью значащими цифрами мантиссы и двумя цифрами порядка. Предположим, что по ходу вычислений в мантиссе появляется одна лишняя цифра, которая исчезает в окончательном результате после округления. Все свойства, которые здесь мы выявим, в равной степени могут быть обнаружены и при вычислениях на компьютере. Рассмотрим прежде всего следующие вычисления в формате с фиксирован- ной запятой: 12.345 + 0.54321 543.21 - 0.012345 6.25 ♦ 6.25 100.0 / 51.2 = 12.88821 = 543.197655 = 39.0625 = 1.953125 Соответствующие вычисления в формате с плавающей запятой и погрешности округления выглядят так:
232 Гл. 13. Вещественные числа вычисление погрешность округления 1.2345Е+01 + 5.4321Е-01 = 1.2888Е+01 0.00021 5.4321Е-Ю2 - 1.2345Е-02 = 5.4320Е-Ю2 -0.002345 6.2500Е+00 ♦ 6.2500Е+00 = 3.9063Е+01 -0.0005 1.0000Е+02 / 5.1200Е+01 - 1.9531Е+00 0.000025 Важным моментом является то, что погрешность округления возникает сверх и помимо погрешности, которая уже может быть в операндах. Это типично для всех арифметических операций с плавающей запятой. Редкое вычисление может обойтись без погрешности округления. В большинстве случаев погрешности округления настолько малы, что ими можно пренебречь, но иногда, как будет показано в гл. 14, они превращаются в серьезную проблему. Наличие погрешностей округления имеет прямое следствие: неразумно предполагать точное равенство никаких двух чисел с плавающей запятой. Например, два числа, равные в условиях точной арифметики одному и тому же числу: 1.2000Е+00 могут фактически превратиться в 1.2001Е+00 и 1.1999Е+00 и тем самым перестанут совпадать. Покажем теперь, к каким печальным последствиям может привести вычи- тание очень близких друг к другу чисел. Возьмем вычисления с фиксированной запятой: 3.14159265 - 3.14000000 = 0.00159265 3.14159265 - 3.14100000 = 0.00059265 3.14159265 - 3.14160000 = 0.00000735 На нашем калькуляторе соответствующие вычисления дадут 3.1416Е+00 - 3.1400Е+00 = 1.6000Е-03 3.1416Е+00 - 3.1410Е+00 - 6.0000Е-04 3.1416Е+00 - 3.1416Е+00 = 0.0000Е-00 однако это не совсем достоверные результаты! Более ’’честно” со стороны каль- кулятора было бы выдать три таких результата: 1.6???Е-03 6.????Е-04 ?.????Е+00 где вопросительные знаки стоят на месте цифр, про которые фактически ничего неизвестно. Мы столкнулись здесь с явлением ненадежности цифр. Ненадеж- ность цифр возникает всякий раз, когда производится вычитание над очень близкими друг к другу числами, и может привести к серьезной потере точности. Чтобы обнаружить другие опасные свойства вещественной арифметики, рассмотрим следующее вычисление: 20.0 + 0.0004 = 20.0004 На нашем калькуляторе получим: 2.0000Е+01 + 4.0000Е-04 - 2.0000Е-Ю1 Примечательно, что ответ оказывается равным большему числу. Таким образом, при сложении или вычитании двух чисел, одно из которых много больше друго- го, результат может быть равным большему числу. Совсем не обязательно при
Гл. 13. Вещественные числа 233 этом меньшее число является малой дробью. Так, например, в следующем сло- жении 2.0000Е+15 + 4.0000Е+10 = 2.0000Е+15 добавление числа 40 000 000 000 ничего не меняет! Значение имеет только относительная величина двух чисел. Хуже всего то, что даже при многократном добавлении маленькой величины большая останется неизменной. Из рассмотренного свойства вытекает, что изменение порядка, в котором производятся вычисления над числами в плавающем формате, может привес- ти к другим результатам. Выполнив вычисления 20.0 + 0.0004 + 0.0003 = 20.0007 0.0003 + 0.0004 + 20.0 = 20.0007 в формате с плавающей запятой слева направо, получим соответственно: (2.0000Е+01 + 4.0000Е-04) 2.0000Е+01 2.0000Е+01 и (3.0000Е-04 + 4.0000Е-04) 7.0000Е-04 2.0001Е+01 + З.ООООЕ-О4 + 3.0000Е-04 + 2.0000Е+01 + 2.0000Е+01 Итак, две суммы не одинаковы! Сложение сначала двух малых чисел, а затем добавление большего дает более точный результат. Обнаруженные нами опасные свойства вещественной арифметики вызваны ограниченным количеством цифр мантиссы. Две другие сложности обусловлены тем, что количество цифр порядка также ограничено. Первая из них видна на примере 3.0000Е+60 ♦ 3.0000Е+60 = 9.0000Е+120 Нам удалось записать результат, но на калькуляторе его невозможно изо- бразить из-за того, что для записи порядка требуются три цифры. Налицо так называемое переполнение порядка, которое эквивалентно переполнению в целочисленной арифметике. Результатом вычисления является число, которое непредставимо при заданных ограничениях. Переполнение порядка вообще-то не слишком серьезная проблема для программиста, поскольку диапазон представимых вещественных чисел даже на микрокомпьютере весьма велик. Есть сходная проблема, связанная с большим отрицательным порядком. В следующем примере: 3.0000Е-60 * 3.0000Е-60 = 9.0000Е-120 результат снова нельзя представить на калькуляторе из-за того, что порядок изображается тремя цифрами. Это отрицательное переполнение порядка. Когда оно происходит в компьютере, его либо можно рассматривать как ошибку, либо ошибкой не считать. В некоторых системах получение результата, чрезвычайно малого по величине, приводит к его исчезновению и записывается просто как нуль. В других системах отрицательное переполнение вызывает ошибку во время исполнения программы. Переполнения обоих видов иногда можно избежать, изменив порядок вы- числений. Пусть, например, нам нужно посчитать следующее произведение: 3.0000Е+60 * 3.0000Е+60 * 1.0000Е-30
234 Гл. 13. Вещественные числа Тогда, считая слева направо, придем к переполнению при первом умножении, и вычисление не удастся продолжить. Если же будем считать по-другому: 1.0000Е-30 ♦ 3.0000Е+60 * 3.0000Е+60 = 3.0000Е+30 ♦ 3.0000Е+60 - 9.0000Е+90 то ответ будет получен. Есть, наконец, еще несколько математических фактов, которые нужно иметь в виду: а) нельзя делить на нуль; б) невозможно извлечь квадратный корень из отрицательного числа; в) нельзя взять логарифм числа, которое меньше либо равно нулю. Этих вещей не можете сделать ни вы, ни калькулятор, ни компьютер. Подведем итог тому, что мы отметили в связи с вещественной арифме- тикой: а) почти всегда имеют место погрешности округления; б) два вещественных числа редко бывают в точности равными; в) ненадежные цифры могут привести к серьезной потере точности; г) добавление или вычитание малого числа может никак не сказаться на результате; д) порядок, в котором проводятся вычисления, может повлиять на резу- льтат; е) получение очень больших чисел может вызвать переполнение порядка, а очень малых - отрицательное переполнение или исчезновение числа (превращение в нуль); ж) деление на нуль, как и другие математически незаконные действия, невозможно. Считая вручную или на калькуляторе, мы прямо сталкиваемся с одной из пере- численных проблем и можем приостановить вычисления. Когда сходное событие возникает в компьютере, в середине вычислений, оно может либо вызвать ава- рийное завершение программы, что нежелательно, либо пройти незамеченным и привести к неточным результатам, что неприемлемо. Следовательно, именно программист отвечает за ситуации, чреватые опасностью, и должен на эта- "пе программирования организовать вычисления так, чтобы избежать их. К сожалению, большую часть ошибок, вызываемых отмеченными свойствами ве- щественной арифметики, трудно обнаружить путем тестирования программы. Тем более важно избегать опасных ситуаций на этапах составления и ручной проверки программы. 13.4. Вещественные числа в стандартном Паскале 13,4.1. Синтаксис вещественных чисел Всякое число, как явствует из гл. 6, является в стандартном Паскале лексемой; его синтаксис приведен на рис. 6.3 (с. 97, 98). Этот синтаксис приме- ним к числам в тексте программы, во входных данных и результатах и включает представление вещественных чисел как с плавающей, так и с фиксированной запятой. К примеру, следующие числа являются синтаксически правильными в формате с фиксированной запятой: 0 0 +3.14159 -0.00062742376.219 и в формате с плавающей запятой:
Гл. 13. Вещественные числа 235 1.0Е+6 -0.25Е-4 +2ЕЗ Обратите внимание, что по обе стороны десятичной запятой всегда есть по крайней мере одна цифра, а мантисса может быть либо целым числом, либо чис- лом в формате с фиксированной запятой, но между мантиссой и экспонентой не должно быть пробелов. Вещественное число, встреченное в тексте программы или в файле дан- ных, преобразуется системой во внутреннее представление, зависящее от про- цессора. Почти все компьютеры хранят и обрабатывают вещественные числа в формате с плавающей запятой, но обычно на основе двоичной, восьмеричной или шеетнадцатеричной системы счисления. Независимо от точности, с которой число записано, процессором оно хранится, вообще говоря, лишь в некотором приближении. Например, определение константы pi в виде const pi = 3.1415926536; не гарантирует, что процессор будет работать с величиной, совпадающей с указанной во всех заданных цифрах. В большинстве процессоров даже такие простые числа, как 0.1, не могут быть представлены точно из-за того, что процессор использует не десятичную систему счисления. 13.4.2. Чтение и вывод чисел Чтобы прочитать в переменную типа real некоторое значение, можно воспользоваться одной из встроенных процедур, read или readln. Это значение в файле данных может быть записано в формате с фиксированной или плавающей запятой, а также в виде целого числа. При чтении числа пробелы или концы строк, предшествующие ему, будут пропущены, а завершится процесс чтения, когда в переменную input' попадет символ, не являющийся частью числа. Например, если first, second, third - переменные типа real, а входной файл содержит 34.516 -1.7Е-5 29 и выполняется оператор read (first, second, third) то переменным будут присвоены значения так же, как если бы выполнялись следующие операторы присваивания: first := 34.516; second :* -1.7Е-5; third := 29 После выполнения оператора read текущая позиция во входном файле будет расположена на пробеле, идущем сразу вслед за цифрой 9 . Значение выражения типа real может быть выведено в выходной файл с помощью одной из встроенных процедур, write или writeln. В полном формате при этом требуется указать одно или два значения ширины поля вывода: х: fl задает вывод выражения х в формате с плавающей запятой с шириной поля вывода не менее fl символов; х : fl : f2 задает вывод выражения х в формате с фиксированной запятой в поле шириной не менее fl символов и с /2 цифрами после запятой. Если общая ширина поля вывода fl слишком мала, то процессор игнорирует ее и берет под значение столько символов, сколько нужно. Если, например, вещест- венная переменная number равна 23.85, то допустимы следующие операторы вы- вода (указано, какие символы будут выданы в выходной файл):
236 Гл. 13. Вещественные числа оператор вывода write (number : 10 : 5) write (number-100.0 : 10 : 5) write (-number : 4 : 1) write (100.0-number : 1 : 1) write (number : 12) write (number-lOO.Q : 12) write (-number : 9) write (number : 1) последовательность символов ~23.85000 ~-76.15000 -23.9 76.2 ~2.38500E+01 -7.61500E+01 -2.39E+01 ~2.4E+01 где ~ обозначает знак пробела. Ширина поля не обязательно задается констан- той; она может быть целочисленным выражением при условии, что его значение больше или равно единице. Синтаксис, которому подчиняются параметры опера- тора write, показан на рис. 11.1 (с. 200-201). Точный формат числа с фиксированной запятой таков: начальные пробелы (возможно, ни одного) знак минус, если число отрицательно цифры перед десятичной запятой десятичная запятая (на самом деле точка) цифры после десятичной запятой Таким образом, число в формате с фиксированной запятой имеет по крайней мере три символа, а именно десятичную запятую (точку) и по одной цифре по обе стороны от нее. Дробная часть округляется до нужного количества десятич- ных цифр. Точный формат числа с плавающей запятой таков: знак числа (либо минус, либо пробел) первая цифра мантиссы десятичная запятая (точка) остальные цифры мантиссы буква ’Е’ или ’е’ знак порядка (’+’ или ’-’) цифры порядка (обычно две). Таким образом, число в формате с плавающей запятой всегда состоит по край- ней мере из восьми символов. Мантисса округляется до требуемого количества значащих цифр. Возможен формат оператора write, в котором ни одна ширина поля вывода вещественного выражения не задана. В этом случае значение будет выведено в плавающем формате с теми величинами полей, которые в данной Паскаль-сис- теме берутся по умолчанию. 13.4.3. Арифметические операции и встроенные функции Сложение, вычитание, умножение и деление - четыре основные арифмети- ческие операции, которые могут дать вещественные значения, - обозначаются в стандартном Паскале следующим образом: операция знак операции сложение + вычитание умножение * деление /
Гл. 13. Вещественные числа 237 Обратите внимание, что операции div и mod не могут употребляться с веще- ственными операндами, а операция ♦* возведения в степень, которая имеется в некоторых языках высокого уровня, в стандартном Паскале отсутствует. Тип данных результата арифметической операции зависит от типов данных участвующих в ней операндов. Правила здесь таковы: а) для операций +, -, * результат является целочисленным в том случае, если оба операнда - целые числа, в противном случае результат являет- ся вещественным; б) для операции / результат всегда вещественный, даже когда оба операн- да целые числа. Предположим, например, что i переменная типа integer, равная 3, а г пере- менная типа real, равная 2.4; в таком случае следующие арифметические операции будут допустимы и дадут указанные результаты: тип результата операция результат z+2 5 integer z/2 1.5 real r+7.6 10.0 real r-4 -1.6 real r*i 7.2 real i/r 1.25 real Когда переменной присваивается числовое значение, важно знать, цело- численное оно или вещественное. Целочисленное значение можно присваивать вещественной переменной; при этом оно будет автоматически преобразовано в значение типа real. Присваивание вещественного значения целочисленной пере- менной является ошибкой программирования. Вещественное значение предва- рительно необходимо преобразовать в целочисленное с помощью одной из пред- назначенных для этого встроенных функций, trunc или round, которые опреде- лены следующим образом: trunc (х), где х выражение типа real, возвращает целое число, отсекая дробную часть х; round (х), где х выражение типа real, возвращает целое число, полученное округлением значения х до ближайшего целого. Работа этих функций иллюстрируется следующими примерами: значение x trunc (x) round (x) 2.25 2 2 3.50 3 4 6.75 6 7 -2.25 -2 -2 -3.50 -3 -4 -6.75 -6 -7 Отсюда следует, что при прежних значениях переменных i и г правильными являются следующие операторы присваивания: оператор i := i + 2 г := i + 2 i:« trunc (r/3) i :- round (r/3) присвоенное значение 5 5.0 0.0 1.0
238 Гл. 13. Вещественные числа Каждая из шести операций сравнения, а именно: = <><><=>= может употребляться с операндами, которые или оба вещественны, или оба це- лочисленны, или один из них целое число, а другой - вещественное. Например, следующие операции сравнения допустимы и дают указанные результаты: операция результат i = 3 true i <> г true г < i - 2 false г > 4.75 false Вместе с тем из-за того, что числа в формате с плавающей запятой редко в точности равны, операции = и о с вещественными операндами дают ненадеж- ные результаты. Еще несколько операций над вещественными числами в стандарте Паскаля представлено встроенными арифметическими функциями. Рассмотренные в разд. 12.3.1 (с. 216) функции abs и sqr могут вызываться как с целочисленным, так и с вещественным параметром. Остается сказать о функциях sqrt, sin, cos, arctan, exp, In. Все они имеют один параметр, который должен быть веществен- ным выражением, и все возвращают результат вещественного типа. Определены они следующим образом: sqrt (х) вычисляет (неотрицательный) квадратный корень из х при условии, что х неотрицателен; sin (х) вычисляет тригонометрическую функцию синус, причем значение х задается в радианах; cos (х) вычисляет тригонометрическую функцию косинус, при- чем значение х задается в радианах; arctan (х) вычисляет главное значение в радианах тригонометри- ческой функции арктангенс от х; вычисляет показательную функцию еЛ; ехр (х) In (х) вычисляет натуральный логарифм (логарифм по основа- нию с) от х, при условии, что значение х больше нуля. Теперь, изучив общие свойства вещественных чисел и особенности работы с ними в стандартном Паскале, мы способны написать несколько программ, использующих их. Это будет сделано в гл. 14. Упражнения 13.1. Переведите следующие дробные числа из формата с фиксированной запятой в формат с плава- ющей запятой с пятью значащими цифрами в мантиссе и двумя цифрами в экспоненте. Напи- шите, какова в каждом случае величина погрешности округления. а) 2.718281828 б) 0.00035716 в) -0.45359237 г) 101325.0 д) -133.32238 е) 100000.0 13.2. Переведите следующие дробные числа из формата с плавающей запятой в формат с фиксиро- ванной запятой: а) 1.0737Е+09 б) 6.1803Е-01 в) -8.8542Е-12 13.3. Считая, что а - 1.0000Е+04, b - 3.0000Е-01, с - 3.0004Е-01, выполните нижеприведенные вычисления и определите погрешность в каждом случае путем сравнения с результатами аналогичных расчетов в формате с фиксированной запятой: а) а + b 6) b - с в) а + Ь + с г) с + Ь + а д) а4 с + b е) а / с+Ь
Гл. 13. Вещественные числа 239 13.4. Определите, можно ли в Паскаль-программе промигать в переменную типа real следующие числа, и если нельзя, объясните, почему: а) 47.214 б) 0.000519 в) .5 г) -79 д) 1.0Е-6 е) 1Е10 13.5. Переменная х равна 147.25. Какая последовательность символов будет выдана в выходной файл при выполнении следующих операторов Паскаля: a) write (х : 12 : 4) в) write (-х : 2 : 2) д) write (З.О*х : 10) б) write (х/2 ; 5 : 1) г) write (х : 14) е) write (х-НО.О : 1) 13.6. Переменная х равна -230.258093. Запишите операторы Паскаля, выводящие ее значение в следующих форматах: а) 230.2581 б) -230.26 в) ~-2.3026Е+02 г) -2.30258093Е+02 13.7. Предположим, что / - переменная типа integer, равная 5, г - переменная типа real, равная 1.3, b - переменная типа Boolean. Для тех из нижеприведенных операторов происваивания, которые являются допустимыми, определите присваиваемое значение, для прочих объясните, почему они недопустимы: а) в) д) ж) г 2 ♦ i + 1 г trunc (г + 2) г trunc (1 - 3 * г) b :- г >- 4 ♦ i б) i 2 ♦ г + 1 г) i :- round (г + 2) е) г round (г - 1) з) b :- г < 10.0
14 ПРОГРАММЫ, РАБОТАЮЩИЕ С ВЕЩЕСТВЕННЫМИ ЧИСЛАМИ Замечания, открывающие гл. 13, и собранный в ней перечень опасностей, сопряженных с вещественной арифметикой, могли привести вас к заключению, что хорошо написать программу, манипулирующую вещественными числами, просто невозможно! Это, разумеется, не так, но при работе с вещественными числами лишняя подозрительность никогда не повредит, а к получаемым резуль- татам следует быть особенно придирчивым. Разработка процедур численных расчетов, включая анализ их точности, сама по себе является важным предметом. Эта область знаний сравнительно не- давно оформилась под названием численного анализа* хотя отдельные ее методы применяются уже несколько столетий. Мы не станем входить в обсуждение серь- езных проблем этой области, а удовлетворимся разбором трех довольно простых задач. Для того чтобы понять первые две, достаточно элементарных математи- ческих знаний. Третья будет чуть сложнее. 14.1. Решение квадратного уравнения Почти все мы когда-то учились решать квадратные уравнения* имеющие следующий вид: ах1 + Ьх + с « О где а, b и с являются коэффициентами уравнения, и существует, как правило, два значения х, называемых корнями уравнения, которые при подстановке дела- ют его левую часть равной правой. Решение уравнения заключается в нахожде- нии этих двух значений, которые определяются по хорошо известной формуле: -£± V(£2 - 4 ас) х « при условии, что величина & - 4«с, называемая дискриминантом* больше или равна нулю. Когда дискриминант отрицателен, говорят, что уравнение имеет комплексные корни. Попробуем составить программу под названием Уравнения, которая реша- ла бы квадратные уравнения. Она полностью специфицирована в табл. 14.1. Изучите, пожалуйста, внимательно эту спецификацию. Поскольку каждое решаемое уравнение задастся одной строкой входных данных, организация программы, решающей несколько уравнений, тривиальна. Требуемый цикл записывается так:
Гл. 14. Программы, работающие с вещественными числами 241 while not eof do solveoneequation где solveoneequation - процедура, которая должна читать данные, выполнять вычисления и выдавать результаты для одного уравнения. Таблица 14.1. Спецификация программы Уравнения Составьте Паскаль-программу под названием Уравнения для решения уравнений вида ах2 + Ьх + с - О причем при каждом запуске программы должно решаться столько уравнений, сколько задано во входных данных. Более точно: Входные данные Каждая строка на входе содержит три числа, представляющие собой коэффициенты а, Ь, с для одного уравнения. Например, данные для уравнения З.Ох2 - 12.0х+ 11.73-0 будут представлены как: 3.0 -12.0 11.73 Результаты Для каждого уравнения программа должна выдавать сначала коэффициенты, а затем найденные корни уравнения. Например, для приведенного уравнения результаты могут иметь такой вид: Квадратное уравнение с коэффициентами: а = 3.0000Е+00 b = -1.2000Е+01 с = 1.1730Е+01 имеет два корня, х1 = 2.3000Е+00 и х2 = 1.7000Е+00 Составить процедуру solveoneequation уже значительно труднее, и прежде чем взяться за карандаш, немного поразмышляем. Взглянув на формулу вычис- ления корней уравнения, увидим по крайней мере два очевидных момента, тре- бующих осторожности: опасность а) деления на нуль и б) извлечения квадратно- го корня из отрицательного числа. Деление на нуль произойдет, если коэффициент а равен нулю. В этом случае квадратное уравнение сводится к виду Ьх + с « 0 и имеет всего один корень, а именно: х = -с/b что, в свою очередь, таит опасность, если Ь равно нулю. В этом последнем случае уравнение сводится к еще более простой форме: с = 0 т.е. уравнением уже нс является и не может быть решено. Попытка извлечь квадратный корень из отрицательного числа произойдет, когда дискриминант отрицателен, и корни уравнения в таком случае будут ком- плексными. Нужно учесть это в процедуре и обеспечить выдачу надлежащего сообщения.
242 Гл. 14. Программы, работающие с вещественными числами Исходя из первых наблюдений, мы можем набросать процедуру solveoneequation (см. первый раздел табл. 14.2). Вложенные условные операторы предусматривают все обсуждавшиеся выше варианты, а единственное предложе- ние, которое нельзя сразу записать на Паскале: подсчитать и выдать корни встречается в той точке программы, когда мы можем быть уверены, что а не ра- вно нулю, а дискриминант неотрицателен. Следовательно, определенно сущест- вуют два корня уравнения и нужно только аккуратно их посчитать. Таблица 14.2. Разработка процедуры solveoneequation Шаги разработки Примечания решить одно уравнение -> begin ввести коэффициенты; вывести заголовок и коэффициенты; if а - 0.0 then if b - 0.0 then выдать сообщение else выдать корень -c/b else begin посчитать дискриминант; if discriminant < 0 then выдать сообщение, что корни комплексные else вычислить и выдать два корня уравнения end end локальные переменные а,Ь, с : real; discriminant: real; вычислить и выдать два корня уравнения -> begin { а <> 0 and discriminant >= 0 } tempi := sqrt (discriminant); temp2 := 2.0 * a; xl (-b + temp 1) /temp2; x2 (-b - tempi)/temp2; вывести xl и x2 end локальные переменные tempi,tetnp2,xl,х2 : real; ОСТОРОЖНО! xl или х2 могут содержать ненадежные цифры вычислить и выдать два корня уравнения { по-другому } -> begin посчитать надежный корень; посчитать другой корень; вывести xl и х2 end посчитать надежный корень -> if b < 0.0 then хГм (-b^sqrt(discriniinant))!(2.0*а) else хГ.~ (-b-sqrt(discriminant))/(2.0*а); локальные переменные х/, х2 : real; посчитать другой корень -> х2 с/ (а * xl) Первый этап в уточнении программы приведен во втором разделе таблицы разработки. В основном это прямое применение формулы, но определены также
Гл 14. Программы, работающие с вещественными числами 243 четыре вещественные переменные. Две из них, xl и х2, нужны для хранения корней уравнения, а другие, tempi и temp2, содержат значения выражений sqrt {discriminant) и 2.0 * а просто для того, чтобы не вычислять их дважды. Всегда ли по этой процедуре корни будут считаться точно? 1 2 3 4 5 6 7 8 9 10 II 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 program Equations (input, output)’, { Решает несколько квадратных уравнений } procedure solveoneequation; var a, b, с, discriminant, xl, x2 : real; begin readin (a, b, c)\ writeln ('Квадратное уравнение с коэффициентами:' writeln; writeln ('a-' : 9, a : 11, ’/> : 7, b : 11, ’c : 7, c : 11); writeln; if a - 0.0 then if b - 0.0 then writeln Сне есть уравнение.') else writeln ('имеет один корень, х -c/b : 11, else begin { а <> 0.0 } discriminant sqr (b) - 4.0 * a ♦ c; if discriminant < 0.0 then writeln ('имеет комплексные корни.') else begin { a <> 0.0 and discriminant >~ 0.0 } if b < 0.0 then xl :~ ( -b + sqrt (discriminant)) / (2.0 * a) else xl := ( -b - sqrt (discriminant)) I (2.0 * a)\ x2 с I (a ♦ xl)\ writeln ('имеет два корня, xl , xl : 11); writeln Си x2 , x2 : 11, ’.’) end end; writeln end { solveoneequation }; begin writeln ('**** РЕЗУЛЬТАТ РАБОТЫ ПРОГРАММЫ УРАВНЕНИЯ ****) ; writeln; while not eo/do sol veoneequation; writeln; writeln (’**** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦♦’) end { Equations } . Листинг 14.1. Текст программы Уравнения Ответ отрицателен и причиной тому - ненадежность цифр. Вы увидите, как время от времени в расчетах появляется сбой, проверив предложенный метод на уравнении х2 - 100х +1=0 Производя расчет с точностью до пяти значащих цифр, получим величину х2: х2 = (1.0000Е+02 - 9.9980Е+01) / 2.0000Е+00 = 2.????Е-02 / 2.0000Е+00 = 1.?9??Е-02
244 Гл.14. Программы, работающие с вещественными числами где вопросительные знаки стоят на месте ненадежных цифр, и, следовательно, только одна цифра в решении точная. Так будет происходить не всякий раз, но то, что для некоторых уравнений это случается, заставляет нас искать другой метод. Итак, один из корней мы можем посчитать надежно, а другой будем вычи- слять исходя из того факта, что произведение корней должно равняться с/а. В трех последних разделах табл. 14.2 показана поэтапная реализация улучшенного способа вычисления корней, основанного на этих соображениях, а в окончатель- ном виде программа приведена на листинге 14.1. Главный урок, который нужно извлечь из этого примера, таков: если вни- мательно изучать предстоящие вычисления и помнить об опасных свойствах вещественной арифметики, часто удается избежать погрешностей в резуль- татах. 14.2. Другая форма оператора цикла и вычисление кубического корня Прежде, чем начать составлять следующую программу, уместно расширить наши познания в области операторов цикла Паскаля. Цикл всегда можно прави- льно записать с помощью оператора ’’пока” {while), однако иногда удобнее вос- пользоваться оператором цикла в несколько иной форме, а именно оператором цикла "пока-не”, или оператором repeat Его формальный синтаксис отображен на рис. 6.10 (с. 109-110), а вот простой пример его употребления: repeat processoneline-, n := n - 1 until n = 0 т.е. повторять processoneline-, n :e n - 1 пока не достигнуто условие n - 0 Последовательность операторов внутри оператора repeat называется его телом, и, как будет видно, ее необязательно заключать в служебные слова begin и end. Оператор ”пока-не” в его полном виде выполняется по следующему пра- вилу: а) выполнить тело оператора один раз; б) вычислить логическое выражение, которое следует за служебным сло- вом until; в) если значение логического выражения есть ложь, то вернуться на шаг а); г) если значение логического выражения есть истина, то выполнение опе- ратора завершено. Сравнивая это с соответствующим правилом выполнения оператора ’’пока” (с. 36-37), обнаружим здесь важное отличие: в операторе цикла ’’пока” тело может не выполниться ни разу, а в операторе ”пока-не” тело всегда выполняется хотя бы один раз1. 1 Собственно, операторы цикла пока (while) и пока-не (repeat) отличаются в двух отношениях. Первое - частица "не”: в операторе пока проверяется условие продолжения цикла, в операторе пока-не - условие его завершения (противоположное условие). Это отличие несущественное, его
Гл. 14. Программы, работающие с вещественными числами 245 Как решить, какой из операторов цикла употребить? Впредь при необхо- димости написать цикл будем отдавать преимущество оператору пока (while). Если же по какой-то причине это окажется неудобным, будем рассматривать как вариант оператор пока-не (repeat). Важно, однако, помнить, что оператор repeat можно использовать для программирования цикла, только если известно, что тело цикла должно во всяком случае выполниться хотя бы один раз, В программе, к разработке которой мы приступаем, будет три цикла, и это позволит сравнить достоинства операторов while и repeat. Программа, названная Таблица, будет вычислять кубический корень числа; ее спецификация приведена в табл. 14.3. Таблица 14.3. Спецификация программы Таблица Составьте на Паскале программу под названием Таблица, которая будет в табличном виде выдавать с точностью до пяти значащих цифр значения кубического корня из х при изменении х в нескольких диапазонах значений. Более точно: Входные данные В каждой строке входных данных содержатся три числа, определяющие один табличный набор: п : число шагов внутри диапазона; хО : начальное значение х; хп : конечное значение х. Например, следующий файл данных: 3 0.0 0.5 20 -100.0 100.0 предписывает построение двух табличных наборов, один - в диапазоне от 0.0 до 0.5 тремя шагами, а другой - в диапазоне от -100.0 до 100.0 20 шагами. Результаты Для каждого табличного набора программа должна выдать исходные данные, а вслед за тем соответствующую им таблицу. Так, первой строке вышеприведенных данных могут отвечать следующие результаты: Таблица значений с числом шагов = 3 в диапазоне: хО = 0.00000Е+00 хп = 5.00000Е-01 X кубический корень из х 0.00000Е+00 0.00000Е+00 1.66667Е-01 5.50321Е-01 З.ЗЗЗЗЗЕ-01 6.93361Е-01 5.00000Е-01 -7.93701Е-01 На первом этапе разработка проста. Программа должна обработать неско- лько табличных наборов, поэтому возникает цикл. Поскольку каждый набор идет отдельной строкой, можно записать цикл как while not eof do processonetabulation легко "компенсировать” логическим отрицанием not. Более существенное отличие, которое здесь подчеркивает Г.Джонстон, в том, когда проверяется условие: до выполнения тела цикла (while) или после (repeat). К сожалению, названия операторов, выбранные нами, ничего не говорят об этой важной разнице. В научной литературе встречаются наименования операторов, отражающие спо- соб организации цикла: цикл с предусловием (while), цикл с постусловием (repeat). - Примеч. ред.
246 Гл. 14. Программы, работающие с вещественными числами где процедура processonetabulation должна читать одну строку входных данных и производить соответствующую табуляцию. Цикл правильно завершится при условии, что processonetabulation читает каждую строку полностью, вместе с маркером конца строки. С помощью оператора repeat цикл запишется так: repeat processonetabulation until eof но для нулевого варианта, а именно когда отсутствуют данные для табуляции, он не сработает. Значит, для данного цикла оператор repeat не подходит. Процедура processonetabulation строится в табл. 14.4. В первой полосе дана ее общая структура, в которой содержатся четыре предложения и одно булево выражение, которые нужно развернуть. Три предложения, а именно: Таблица 14.4. Частичная разработка процедуры processonetabulation Шаги разработки Примечания обработать один табличный набор -> begin ввести одну строку данных; вывести данные; если данные некорректны то выдать сообщение об ошибке иначе посчитать и выдать таблицу end локальные переменные п : integer, хО, хп : real; данные некорректны -> (п < 1) ог (лгО >- хп) посчитать и выдать таблицу -> begin { п >- 1 and хО < хп } вывести заголовки столбцов; interval(хп - хО)/п; х хО; пока х <- хп выполнять begin вывести х и кубический корень из х; х х + interval’, end { х > хп } end локальные переменные intervals : real; ОСТОРОЖНО! Навряд ли х станет равным хп Для вычисления корня написать отдельную функцию посчитать и выдать таблицу -> begin { п >- 1 and хО < хп } вывести заголовки столбцов; interval(хп - хО) /п; х хО; while п >- 0 do begin вывести х и кубический корень из х; х х + interval’, п п - 1 end { п < 0 } end
Гл 14. Программы, работающие с вещественными числами 247 прочесть строку данных; вывести данные; выдать сообщение об ошибке; записываются на Паскале просто, остальное требует дополнительных размыш- лений. Прежде всего нужно уяснить, какие данные будут некорректными. Если считать, что ошибки, связанные с заданием нечисловых данных, будет ’’ловить” система, то потребуется только проверять наличие в каждом табличном наборе хотя бы одной точки и того, что хО, начальное значение х, меньше, чем хп, конечное значение. Условие корректности данных, следовательно, будет выгля- деть как (п >= 1) and (хО < хп) и по правилу Моргана логическое условие данные некорректны можно записать в виде (п < 1) and (хО >= хп) что и показано во второй полоске таблицы разработки. Теперь остается развернуть предложение посчитать и вывести таблицу Его набросок приведен в третьей полосе таблицы разработки, и здесь заслужива- ют обсуждения следующие три момента. Во-первых, нужно присмотреться к оператору interval :« (хп - хО) / п в котором содержится потенциально опасная операция деления. Здесь, впрочем, проблем не будет, поскольку случай п « 0 мы уже ’’перехватили” как некоррек- тность данных. Во-вторых, цикл записан в виде оператора пока. Как обычно, мы должны убедиться, что он завершится и его тело выполнится нужное число раз. Из утверждения, следующего за оператором пока, явствует, что условие его окончания есть х > хп Оно выглядит довольно естественным, но именно здесь должно проявиться наше недоверие к вещественной арифметике. Из-за погрешностей округления есть вероятность, что х будет чуть больше хп в тот момент, когда мы считаем его равным хп. Чем это опасно, мы увидим, если выполним данный вариант прог- раммы на первом наборе данных из табл. 14.3, проведя вычисления с веществен- ными числами в формате с плавающей запятой. Заметим прежде всего, что вычисление интервала (5.0000Е-01 - О.ООООЕ+ОО) / 3 - 1.6667Е-01 уже содержит небольшую погрешность округления. Далее, вырабатывая последо- вательные значения х, получим 0.0000Е+00 1.6667Е-01 3.3334Е-01 5.0001Е-01 Как видим, погрешность округления налицо и к тому же она достаточно велика, чтобы вызвать преждевременное окончание вычислений. Кубический корень не
248 Гл. 14. Программы, работающие с вещественными числами будет посчитан в точке 5.0001Е-01. Следовательно, условие, управляющее вы- полнением цикла, ненадежно: иногда оно будет работать, но в некоторых слу- чаях будет теряться последняя строка таблицы. Никогда не пользуйтесь для управления циклом значениями веществен- ного типа, если то же самое можно осуществить с помощью целых чисел; сделайте это своей постоянной практикой. В нашем случае известно, что в таб- лице должна быть ровно п+1 строка, поэтому в цикле можно просто отнимать от п. Последняя полоса табл. 14.4 содержит исправленную таким образом схему, и теперь цикл управляется выражением и>=0 т.е. совершенно безупречным образом. Оператор пока-не, соответствующий тому же циклу, выглядел бы так: выполнять вывести х и кубический корень из х; х := х + interval; п := п-1 пока-не п < 0 что в равной степени приемлемо, так как п перед началом цикла заведомо больше нуля. В-третьих, обсудим, как развернуть предложение вывести х и кубический корень из х Если нам удастся написать функцию cuberoot, вычисляющую кубический корень из заданного числа, то это можно сделать так: write (х : 16, ’ ’ : 10, cuberoot (х) : 16) что обеспечивает выдачу чисел в плавающем формате 16 знаками. Соединяя отдельные части процедуры, получим processonetabulation в виде, показанном в строках 25-51 листинга 14.2 (с. 252). Теперь нужно написать функцию cuberoot. Заголовок можно взять такой: function cuberoot (number: real): real; где параметр number задает число, из которого извлекается кубический корень. В спецификации программы (с. 245) говорится, что результат нужно посчитать с точностью до пяти значащих цифр. Мы воспользуемся так называемым итера- тивным методом вычисления, идея которого состоит в том, чтобы взять началь- ное приближение, а затем улучшать его до получения результата с нужной точ- ностью. Попытаемся составить функцию, способную правильно работать с чис- лами любой величины, как с очень большими, так и с очень малыми. Допустим, что задано число N и что А есть его кубический корень в некотором приближении. Мы хотим найти поправку С, добавление которой к Л дает более хорошее приближение. Применим следующую формулу1: 1 Приведенную формулу можно получить, например, следующим образом. Если С - такая поправка, что при добавлении к приближенному значению А она дает точное значение кубического корня, то (А + С)3 - N и, следовательно, А3 + ЗА2С + ЗАС2 + С3 - N откуда, отбрасывая слагаемые, содержащие С2 и С3, которые будут пренебрежимо малы при малом С, получаем
Гл. 14. Программы, работающие с вещественными числами 249 Например, если ЛГ- 2.0 и за первое приближение кубического корня берется 1.0, то получаем следующие результаты: приближение 1.0 1.333333 1.263889 1.259934 1.259921 поправка 0.333333 -0.069444 -0.003955 -0.000013 0.000000 где очередное приближение складывается из предыдущего приближения и по- правки. Итак, с точностью до пяти значащих цифр кубический корень из 2.0 равен 1.2599. Как видим, поправка постепенно становится очень малой и в неко- торый момент процесс, как говорят, сходится. Сразу отметим два важных момента. Первое: процесс потерпит неудачу в результате деления на нуль, если какое-то приближение окажется равным нулю. Второе: есть опасность переполнения порядка, положительного или отрицатель- ного, когда очередное приближение возводится в квадрат. Отсюда два следствия: а) метод непригоден для вычисления кубического корня из нуля; б) начальное приближение нужно выбирать достаточно осмотрительно. В приведенном примере для извлечения кубического корня из числа 2.0 мы взяли начальное приближение 1.0. Если по какой-то причине мы начали бы с числа -1.0, то процесс потерпел бы неудачу, так как второе приближение равнялось бы нулю. Один из способов справиться с этой проблемой и в то же время избежать переполнения - выбрать начальное приближение по следующему правилу: если исходное число number положительно, начать вычисление с числа sqrt(numbef), если исходное число отрицательно, начать с -sqrt(-number), где sqrt - встроенная функция для извлечения квадратного корня. При таком начальном выборе вычислительный процесс всегда сходится к правильному резу- льтату, хотя, быть может, и после большого числа шагов. Основные этапы разработки функции cuberoot показаны в табл. 14.5. Об- щая ее структура дана в первой полосе, и единственное, что нельзя сразу пере- вести в Паскаль, это предложение выполнить итеративный процесс Развернуть его, очевидно, нужно в оператор цикла, но как контролировать выход из цикла? Число повторений заранее неизвестно, поэтому целочисленную переменную использовать для управления циклом не удастся. Нужно ли нам прекращать вычисление, мы должны решить путем сравнения величины послед- ней поправки с последним приближением. К примеру, итеративный процесс мо- жно остановить, когда abs (correction I approx) < tolerance где tolerance - достаточно малая константа. А3 + ЗЛ2С - N Это соотношение дает формулу для вычисления С.
250 Гл. 14. Программы, работающие с вещественными числами Во второй полосе табл. 14.5 отражена попытка развернуть искомый цикл в оператор пока. но это как раз тот случай, когда оператор пока не очень удобен. Дело в том, что условие не пора закончить явилось бы отрицанием указанного выше булевского выражения abs (correction I approx) >= tolerance однако в первый раз, когда это условие должно вычисляться, еще не посчитана поправка. Разумно употребить оператор цикла в другой форме, тем более, что хотя бы один шаг итерации выполнить необходимо. Новый вариант раскрытия цикла приведен в третьей полосе таблицы. Таблица 14.5. Таблица разработки (частично) для функции cuberoot Шаги разработки Примечания cuberoot { функция вещественного типа } -> begin if number - 0.0 then cuberoot0.0 else begin взять начальное приближение; выполнить итерацию; cuberootпоследнее приближение; end end параметр-значение number : integer, локальные переменные approx, correction : real', выполнить итеративный процесс; -> пока не пора закончить выполнять begin вычислить поправку; добавить поправку к приближению; end; Неудобно! Попробовать оператор пока-не выполнить итеративный процесс; { поправлено } -> повторять вычислить поправку; добавить поправку к приближению; пока-не пора закончить; пора закончить -> (abs (correction!approx) <- tolerance локальная константа tolerance * 0.001 Значение константы tolerance нужно подбирать с известной осторожнос- тью. Если сделать ее чересчур маленькой, например равной 0.0, то мы рискуем вообще не выйти из цикла, а если слишком большой, то результат не будет то- чен. Нужен компромисс. Можно показать - экспериментально или прибегнув к элементам высшей математики - что для получения пяти точных цифр резуль- тата значение 0.001 удовлетворительно. За этим стоит тот факт, что при однаж- ды полученном хорошем приближении итеративный процесс затем сходится очень быстро.
Гл. 14. Программы, работающие с вещественными числами 251 1 2 3 4 5 6 7 8 9 10 И 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 program Tabulate (input, output}', { Строит таблицу кубических корней х в нескольких интервалах значений х } function cuberoot (number : real) : real; const tolerance - 0.001; var approx, correction : real; begin if number - 0.0 then cuberoot0.0 else begin if number > 0.0 then approx sqrt (number) else approx - sqrt (-number); repeat correction := (number I sqr (approx) - approx) / 3.0; approx approx + correction until abs (correction / approx) < tolerance; cuberoot approx end end { cuberoot}; procedure processonetabulation; var n : integer; xO, xn, x, interval: real; begin readin (n, xO, xn); writein ('Строится таблица из n : 1, ’ значений в интервале:'); writein; writein Сот хО хО : 12, ’ до хп хп : 12); writein; if (п < 1) or (хО >- хп) then begin writein ('Неверные данные : число точек должно быть'); writein ('больше нуля, а хО должно быть меньше хп.') end else begin { п >- 1 and хО < хп } writein Сх' : 8, 'кубический корень из х' : 38); writein; interval(хп - хО) / п; х :« хО; while п >- 0 do begin writein (х : 16, ' ' : 12, cuberoot (х) : 16) ; х х + interval; п п - 1 end { n < О } end; writein end { processonetabulation }; begin writein (’*♦** РЕЗУЛЬТАТ РАБОТЫ ПРОГРАММЫ ТАБЛИЦА ♦**♦’); writein; while not eof do processonetabulation; writein; writein (’♦♦*♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦**♦’) end { Tabulate }. Листинг 14.2. Текст программы Таблица
252 Гл. 14. Программы, работающие с вещественными числами В окончательном виде функция cuberoot содержится в строках 5-23 лис- тинга 14.2, и, читая ее текст, в трех местах вы должны услышать предупреди- тельный звонок. Первый раз - в строке 18, где предложение вычислить поправку записано оператором correction := (number I sqr(approx) - approx) I 3.0 что непосредственно следует из ранее приведенной формулы. Две величины, составляющие в этой формуле разность, становятся все более близки друг к другу по мере сходимости процесса. Значит, вероятно появление ненадежных цифр. Не повредит ли это точности результатов? К счастью, нет. При условии, что точна хотя бы одна цифра в величине поправки, новое значение approx будет улучшать предыдущее, и процесс будет сходиться. Проблема ненадежных цифр может возникнуть, лишь когда значение tolerance слишком мало для данного компьютера. В таком случае вычисление зациклится. Еще две точки бдительности - в строках 18 и 20, где происходит деление. Впрочем, обе операции деления: number / sqr(approx) и correction I approx совершенно надежны, так как мы убедились, что ни approx, ни sqr(approx) не могут равняться нулю. Разработка функции cuberoot хорошо иллюстрирует тот факт, что проек- тирование программ - точная наука. Иная программа, содержащая мелкие нелады, может работать почти всегда, но всегда работать правильно будет лишь безошибочная программа. Из окончательного текста программы Таблица (листинг 14.2) можно изв- лечь еще два урока. Первое: вводя оператор repeat, мы заметили, что всякий цикл можно правильно записать с помощью оператора while. Оператор repeat, используемый в функции cuberoot в строках 17-20, можно заменить на correction :== approx*, while abs (correction!approx) >= tolerance do begin correction := (number I sqr(approx) - approx) I 3.0; approx := approx + correction end; Единственное, что потребовалось сделать, - ввести фиктивное начальное значение для переменной correction. Так что неудобство оператора while - как и преимущество оператора repeat - не так уж велики!.. Второе: глядя на функцию cuberoot, вы можете задаться вопросом, зачем нужна переменная approx. Конечно, гораздо проще основные вычисления записать так: begin if number > 0.0 then cuberoot:= sqrt(number) else cuberoot:- -sqrt(-number): repeat correction :e (number!sqr(cuberoot) - cuberoot) / 3.0; cuberoot :e cuberoot + correction until abs (correction!cuberoot) < tolerance*, end
Гл. 14. Программы, работающие с вещественными числами 253 иными словами, пользоваться идентификатором cuberoot, как обычной перемен- ной, получая в ней же окончательный результат. Но поступив таким образом, мы бы допустили в нашу программу четыре ошибки! Таблица 14.6. Образцы результатов работы программы Таблица, которые показывают наличие ошибок округления **** РЕЗУЛЬТАТ РАБОТЫ ПРОГРАММЫ ТАБЛИЦА **** Строится таблица из 10 значений в интервале: от хО = -5.00000Е-01 до хп = 5.00000Е-01 х кубический корень из х 5.000000000Е-01 4.000000060Е-01 3.000000119Е-01 2.000000179Е-01 •1.000000238Е-01 -1.490116119Е-08 9.999998659Е-02 1.999999881Е-01 2.999999821Е-01 3.999999762Е-01 4.999999702Е-01 -7.937005162Е-01 -7.368063927Е-01 -6.694329381Е-01 -5.848035812Е-01 -4.641590416Е-01 -2.460783347Е-03 4.641589820Е-01 5.848035146Е-01 6.694328868Е-01 7.368063331Е-01 7.937004250Е-01 Строится таблица из 8 значений в интервале: от хО = -5.00000Е-01 до хп = 5.00000Е-01 х кубический корень из х -5.000000000Е-01 -3.750000000Е-01 -2.500000000Е-01 -1.250000000Е-01 0.000000000Е+00 5.000000000Е-01 3.750000000Е-01 2.500000000Е-01 1.250000000Е-01 -7.937005162Е-01 -7.211251855Е-01 -6.299605370Е-01 -5.000000000Е-01 0.000000000Е+00 7.937005162Е-01 7.211251855Е-01 6.299605370Е-01 5.000000000Е-01 **** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ****
254 Гл. 14. Программы, работающие с вещественными числами Когда имя функции встречается в выражении, оно обозначает вызов функции, поэтому имя функции не может употребляться в качестве обычной переменной. Четырежды в приведенном отрывке мы пытались вызвать функцию из нее са- мой, а это совсем не то, что нужно. В данном случае ошибки будут обнаружены во время компиляции, поскольку функция описана с одним формальным пара- метром, а при обращении к ней не задано ни одного фактического. Обращение к функции из нее самой не всегда является ошибкой програм- мирования, но нужно делать это правильным образом. Процедура или функция, обращающаяся к себе самой, называется рекурсивной, и рекурсия, надо заме- тить, является очень мощным средством программирования. К сожалению, тру- дно найти простую задачу, которая наилучшим образом решается именно рекур- сивно, поэтому в настоящей книге рекурсия не обсуждается. Здесь вопрос затронут только для того, чтобы предупредить вышеуказанную ошибку. Боль- шинство из тех, кто начинает программировать функции, хоть однажды, а делают ее. Итак, будьте начеку! Наконец, не поленитесь внимательно изучить результаты, приведенные в табл. 14.6. Они представляют собой два набора табличных данных, каждый из которых определен в диапазоне от -0.5 до +0.5. Первый набор состоит из десяти точек, так что значения следуют через 0.1. Так как величина 0.1 на большин- стве компьютеров непредставима точно, почти все значения х имеют погреш- ность. В частности, число, которому следует быть нулем, на самом деле является малым отрицательным числом. Дополнительно выданные десятичные знаки позволяют оценить погрешность округления. Во второй табличке число точек равно восьми, а шаг соответственно равен 0.125. На этот раз значения х пред- ставлены без погрешности по той причине, что 0.125 есть число вида 2“*, а такие числа в компьютере представляются точно. Урок, который можно отсюда извлечь, таков: при вычислении значений функции в точках с равным интервалом, будь то для вывода на внешнее устройство или для внутреннего использования в программе, можно избежать необязательных ошибок округления, беря шаг равным 2~к или кратным этой величине. Таким образом, программу Таблица лучше запустить с числом точек, равным 8, 125 или 1024, нежели 10, 100 или 1000. 14.3. Губительные погрешности округления Вы уже уяснили себе, по-видимому, что вычисления над вещественными числами таят подводные камни. Из того, с чем мы сталкивались до сих пор, однако, трудно было представить себе, до какой степени неверными порой быва- ют результаты. Чтобы восполнить этот ’’пробел”, рассмотрим простую на вид программу, названную Интегралы и приведенную на листинге 14.3. Она пред- назначена для подсчета значений интеграла в диапазоне значений параметра к от нуля до некоторой заданной величины. В программе использован тот факт, что для значений к, больших нуля, интегралы этого вида удовлетворяют формуле 4-1-Л4.1 что вы можете установить сами, взяв интеграл по частям. Формула такого вида называется рекуррентным соотношением, и, при условии, что интеграл можно
Гл. 14. Программы, работающие с вещественными числами 255 вычислить для любого значения Л, она дает нам способ получить результат для соседних значений Л, а значит, для очень большого числа значений. Когда к равно нулю, интеграл сводится к виду 1ь в -С х^ dx ej0 и его можно посчитать: 1 /0 - 1 - — 0.6321206 е Следовательно, Zi ~ 1 - == 1 - 0.6321206 = 0.3678794 /2 - 1 - 2Zj « 1 - 0.7357588 ~ 0.2642412 I3 “ 1 - 3/2 - 1 - 0.7927236 « 0.2072764 и т.д. Это не сложнее ресторанного подсчета на салфетке. Программа, приведенная на листинге 14.3, читает три элемента входных данных, а именно: initial : начальное значение к final : конечное значение к integral : величина интеграла для начального значения к а затем выводит таблицу значений интеграла, соответствующих последовате- льным значениям к. Например, при входных данных 15 0.632120 программа, взяв 0.632120 в качестве /0, выдаст значения интеграла от /0 до Z15 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 program Integrals (input, output) ; { Строит таблицу значений интеграла по рекуррентной формуле } var initial, final, к : integer, integral: real; begin writein (’♦♦♦ РЕЗУЛЬТАТЫ ПРОГРАММЫ ИНТЕГРАЛЫ ♦♦♦’); writein; writein (*k* : 8, 'интеграл" : 24); writein; readln (initial, final, integral); к initial; writein (k : 8, integral: 24 : 6); while к <> final do begin к к + 1; integral1.0 - к ♦ integral; writein (k : 8, integral: 24 : 6) end; { к - final} writein; writein (’♦♦♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦’) end {Integrals }. Листинг 14.3. Текст программы Интегралы, результаты которой демонстрируют катастрофический рост погрешности округления
256 Гл. 14. Программы, работающие с вещественными числами Перед тем, как взглянуть на результаты работы программы, прикинем, каких результатов мы, собственно, можем ждать. В зыбком мире вещественных чисел осторожность никогда не будет лишней! Поскольку интеграл представляет собой площадь под кривой, можно предсказать характер результатов, нарисовав несколько подходящих кривых. На рис. 14.1 мы видим кривые для к=0,1,2,3. Исходя из этих графиков и из соотношений х^ ех >= 0.0 и ех <- ех имеющих место для всех значений х в интервале от 0.0 до 1.0, можно заключить, что: а) величина 1к должна быть для всех к положительной, б) всегда будет меньше 1к. Рис. 14.1. Кривые, соответствующие интегралам /0, Iv /2, 73 Следовательно, величины от /0 до /15 должны представлять собой убываю- щую последовательность положительных чисел. Посмотрим теперь на табл. 14.7, где показаны две группы результатов, полученных программой Интегралы на следующих наборах входных данных: О 15 0.632120 О 15 0.632121 Поскольку мы ожидали увидеть убывающую последовательность положительных чисел, результаты катастрофически неверны, особенно принимая во внимание последние строки в каждой группе. Причина - в погрешностях округления, точнее в ошибке округления нача- льного значения интеграла. Так как начальное значение неточно, все расчеты являются приближенными. Пусть теперь Jk обозначает приближенный резуль- тат, получаемый нами вместо точного значения 7^, и допустим, что начальная погрешность величины Jo равна г. Тогда получим А) = А) + г а последующие расчеты будут таковы:
Гл. 14. Программы, работающие с вещественными числами 257 Таблица 14.7. Образцы результатов работы программы Интегралы *** РЕЗУЛЬТАТЫ ПРОГРАММЫ ИНТЕГРАЛЫ *** *** РЕЗУЛЬТАТЫ ПРОГРАММЫ ИНТЕГРАЛЫ *** к интеграл к интеграл 0 0.632120 0 0.632121 1 0.367880 1 0.367879 2 0.264240 2 0.264242 3 0.207280 3 0.207274 4 0.170880 4 0.170905 5 0.145598 5 0.145477 6 0.126410 6 0.127139 7 0.115133 7 0.110026 8 0.078934 8 0.119789 9 0.289597 9 -0.078102 10 -1.895966 10 1.781021 11 21.855621 11 -18.591232 12 -261.267456 12 224.094788 13 3397.477051 13 -2912.232178 14 -47563.679688 14 40772.250000 15 713456.^87500 г 15 -611582.750000 *** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ *** *** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ *** 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 program Integrate (input, output)', { Строит таблицу значений интеграла по рекуррентной формуле } var initial, final, k : integer, integral: real', begin writeln (’♦♦♦ РЕЗУЛЬТАТЫ ПРОГРАММЫ ИНТЕГРАЛЫ2 ♦♦♦’); writeln; writeln Ck' : 8, 'интеграл' : 24); writeln; readin (initial, final, integrab; k final; writeln (k : 8, integral: 2’4 : 6); while k <> initial do begin integral(1.0 - integral) I k; k .-k - 1; writeln (k : 8, integral: 24 : 6) end; { k - initial} writeln; writeln (’♦♦♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦’) end {Integrate }. Листинг 14.4. Текст программы Интегралы2, результаты которой демонстрируют затухание погрешности округления 9 Заказ № 1110
258 Гл. 14. Программы, работающие с вещественными числами точный расчет приближенный расчет Л « 1 - /0 Ji “ 1 - А) = 1 - </0+г> “ А‘г /2в 1 - 2/j J2 » 1 - - 1 - 2(/rr) - /2+2г /3» 1 - 3/2 J3 - 1 - 3J2 « 1 - 3(/2+2r) - 73-6r и т.д. Величины Jk и Ik связаны общей формулой: Jk “ 4 + для четных к Jk в 1к - п\г для нечетных к Таблица 14.8. Образцы результатов работы программы Интегралы2 *** РЕЗУЛЬТАТЫ ПРОГРАММЫ ИНТЕГРАЛЫ2 *** *** РЕЗУЛЬТАТЫ ПРОГРАММЫ ИНТЕГРАЛЫ2 *** к интеграл к интеграл 25 0.000000 25 100.000000 24 0.040000 24 -3.960000 23 0.040000 23 0.206667 22 0.041739 22 0.034493 21 0.043557 21 0.043887 20 0.045545 20 0.045529 19 0.047723 19 0.047724 18 0.050120 18 0.050120 17 0.052771 17 0.052771 16 0.055719 16 0.055719 15 0.059018 15 0.059018 14 0.062732 14 0.062732 13 0.066948 13 0.066948 12 0.071773 12 0.071773 11 0.077352 11 0.077352 10 0.083877 10 0.083877 9 0.091612 9 0.091612 8 0.100932 8 0.100932 7 0.112384 7 0.112384 6 0.126802 6 0.126802 5 0.145533 5 0.145533 4 0.170893 4 0.170893 3 0.207277 3 0.207277 2 0.264241 2 0.264241 1 0.367879 1 0.367879 0 0.632121 0 0.632121 *** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ *** *** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ *** Факториал чрезвычайно быстро растет, и даже очень небольшая начальная по- грешность скоро превращается в величину, превышающую само значение интег-
Гл. 14. Программы, работающие с вещественными числами 259 рала. Поэтому нижние строки результатов в табл. 14.7 фактически показывают значение погрешности и посему бесполезны. Мы не можем отмахнуться от этой ошибки, сваливая все на компьютер. В равной степени разочаровывающий результат был бы получен, имей мы терпе- ние выполнить выкладки на салфетке. Изъян лежит в самой формуле, приме- нявшейся нами, поскольку в ней раз за разом перемножаются числа, большие единицы. Всякий раз, когда величина погрешности округления умножается на число, большее единицы, или делится на число, меньшее единицы, погрешность округления возрастает. То, что столь невинное на вид вычисление приводит к таким ужасным ре- зультатам, может поразить, но не менее поразительно, как удается сладить с этой проблемой. Нужно подобрать значение для некоторого k, превосходящего наибольшее интересующее нас значение, а затем считать ’’назад” при помощи того же рекуррентного соотношения, только обращенного: 4-1 “ <1 - 4> / * На этот раз погрешность в начальном приближении на каждом шаге де- лится на число, большее единицы, поэтому каким бы плохим ни было начальное приближение (в пределах разумного), погрешность в итоге сойдет на нет. Подтверждение наших выкладок мы видим в программе Интегралы! (лис- тинг 14.4) и ее результатах (табл. 14.8). Эти результаты получены на двух наборах входных данных: О 25 0.0 0 25 100.0 в которых третий элемент - прикидка значения 1к. Хотя второе значение выгля- дит странноватым, обе колонки результатов в точности совпадают, начиная с /18 и далее вниз, подтверждая наш исходный расчет для /0. Примеры, рассмотренные в этой главе, вероятно, представили веществен- ную арифметику в довольно мрачном свете. В ваших программах все эти проблемы могут и не возникнуть, однако следует всегда помнить, что есть повышенная опасность сбоев или потери точности в программах, имеющих дело с числами вещественного типа. Упражнения 14.1. Ниже приведены начальные числа последовательности, известной как ряд Фибоначчи: 1 1 2 3 5 8 13 21 34 За исключением первых двух, каждый член последовательности является суммой двух предыду- щих. Эти числа обладают интересным свойством: отношение соседних членов приблизительно равно хорошо известному числу, называемому золотым сечением. В следующей таблице дается несколько первых отношений: 1/1 = 1.000000 1/2 = 0.500000 2/3 = 0.666667 3/5 - 0.600000 5/8 = 0.625000 8/13 = 0.615385 13/21 “ 0.619048 21/34 » 0.617647 9*
260 Гл. 14. Программы, работающие с вещественными числами Напишите программу ЗолотоеСечение, которая читает малое вещественное число в переменную tolerance, а затем выводит таблицу в вышеприведенном виде и которая завершается, либо когда два соседних значения отличаются меньше, чем на величину tolerance, либо когда вот-вот произойдет целочисленное переполнение. 14.2. Напишите функцию compare, которая, получая два вещественных значения, х и у, будет воз- вращать целочисленный результат следующим образом: О - если х и у "равны”, причем два значения считаются равными, если они отличаются меньше, чем на 0.001; 1 - если х и у не "равны” и х < у 2 - если х и у не "равны” и х > у. Напишите процедуру checktriangle, которая будет читать три вещественных числа А, В, С, выражающие длины сторон треугольника в сантиметрах, а затем выводить их, указывая дополнительно, какого типа треугольник можно образовать из этих отрезков. Возможные типы треугольников: никакой (невозможно образовать), неравносторонний треугольник без прямого угла, прямоугольный неравносторонний треугольник, равнобедренный треугольник без прямого угла, прямоугольный равнобедренный треугольник, равносторонний треугольник. (Подсказка: расположите А, В и С в неубывающем порядке.) Напишите простую программу ТестТреугольника, которая протестирует вашу процедуру. 14.3. Некая компания проводит рассылку счетов в конце каждого месяца. Если счет оплачивается не позже 14-го числа следующего месяца, клиент получает 5-процентную скидку. Оплата после 14-го до конца месяца происходит в обычном размере, а если счет оплачивается еще позже, то начисляются 6-процентные пени. Протокол взаимных расчетов записывается в файле, где каждая строка содержит следующие данные для одной операции: номер клиента : целое число, сумма счета : вещественное число, дата высылки счета: 6-разрядное целое число, содержащее 2 цифры для года, 2 цифры для месяца и 2 цифры для дня, уплаченная сумма : вещественное число, дата оплаты : 6-разрядное целое, такое же, как выше, так что строка 31206 732.46 890430 650.00 890512 хранит тот факт, что клиенту с номером 31206 был выслан счет на сумму 732 доллара 46 центов 30 апреля 1989 года, который был оплачен на 650 долларов ровно 12 мая того же года. Файл данных проверен на корректность. Напишите программу СведенияПоКлиенту, читающую такой файл и печатающую сводку, в ко- торой сведения о каждом клиенте расположены в отдельной строке и имеют вид номер клиента статус где статус может быть одним из следующих: ОПЛАЧЕНО ПЕРЕПЛАТА: хх долларов и хх центов НЕДОПЛАТА: хх долларов и хх центов Таким образом, приведенная выше строка входных данных должна вызвать следующий выходной результат: 31206 НЕДОПЛАТА: 82 долларов и 46 центов 14.4. Напишите программу БанковскийСчет для выдачи сведений за месяц об операциях по данному банковскому счету. Входные данные включают имя владельца счета (до 30 символов, заканчивающихся дробной чертой) и предшествующий баланс (вещественное число), за которыми следуют операции. Каждая операция записана с новой строки и состоит из следующих данных:
Гл. 14. Программы, работающие с вещественными числами 261 а) код операции (обязательно первый символ строки): П обозначает приход, Р - расход; б) дата операции (до 12 знаков, оканчивающихся дробной чертой); в) дополнительная информация (до 24 знаков, оканчивающихся дробной чертой); г) сумма (вещественное число). Последовательность операций завершается строкой, первым символом которой является буква К. Пример входных данных: Васильев Николай Семенович/791.65 Р 13 ноя 89/Чек 290/10.00 Р 14 ноя 89/АХЧ МГУ/22.97 П 21 ноя 89/Наличные/19.44 Р 24 ноя 89/Заказ/160.50 П 30 ноя 89/Вьщача кредита/497.63 К Считайте, что входные данные проверены на корректность. Сводка по операциям должна быть напечатана в формате, показанном в табл. 14.9. Таблица 14 9 О КОМ СВЕДЕНИЯ : Васильев Николай Семенович С КЕМ РАСЧЕТЫ : ВСЕМИРНЫЙ БАНК ДАТА ОПЕРАЦИИ БЫЛО НА БАЛАНСЕ РАСХОД ПРИХОД БАЛАНС 791.65 13 ноя 89 Чек 290 10.00 781.65 14 ноя 89 АХЧ МГУ 22.97 758.68 21 ноя 89 Наличные 19.44 778.12 24 ноя 89 Заказ 160.50 617.62 30 ноя 89 Выдача кредита 497.63 1115.25 СТАЛО НА БАЛАНСЕ 1115.25 14.5. Для всякого положительного числа N, опираясь на формулы aQ - 1.0 и - 1/2 (AT/a^j+a^j) для к > О можно построить итеративный метод поиска квадратного корня из N. Напишите функцию с заголовком function squareroot (number : real) : real', которая по этому методу вычисляет квадратный корень из своего параметра number с точностью до 5 значащих цифр. Напишите также программу TestSquareRoot (ТестКвадратногоКорня) для тестирования функции: постройте таблицу, содержащую рассчитанные значения вашей функ- ции и значения, рассчитанные встроенной функцией sqrt. 14.6. Формулы /о- 2_______ /’о-^2/0 служат основой метода Виета, созданного для вычисления последовательных приближений значения числа tr. Несколько первых шагов вычисления по формулам показаны в табл. 14.10.
262 Гл. 14. Программы, работающие с вещественными числами Таблица 14.10 к 4 Ъ 0 I 2 3 1.414214 0.707 Ю7 2.828427 1.847769 0.653282 3.061467 1.961571 0.640729 3.121445 1.990369 0.637644 3.136549 а) Напишите программу под названием TestVieta (ТестВиета), которая будет строить таблицу значений Д, Pk и 17^ для 1-0..20. б) Напишите функцию с заголовком: function pi (decimalplaces: integer) : real; которая рассчитывала бы значение ire числом точных знаков равным заданному параметру. в) Напишите простую программу TestPi (ТестГГи) и с ее помощью исследуйте ограничения, которые накладывает на функцию pi ваш Паскаль-процессор.
15 ЭФФЕКТИВНОСТЬ ПРОГРАММ Эта глава является своего рода введением в обсуждение структур данных. Она посвяшена эффективности программ - теме менее важной, чем ясность и устойчивость к ошибкам, однако заслуживающей внимания каждого уважаю- щего себя программиста. Какую программу можно назвать эффективной? Первым приходит в голо- ву предположение, что эффективной будет такая программа которая производит вычисления максимально быстро. Другими словами, эффективность - это эконо- мия времени. Однако понятие эффективности должно включать и экономию па- мяти. В дальнейшем мы увидим, что время выполнения программы и занимае- мое ею место часто зависимы, и попытка сократить время приводит к увеличе- нию объема памяти (и наоборот). В данной главе мы обсудим, каким образом можно оценить время выпол- нения программы, объем занимаемой ею памяти и некоторые пути повышения ее эффективности. 15.1. Профили и анализ программ При изучении эффективности важно знать, как часто исполняется каждая строка программы. В некоторых языковых системах имеются средства, обеспе- чивающие распечатку этой информации после выполнения программы над кон- кретным множеством входных данных. Так получается профиль. Например, в табл. 15.1 приведен текст программы Интегралы! из гл. 14 с профилем вдоль ее левого поля. Этот профиль соответствует запуску программы, в результате кото- рого получено подмножество данных, показайных в табл. 14.8 (с. 258). Обычно запуск программы с разными наборами входных данных дает разные профили. Концепцию профиля можно обобщить, если для каждой строки программы вывести формулу, устанавливающую соотношение между входными данными и тем, сколько раз выполняется та или иная строка. Каждая формула выражается через несколько величин, которые характеризуют размер задачи и выводятся из входных данных или констант программы. Набор таких формул называется аналитическим профилем программы. Аналитический профиль программы Интегралы! приводится в табл. 15.2. Против некоторых строк стоят просто числа - значит, эти строки исполняются фиксированное число раз независимо от данных. Рядом с другими стоит фор- мула, включающая N. Значение W - равно числу интегралов, которые необхо- димо вычислить, и оно определяется как разность между первым (initial) и последним (final) вводимым значением. Аналитический профиль из табл. 15.2 подойдет и для табл. 15.1, если вместо W мы подставим значение 25.
264 Гл. 15. Эффективность программ Таблица 15.1. Текст программы Интегралы2 с профилем 1 2 профиль program Integrate (input, output); 3 { Строит таблицу значений интеграла по рекуррентной формуле } 4 5 var 6 initial, final, k : integer, 7 integral: real; 8 1 begin 9 1 writeln (’•♦♦ РЕЗУЛЬТАТЫ ПРОГРАММЫ ИНТЕГРАЛЫ2 ♦♦♦’) ; 10 1 writeln; 11 1 writeln Ck' : 8, 'интеграл' : 24); writeln; 12 1 readln (initial, final, integral); 13 1 k final; 14 1 writeln (k : 8, integral: 24 : 6); 15 26 while k <> initial do 16 25 begin 17 25 integral(1.0 - integral) I k; 18 25 k:-k-l; 19 25 writeln (k : 8, integral: 24 : 6) 20 25 end; 21 { k - initial} 22 1 writeln; writeln (’*** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦’) 23 1 end {Integrate}. авали- Таблица 15.2. Текст программы с аналитическим профилем, где N - количество вычисляемых интегралов 1 гический профиль program Integrate (input, output); 2 3 { Строит таблицу значений интеграла по рекуррентной формуле } 4 5 var 6 initial, final, k : integer, 7 integral: real; 8 1 begin 9 1 writeln (’♦♦* РЕЗУЛЬТАТЫ ПРОГРАММЫ ИНТЕГРАЛЫ2 ♦♦♦’); 10 1 writeln; И 1 writeln Ck' : 8, 'интеграл' : 24); writeln; 12 1 readln (initial, final, integral) ; 13 1 k final; 14 1 writeln (k : 8, integral: 24 : 6); 15 14W while k <> initial do 16 N begin 17 N integral(1.0 - integral) / k; 18 N k k - 1; 19 N writeln (k : 8, integral: 24 : 6) 20 N end; 21 { k - initial} 22 1 writeln; writeln (’*** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ *♦♦’) 23 1 end {Integrate }. Получив аналитический профиль программы, можно выяснить, сколько раз выполнялись определенные ключевые операции. Для этого надо просмотреть программу, найти эти операции и просуммировать соответствующие величины в аналитическом профиле. Например, в программе Интегралы2 мы можем под-
Гл. 15. Эффективность программ 265 считать число присваиваний и число арифметических операций следующим образом: <а) одно присваивание в строке 13 выполнено 1 раз: 1 одно присваивание в строке 17 выполнено N раз: N одно присваивание в строке 18 выполнено N раз: N общее число присваиваний : 2АМ (б) одно вычитание в строке 17 выполнено N раз: N одно деление в строке 17 выполнено N раз: N одно вычитание в строке 18 выполнено N раз: N . общее число арифметических операций: 3# Подобная информация может использоваться для выбора наиболее эффективного способа вычисления из нескольких имеюшихся. Таблица 15.3. Вычисление общего времени исполнения прог- раммы Интегралы2 с использованием условных времен исполнения для каждой строки строка аналитический профиль время однократного исполнения (усл.ед.) общее время исполнения каждой строки 8 1 0 0 9 1 10 10 10 1 2 2 11 1 12 12 12 1 15 15 13 1 2 2 14 1 10 10 15 1+W 2 2+2ЛГ 16 N 0 0 17 N 4 4N 18 N 2 2N 19 N 10 10N 20 N 0 0 21 22 1 12 12 23 1 0 0 Итого : 65+18JV Теперь предположим, что можно выяснить время выполнения каждой строки; тогда аналитический профиль даст нам возможность вычислить общее время выполнения программы. Полагая, что время выполнения каждой конкрет- ной строки всегда одинаково, мы можем просто перемножить время для каждой строки на число ее исполнений и результаты сложить. Такие расчеты показаны в табл. 15.3 для программы Интегралы2\ время для каждой строки приведено в условных единицах. То, что мы использовали здесь условное время, несущест- венно; важна формула для времени исполнения данной программы, которую мы получили с помощью проведенных вычислений. Эта формула имеет вид t -= а + bN ,
266 Гл. 15. Эффективность программ где t - время, а и b - константы. Другими словами, время выполнения пропорци- онально N - количеству вычисляемых интегралов. Точное измерение времени выполнения каждой строки (что затруднительно, если программа написана на языке высокого уровня) повлияет лишь на величину констант а и b\ t останется пропорциональным А. Характер зависимости времени выполнения программы или подпрограммы от размера решаемой задачи называется временной сложностью и обычно запи- сывается в сокращенной форме, заимствованной из математики. Например, можно сказать: для программы Интегралы2 временная сложность t составляет О (ЛЭ, где N - количество вычисляемых интегралов. Фраза ”t есть 0(A)” означает, что t - величина порядка А, т.е. при достаточно больших N, t пропорционально N. Если бы формула для времени выполнения оказалась такой: t e а + bN + cN2 , где а, b и с - константы, мы проигнорировали бы два первых слагаемых и сказали бы, что временная сложность - О (А2). Аналогично, если бы формула выглядела так: t« а то временная сложность была бы 0(1). Это означает, что время выполнения постоянно и не зависит от размера задачи. Чтобы выяснить временную сложность процесса, нам фактически необхо- димо знать лишь тот член формулы времени выполнения, который растет быст- рее других с увеличением размера задачи. Это мы можем узнать, рассмотрев только одну строку программы, а именно ту, которая выполняется чаще других (или одну из таких строк, если несколько строк выполняется одинаковое число раз). Формула для числа выполнений такой строки определяет временную слож- ность всего процесса. Например, в программе Интегралы2 чаще других испол- няется строка 15. Она выполняется А+1 раз, следовательно, временная слож- ность программы есть 0(A). 15.1.1. Правила анализа программ К сожалению, осуществление полного анализа программы часто оказывается очень сложной задачей, требующей глубокого знания математики и статистики. Хотя и не в полной мере, но мы все-таки попытаемся обсудить эту задачу, поскольку некоторые программы и подпрограммы можно проанализиро- вать с помощью нескольких простых правил, которые понадобятся нам в после- дующих главах. Чтобы обосновать необходимые нам правила, обратимся к физику XIX в. Густаву Роберту Кирхгоффу, а именно к закону, известному как первый закон Кирхгоффа. В нем утверждается: токи в электрической цепи текут таким обра- зом, что сумма токов, входящих в любую точку, равна сумме токов, выходящих из нее. В программе не течет электрический ток, но мы можем говорить о потоке управления^ который основывается на понятии точки выполнения. Точка выпол- нения - это точка, в которой выполняется данная программа в указанный момент времени. По ходу исполнения программы точка выполнения движется по тексту программы, и это движение называется потоком управления. В программировании эквивалент приведенного закона формулируется так: Поток управления в программе устроен следующим образом: сколько раз управление передается в конкретную точку программы из других точек, столько раз управление передается из данной точки в другие точки.
Гл. 15. Эффективность программ 267 За отсутствием лучшего названия, мы будем называть это утверждение законом Кнрхгоффа и выведем из него правила анализа четырех типов изучен- ных нами структурных операторов. Эти правила применимы, если нет переходов внутрь тела структурного оператора или из него. В составном операторе управление последовательно передается от одного оператора к другому. Существует ровно один способ входа в каждую точку программы и ровно один способ выхода из нее. Отсюда мы получим Правило 1 для составного оператора begin 51; 52; 5Л; end если управление п раз передается на начало оператора, каждый из операторов SI, S2, ..., Sk выполняется п раз. Рис. 15.1. Правило 1 В условном операторе в зависимости от значения логического выражения выполняется один из входящих в него операторов (либо ”то”, либо ’’иначе”), а после этого управление передается оператору, следующему за условным опера- тором. Таким образом, мы получаем: Правило 2 для условного оператора if b then 51 else 52 если управление приходит п раз от предыдущего оператора и значения логичес- кого выражения таковы, что оператор 51 исполняется т раз, то оператор 52 исполняется п-т раз.
268 Гл. 15. Эффективность программ Рис. 15.2. Правило 2 В операторе пока (while) переход на заголовок цикла осуществляется либо с предыдущего оператора, либо с конца тела цикла. Аналогично, выйти из заголовка можно или на начало тела цикла, или на следующий за while опера- тор. Таким образом, мы имеем: Правило 3 для оператора while while b do SI если управление n раз приходит от предыдущего оператора и тело цикла S1 в сумме выполняется т раз, то логическое выражение b вычисляется п + т раз, причем п раз принимает значение false и т раз принимает значение true. Рис. 15.3. Правило 3
Гл. 15. Эффективность программ 269 Аналогично в операторе пока-не (repeat) можно перейти к началу цикла двумя способами: или после выполнения предыдущего оператора, или обнару- жив, что логическое выражение ложно. Выход из оператора возможен или на начало цикла, или на следующий оператор. Получаем следующее правило. Правило 4 для оператора repeat repeat SI; 52; S*; until b если управление n раз передается от предыдущего оператора и последова- тельность операторов 51, S2, ..., Sk в теле цикла в сумме исполняется т раз, то каждая часть оператора проходится ш раз, при этом условное выражение b при- нимает т~п раз значение false и п раз - значение true. На рис. 15.1 - 15.4 число на каждой линии указывает, сколько раз управ- ление передается по соответствующей связи. Чтобы вычислить, сколько раз выполняется тот или иной фрагмент кода (квадратик), необходимо сложить величины, стоящие на всех линиях, входящих в данный квадратик. Разумеется, согласно правилу Кирхгоффа эта величина должна быть равна сумме чисел на линиях, выходящих из данного квадратика. 15.1.2. Примеры анализа программ Полученные выше правила достаточны для анализа многих простых про- грамм. Рассмотрим два примера. В качестве первого примера вернемся к программе ДлинаТекста, разрабо- танной в гл. 12. Она копирует текст из входного файла в выходной файл и под- считывает число слов. Для проведения детального анализа нам нужно знать четыре величины: L : число строк в тексте,
270 Гл. 15. Эффективность программ М : число букв в тексте, N : число символов, не являющихся буквами, W : число слов в тексте. Программа и ее аналитический профиль показаны в табл. 15.4, а величи- ны, записанные перед строками, могут быть получены в результате действий над операторами головной программы, которые выполняются в следующей последо- вательности. Шаг 1: Операторы головной программы исполняются один раз. Согласно Правилу 1 мы можем записать цифру 1 перед строками 54 -58. Шаг 2: Процедура processthetext вызывается однажды в строке 56, следова- тельно, согласно Правилу 1 каждая из строк 45-48 и 50-52 выполняется один раз. Правда, для строки 48 это лишь первый из общего числа про- ходов. Шаг 3: Поскольку оператор 49 обрабатывает одну строку текста, он дол- жен исполняться L раз. Поэтому мы записываем величину L в строке 49 и, согласно Правилу 3, добавляем еще L проходов для строки 48, полу- чая в сумме 1 + L проход. Шаг 4: Поскольку вызов процедуры processoneline стоит в строке 49, он происходит L раз. Согласно Правилу 1 это позволяет записать величину L перед строками 31-33 и 39-40. Шаг 5: Тело оператора пока в processoneline выполняется один раз для каждого слова. Следуя Правилу 1, мы запишем величину W перед стро- ками 34-38, а по Правилу 3 прибавим W к счетчику в строке 33, полу- чая в сумме L+W. Шаг 6: Так как процедура copytoendofword вызывается в строке 35, строки 25, 26 и 28 должны исполняться W раз. Каждый раз при исполнении строки 27 копируется один символ, а логическое выражение в строке 26 гарантирует, что этот символ - буква. Значит, строка 27 должна испол- няться М раз, и мы добавляем еще М выполнений к строке 26. Шаг 7: Процедура copyaletterorendofline вызывается L раз в строке 32 и W раз в строке 37. Следовательно, строки 19, 20 и 22 исполняются L + W раз каждая. Символы, которые копируются вызовом copyonecharacter в строке 21, не всегда являются буквами, следовательно, можно записать величину N перед строкой 21 и прибавить N к имеющейся величине перед строкой 20, получая L+W+N. Шаг 8: Процедура copyonecharacter вызывается из строк 21 и 27 в общей сложности N+M раз, следовательно, строки 14-16 исполняются N + М раз каждая. Шаг 9: Функция letter вызывается L+W+N раз в строке 20 и W+M раз в строке 26; следовательно, строки 6-9 исполняются L+2W+M+N раз каж- дая, и этим завершается анализ. Из аналитического профиля программы ДлинаТекста видно, что формула для полного времени исполнения программы имеет вид t « aL + bW + cN + dM , где a, b и с - константы. В этом случае время исполнения зависит не только от размера задачи, но от четырех параметров, а временная сложность определяется как О(£+1У+7^+Л/). Однако ясно, что число символов больше числа строк и числа слов, следовательно выражение для временной сложности можно упростить до О(С), где С - общее число символов в тексте.
Гл. 15. Эффективность программ 271 Таблица 15.4. Текст программы ДлинаТекста с анали- тическим профилем 1 профиль 2 3 4 5 6 7 Л+2ИЧ-Х+Л/ 8 t+2W+X+M 9 L+ZW'+V+A/ 10 11 12 13 14 N+M 15 N+M 16 N+M 17 18 19 L+W 20 L+W+N 21 N 22 L+W 23 24 25 W 26 W+M 27 M 28 W 29 30 31 L 32 L 33 L+W 34 W 35 W 36 W 37 w 38 w 39 L 40 L 41 42 43 44 45 1 46 1 47 1 48 1+L 49 L 50 1 51 1 52 1 53 54 1 55 1 56 1 57 1 58 1 59 1 program TextLength (input, output)', { Скопировать текст и подсчитать в нем количество слов } function letter (с : char) : Boolean', begin { Здесь предполагается, чпю имеются последовательные наборы 'А'..?' и ,a,..,zt и в обоих символы идут подряд } letter (с >- *А’) and (с <- ’Z’) or (с >- ’а’> and (с <- ’z’> end { letter}; procedure copyonecharacter; var ch : char, begin read (ch); write (ch) end { copyonecharacter }; Параметры аналитического профиля: L - число строк М - число букв N - число небуквенных символов W - число слов procedure copytoaletterorendofline; begin while not letter (input) and not eoln do copyonecharacter end { copytoaletterorendofline }; procedure copytoendofword; begin while letter (input) do copyonecharacter end { copytoendofword } procedure processoneline (var wordcount: integer); begin copytoaletterorendofline-, while not eoln do begin { input - первая буква слова } copytoendofword; wordcountwordcount + 1; copytoaletterorendofli ne end; readln', writeln end { processoneline }; procedure processthetext; var numberofwords : integer; begin writeln ("Обрабатывается текст:"); writeln; numberofwords 0; while not eof do processoneline (numberofwords); writeln; writeln ("В этом тексте ", numberofwords : 1, ’ слов."); end { processthetext}; begin writeln (’♦♦*♦ РЕЗУЛЬТАТ РАБОТЫ ПРОГРАММЫ ДЛИНА ТЕКСТА ♦♦♦*’): writeln; processthetext; writeln; writeln (’**** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦♦’) end { TextLength }.
272 Гл. 15. Эффективность программ Таблица 15.5. Образец результатов и текст программы СтепеннаяТаблица с аналитическим профилем в предположении корректности входных данных 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 профиль N N IWD/2 VWH1/2 V(VH)/2 N N 1 1 1 0 0 о о 1 1 1 1 N N N N 1 1 1 1 1 1 1 1 program PowerTable (input, output) ; { для k в степени к составить таблицу значений; к - 1,..., limit} function self power (number : integer) : real; var answer : real; count: integer, begin { number > 0 } answer 1.0; countnumber; repeat answer answer*number, countcount-1; until count - 0; self poweranswer, end { selfpower}; procedure dothetabulation; var limit, к : integer, begin readln (limit); if limit < 0 then begin writeln (’Неверные данные : limit - limit: 1); writeln Vlimit должен быть больше O') end else begin writeln Ck': 4, 'к в степени к' : 22); writeln; к 0; repeat к Ж; writeln (к : 4, * ’ : 7, selfpower(k) : 14) until к - limit end end { dothetabulation }; begin writeln (’♦♦♦♦ РЕЗУЛЬТАТ^ ПРОГРАММЫ СТЕПЕННАЯ ТАБЛИЦА ♦♦••’); writeln; dothetabulation; writeln; writeln (’♦♦** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦**♦’) end { PowerTable}. Параметры аналитического профиля: N - число строк в таблице результатов **** РЕЗУЛЬТАТЫ ПРОГРАММЫ СТЕПЕННАЯ ТАБЛИЦА к кв степени к 1 1.0000000Е+00 2 4.0000000Е+00 3 2.7000000Е+01 4 2.5600000Е+02 5 3.1250000Е+03 6 4.6656000Е+04 **** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ****
Гл. 15. Эффективность программ 273 В качестве другого примера рассмотрим программу под названием СтепеннаяТаблица, содержащую процедуру dothetabulation и функцию selfpower. Ее анализ приведен в табл. 15.5, результаты показаны для входного числа 6. Это число считывается в строке 21 и определяет длину таблицы результатов. Дця общности мы обозначим его как N. Программа СтепеннаяТаблица проверяет правильность входных данных, поэтому прежде чем приступать к ее анализу, мы должны решить, считаем мы исходные данные корректными или нет. Для некорректных данных анализ прог- раммы тривиален, поскольку большая часть ее не выполняется. Мы будем счи- тать все входные данные корректными, т.е. полагать, что N больше 0. Анализ предполагает следующие шаги. Шаг 1: Поскольку операторы головной программы выполняются один раз, строки 38-43 выполняются по одному разу каждая согласно Правилу 1. Шаг 2: Процедура dothetabulation вызывается однажды в строке 41, следо- вательно, согласно Правилу 1 строки 20-22 и строка 36 должны выпол- няться по одному разу каждая. Шаг 3: Поскольку мы предполагаем, что данные корректны, строки 23-26 выполняться не будут, отсюда согласно Правилу 2 часть ’’иначе” услов- ного оператора (после else) выполнится один раз. Так как в этой части содержится составной оператор, перед строками 27-30 и строкой 35 можно поставить число 1. Мы не ставим цифру 1 перед строкой 31, так как по правилу 4, она исполняется столько же раз, сколько и тело опе- ратора repeat. Шаг 4: Оператор 33 выводит одну строку в таблицу результатов и испол- няется N раз. Следовательно, согласно Правилу 4 мы можем поставить величину N перед строками 31-34. Шаг 5: Поскольку функция selfpower вызывается N раз в строке 33, строки 9, 10, 14 и 15 выполняются N раз каждая согласно Правилу 1. Шаг 6: Чтобы проанализировать строки 11-13, надо рассмотреть каждый из N вызовов функции selfpower отдельно, поскольку число исполнений тела оператора repeat в функции зависит от параметра number. Дейст- вительно, за один вызов функции строка 12 выполняется number раз. Таким образом, общее число исполнений строки 12 составляет 1 + 2 + 3 + ... + А , что в сумме дает1 N W+ 1) / 2. Таким образом, строки 11-13 исполняются Ж+1)/2 раз каждая, что завершает анализ. Из нашего анализа программы СтепеннаяТаблица видно, что формула для времени исполнения программы имеет вид: tв + bN + с и, таким образом, временная сложность программы - О (А2). К сожалению, ред- ко можно провести полный анализ так просто, как в приведенных примерах. В 1 Пусть S - требуемая сумма, тогда : 5 - 1 + 2 + 3 + ... + N 5 - N + N-\ + N-2 + ... + 1 . Сложив эти две строки, получим : 2S - (ЛН-1) + (ЛГ+1) + W+1) + ... + (W+1) - N<N+l ) и, следовательно, 5 - ЛКЛМ)/2
274 Гл. 15. Эффективность программ частности, если программа содержит много циклов, выполняемых непредсказуе- мое число раз, и много условных операторов, необходимо вводить оценки, осно- ванные на вероятности того, что определенные условные выражения истинны или ложны. Это требует знания теории вероятности. Также надо уметь суммиро- вать различные ряды. Однако для понимания этих вопросов достаточно знания основ математического анализа. 15.2. Объем памяти Во время исполнения программы оперативная память компьютера занята командами программы и внутренними данными, с которыми оперирует програм- ма. При изучении эффективности нас будет интересовать объем памяти, заня- тый данными. Важно с самого начала отметить, что входной и выходной файлы обычно не содержатся в оперативной памяти; адресное пространство используется для хранения переменных и передачи параметров. В структурных языках типа Пас- кадь число одновременно существующих в памяти переменных меняется в тече- ние работы программы. Поэтому наибольший интерес представляет оценка мак- симального объема необходимой памяти. Эту оценку можно назвать объемной сложностью программы, которая, как и временная сложность, выражается через несколько величин, отражающих размер решаемой задачи. Вычисление объемной сложности программы - процесс довольно простой. Поступим следующим образом: а) запишем схему вызовов программы; б) рядом с каждой строкой схемы вызовов укажем соответствующие пара- метры, возникающие локальные переменные и объем памяти для каж- дой из них; в) изучая эту информацию, найдем место (или места) в программе, в котором параметры и переменные занимают наибольшую память. Для иллюстрации положим, что каждый тип переменной или параметра занимает следующее число единиц памяти: переменная типа char : 1 единица, переменная типа Boolean : 1 единица, переменная типа integer : 2 единицы, переменная типа real : 4 единицы, изменяемый параметр : 2 единицы (для любого типа данных), входной параметр : столько, сколько переменная того же типа. Рассмотрим память, занятую под данные программой ДлинаТекста из табл. 15.4. Получим такие результаты: Схема вызовов Новая память данных Всего используется TextLengh input (1 ед.) 1 processthetext numberofwords (2 ед.) 3 processonline wordcount (2 ед.) 5 copytoaletterorendofline не требуется 5 letter 4 с (1 ед.) 6 copyonecharacter ch (1 ед.) 6 copytoendofword не требуется 5 letter с (1 ед.) 6 copyonecharacter ch (1 ед.) 6 Мы видим, что данные занимают наибольший объем в процессе выполнения функции letter или подпрограммы copyonecharacter. Всего задействовано 6 еди- ниц памяти, в которых хранится информация input, numberofwords, wordcount и
Гл. 15. Эффективность программ 275 с или ch. Эта оценка приблизительна, поскольку во время вычисления выраже- ний могут потребоваться дополнительные объемы памяти. Однако она достаточ- на, чтобы видеть, как мало памяти потребляет данная программа. Поскольку объем памяти не зависит от размера обрабатываемого текста, объемная слож- ность будет 0(1). Очевидно, что для программы ДлинаТекста и для остальных написанных нами программ объем памяти, занимаемый данными, не является проблемой. Когда же мы будем работать со структурными типами данных, изучение затрат памяти потребует большего внимания. 15.3. Выбор алгоритма Выбор правильного алгоритма или алгоритмов - один из ключевых во- просов оптимизации времени исполнения компьютерной программы. Он мо- жет влиять и на объем занимаемой памяти. Алгоритм можно определить как точное описание процесса в терминах соответствующих подпроцессов. Из этого как будто следует, что алгоритм и программа - синонимы, однако это неверно. Программа или подпрограмма реа- лизует алгоритм на конкретном языке программирования. Один и тот же алго- ритм можно реализовать различными способами, используя разные языки про- граммирования или разные свойства одного языка. Таким образом, алгоритм яв- ляется более абстрактным и общим понятием, чем программа или подпрограмма. Покажем на простом примере, что выбор алгоритма приводит к существен- но разным программам. Наибольшим общим делителем двух положительных целых называется наибольшее целое число, которое делит каждое из них без остатка. В табл. 15.6 приведены две функции с именем gcd, каждая из которых находит наибольший общий делитель параметров а и b в предположении, что оба параметра положи- тельны. Первую функцию мы уже встречали в качестве простой программы в упр. 2.5, а вторая основана на таком известном методе, как алгоритм Евклида. На первый взгляд эти функции могут показаться одинаковыми, и для малых чи- сел ни одна не дает преимущества в вычислении. Однако если сравнить проде- ланную работу для значений параметров a « 1000000 и b • 2 , обнаружится существенная разница. Приведем точное число операций в ходе выполнения каждой функции для данного конкретного случая: первый вариант второй вариант операции присваивания 500000 4 операции сложения 499999 0 операции умножения 0 1 операции сравнения 999999 1 Очевидно, что вторая функция будет работать в сотни или тысячи раз быстрее первой. Вспомните это при выполнении упр. 15.6! Из приведенного примера следует еще один вывод. Смогли бы вы изобре- сти один из приведенных алгоритмов, если бы вас попросили написать функцию для поиска наибольшего общего делителя? Нашлось бы немного людей, способ- ных без знания предмета написать столь короткую и эффективную функцию, как первая. И совсем немногие смогли бы изобрести вторую. Новые замечатель- ные алгоритмы открываются исключительно гениальными людьми, а лучшее, на
276 Гл. 15. Эффективность программ Таблица 15.6. Неэффективный и эффективный варианты по- иска наибольшего общего делителя для двух положительных чисел 1 2 3 4 5 6 7 8 9 10 11 12 1 2 3 4 5 6 7 8 9 10 И 12 13 14 function gcd (a, b : integer): integer, { Ищет наибольший общий делитель чисел а и Ь} begin { яХ) and д>0 } while а <> b do if а > b then а а - b else b b - а\ {а-Ь} gcd а end { gcd }; function gcd (a, b ‘. integer): integer, { Ищет наибольший общий делитель чисел а и b, с помощью алгоритма Евклида } var remainder : integer, begin { a > 0 and b > 0 } repeat remainder a mod b\ a :- b, b remainder until b - 0; gcd a end { gcd }; что может надеяться большинство из нас, - это пользоваться плодами их ума. Узнавать о новых алгоритмах и учиться их применять - важная часть про- граммистского образования. Этот процесс должен продолжаться в течение всей вашей программистской карьеры по мере изобретения новых и все более эффек- тивных алгоритмов. 15.4. Сокращение накладных расходов Выбрав хороший алгоритм, оценим общее время исполнения. Оно, конеч- но, зависит от деталей реализации. Хорошая кодировка алгоритма может при- нести не меньшие выгоды, нежели его правильный выбор. При обсуждении алго- ритма полезно различать операции, управляющие вычислением, и операции, ко- торые имеют прямое отношение к вычислению результатов. Операции, участву- ющие только в управлении вычислениями, рассматриваются как накладные расходы. Эти расходы можно сравнить с накладными расходами в бизнесе. Их сокращение иногда приводит к повышению эффективности программы. Один из способов сократить накладные расходы - упростить условные выражения, управляющие циклами. Примером, хотя и не слишком хорошим, является программа ТаблицаСтепеней (с. 209), содержащая следующую после- довательность операторов: х 0; succesful := true*, while (х о maximum) and succesful do
Гл. 15. Эффективность программ 277 begin х :в х + 1; calculatepower (х, k, xpowerk, succesful); if succesful then writeln (x : 20, xpowerk : 15) end; { (x e maximum) or not succesful} Тот же процесс может быть описан по-другому: х 0; while х о maximum do begin х :e х + 1; calculatepower (х, Л, xpowerk, succesful); if succesful then writeln (x : 20, xpowerk : 15) else x :e maximum { выход из цикла } end; { (x e maximum) or not succesful} Во второй версии программы часть ’’иначе” условного оператора, находя- щегося внутри цикла, гарантирует, что х принимает значение maximum, когда переменная succesful становится ложной. Это позволяет корректно закончить выполнение цикла, лишний раз не анализируя величину succesful в его заголов- ке. Недостаток второй программы заключается в том, что она менее ясна. Однако если программа содержит цикл, исполняющийся очень много раз, выиг- рыш в скорости может перевесить потерю в ясности. В данном случае цикл исполняется не очень много раз, следовательно, предпочтителен первый вариант кода. Вот почему этот пример не слишком хорош! Другой способ сократить накладные расходы - комбинировать операто- ры так, чтобы фрагменты, содержащие накладные расходы, совместно испо- льзовались всеми программами. Рассмотрим, например, следующую последова- тельность операторов из программы СовершенныеЧисла (с. 225): if odd (n) then low 3 else low 2; high :e n div low; if odd (n) then step := 2 else step := 1; Запишем ее иначе: if odd (n) then begin low :- 3; step :-= 2; end else begin low 2; step := 1; end; high := n div low; Выигрыш состоит в том, что функция odd теперь вычисляется один раз. В дан- ном примере комбинируются условные операторы. Выигрыш будет получен и в тех случаях, когда комбинируются операторы цикла. Подобные изменения все- гда надо производить очень осторожно, сохраняя корректность программы.
278 Гл. 15. Эффективность программ 15.5. Эффективность вычислений Обсудив, как повысить эффективность программы за счет сокращения на- кладных расходов, обратимся к эффективности вычислений. Для иллюстрации различных аспектов эффективности мы рассмотрим некоторую функцию. При- мер будет численным, хотя некоторые выводы из него применимы к нечислен- ным операциям. Рассмотрим функцию, подсчитывающую значение синуса для величин диапазоне от 0 до п путем суммирования ряда: х в х3 х5 * _ + _ 3! 5! х7 В - + ... 7! sin (х) s х Результат требуется вычислить с точностью до пяти десятичных знаков. Мы мо- жем, конечно, воспользоваться встроенной функцией sin, но суммирование ря- дов преподаст нам ряд ценных уроков. Многие математические функции представимы в виде рядов; для сумми- рования ряда с заданной точностью надо уметь вычислять его слагаемые одно за другим и складывать их до тех пор, пока добавление следующего члена не пере- станет влиять на результат. По соглашению, члены ряда нумеруются от 0: если tk обозначает Л-й член ряда, то - сумма всех членов ряда по к-й включительно - выглядит так: sk = 'о + 0 + h + - + h Для вычислительных целей это может быть записано в следующей форме: so = 'о и Sk = 5A-l + tk ДЛЯ k>Q Запишем несколько первых слагаемых ряда синуса: х3 х5 х7 М — х, /i — - — , ty ~ — > /3 ~ ~ — ° 3! 5! 7! а для х = 0.5 ход вычислений таков: $к 0 0.5 0.5 1 -0.020833 0.479166 2 0.000260 0.479426 3 -0.000002 0.479424 Отсюда мы получаем, что sine (0.5) равен 0.47942 с точностью до пятого знака. Суммирование рядов таким способом в некотором отношении напоминает вычисление кубических корней, которое мы изучали в гл. 14 (с. 248-249). По- следовательные члены ряда синуса аналогичны поправкам к приближению куби- ческого корня, а окончание процесса зависит от их величины. Чтобы получить в данном случае точность в пять десятичных знаков, вычисления можно прекра- тить, если получено слагаемое меньшее 0.00005. В этом и других аналогичных рядах не следует впрямую вычислять каж- дое новое слагаемое. Болес продуктивный подход - проанализировать соотно- шение между соседними членами. В нашем примере мы получим х2 х2 х2 1 2*3 0 4*5 6*7
Гл. 15. Эффективность программ 279 откуда мы получаем общее выражение: х2 - ------л , для к > 0. к 2t(2t-l)*1 $0 “ *0 Sk “ Sk-1 + *к Записав рядом известные нам формулы Vх х2 к 2к(2к-\)к получим простой способ вычисления. Таким образом, из нашего примера можно извлечь первый урок: часто необходимо провести математические преобразо- вания для реорганизации или упрощения формул с целью получить эффектив- ный метод их вычисления. Теперь посмотрим на табл. 15.7 (с. 280), где приведены две функции для суммирования рядов синуса. Первая прямо реализует только что полученную формулу. Последовательно вычисляемые величины t и 5 хранятся в переменных term и sum соответственно, а четыре полученных формулы прямо реализованы операторами Паскаля в строках 8, 12 и 13. Однако, есть ряд улучшений, кото- рые можно внести в эту функцию. Мы рассмотрим три из них. Начнем с наименее очевидного усовершенствования. Мы поймем, что нуж- но сделать, если протестируем функцию sine и напечатаем для сравнения после- довательные величины k, term и sum. Выдача двух таких тестов приведена в табл. 15.8, 1де последняя величина в колонке сумм является значением функ- ции. Для вычисления взяты значения xr*pi/4 и x^3pi/4. Так как синус симметри- чен относительно pi/2, окончательные результаты одинаковы, но для вычисле- ния с помощью второй функции требуется почти вдвое больше слагаемых, чем для вычислений с использованием первой. Здесь и лежит ключ к улучшению! Если х больше, чем pi/2, эффективнее вычисляется sin(pi-x). Поэтому во второй версии функции sine из табл. 15.7 вводится новая переменная с именем piminusx, которая гарантирует, что величина х, применяемая для суммирования в строках 9-11, есть наименьшее из х и pi-x. Общий вывод таков : использование симметрии вычислений может привести к повышению эффективности. Хотя этот вывод сделан на основе анализа математического примера, он справедлив и для многих видов нечисленной обработки. Второе улучшение первой функции из табл. 15.7 следует с очевидностью из ее текста. Величина х остается постоянной во время каждого вычисления функции. А значит, величина sqr(x) также не меняется и не стоит постоянно вычислять ее внутри оператора цикла. Часто выгоднее вычислить величину один раз и хранить ее, чем повторять вычисления. В данном случае вычисле- ние должно быть сделано вне цикла. Для этого во второй функции вводится но- вая переменная с именем xsquared. Она инициализируется в строке 12 и исполь- зуется в строке 17. Это простой пример предварительного подсчета, производи- мого с целью не повторять эти вычисления снова и снова. В гл. 17 мы продемон- стрируем такую обработку более явно. Подобные изменения сокращают время исполнения программы, но увеличивают затраты памяти. Наше третье улучшение в функции sine также связано с повторными вы- числениями. Выражение 2*Л подсчитывается дважды в строке 12 первого вари- анта функции. Выражение нельзя вынести из цикла, так как к меняется при каждом исполнении тела цикла. Однако часто можно экономить арифметичес- кие операции, выражая формулу через другие переменные. В данном случае мы полагаем i равным 2*к, и тогда формула для t^ принимает вид
280 Гл. 15. Эффективность программ X2 а в пересмотренном варианте функции выражение 2*Л заменяется на L Следова- тельно, оператор для вычисления каждого нового слагаемого выглядит так: term :e -term ♦ xsquared I (i ♦ (i + 1)) что почти оптимально по скорости. Это все, чему можно научиться на нашем примере, но есть три общих момента, о которых всегда должен помнить программист, внося изменения в существующую программу в попытке повысить ее эффективность: Таблица 15.7. Две функции для вычисления синуса суммированием ряда. Вторая функция более эффективна, чем первая 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function sine (х : real) : real; const tolerance - 0.00005; { точность - 5 десятичных знаков } var k : integer, iterm, sum : real; begin { 0.0 <- x and x <- pi} k 0; term x; sum term; while abs (term) > tolerance do begin k Л+1; term - term * sqr(x) I (2*Л*(2*Л+1)>; sum sum + term end; { abs (term) <- tolerance } sine sum end { sine }; function sine (x : real) : real; const tolerance - 0.00005; { точность - 5 десятичных знаков } pi- 3.1415926536; var i: integer, { соответствует 2*k в оригинальной формуле } piminusx, xsquared, term, sum : real; begin { 0.0 <- x and x <- pi} piminusx pi - x; if piminusx < x then x piminusx; { так как sine(x) - sine(pi - x) } xsquared sqr(x); { чтобы потом не вычислять } i0; term x; sum term; while abs(term) > tolerance do begin i i + 2; term - term ♦ xsquared / (i ♦ (i + 1 >); sum sum + term end; { abs (term) <- tolerance } sine sum end { sine }; '
Гл. 15. Эффективность программ 281 Таблица 15.8. Результаты тестов для первой функции sine из таблицы 15.7» показывающие ее неэффективность х = 0.785398 k term sum 0 0.785398 0.785398 1 -0.080745 0.704653 2 0.002490 0.707143 3 -0.000037 0.707106 4 0.000000 0.707107 х = 2.356194 к term sum 0 2.356194 2.356194 1 -2.180127 0.176067 2 0.605165 0.781232 3 -0.079992 0.701240 4 0.006168 0.707408 5 -0.000311 0.707096 6 0.000011 0.707107 7 0.000000 0.707107 а) При модификации программы очень легко внести ошибки; производите изменения очень осторожно. б) Вносимые изменения могут сэкономить существенное время в тех час- тях программы, которые исполняются много раз, так что сосредоточьте ваши усилия на этих частях. в) Ясность важнее эффективности; не делайте вашу программу запутанной ради экономии нескольких миллисекунд или незначительного объема памяти; это может дорого обойтись при изменениях программы в даль- нейшем. В следующих главах мы подробнее обсудим вопросы повышения эффек- тивности, причем акцент будет сделан на применении хороших алгоритмов. Упражнения 15.1. Подсчитайте число исполнений каждой строки следующей процедуры (листинг 15.1>, если из- вестно» что вся процедура исполняется один раз и печатает шесть чисел. Затем найдите» сколько всего раз выполняются (а) операции сложения и (б) операции умножения. Перепишите процедуру» сделав ее более эффективной» но сохранив существующую структуру с циклами пока. ' 15.2. Рассмотрите процедуры на листингах 15.2 и 15.3, каждая из которых должна подсчитывать число слов и число пробелов в строке текста.
282 Гл. 15. Эффективность программ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 procedure specialnumbers; var h, t, и : integer, begin h 0; while h < 10 do begin t0; while t < 0 do begin и 0; while и < 10 do begin if - 100*/t+10*t+u then writeln (100*/t+10*t+u : 10); и u+1 end; tt+1 end; h Л+1 end; end { specialnumbers }; Листинг 15.1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 procedure countlinel (var words, spaces : integer); var notinword : Boolean; ch : char, begin words 0; spaces 0; notinword true; while not eoln do begin read (ch); if ch - ’ ’ then begin spaces spaces + 1; notinword .— true end else if notinword then begin words words + 1; notinword false end end; readln end { countlinel }; Листинг 15.2 а) Строка текста состоит из 70 символов и маркера конца строки. Подсчитайте, сколько всего раз исполняется каждая строка каждой процедуры, если найдено 12 слов и 15 пробелов, причем перед первым словом пробелы не встречаются. В какой процедуре накладные расходы меньше? б) Какие из операторов while в приведенных процедурах можно заменить на операторы repeat*! Для тех операторов, которые заменить нельзя, укажите почему. Если такая замена возмож- на, как она повлияет на эффективность процедуры? 15.3. Проанализируйте, сколько раз исполняется каждая строка следующей программы (листинг 15.4), если известно, что величина, считываемая в строке 19 программы, равна N. Какова временная сложность программы?
Гл. 15. Эффективность программ 283 1 2 3 4 5 6 7 8 9 10 И 12 13 14 15 16 17 18 19 20 21 procedure countline2 (var words, spaces : integer); var ch : char, begin words 0; spaces 0; while {input - ’ ’) and not eoln do begin spaces spaces + 1; read (ch) end; while not eoln do begin words words + 1; while input o’’ do read (ch); while (input - ’ ’) and not eoln do begin spaces :- spaces + 1; read (ch) end end; readln end { countline2 } ; Листинг 15.3 15.4. Для вычисления натурального логарифма loge(x) от любого строго положительного х может использоваться следующий ряд: 1 х-1 1 /г-1¥ 1 /\-1 | - loge(x) - ------ + -I-----1 + -|----1 + ... 2 хН 3ух+1 J 5 у v-Ы J Напишите функцию с заголовком function loge (х : real) : real; которая вычисляет логарифмы по этому методу с точностью до пяти десятичных знаков. Напишите также программу TestLog (ПроверкаЛогарифма) для тестирования вашей функции путем сравнения ее результатов со значениями, вычисленными встроенной функцией In. 15.5. На листинге 15.5 приведена функция, которая вычисляет величину f для значений х в диапазоне от -2.0 до 2.0. Она суммирует ряд, пока не будет достигнут результат. Функция работает правильно при всех значениях х, кроме одного, но крайне неэффективна. а) Запишите формулу для ряда, который вычисляет функция /. б) Для какой величины х функция не сработает, и в какой точке программы произойдет отказ? в) Перепишите функцию так, чтобы она правильно работала для всех х из указанного диапазо- на и производила вычисления более эффективно. 15.6. Ряд 1 + 1/2 + 1/3 + ... называется гармоническим, a H(N) обозначает сумму первых N членов. Например: H(N) -1 + 1/2+ 1/3 + ... + Y/N . Напишите программу под названием Harmonic, которая считывает величину М, а затем вычисляет и табулирует величины H(N) для N в диапазоне от 1 до М. Каждое значение H(N) должно быть записано в виде рациональной дроби, т.е. в виде: числитель / знаменатель где числитель и знаменатель - целые числа без общих множителей. Например, при М - 4 резу- льтаты выглядят так: Я(1) - 1/1 Я(2) - 3/2 Ж31-11/6 Я(4) - 25/12 Протестируйте программу сначала на маленькой величине М, например 4, затем на большей величине М, например 50.
284 Гл. 15. Эффективность программ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 *29 30 31 32 33 34 35 program Table Unput, output); procedure writeline (first, last: integer); var factor: integer, begin factor first, write (first ♦ first: 8 ♦ first); while first < last do begin firstfirst + 1; write (first ♦ factor : 8) end; writeln end { writeline}; procedure writetable; var size, i: integer, begin readln (size)', write(' ’); i0; while i < size do begin ii+ 1; write (i: 8) end; writeln; writeln; i0; while i < size do begin /:-/+!; write (i: 2); writeline (i, size) end end { writetable }; begin writeln (’♦*♦♦ РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ ТАБУЛЯЦИЯ ♦♦♦•); writeln; writetable; writeln; writeln (’♦*♦♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦*♦♦’) end { Table}. Листинг 15.4. Текст программы Табуляция 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function f (х : real) : real; const tolerance - 0.00001; var v. integer, sum, term : real; begin i0; term (1 - x) I 2.0; sum term; while abs (term/sum) > tolerance do Замечание. Функция power возводит величину, указанную первым парамет- ром, в степень, указанную вторым пара- метром, и возвращает результат типа real; функция factorial вычисляет фак- ториал от своего параметра и возвра- щает результат типа real. begin i:-i+ 1; term power (-1, i) * power (1 - x, 2*i + 1)/ (2.0 ♦ factorial (2*i + 1)); sum sum + term end; /sum end {f}; Листинг 15.5
16 НОВЫЕ ТИПЫ ДАННЫХ Мысль о том, что элемент информации связан с некоторым типом данных, возникла, когда мы впервые встретились с переменной типа integer. Затем мы познакомились с другими типами данных: char, Boolean и real. Теперь тот или иной элемент информации вы должны уметь отнести к одному из этих типов данных, например: элемент информации тип данных 43716 integer * char false Boolean -27.6 real В повседневной жизни мы пользуемся множеством слов, которые позволяют описывать совокупности объектов и фактически выполняют роль типов данных. Вот несколько примеров объектов и соответствующих им типов данных: элемент информации тип данных 1991 Ъ почему 20:45 год заглавная буква слово время суток Эти типы данных относительно просты, но и более сложным совокупностям можно давать имена. Абзац текста, автобусное расписание, календарь, телефонный справочник могут рассматриваться как типы данных. Разрабатывая программу, полезно вводить тип данных, подходящий для решаемой задачи. В стандарте Паскаля такой тип данных называется новый тип. Его формальный синтаксис показан на рис. 16.1. В данной главе впервые обсуж- дается, как определяются новые типы. Мы рассмотрим следующие типы: диапа- зоны, массивы и записи. 16.1. Определение типа Итак, нам нужно определить новый тип данных. В стандартном Паскале это можно сделать двумя разными способами, которые мы будем называть явным определением типа и неявным определением типа. Явное определение типа описывает новый тип данных и дает ему имя. Таким способом тип задается в разделе определения типов блока.
286 Гл. 16. Новые типы данных Рис. 16.1а. Новые типы данных новый-тип тип-диапазон новый-структурный-тип тип-массив тип-индекса тип-запись список-полей секция-записи тип-компонента - тип-диапазон | новый-структурный-тип . - константа константа . - [ "packed” ] (тип-массив | тип-запись) . - "array” ”[” тип-индекса { тип-индекса } "1" "of” тип-компонента. - тип-диапазон | имя-порядкового-типа. (см. примечание) - "record” список-полей "end” - секция-записи { секция-записи } [ . - идентификатор { идентификатор] тип-компонента. - имя-типа | новый-тип . Примечание. Порядковыми типами являются Boolean, char, integer и любой диапазон1. Использовать тип integer в качестве типа индекса на практике не имеет смысла из-за ограничений объема памяти для программы. Рис. 16.16. Новые типы данных Например, следующая последовательность строк образует раздел определения типов и определяет два новых типа данных, названных yeartype и capitallettertype'. type yeartype " 1900..2099; { тип ’’год” } capitalletter e ’A’..’Z’; { тип ’’заглавная буква” } Если новый тип данных определен таким образом, его имя затем можно упо- треблять внутри данного блока везде, где это синтаксически допустимо, например, в разделе описания переменных: 1 Это не все порядковые типы, имеющиеся в стандартном Паскале. - Примеч. ред.
Гл. 16. Новые типы данных 287 var thisyear, nextyear: yeartype', firstletter : capitallettertype\ Следующий заголовок функции: function yearcode (anyyear : yeartype) : capitallettertype’, также является правильным. Мы будем придерживаться практики именования всех новых типов дан- ных идентификаторами, завершающимися словом type. В стандартном Паскале это не обязательно, и значит, не обязательно для вас. Мы следуем этой практи- ке потому, что так удается избежать путаницы между идентификаторами, обо- значающими переменные, и идентификаторами, обозначающими новые типы данных. Подобно другим разделам, раздел определения типов должен занимать определенное место внутри блока, а именно: после раздела определения конс- тант и перед разделом описания переменных (см. рис. 6.15, с. 115-116). Такой порядок позволяет вновь определенные константы использовать в определениях типа и вновь определенные типы использовать в последующих описаниях переменных, процедур и функций. Теперь обратимся к неявному определению типа. Неявное определение типа описывает новый тип данных не именуя его. Обычно неявное определе- ние типа происходит внутри описания переменных. Например, в следующих строках var thisyear, nextyear : 1900..2099; firstletter : ’A’./Z’; описываются переменные thisyear, nextyear и firstletter и неявно определяются два новых типа данных. Поскольку новому типу данных, определенному таким спо- собом, имя не дается, ниоткуда в программе на него сослаться нельзя. Мы увидим позже, что неявные определения типа могут также встречаться внутри определений структурных типов данных. Неявное определение типа в стандартном Паскале не допускается в следу- ющих двух местах, где оно могло бы показаться уместным. Во всякой специфи- кации параметра и в описании результата функции тип данных всегда должен быть именем типа и никогда - неявным определением нового типа (см. рис. 11.1, с. 200-201, и рис. 6.15, с. 115-116). Например, в выражении function yearcode (anyyear : 1900..2099) : ’A’./Z’; как тип параметра, так и тип результата заданы неправильно. 16.2. Диапазоны Выше, в разд. 16.1 нам дважды встретился простейший из новых типов данных: yeartype = 1900..2099; capitallettertype = ’ А’..’ Z’; Каждый из типов в этих определениях является типом диапазона', поскольку задан диапазоном значений некоторого ранее определенного типа данных. Этот 1 Или диапазонными типами. - Примеч. ред.
288 Гл. 16. Новые типы данных исходный тип данных называется базовым типом и может быть любым простым типом данных, кроме типа real. Диапазон задается двумя константами базового типа, разделенными символами Первая константа определяет нижнюю границу диапазона и должна не превосходить второй константы, определяющей верхнюю границу диапазона. Часто вместо значений используются имена констант, определенные в предшествующем разделе определения констант. В приведенных примерах для нового типа yeartype базовым является тип integer. Любое целое число в диапазоне от 1900 до 2099 является, таким образом, значением типа yeartype, а также значением типа integer. Подобно этому, базовым для capitallettertype является тип char, и capitallettertypt включа- ет все символы от ’Л9 до ’Z’ из набора символов, обрабатываемых процессором. Кроме диапазона значений, всякий тип данных должен обладать набором операций. Для диапазона допустимыми являются те же самые операции, что и для базового типа. Далее, значение диапазонного типа допустимо в качестве фактического параметра любой встроенной функции, которая допускает значе- ние соответствующего базового типа. Чтобы проиллюстрировать применение и некоторые преимущества типов диапазона, рассмотрим вариант программы ТаблицаСтепеней, которую мы раз- рабатывали в гл. 11. Видоизмененная программа называется ТаблицаСтепеней2 (см. листинг 16.1). За исключением способа, которым описаны переменные и заданы параметры, она идентична исходной программе. В ней мы находим три типа диапазона, два из которых определены явно, третий - неявно. Типы данных naturalnumbertype (тип ’’натуральное число”) и positivenumbertype (тип ’’положительное число”) определены явно в разделе определения типов в строках 5-7. Единственное отличие между ними заключает- ся в том, что диапазон naturalnumbertype включает значение нуль, тогда как positivenumbertype - нет. Тип naturalnumbertype упоминается: в строке 10, как тип параметра power, в строке 34, как тип переменной к. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 program TabulatePowers2 (input, output); { вводит число к и строит таблицу значений х в степени к для х-1,2,3 ...} type naturalnumbertype - 0..maxint; positivenumbertype - 1.. maxint; procedure calculatepower (number, positivenumbertype; power, naturalnumbertype; var answer, positivenumbertype; var OK: Boolean); { посылает в answer значение number*power и OK присваивает true, если вычисление степени возможно, в противном случае присваивает OK false } var limit: positivenumbertype; begin { number>Q and power>-0 } answer 1; limitmaxint div number, while (power <> 0) and (answer <- limit) do begin answer answer*number, power power-1 end; {power-0 or answer>limit} OK power - 0 end {calculate power};
Гл. 16. Новые типы данных 289 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 procedure processonetabulation; const maximum - 50; var x : 0.. maximum; к : naiuralnumbertype; xpowerk : positivenumbertype; successful: Boolean; begin readin U>; writeln (’X* ; 20,’JT’ : 14, к : 1); writeln; x 0; successfultrue; while (x <> maximum) and successful do begin x x + 1; calculatepower (x, k, xpowerk, successful) if successful then writeln (x : 20, xpowerk : 15) end { (x - maximum) or not successful} end { processonetabulation }; begin writeln (’**** РЕЗУЛЬТАТЫ ПРОГРАММЫ ТАБЛИЦА СТЕПЕНЕЙ writeln; processonetabulation; writeln; writeln (’♦♦♦♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦♦’) end { TabulatePowers }. Листинг 16.1. Версия программы ТаблицаСтепеней, в которой используются типы диапазона Тип positivenumbertype употребляется: в строке 9, как тип параметра number, в строке 11, как тип параметра answer, в строке 18, как тип переменной limit, в строке 35, как тип переменной xpowerk,. Неявное определение диапазона встречается в строке 33, где тип перемен- ной х определен как 0..maximum. Так как maximum - константа, которой в стро- ке 31 установлено значение 50, х попадает в диапазон 0..50. В этом случае явное определение нового типа данных неоправданно, так как он используется только для описания х. Итак, мы видим, что ТаблицаСтепеней2 по сравнению с исходной про- граммой усложнена. Что же мы выгадали, применяя диапазоны? Наиболее важ- ным преимуществом наличия в языке программирования типов диапазона яв- ляется то, что они позволяют процессору обнаруживать некоторые ошибки, которые в противном случае установить было бы нельзя. Стоит только при описании величины заключить ее в диапазон, как любое событие, заставляющее ее выйти из этого диапазона, будет зафиксировано как ошибка. Иногда такая ошибка может быть обнаружена при компиляции программы или в процессе ее выполнения. По существу, программист использует типы диапазона для того, чтобы точно определить, какие значения могут появиться ’’законно”, а связанные с этим хлопоты окупаются тем, что любое недопустимое значение будет с гарантией обнаружено. Таким образом, в руках у программиста оказыва- ется мощное средство контроля. Можно выявить по крайней мере три преиму- щества применения диапазонных типов в программе ТаблицаСтепеней!. Во-первых, заметим, что процедура calculatepower имеет предусловие 10 Зак;п№1110
290 Гл 16. Новые типы данных { number > 0 and power >-0 } которое включается в исходную версию как предупреждение всякому, кто захо- тел бы переместить процедуру в другую программу. Если условие не выполнено, процедура не работает. В модифицированной программе применение диапазон- ных типов в спецификациях параметров делает невозможным вызов данной про- цедуры с неподходящими параметрами. Любая попытка такого вызова будет об- наружена процессором. Таким образом, включая в программы диапазонные типы, можно сделать программы более устойчивыми. Некоторое неудобство состоит в том, что если такая процедура переносится в другую программу, то вместе с ней нужно перенести все определения типов данных, используемых ею. Во-вторых, спецификация программы ТаблицаСтепеней позволяла нам допустить, что данные (а именно значение к) всегда корректны. Однако если исходная программа случайно исполняется с отрицательным к, цикл в процедуре calculatepower не завершится, и будет потеряна масса машинного времени. Что происходит в новом варианте программы? Поскольку переменная к описывается диапазоном naturalnumber, любая попытка ввести в к отрицательное число вызо- вет ошибку во время исполнения программы. Таким образом, использование диапазонных типов может обеспечить "ленивую” проверку данных. Третье преимущество носит наиболее тонкий характер. Вы уже убедились, конечно, что некоторые ошибки закрадываются в ваши программы на заключи- тельных этапах их разработки перед самой передачей процессору. В нашем случае, например, строка 41 легко могла бы превратиться в while (х <=“ maximum) and successful do и возникший нелад трудно было бы заметить даже после просмотра большого числа тестовых результатов: он приводит всего лишь к получению одной лишней строки результатов для маленьких величин к. Если же точно определен диапа- зон, в котором должен лежать х, как это сделано в новом варианте программы, то выполнение оператора х :в х + 1 в строке 43 вызовет ошибку исполнения, когда х уже равен maximum. Таким об- разом, применение диапазонных типов помогает обнаружить случайно вкрав- шиеся в программу нелады. Итак, диапазонные типы являются ценным инструментом программирова- ния, без которого не может обойтись никакой хороший язык и которым должен широко пользоваться всякий грамотный программист. 16.3. Структурные типы Во всех программах, которые мы изучали или составляли до сих пор, тре- бовалось хранить только очень простые элементы данных. Теперь наша задача научиться строить более крупные структуры данных, чтобы хранить и обрабаты- вать в программах сложные виды информации. Такие структуры данных образу- ются путем объединения простых элементов данных и называются структурными типами. Инженер, создающий сложное оборудование, обычно называет его состав- ные части компонентами. Мы будем употреблять тот же термин для обозначения частей структуры данных. Компоненты, соединяемые при создании большого объекта, могут быть как однородными, подобно звеньям металлической цепочки, или же разнородными, подобно элементам электрической цепи.
Гл. 16. Новые типы данных 291 В стандартном Паскале структурный тип данных, в котором все компонен- ты относятся к одному типу, например, ’пятьдесят букв’, называется типом- массивов. Если же компоненты принадлежат к различным типам (например, ’двадцать символов, два целых и одно вещественное число’), то эта структура относится к типу, называемому типом-записью1 2-. Переменная, которая отнесена к типу данных ’’массив”, есть переменная-массив. Переменная, которая отнесена к типу данных ’’запись”, есть переменная-запись. Обычно их просто называют массивом и записью. 16.4. Одномерные массивы Простейший массив похож на цепочку. Он состоит из фиксированного чис- ла компонентов, следующих друг за другом, и называется одномерным масси- вом. Как во всяком массиве, его компоненты относятся к одному типу данных. Этот тип данных называют типом компонента. В некоторых случаях нам приходится пользоваться бланками, в которых информация должна записываться в маленькие клеточки. Так, для имени может быть отведена следующая форма: NAME и каждая буква помещается в отдельную клеточку: NAME Я Р О С л А В Заполнив такую форму, мы записали имя в одномерный массив, имеющий две- надцать компонентов, каждый из которых есть буква. Если создать подобную структуру данных в оперативной памяти компьютера, то можно будет хранить и обрабатывать информационные объекты размером до 12 символов. Значительная часть операций над хранимой в массиве информацией пред- полагает доступ к отдельному компоненту. Чтобы иметь возможность указать на любой компонент, нам потребуется индекс, который действует подобно указательному пальцу. Один из способов связать индексы с компонентами массива - просто пронумеровать их. Например, компоненты нашего массива можно, пронумеровать следующим образом: 1 2 3 4 5 6 7 8 9 10 11 12 NAME В данном случае индексы лежат в диапазоне 1..12. Так как число компонентов в одномерном массиве постоянно, индекс, а тем самым размер массива, можно определить с помошью типа данных, имеющего постоянный диапазон значений3. Обычно для этого используется диапазонный тип. Тип данных, определяющий размер массива, называется типом индекса. 1 Или типом массива. - Примеч. ред. 2 Или типом записи. - Примеч. ред. 3 Думается, не совсем точно сказано. Для индексирования массива требуется некоторое упорядоченное множество значений (в Паскале - порядковый тип, см. рис. 16.16 и примечание к нему, с. 286). - Примеч. ред. 10*
292 Гл. 16. Новые типы данных 16.4.1. Определение типа массива и описание переменных Чтобы использовать массив в программе на стандартном Паскале, необхо- димо сделать две вещи, два отдельных шага. Во-первых, определить параметры массива, а именно тип его индекса и тип компонента, иными словами, опреде- лить тип-массив. Во-вторых, описать переменную, имеющую этот тип в качестве типа данных, иными словами, описать переменную-массив. Осуществить это можно двумя способами. Первый - явно определить но- вый тип данных в разделе определения типов блока программы, а затем описать переменную в разделе описания переменных того же или вложенного блока. На- пример, в следующей последовательности строк определяется тип-массив с име- нем wordtype и описывается переменная с именем паше, имеющая тип wordtype'. type wordtype - array [1..12] of char; var name : wordtype; Тип индекса в определении типа заключается в квадратные скобки после служе- бного слова array, а тип компонента задается после служебного слова of. Синта- ксис для типа массива приведен на рис. 16.1. В нашем последнем примере 1..12 - тип индекса, a char - тип компонента. Таким образом, wordtype - структурный тип данных, состоящий из двенадцати компонентов, пронумерованных от 1 до 12, каждый из которых способен хранить один символ. Выше мы обсуждали структуру именно этого типа. Поскольку в описании переменных name имеет тип wordtype, name является переменной-массивом с той же самой структурой. Второй способ сделать указанные два шага намного короче. Тип-массив определяется неявно вместе с описанием переменной. Вот как можно описать переменную-массив под именем name'. var name : array [1 ..12] of char; Здесь новый тип данных, array [1..12] of char, при определении не снабжен именем. В предыдущих примерах тип индекса определялся как диапазон 1..12 в рамках определения типа-массива. Часто бывает удобно явно определить тип индекса через диапазон, дав ему подходящее имя, с тем чтобы можно было упо- треблять его и в других местах программы. Типична такая последовательность определений: const maxwordindex =12; type wordindextype = \..maxwordindex; wordtype = array [wordindextype] of char; Способ явного определения типа индекса имеет следующие преимущества: (а) имя константы maxwordindex и два имени типов wordindextype и wordtype доступны для использования всюду в пределах блока; (б) размер массива может быть изменен просто переопределением значе- ния константы. Отметим, что идентификатор типа данных должен быть определен до того, как он используется в определении другого типа данных. Так, в последнем примере wordindextype определяется до wordtype. Разумеется, тип индекса массива не обязательно диапазон 1..12, а тип компонента - не обязательно char. В стандартном Паскале типом индекса мо-
Гл. 16. Новые типы данных 293 жет быть любой тип диапазона или любой другой простой тип данных, кро- ме типа real, а типом компонента - вообще любой тип данных. Оба этих типа могут быть также определены с помошью имени предопределенного типа, имени ранее определенного типа или же неявным образом. Например, правильными являются следующие описания: array [-10..10] of real определяющее структуру из 21 компонента, пронумерованных от -10 до 10, каждый из которых способен хранить одно вещественное число; array [’А’..’Е’] of 0..1000 определяющее структуру всего из пяти компонентов, индексированных симво- лами от А до Е, для хранения значений, ограниченных диапазоном 0..1000; array [char] of Boolean определяющее структуру, в которой столько компонентов, сколько символов в наборе символов, и каждый компонент может хранить одно значение булевского типа. В принципе допустимо определение типа-массива с индексом типа integer, но это абсолютно бесполезно, так как переменная этого типа будет содержать слишком большое число компонентов и память компьютера сразу переполнится. 16.4.2. Индексированные переменные Теперь, когда мы можем определять типы массивов и описывать перемен- ные массива, у нас есть способ хранения любой совокупности, представляемой в виде последовательности значений одного типа данных. Чтобы научиться мани- пулировать такими данными, разберем программу Заголовок, специфицирован- ную в табл. 16.1 и приведенную на листинге 16.2. По своему назначению эта программа подобна программе СловоВРамке из гл. 7: она должна прочесть неко- торые данные и вывести их в рамку, сформированную из звездочек. Данные для чтения - заголовок длиной до 46 символов, ширина рамки - 50 символов. Таблица 16.1. Спецификация программы Заголовок Напишите на Паскале программу под названием Заголовок (HeadLiner), которая читала бы фразу длиной до 46 символов и выводила бы его в центр рамки, образованной звездочками. Входные данные Одна строка текста длиной до 46 символов. Например: МИКРОКОМПЬЮТЕРЫ ОПЯТЬ ДЕШЕВЕЮТ Результаты Пять строк из 50 символов на каждой строке, как например: ***tHV*******************tHV*********** * * * МИКРОКОМПЬЮТЕРЫ ОПЯТЬ ДЕШЕВЕЮТ * * * Заголовок должен быть выведен приблизительно в центре рамки. Очень важна в спецификации последняя строка. В ней говорится, что заголовок должен быть выведен ’’приблизительно в центре рамки”. Чтобы сделать это,
294 Гл. 16. Новые типы данных 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 program HeadLiner (input, output); { читает заголовок и выводит его в рамку из звездочек } procedure processoneline; const maxlineindex - 47; var headline', array ( \ ..maxlineindex) of char, i: \..maxlineindex; length, spaces : 0..maxlineindex; begin { прочитать и запомнить заголовок } i 1; while not eoln and (i <> maxlineindex) do begin read (head line [i)); i j+1 end; { eoln or i - maxlineindex } length i - 1; readln; { вывестичасть рамки до заголовка } . , , , . AАсАсАсArAf Аг A* А* А* Аг Аг Aг Аг A* A* Aг Аг Aг Aг Аг Aг А* А АгАгАгАгАгАгАгАгАгАг А*АгАгА" А А* А АгАгАгАгАгАгАг^ • writeln С* *’>; spaces (maxlineindex - length - 1) div 2; write : spaces); { вывести заголовок } i1; while i <- length do begin write (headlined)); i i + 1 end; { вывести часть рамки после заголовка } if odd (length) then spaces spaces + 1; writeln : spaces, writeln (’* *’); Writeln (’ *1^******1*n*n*r★★★★★★ ★★★★★★★ ’) • end { processoneline }; begin writeln (’**** ВЫВОД ИЗ ПРОГРАММЫ ЗАГОЛОВОК ****’); writeln; processoneline; writeln; writeln (’**** КОНЕЦ ВЫВОДА ****’) end { HeadLiner }. Листинг 16.2. Текст программы Заголовок, использующей массив процессору нужно посчитать, сколько выдать пробелов перед первым символом заголовка, для чего, в свою очередь, должна быть известна его длина. Поэтому заголовок нужно ввести и запомнить, с тем чтобы найти его длину до того, как будет выведен первый символ. Возможен следующий алгоритм решения задачи:
Гл. 16. Новые типы данных 295 begin ввести и запомнить заголовок вывести часть рамки до заголовка вывести заголовок вывести часть рамки после заголовка end Теперь давайте обратимся к программе, в которой этот алгоритм реализу- ется процедурой processoneline, и рассмотрим некоторые детали. Начнем со строки 9, в которой мы находим описание переменной headline-, array [1..maxlineindex] of char где maxlineindex - константа, определенная в строке 7 и равная 47. Переменная headline, следовательно, есть переменная-массив из 47 компо- нентов типа char. Таким образом, переменная содержит на один символ больше, чем требуется для хранения самого длинного разрешенного заголовка. Сейчас мы увидим, почему желателен дополнительный элемент. В строках 10 и 11 описываются три неструктурные переменные - i, length и spaces, каждая из которых принадлежит к типу диапазона. Эти переменные используются следующим образом: i служит индексом компонентов массива headline, length хранит длину заголовка, spaces хранит число пробелов, выводимых перед заголовком в третьей строке результатов. Определенные диапазоны таковы, что i должно всегда принадлежать диапазону 1..47 - тому же самому, что и тип индекса массива, но length и spaces могут, кроме того, иметь значение нуль. Теперь посмотрим на строки 17 и 32. В обеих вы найдете выражение headline [£] называемое индексированной переменной и обозначающее один отдельный ком- понент массива headline. Например, если переменная i имеет значение 12, тогда headline [i] есть компонент с индексом 12. Некоторым читателям понятие массива покажется естественным, однако другие могут найти его необычным. Для лучшего осознания, что такое массив, в табл. 16.2 приводится детальная трассировка, в которой демонстрируется выполнение процедуры processoneline для заголовка длиной пять символов. Как и для любой другой процедуры, локальные переменные создаются при входе в processoneline, и одна из них, headline, есть переменная типа массив. Она эквивалентна 47 символьным переменным, а потому должна быть представ- лена в трассировке 47 колонками. Однако в данном случае используются только пять компонентов массива, поэтому компоненты 6..47 представляются колонкой, озаглавленной ’’etc.” (прочие). Начальное значение для каждого компонента массива и значения трех других переменных неопределены. На первом шаге алгоритма заголовок читается и запоминается, и верхняя часть трассировки показывает, каким образом это делается. Величина i, будучи инициализированной значением один в строке 14, после каждого оператора вво- да увеличивается на единицу в строке 17. Поэтому каждый очередной символ, вводимый из входного файла, попадает в следующую компоненту массива. Обратите внимание на одну деталь, связанную с индексом i. После выпол- нения строки 17 i всегда указывает номер компонента, в котором будет сохранен следующий символ. Следовательно, если в заголовке 46 символов, последнее значение i будет 47, и потому диапазон значений i должен быть 1..47, а не 1..46. Это, в свою очередь, объясняет, почему тип индекса массива также 1..47. При
296 Гл, 16. Новые типы данных определении массива и индексирующих его переменных всегда проще и понятнее иметь для индекса массива и этих переменных одинаковый тип данных, хотя это часто подразумевает по крайней мере один дополнительный компонент в массиве. На втором шаге алгоритма выводится рамка, содержащая заголовок. После того как выданы первые две строки рамки, в строке 25 вычисляется количество пробелов, необходимых перед заголовком, и в строке 26 эти пробелы выводятся с предшествующей им. звездочкой. Далее следует цикл, в котором последователь- ным изменением величины i от 1 до 5, осуществляется доступ к компонентам от headline [1] по headline [5] и выводятся хранящиеся в них значения. Цикл заве- ршается, когда i становится равным 6. Остальные операторы достраивают рамку. Таблица 16.2. Трассировка программы Заголовок Входные данные БДИ!! Трассировка headline строка ход выполнения 1 2 3 4 5 etc. Z length spaces 12 вход в processoneline ?????? ? •> О 14 1 15 (not eoln etc.) - true 17 ’Б’ 2 15 (not eoln etc.) - true 17 ’Д’ 3 15 (not eoln etc.) - true 17 ’И’ 4 15 (not eoln etc.) - true 17 5 15 (not eoln etc.) - true 17 ч» 6 15 (not eoln etc.) - false 20 5 23 вывод : ******** , e , 24 вывод : * ... 25 20 26 вывод : * ... 29 1 30 (Z <- length) - true 32 вывод: Б 2 30 (Z <- length) - true 32 вывод: Д 3 30 (Z <- length) - true 32 вывод: И 4 30 (Z <- length) - true 32 вывод: ! 5 30 (Z <- length) - true 32 вывод: ! 6 30 (Z <- length) - false 36 odd (length) - true 21 37 вывод: J_A7\S4AA A AA A 38 вывод: Л ... 39 вывод* ********** ... 40 выход из processoneline
Гл. 16. Новые типы данных 297 Результаты (из полной программы) **** ВЫВОД ИЗ ПРОГРАММЫ ЗАГОЛОВОК **** БДИ!! **** КОНЕЦ ВЫВОДА **** В программе Заголовок и в обсуждении индексированных переменных до сих пор фигурировали очень простые индексированные переменные. Общий фор- мат для связанной с одномерным массивом индексированной переменной - имя переменной массива, за которым следует выражение в квадратных скобках. Оно называется индексным выражением, а его значение должно лежать в преде- лах, указанных типом индекса массива. Соответствующий синтаксис показан на рис. 6.4. Тип данных индексированной переменной в одномерном массиве есть тип компонента массива, и индексированная переменная может быть употреблена всюду, где переменная такого типа корректна. Например, при условии, что величины i и length таковы, что все значения различных индексных выражений лежат в диапазоне 1..47, допустимо каждое из следующих выражений, включающее массив headline-. headline [1] := ’Т’; ch :e headline [z]; if headline [i - 1] = ... then ...; while headline [i + 1] <> do ...; headline [z] := headline {length - i + 1]; Рассмотрим массив count count: array [char] of integer; Его индексное выражение должно быть типа char, и индексированная перемен- ная обозначает величину типа integer. Таким образом, допустимо каждое из следующих выражений: count [сА] := count [ch]+1; while county А’] < count [’ ’] do ...; writein (’There are ’, count [ch (127)] : 1, ’ DEL characters’); где ch есть переменная типа char, а встроенная функция chr возвращает символ, порядковый номер которого указан параметром функции. В том случае, когда значение индексного выражения лежит вне типа инде- кса массива, говорят, что оно выходит за границы массива. При выполнении оператора, содержащего такое выражение, процессор попытается обратиться к несуществующему компоненту массива. В стандартном Паскале такое событие вызывает ошибку исполнения. Она называется выходом за границы массива, а автоматический контроль, ее обнаруживающий, есть контроль границ массива. Хотя проверка границ увеличивает время доступа к компоненту массива, она является превосходной гарантией от ошибок в программах. В некоторых нестандартных Паскаль-системах, а также во многих других системах программирования автоматической проверки границ массива не произ-
298 Гл. 16. Новые типы данных водится. Часто в системе имеется специальный ’’флажок°, который может при- вести в действие такую проверку, но иногда и это невозможно. При выполнении оператора, в котором нарушаются границы массива, происходит обращение к некоторой ячейке памяти вне массива. Ее содержимое, независимо от того, что оно собой представляет, будет либо извлечено и использовано, либо затерто, так что выход за границы массива, если он не обнаруживается, таит в себе потенци- альную опасность. Таким образом, если вам когда-нибудь придется иметь дело с системой программирования, в которой проверка границ массива не делается автоматически, но может быть ’’включена”, советуем ею пользоваться. Отметим, что в стандартном Паскале есть возможность присвоить зна- чение всего массива другой переменной того же самого типа массива с помо- щью единственного оператора присваивания. Например, если массивы linel и Ипе2 описаны так, как показано ниже: type linetype^ array [1..100] of char, var linel, line2 : linetype', то оператор присваивания Une2 := linel перешлет все значения с line[l] по Zzne7[lOO] в компоненты с Zzne2[l] по Ипе2 [100], оставляя linel без изменений. Нельзя, однако, вводить значения в массив ’’одним махом” - при помощи оператора read с переменной массива в ка- честве параметра. За исключением специального случая упакованного массива символов (см. гл. 18) не может употребляться и оператор write с переменной типа массив в качестве параметра с целью вывода всех значений массива. 16.5. Оператор цикла с шагом for В главе о новых типах данных это может показаться не совсем уместным, но именно теперь удобно ввести новый вид оператора - оператор цикла с шагом. или оператор for. Оператор for, подобно операторам while и repeat, является оператором цикла, а обращение к нему в этом месте оправдывается тем, что он часто используется в связи с массивами. Формальный синтаксис оператора for приведен на рис. 6.10 (с. 109-110). В определение оператора for входит другой оператор, называемый телом оператора for, который должен быть выполнен один раз для каждого значения некоторой переменной из заданной последовательности значений. Эта перемен- ная называется управляющей переменной. Существуют две формы оператора, одна из которых основана на возрастающей последовательности значений, а другая - на убывающей последовательности. Так, оператор for count := 1 to 8 do begin read (ch); write (ch) end или для count :e 1 до 8 выполнить begin read (ch); write (ch) end
Гл. 16. Новые типы данных 299 изменяет значение управляющей переменной count от 1 до 8 путем добавления на каждом шаге по 1 и, следовательно, выполняет тело оператора 8 раз, читая и выводя 8 символов. А оператор for ch :» ’Z’ downto ’A’ do write (ch) присваивает управляющей переменной ch одну за другой буквы алфавита в обратном порядке и, таким образом, выводит следующую совокупность букв: ZYXWVUTSRQPONMLKJIHGFEDCBA На употребление оператора цикла с шагом накладывается ряд ограниче- ний, а именно: а) Управляющая переменная должна быть описана локально в блоке, где встречается оператор for, и ее тип данных должен быть одним из прос- тых типов данных, за исключением типа real. б) Тип выражений, определяющих начало и конец последовательности значений и называемых соответственно начальным и конечным значени- ями^ должен быть совместим с типом данных управляющей переменной. Например, начальное и конечное значения типа integer не будут совместимыми с управляющей переменной типа char. в) По завершении выполнения оператора for значение управляющей пере- менной не определено. г) Переменная, используемая в некотором блоке в качестве управляющей переменной оператора for, не должна появляться внутри этого операто- ра или внутри любой подпрограммы, описанной в этом же блоке, ни в одном из следующих качеств: переменной, расположенной в левой части оператора присваивания фактического параметра процедуры read или процедуры readin или фактического параметра любой другой процедуры, передаваемо- го по ссылке; управляющей переменной другого оператора for. Существенная разница между оператором цикла с шагом и другими операторами цикла заключается в том, что число повторений тела цикла определяется сразу при входе в оператор. Цель ограничения (г) состоит в том, чтобы защитить управляющую переменную от изменений, гарантируя таким образом выполне- ние тела правильное число раз. Какой же выбрать оператор при необходимости организовать цикл? Алго- ритм выбора можно представить следующим образом: если число повторений тела цикла известно при входе в оператор цикла то используйте оператор for иначе если тело цикла будет выполнено по крайней мере один раз то используйте оператор repeat иначе используйте оператор while. Рассмотрим для примера два цикла в программе Заголовок. Первый цикл, в строках 14-18, выглядит так: while not eoln and (i <> maxlineindex) do begin read (headline [z]); i := i + 1 end. Он может быть записан более компактно:
300 Гл 16. Новые типы данных for i:- 1 to length do write (headline [/]) Подобно исходному, этот исправленный оператор работает правильно даже тогда, когда length имеет значение нуль, так как тело оператора цикла с шагом не выполняется, если конечное значение меньше начального в случае возраста- ющей последовательности или если конечное значение больше начального в случае убывающей последовательности. Во всех последующих программах для каждого цикла мы будем использо- вать наиболее подходящий оператор цикла. В них мы встретимся с новыми примерами использования оператора for. 16.6. Записи Запись есть структура данных, построенная из компонент, имеющих в общем случае разные типы данных. Каждый компонент, следовательно, может иметь свой собственный тип. Например, чтобы хранить в компьютере время суток, например, 10.45 а.т. или 7.15 р.т.1, можно использовать структуру данных следующего формата: часы минуты до полудня в которой 10.45 а.т. может храниться как часы минуты до полудня 10 45 true а 7.15 р.т. как часы минуты до полудня 7 15 false Эта структура является записью с тремя компонентами. Компонент записи называется также полем и обозначается идентификато- ром - именем поля. В данном примере ’’часы”, ’’минуты” и ”до полудня” явля- ются именами полей. Поле ’’часы” должно быть типа 1..12, поле ’’минуты” - типа 0..59 и поле ”до полудня” - булевского типа. 16.6.1. Определение типа запись и описание переменных Как и в случае с массивом, прежде чем использовать запись в программе на стандартном Паскале, надо сделать две вещи: определить тип-запись и описать переменную-запись. Эти действия можно выполнить порознь в разделе определения типов и в разделе описания переменных того же или вложенного блока. С другой стороны, можно определить тип вместе с описанием переменной - в этом случае новый тип записи определяется неявно. 1 а.т. - до полудня, р.т. - после полудня (лат.). - Примеч. пер.
Гл. 16. Новые типы данных 301 Так, например, если тип записи, которую мы использовали в приведенном примере, назвать timetype (тип ’’время”), можно будет определить новый тип записи и описать две переменные этого типа с именами now (’’теперь”) и later (’’потом”) следующим образом: type timetype - record hour : 1..12; minute : 0..59; beforenoon : Boolean end; var now, later : timetype', Можно описать те же переменные по-другому: var now, later : record hour : 1..12; minute : 0..59; beforenoon : Boolean end; В этом варианте новый тип данных также определяется, но не получает имени. В определении типа записи после служебного слова record следует список полей. состоящий из одного или нескольких элементов, называемых секциями записи. Секция записи определяет одно или несколько имен полей и тип соот- ветствующей компоненты. Если в секции перечисляются несколько имен полей, то все они однотипны. Tun компонента в секции записи может быть любым типом данных и быть именем предопределенного типа, именем ранее опреде- ленного типа или неявным определением нового типа данных. Если секций за- писи больше одной, они разделяются точкой с запятой, а вся последовательность завершается служебным словом end. Заметим, что после слова record нет слова begin. Формальный синтаксис показан на рис. 16.1. В следующих примерах при- водятся корректные определения типа-записи: record х, у : real end определяет тип в виде записи из двух полей, каждое типа real', record number : integer, letter : capitallettertype', valuel, value2 : -1000.. 1000 end определяет тип записи из четырех полей, из которых поле number - типа integer, поле letter имеет ранее определенный тип capitallettertype, а последние два поля - valuel и value2 - имеют новый тип данных, определенный неявно как диапазон - 1000..1000. 16.6.2. Выборка поля записи Теперь обратимся к проблеме обработки информации, хранящейся в пере- менной типа запись. Соответствующие правила проиллюстрированы в программе ЛучшийТовар, см. табл. 16.3 и листинг 16.3.
302 Гл. 16. Новые типы данных Таблица 16.3. Спецификация программы ЛучшийТовар Написать на Паскале программу ЛучшийТовар (Bestseller), которая считывала бы файл, содержащий информацию по сбыту за несколько лет, распечатывала этот файл и находила продукцию с максимумом сбыта. Точные характеристики таковы: Входные данные Каждая строка файла данных будет содержать информацию об одном виде продукции в следующей форме: год продажи : целое в диапазоне 1900..2099; код продукции : четырехзначное' целое с последующей заглавной буквой; общий объем сбыта : вещественное число, обозначающее сумму в долларах. Например, типичной строкой входных данных является: 1989 3872G 30675.80 Можно считать, что данные корректны. Результаты Входные данные распечатываются под соответствующими заголовками и завершаются следующим предложением: Лучший товар: 3872G с общей суммой продаж $30675.80 за 1989. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 program Bestseller (input, output); { находит наиболее ходкий товар из отчетов по продажам } procedure findbestproduct; var any, best: record year : 1900..2099; number : 0..9999; code : sales : real end; begin writein (’ГОД ПРОДУКТ СУММА ПРОДАЖ'); writein; best.sales :- 0.0; while not eof do begin readln (any.year, any.number, any.code, any.sales); writein (any.year : 4, any.number : 12, any.code, any.sales : 18 : 2); if any.sales > best.sales then best:- any end; writein; writein ('Лучший товар : ’, best.number : 1, Sest.code, ’ с общей суммой продаж '); writein best.sales : 4 :2, ' in ', best.year : 4, end {findbestproduct}; begin writein (’♦♦♦♦ РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ ЛУЧШИЙ ТОВАР ♦♦♦♦’); writein; findbestproduct; writein; writein (’♦♦♦♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦♦’) end { Bestseller}. Листинг 16.3. Текст программы ЛучшийТовар, использующей записи
Гл. 16. Новые типы данных 303 Эта программа считывает файл, содержащий информацию по сбыту за не- сколько лет, распечатывает его и находит продукцию, имевшую максимальный объем сбыта. Алгоритм этих вычислений реализован в программе процедурой findbestproduct. Сначала рассмотрим строки 7-12, в которых описаны переменные: any у best: record year : 1900..2099; number : 0..9999; code : ’A’..’Z’; sales : real end Здесь описаны две записи одного и того же типа с именами any и best. Данный тип записи можно представить в виде year number code sales (год) (номер) (код) (объем продаж) где код продукции, представленный не более чем четырехзначным числом, за которым следует одна заглавная буква, хранится в двух полях с именами number и code. Год продажи хранится в поле с именем year, а общий доход от продаж - в поле с именем sales. Обе переменные-записи any и best относятся к этому типу данных. Таким образом, строку данных, указанную в спецификации и хранящу- юся в переменной best у можно представить так: year number code sales 1989 3872 ’G’ 30675.80 Второе, что надо отметить в программе, - выражение в строке 15: best.sales называемое выборкой поля. Выборка поля отсылает нас к значению определенно- го поля переменной типа запись, в данном случае - поля sales переменной best. Формальный синтаксис выборки поля дан на рис. 6.4 (с. 103-104), и в общем случае она представляет собой имя переменной-записи, за которым через точку следует одно из имен полей, принадлежащих данному типу записи. Таким образом, каждое из приведенных ниже выражений обозначает поле, связанное с переменной-записью best best.year best.number best.code best.sales Эти выражения встречаются в строках 25-27 программы. Подобные же выборки полей, связанные с другой переменной-записью any, встречаются в строках 18-21. Употребление в выборке поля имени поля, которое не принадлежит типу записи, описывающему данную переменную, является Ошибкой програм- мирования. Выборка поля имеет свой тип данных, каковым является тип компонента обозначаемого поля; она может фигурировать всюду, где допустимо употребле- ние переменных такого типа. Следовательно, best.sales имеет тип real, и следующие фрагменты являются корректными:
304 Гл 16. Новые типы данных best.sales :в 731.36; х :• best.sales - 250.0; while trunc (best.sales / 1000.0) > 10 do ... Последнее, на что хотелось бы обратить внимание в программе ЛучшийТовар, это фрагмент best :« any в строке 22. Данный оператор присваивания предписывает целиком переслать текущее значение переменной-записи any в переменную-запись best. Будучи равносилен последовательности операторов best.year :» any.year, best.number :e any.number\ best.code :« any.code; best.sales := any.sales', он и короче, и эффективнее. Оператор ввода read в строке 18 не может быть сокращен до readln(any), и равным образом нельзя вывести запись в файл, указав один параметр вывода. В Паскале можно образовать специальные файлы, в которые запись может помещаться и из которых она может читаться целиком, однако такие файлы не рассматриваются в данной книге. Для тех, у кого после описания записей остались неясности, приведем час- тичную трассировку программы ЛучшийТовар (табл. 16.4). Она демонстрирует исполнение процедуры findbestproduct с очень небольшим набором тестовых дан- ных. Надеемся, что внимательное изучение этой трассировки прольет свет на все загадки! Таблица 16.4. Частичная трассировка программы ЛучшийТовар Входные данные 1989 4926К 75.00 1991 7315М 50.00 Трассировка строка ход выполнения year any numb code sales year best numb code sales —————————————————————— —————— ———————— -——————-I ———————— - —— — 13 вход в findbestproduct ? 9 ? 9 9 9 7 ? 14 15 16 18 19 21 22 16 18 19 21 16 24 25 27 28 вывод: ГОД ПРОДУКТ . not eof - true вывод: 1989~~~ 4926К .. any.sales > best.sales - true not eo/- true вывод: 1991 7315M . any.sales > best.sales - true not eof - false вывод: # вывод: Лучший'товар ... вывод: $75.00 в 1989. выход из findbestproduct 1989 1991 4926 7315 ’К’ ’M’ 75.00 50.00 [989 4926 K’ 0.0 Г5.00
Гл. 16. Новые типы данных 305 Результаты (программы в целом) **** РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ ЛУЧШИЙ ТОВАР **** ГОД ПРОДУКТ СУММА ПРОДАЖ 1989 1991 4926К 7315М 75.00 50.00 Лучший товар: 4926К с общей суммой продаж $75.00 в 1989. **** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ **** 16.7. Оператор присоединения with Вы, наверное, заметили, что при выборке полей переменной-записи мы были вынуждены многократно повторять ее имя. Так, например, строки 18-20 листинга 16.3 содержат восемь упоминаний переменной any. На листинге 16.4 представлен второй вариант программы ЛучшийТовар. В новой версии нет пов- торов. Это удалось сделать с помощью особого структурного оператора стандарт- ного Паскаля, называемого оператором присоединения, или оператором with. Описание его синтаксиса приведено на рис. 6.10 (с. 109-110). Простейшая форма оператора присоединения такова: with И do S1 где И - имя переменной-записи, a S1 - оператор. Оператор 51 называется телом Оператора присоединения, и внутри него выборка поля переменной VI может быть обозначена просто именем этого поля. Таким образом, например, оператор with any do begin readln (year, number, code, sales); writein (year : 4, number : 12, code, sales : 18 : 2); end; из строк 18-22 листинга 16.4 полностью эквивалентен записи readln (any.year, any.number, any.code, any.sales); writein (any.year : 4, any.number : 12, any.code, any.sales : 18 : 2); в строках 18-20 предыдущей ее версии (листинг 16.3). Аналогично, оператор with в строках 27-32 листинга 16.4 означает то же, что и строки 25-27 листинга 16.3. В большинстве случаев применение операторов with сокращает и проясняет те части программы, где используются записи. Это может также повысить эффек- тивность программы при исполнении.
306 Гл 16. Новые типы данных 1 2 3 4 5 6 7 8 9 10 И 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 program Bestseller (input, output); { находит наиболее ходкий товар из отчетов по продажам } procedure findbestproduct; { найти лучший товар } var any, best: record year: 1900..2099; number: 0..9999; code: ’A’./Z*; sales : real end; begin writeln ('ГОД ПРОДУКТ СУММА ПРОДАЖ*); writeln; best.sales 0.0; while not eof do begin with any do begin readln (year, number, code, sales); writeln (year : 4, number : 12, code, sales : 18 : 2) ; end; if any.sales > best.sales then bestany end; writeln; with best do begin writeln ('Лучший товар : number : 1, code, ’ с общей суммой продаж ’); writeln C$\ sales : 4 : 2, * in ', year: 4, *.*) end end {findbestproduct}; begin writeln (’♦♦♦♦ РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ ЛУЧШИЙ ТОВАР ♦♦♦♦’); writeln; findbestproduct; writeln; writeln (’♦♦♦♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦♦’) end { Bestseller}. Листинг 16.4. Текст профаммы ЛучшийТовар2 с применением операторов присоединения 16.8. Сочетание структур данных Ранее, в гл. 2, мы определили последовательность, выбор и повторение как фундаментальные средства, структурирующие программу, и установили, что более сложные структуры можно получить вложением этих конструкций друг в друга. После этого мы подробно рассмотрели применение операторов цикла вну- три составных операторов и другие способы комбинирования операторов для описания достаточно сложных ситуаций. В этой главе введены типы данных массив и запись. Они выступают как два фундаментальных способа построения структур данных. Еще раз повторим, что более сложные структуры могут быть построены из простых вложением их друг в друга. Тот факт, что компонент в массиве может иметь любой тип дан- ных, означает, что можно определить массив, состоящий из записей. Наоборот, запись может содержать в качестве компонента массив, поскольку компонент за-
Гл. 16. Новые типы данных 307 писи также может быть любого типа. Таким образом, в данной главе описаны основы построения сложных структур данных. Упражнения 16.1. Напишите программу Word Square (СловоВКвадрате), которая бы считывала любое слово до 20 букв длиной и печатала его по сторонам квадрата указанным ниже способом: СЛОВО л в о о в л оволс 16.2. Напишите программу под названием CharCount (СчетчикСимволов), которая анализировала бы, сколько раз встречается каждый изображаемый символ1 (кроме пробела) в данном фрагменте текста. Текст следует печатать в том же виде, в каком он находился во входном файле, следом за ним напечатайте результаты анализа в виде соответствующей таблицы. 16.3. Напишите программу под названием WordEnding (Окончание), которая бы считывала двухбук- венное окончание слова (таким образом, в первой строке данных должно быть только два символа), затем считывала фрагмент текста и печатала список слов текста с указанным окончанием. Слова следует записывать одно под другим. Если слово встречается в тексте более одного раза, оно несколько раз появится и в списке. 16.4. Напишите программу PascalPattern (ШаблонПаскаля) для вывода на печать 32 строк следую- щего шаблона: X X X ХОХ X X X X X О О О X X X 0 0 X X Этот шаблон получен замещением четных чисел в треугольнике Паскаля на ’X’, а нечетных - на *0’. Первые строки треугольника Паскаля таковы: 1 1 1 1 2 1 13 3 1 1 4 6 4 1 1 5 10 10 5 1 где в начале и конце каждой строки стоит 1, а любое другое число есть сумма двух чисел, стоящих справа и слева над ним. Для создания указанного шаблона не требуется вычисления фактических величин в треугольнике Паскаля. 16.5. Напишите программу SalesAnalysis (АнализПродаж) для чтения и анализа файла, который со- держит информацию по сбыту изделий за 13-недельный период. Входные данные Каждая строка данных содержит информацию об одной продаже в следующем виде: номер недели : целое в диапазоне 1..13; код продукта : две заглавные буквы и следующее за ними целое из трех цифр; 1 См. гл. 3, с. 45. - Примеч. ред.
308 Гл. 16. Новые типы данных продано штук : положительное целое; цена за штуку : положительное вещественное число, выражающее денежную сумму. Приведем несколько типичных строк данных: 11 КХ523 20 5.89 7 LJ024 75 11.34 11 МА 103 200 4.73 Строки не упорядочены. Все данные корректны. Результаты Выдача должна иметь вид следующей таблицы: НЕДЕЛЯ СУММА ПРОДАЖ 1 2200.56 2 3278.80 и т.д. и завершаться сообщением следующего типа: Максимальная разовая продажа была на неделе А, когда В штук товара С было продано по D долларов за штуку. Здесь А, В, С и D - характеристики продукции, на которую прищелся наибольший разовый сбыт в рассматриваемый период. 16.6. Напишите программу MarkList (СписокОценок), читающую из файла, который содержит ин- формацию о студентах, проходящих курс, и выводящую на печать итоговый список с последующим сообщением о двух лучших студентах. Входные данные Первая строка файла данных содержит название курса - это последовательность, состоящая не более чем из 50 символов и завершающаяся знаком Каждая следующая строка содержит перечисленные ниже данные на одного студента: Номер студента : целое в диапазоне 700000..999999; Оценка на экзамене вещественное число в диапазоне от 0.0 до 80.0; Оценка за практику вещественное число в диапазоне от 0.0 до 20.0; Факультет : ’А’ для факультета искусств и ’S’ для факультета наук. Пример типичной строки данных для одного студента: 792146 68.3 16.5 А Все данные корректны. Результаты Печатается таблица с номером студента, оценкой за экзамен, оценкой за практику, общей оценкой и обозначением факультета для каждого студента; затем печатается следующее сообщение: Лучшие студенты факультетов: Факультет наук : студент хххххх, с суммарной оценкой в хх% Факультет искусств : студент хххххх, с суммарной оценкой в хх% Оценки за экзамен и за практику печатаются в таблице с одним десятичным знаком, а сум- марные оценки печатаются как целые числа.
17 ИСПОЛЬЗОВАНИЕ СТРУКТУРНЫХ ПЕРЕМЕННЫХ В гл. 16 мы пользовались массивом для представления заголовка и запи- сью для хранения информации о продукции, которую продает компания. В дан- ной главе мы продолжаем исследовать применение массивов и записей для пост- роения структурных переменных. Структурные переменные позволяют хранить сложно организованную информацию, но для решения задач этого мало; надо уметь ее обрабатывать. Например, может понадобиться удалить слово из заго- ловка или изменить сведения о продукте. В язык программирования встроены некоторые необходимые операции, но далеко не все. Следовательно, мы должны сами конструировать необходимые операции, т.е. создавать набор процедур и функций для каждого нового типа данных. Процесс, в котором определяется новый тип данных и составляется набор подпрограмм, обеспечивающих операции над ним, называется реализацией типа данных. Фактически мы расширяем наш язык программирования так, что стано- вится возможным объявлять переменные нового типа и осуществлять над ними операции. 17.1. Представление справочной таблицы В качестве первого примера рассмотрим программу, которую ранее мы уже дважды изучали, а именно программу ДлинаТекста. Она была разработана в гл. 12 (с. 220), а в гл. 15 (с. 271) мы проанализировали время ее выполнения и занимаемую ею память. Чтобы достигнуть большей эффективности, немного из- меним эту программу и покажем, как можно использовать массив для построе- ния справочной таблицы, которая в данном случае показывает, какие символы из некоторого набора являются буквами. В программе ДлинаТекста объектом нашего внимания будет функция function letter (с : char) : Boolean} begin letter := (c >= ’A’) and (c <= ’Z’) or (c >= ’a’) and (c <= ’z’) end {letter}; являющаяся наиболее часто исполняемой частью программы. Получив символ с, функция возвращает величину true, если с является буквой, и false в противном случае. Результат получается посредством четырех сравнений и, таким образом, требует значительного объема вычислений. Если функция вызывается сто раз для анализа одного и того же символа, каждый раз производится полное вычис- ление.
310 Гл 17. Использование структурных переменных В гл. 15 (с. 279) мы отметили, что порой выгоднее вычислить величину и запомнить ее, чем повторять вычисления. Так и надо поступить в данном слу- чае. Именно: для каждого символа мы можем раз. и навсегда определить, являет- ся ли он буквой, и сохранить полученные результаты в виде таблицы. Каждый элемент таблицы должен соответствовать одному символу из набора и содержать величину булевского типа. Такая таблица может быть представлена массивом с индексом типа char и компонентами типа Boolean. Итак, мы задаем новый тип данных array [char] of Boolean Этот подход применен в новом варианте программы, названной ДлинаТекста2 (листинг 17.1). Некоторые части листинга опущены, так как не имеют отноше- ния к обсуждаемой теме тем более, что они идентичны аналогичным разделам исходной программы. Рассмотрим три вопроса, требующих более детального обсуждения. Во-первых, переменная-массив для хранения нашей таблицы названа letter и описана в строке 6, в разделе описания переменных головной программы, по- этому она является глобальной переменной. Это наиболее удобный способ сде- лать массив доступным для трех процедур: initialize letter, copytoaletterorendoffile и copytoendofword. Функция, которую заменил массив, также была глобальной. Во-вторых, перед тем как использовать какую бы то ни было таблицу, в нее надо занести некоторые величины, другими словами, ее надо инициализиро- вать. Эту задачу выполняет процедура initializeletter, которая считает, что используется набор символов ASCII. Задача решается так: сначала первые 128 символов нашей таблицы устанавливаются в false, затем в 52 элемента, соответствующие большим и маленьким буквам кодировки ASCII, заново записывается true. Процедура вызывается один раз в строке 63 программы, так что таблица инициализируется перед обработкой текста. В-третьих, заметим, что в строках 26 и 32 нам теперь нужно обратиться в массив, а не вызывать функцию. Поэтому вместо вызова функции letter с факти- ческим параметром input**. letter (input*) мы пишем индексированную переменную с индексным выражением input*: letter [триГ] Так как буферная переменная input* имеет тип char, такое обращение к элемен- ту массива letter корректно. Как же повлияли внесенные изменения на качество программы? Она оста- ется правильной и столь же ясной. Изменились только время выполнения и объем занимаемой памяти. Исследуем сначала время выполнения при обработке страницы в 50 строк, содержащей, например, 600 слов, 2700 букв и 800 остальных символов. Из на- шего анализа программы ДлинаТекста (с. 271) видно, что функция letter будет вызываться 4750 раз. В пересмотренной программе каждый вызов заменяется на обращение к массиву, анализируя обращения к функциям, сравнения внутри функции и операции с массивом, получим следующие цифры при обработке одной страницы: ДлинаТекста ДлинаТекста2 Обращения к функции 4750 0 Сравнения 19000 0 Доступ к массиву 0 4750
Гл. 17. Использование структурных переменных 311 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 23 24 25 26 27 28 29 30 31 32 33 34 35 36 47 48 59 60 61 62 63 64 65 66 program TextLength2 (input, output)’, { Копировать текст и сосчитать число слов в нем } । var letter : array [char] of Boolean’, procedure initializeletter, var c : char, begin { Использует набор символов ASCII} for c chr(0) to chr( 127) do letter [c] false; for c ’A* to ’Z’ do letter[c] true; for c ’a’ to ’z’ do letter[c] true end { initializeletter}; procedure copyonecharacter; procedure copyaletterorendoffile; begin while not letter[inpuC] and not eoln do copyonecharacter end { copyaletterorendoffile }; procedure copytoendofword; begin while letter[inpuC] do copyonecharacter end { копировать до конца слова }; procedure processoneline (var wordcount: integer); procedure processthetext; begin writein (’♦♦♦♦ ВЫВОД ИЗ ПРОГРАММЫ TEXTLENGTH ♦♦♦♦’); writein; initializeletter, processthetext; writein; writein (’♦♦♦♦ КОНЕЦ ВЫДАЧИ ♦♦♦♦’) end { TextLength2 } . Листинг 17.1. Пересмотренный вариант программы ДлинаТекста (с. 222), использующий массив вместо функции с целью повышения эффективности Если считать, что доступ к массиву в грубом приближении занимает сто- лько же времени, сколько вызов функции, а это вполне обоснованно, то эконо- мия на сравнениях делает программу ДлинаТекста2 более быстрой. Данный анализ не учитывает работу по инициализации массива letter в программе ДлинаТекста2, но эта работа пренебрежимо мала, если допустить, что текст не ограничивается несколькими строками. Если в вашем Паскаль-процессоре есть средства временной оценки программ, вы можете поэкспериментировать с этими двумя программами и сравнить их по быстродействию. Аналогичный расчет можно получить для занимаемой программами памя- ти. Максимальный размер памяти, занятой переменными и параметрами во вре- мя исполнения программы ДлинаТекста^ - 6 единиц (с. 274). Этот подсчет осно- ван на допущениях о том, сколько единиц памяти требует каждый тип данных.
312 Гл 17. Использование структурных переменных Считая, что величина типа Boolean занимает одну единицу памяти, получим, что использование массива letter, который имеет 128 компонент типа Boolean и существует все время вычислений, добавляет 128 единиц к общему объему. Сра- внительная таблица выглядит так: ДлинаТекста ДлинаТекста2 Максимальное число используемых единиц памяти 6 134 На этот раз ДлинаТекста эффективнее, чем ДлинаТекста?. Таким образом, применяя массив вместо функции, мы уменьшили время исполнения и увеличи- ли нужное программе пространство. В данном примере лучше использовать справочную таблицу, так как фун- кция, которую она заменяет, вызывается очень много раз. Но так бывает не всегда, и программисту нужно просмотреть разные пути реализации одного алгоритма, а затем выбрать подходящий вариант. Метод, наилучший для одной программы, может не подходить для другой. 17.2. Представление строки текста Второй пример этой главы также построен на программе, которую мы уже изучали. На этот раз это будет программа Заголовок из гл. 16 (с. 294). Рассмот- рим исправленный вариант, написанный в совершенно ином стиле. Он покажет нам, как с помощью структурного типа данных можно собрать вместе все пере- менные, связанные, по мнению программиста, с одним информационным объек- том , и как реализовать операции над этим типом. В новом варианте программа называется Заголовок2 (листинг 17.2). Когда мы создавали программу Заголовок, схема алгоритма выглядела так: begin читать и хранить заголовок; печатать часть рамки перед заголовком; печатать заголовок; печатать часть рамки после заголовка; end Из этой схемы следует, что центральной информационной единицей является заголовок, над которым и производятся две основные операции. Сначала он читается и запоминается, затем печатается. Давайте обсудим структуру типа данных, подходящего для представления заголовка или, в общем случае, строки текста. Если вы посмотрите на листинг 16.2 (с. 294) то обнаружите, что головная процедура исходной программы содер- жит следующие описания: var headline : array [\..maxlineindex] of char, i : \..maxlineindex', length, spaces : 0..maxlineindex', где maxlineindex - константа co значением 47. Так какая же из этих переменных представляет заголовок? Наиболее очевидным выглядит ответ, что заголовок представляется массивом headline. Но это только часть ответа. Переменная length, содержащая длину заголовка, который хранится в массиве, также являет- ся важной частью его представления. Можно пойти и дальше. Переменная i слу- жит для указания на элементы массива. Таким образом, наш окончательный от- вет на вопрос таков: заголовок представляется переменными headline, length и i.
Гл 17. Использование структурных переменных 313 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 program HeadLiner2 (input, output); { чтение заголовка и вывод его в рамку из звездочек } const maxlineindex - 47; type lineindextypeindex', linetype - record ch : array (lineindextype] of char, cursor : lineindextype', length : 0.. maxlineindex end; procedure readoneline (var anyline : linetype); begin with anyline do begin cursor1; while not eoln and (cursor <> maxlineindex) do begin read (ch(cursor]); cursor cursor + 1 end; { eoln or cursor - maxlineindex } length cursor - 1; readln end end { readoneline}; procedure writeoneline (var anyline : linetype); var / : lineindextype', {переменную cursor нельзя использовать в операторе for} begin with anyline do for / 1 to length do write (ch [/]) end { writeoneline }; procedure processoneline', var headline : linetype", spaces : integer, begin readoneline (headline) ', writeln (»iWr******************** A ************* A k A A A A *** A ***») • writeln (’* **); spaces (maxlineindex - headline.length - 1) div 2; write (’*’, ’ ’ : spaces); writeoneline (headline) ; if odd (headline.length) then spaces spaces + 1; write (’*’, ’ ’ : spaces); writeln (’* *’); writeln (9''k^'k''k'k'^k'k'k'k'k'k'k'kk{TkTkk('k'k'k'k'kiJ(''k')c)<Tk'k'Tk'j<'k‘k'k^'k'jck'k^'k'k'k'k^'k'k'kyk’) * end { processoneline }; begin writeln (’**** РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ ЗАГОЛОВОК ****’); writeln-, writeln-, writeln (’**** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ****’); end {HeadLiner}. Листинг 17.2. Использование нового типа данных для представления строки текста
314 Гл. 17. Использование структурных переменных Если для полного представления информационного объекта необходимы несколько переменных, удобно их объединить. В стандартном Паскале это мож- но сделать с помощью записей. Поэтому в строках 9-13 программы Заголовок2 вы найдете описание нового типа данных с именем linetype'. linetype - record ch : array [lineindextype} of char; cursor : lineindextype; length : 0..maxlineindex end; здесь maxlineindex, как и ранее, константа, величина которой на единицу больше, чем длина максимальной из строк хранимого текста, a lineindextype - диапазон 1.. maxlineindex. Структура этого нового типа данных может быть изображена следующим образом: ch cursor length 1 2 3 4 5 6 ... 46 47 где первое поле, ch, содержит массив символов; второе поле, cursor, может использоваться для указания на один из компонентов поля ch; третье поле, length, предназначено для хранения длины строки из поля ch. В этой структуре мы имеем возможность хранить всю необходимую информацию о строке текста длиной до 46 символов. Лишний компонент, ch [47], вводится во избежание ошибок выхода индексов за границу массива (см. с. 295-296) и иногда, как мы увидим в гл. 18, может использоваться в других целях. После определения нового структурного типа данных с его помощью можно описывать структурные переменные и задавать параметры. Так, на- пример, в строке 39 описывается переменная headline, а в строках 15 и 29 заголовки процедур содержат формальный параметр anyline; все они имеют тип linetype. Компоненты внутри такой переменной или параметра обозначаются обычным образом. Например, переменная anyline включает три отдельных поля: anyline. ch anyline. cursor anyline. length а так как поле ch содержит массив, конкретный элемент массива обозначается anyline.ch [е] где е - индексное выражение. Если за индекс взять величину, хранящуюся в поле cursor, то это запишется как anyline.ch [anyline.cursor} а при употреблении оператора with более просто: ch [cursor] как сделано в операторе read в 22 строке программы. Операции над структурными типами реализуются путем разработки набо- ра соответствующих им подпрограмм. Для данного примера нам необходимы две операции: а) операция, которая вводила бы и хранила строку текста; б) операция, которая выводила бы ранее сохраненную строку текста.
Гл. 17. Использование структурных переменных 315 Первую из этих операций реализует в строках 15-27 процедура readoneline, которая читает из входного файла последовательность символов и сохраняет ее в поле ch своего параметра, при этом cursor указывает на первый свободный элемент массива, а длина последовательности записывается в поле length. Например, если был считан заголовок ”БДИ!!”, параметр-запись находится в следующем состоянии: ch cursor length 1 2 3 4 5 6 ... 46 47 LQ •Д' 'И’ | ’!’ | ’! ' | ? 1 1 1 1 1 | ? | ? 6 1 5 1 1 Логика процедуры та же, что и в строках 14-20 исходной программы. Этой процедуре формальный параметр anyline передается по ссылке, так как процеду- ра должна иметь полный доступ к записи, с которой она оперирует. Соответст- вующий фактический параметр должен, конечно, иметь тип linetype} при вызове процедуры в строке 42 фактическим параметром является переменная headline. Таким образом, оператор в строке 42 считывает входные данные в headline. Вторую операцию реализует в строках 29-35 процедура writeoneline, кото- рая выдает компоненты своего параметра ch[\}..ch[length}. Две детали этой процедуры заслуживают комментария не по причине фундаментальной важнос- ти, а из-за особенностей стандартного Паскаля: а) Так как вывод символов требует доступа к записи только по чтению, может показаться, что параметр anyline надо передавать по значению. Однако передача параметра по значению влечет за собой создание его копии внутри подпрограммы и если параметр является структурным ти- пом, на это тратится и память, и время. Следовательно, когда пара- метр некоторой подпрограммы имеет структурный тип, его надо пе- редавать по ссылке, если только для правильного выполнения рассма- триваемой подпрограммы не требуется создания дубликата факти- ческого параметра. В данном случае, как и во многих других, дубликат не требуется и параметр процедуры writeoneline передается по ссылке. б) Поскольку переменная в цикле for должна быть локальной (см. с. 299), в строке 34 нельзя пользоваться переменной anyline.cursor. Чтобы обой- ти это ограничение, введена переменная j. Поле cursor теперь становит- ся не столь необходимым, но как общий прием программирования оно остается полезным. И снова стоит спросить себя, как влияют внесенные изменения на каче- ство программы. Самым важным результатом является то, что мы видим про- грамму разделенной на две почти независимые части. Первая реализует структуру данных, а вторая использует ее. Первую часть образуют строки 5-35. Здесь определяется новый тип данных с именем linetype и содержатся две подпрограммы, оперирующие этим типом данных. Мы можем включить этот фрагмент в любую подпрограмму, в которой должны вводиться и выводиться строки текста. Единственное, что придется изменить, - величину константы maxlineindex. Например, чтобы допустить ввод строк длиной до 80 символов, константу надо поменять на 81. Новый тип данных, определенный и реализованный таким образом, иногда называют абст- рактной структурой данных. Вторая часть программы включает строки 37-52. Здесь описан алгоритм процесса, обрабатывающего строку текста. Он пользуется абстрактной структу- рой данных, определенной в первой части, но в основном не зависит от особен- ностей реализации этой структуры данных. Вообще если программа составлена
316 Гл 17. Использование структурных переменных так, что имеет вид алгоритма, использующего одну или несколько абстракт- ных структур данных, то это наилучшим образом сказывается на ясности программы и простоте ее модификации. 17.3. Сведения о рабочем Рассмотрим задачу программирования, представленную в табл. 17.1. Прог- рамма, создаваемая под именем ПлатежнаяВедомость (WageList), должна чи- тать файл со сведениями о рабочих компании и готовить информацию для рабо- ты кассиров. Рассмотрим два способа написания этой программы на стандартном Паскале. Таблица 17.1. Спецификация программы ПлатежнаяВедомость Написать на Паскале программу ПлатежнаяВедомость (WageList) для чтения файла со сведениями о рабочих компании и создания ведомости для кассиров. Точные характеристики таковы. Входные данные Каждая строка файла данных содержит информацию об одном рабочем в следующем виде: номер : целое в диапазоне 10000..99999; оплата в час : вещественное число; рабочих часов в день : последовательность из 7 чисел, каждое - целое или вещественное Пример типичного набора данных: 76102 10.50 0 7.5 7.5 0 7.5 7.5 6 Все данные корректны. Результаты Под соответствующими заголовками должны быть напечатаны следующие данные по каждому рабочему: номер, общий заработок, налог, чистый заработок, где общий заработок равен числу отработанных часов, умноженному на оплату в час, налог составляет 30% общего заработка, а чистый заработок равен общему заработку минус налог. Например, результат может выглядеть так: 76102 378.00 113.40 264.60 Первая версия программы, ПлатежнаяВедомость 1 (листинг 17.3), напи- сана в стиле, который мы разработали в предыдущих главах. Головная проце- дура с именем processallworkers печатает заголовок и затем многократно вызывает процедуру processoneworker, пока не кончатся данные. последовательность действий, необходимых при обработке данных по одно- му рабочему, выглядит так: begin ввести данные по одному рабочему; вычислить общий заработок; вычислить налог; вывести результаты по одному рабочему end.
Гл. 17. Использование структурных переменных 317 1 2 3 4 5 6 7 8 9 10 И 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 program WageListl (input, output)', { читает файл со сведениями о рабочих компании и готовит информацию для работы кассиров. } procedure processoneworker, const taxrate - 0.30; { процент налога } type daytype- 1..7; var number : 10000..99999 wagerate, totalhours, grosswage, tax : real', hours : array [daytype] of real', day : daytype', begin { ввести данные no одному рабочему } read (number, wagerate)', for day 1 to 7 do read (hours[day])', readln', { вычислить общий заработок } totalhours 0.0; for day 1 to 7 do totalhours totalhours + hours[day]', grosswage totalhours ♦ wagerate', { вычислить налог } tax grosswage ♦ taxraie', { выдать результаты no одному рабочему } writein (number : 6, grosswage : 14 : 2, tax : 11 : 2, grosswage - tax : 11 : 2) end { processoneworker}; procedure processallworkers', begin writelnCНОМЕР ОБЩИЙ ЗАРАБОТОК НАЛОГ ЧИСТЫЙ ЗАРАБОТОК*)', writein', while not eof do processoneworker end { processallworkers }; begin writein (’♦♦♦♦ РЕЗУЛЬТАТЫ ПРОГРАММЫ ПЛАТЕЖНАЯ ВЕДОМОСТЬ ♦♦♦♦’); processallworkers', writein', writein (’♦♦♦♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦♦’> end { WageList}. Листинг 17.3. Первый "простой" вариант программы ПлатежнаяВедомость Поскольку каждое из этих действий занимает одну-две строки, весь про- цесс реализован в процедуре processoneworker в виде последовательности опера- торов, чередующихся с комментариями. Процедура содержит также описания var number : 10000..99999; wagerate, totalhours, grosswage, tax : real; { почасовая оплата, всего часов, общий заработок, налог} hours : array [daytype} of real*, day: daytype*,
318 Гл. 17. Использование структурных переменных 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 program WageList2 (input, output)', { читает файл со сведениями о рабочих компании и готовит информацию для работы кассиров. } type daytype- 1..7; workertype - record number : 10000..99999; wageraie, grosswage, tax : real; hours : array {daytype} of real', end; procedure readworkerdata (var anyworker : workertype) ', var day : daytype', begin with anyworker do begin read (number, wagerate)', for day 1 to 7 do read (hours [day]); readln end end { readworkerdata }; procedure calculategrosswage (var anyworker : workertype)', var day '. daytype', totalhours : real', begin { в anyworker должны быть определены часы и почасовая оплата } with anyworker do begin totalhours 0.0; for day 1 to 7 do totalhours totalhours + hours [day]; grosswage totalhours ♦ wageraie', end end { calculategrosswage }; procedure calculatetax (var anyworker : workertype); const taxrate - 0.30; begin { в anyworker должен быть определен общий заработок } with anyworker do tax grosswage * taxrate end { calculatetax}; procedure writeworkerdetails (var any worker : workertype); begin { в anyworker должны быть номер, общий заработок и налог } with anyworker do writeln (number : 6, gross wage : 14 : 2, tax : 11 : 2, grosswage - tax : 11 : 2) end { writeworkerdetails }; procedure processoneworker; var thisworker: workertype', begin readworkerdata (thisworker)', calculategrosswage (thisworker); calculatetax (thisworker)', writeworkerdetails (thisworker) end { processoneworker };
Гл. 17. Использование структурных переменных 319 65 66 67 68 69 70 71 72 73 74 75 76 77 procedure processallworkers; begin writelnC НОМЕР ОБЩИЙ ЗАРАБОТОК НАЛОГ ЧИСТЫЙ ЗАРАБОТОК*) ', writeln', while not eof do processoneworker end { processallworkers }; begin writeln (’*♦♦♦ РЕЗУЛЬТАТЫ ПРОГРАММЫ ПЛАТЕЖНАЯ ВЕДОМОСТЬ ♦♦♦♦’); processallworkers', writeln', writeln (’♦♦♦♦ КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦♦’) end { WageList}. Листинг 17.4. Текст программы ПлатежнаяВедомость2. Строки 6-53 определяют новый тип workertype и реализуют операции над ним. Строки 55-77 описывают требу- емый алгоритм с помощью ранее определенных типов данных и операций где daytype есть диапазон 1..7. Переменная-массив hours нужна только для того, чтобы полностью отделить чтение данных от непосредственных расчетов. Мы можем также написать программу ПлатежнаяВедомость, используя принципы, примененные в программе Заголовок (с. 313). В этом случае нам нужен структурный тип для представления сведений о рабочем, а также подпро- граммы, реализующие различные стадии вычисления его заработной платы. В результате получится программа, представленная на листинге 17.4. Раздел определения типов (строки 6-12) включает следующее определение нового типа данных workertype: workertype - record number : 10000..99999; wagerrate, grosswage, tax : real', hours : array [daytype} of real', end; где daytype - опять диапазон 1..7. Такая структура записи позволяет хранить всю информацию, которая относится к одному рабочему. Затем, в строках 14-53, приведены четыре процедуры, каждая из которых совершает одну операцию над параметром anyworker с типом workertype: процедура readworkerdata читает одну строку данных и сохраняет ее в соответствующих полях записи anyworker, процедура calculategrosswage вычисляет общий заработок и сохраняет его в поле anyworker. gross wage в предположении, что ранее определены поля anyworker.hours и anyworker.wagerate', процедура calculatetax вычисляет налог и сохраняет его в поле anyworker.tax, в предположении, что поле anyworker.grosswage уже определено; процедура writeworkerdetails выводит результаты, в предположении, что поля number, grosswage и tax в записи anyworker определены. Процедуры совершенно независимы друг от друга, если не считать требований на определенность некоторых полей. В остальной части программы ПлатежнаяВедомость2 используются стру- ктуры данных и операции, определенные в строках 6-53. В этой части головная программа и процедура processallworkers те же, что в программе ПлатежнаяВедомость 1. Процедура processoneworker просто описывает перемен- ную thisworker и по одному разу вызывает каждую из указанных процедур для выполнения нужных действий.
320 Гл. 17. Использование структурных переменных Легко, разумеется, читать завершенную программу, но под силу ли нам подобную программу составить? Совсем нового здесь, в сущности, немного. Мы начинаем, как обычно, с пошагового уточнения и получаем первый набросок процедур processallworkers и processoneworker. В этот момент становится ясно, что в процедуре processoneworker требуется одна или несколько переменных для хранения сведений о рабочем. Напрашивается решение завести переменную- запись неизвестной пока структуры для хранения всех этих величин. Затем процесс поэтапного уточнения можно продолжить и, когда каждый из подпроцессов в processoneworker будет определен, можно описать необходи- мые поля записи. Всякая информация, которой пользуются два или более подпроцесса, должна быть частью записи. Просматривая программу ПлатежнаяВедомость, вы увидите, что ситуация такова: поле подпрограммы, которые обращаются к нему number readworkerdata, writeworkerdetails wagerate readworkerdata, calculategrosswage hours readworkerdata, calculategrosswage grosswage calculategrosswage, calculatetax, writeworkerdetails tax calculatetax, writeworkerdetails С другой стороны, любая информация, необходимая только в одном подпро- цессе, может быть локальной переменной соответствующей подпрограммы. Например, переменная totalhours требуется при вычислении общего заработка, но не нужна в других частях программы. Поэтому она является локальной в процедуре calculategrosswage. В программе существуют две переменные с именем day - одна в readworkerdata, другая - в calculategrosswage, но эти переменные полностью независимы. Они должны быть локальными уже потому, что служат управляющими переменными в операторах цикла с шагом. Каковы же сравнительные достоинства программ ПлатежнаяВедомость 1 и ПлатежнаяВедомостъ21 Для решения задачи, сформулированной в табл. 17.1, ПлатежнаяВедомость! вполне удовлетворительна. Она ясна, правильна и эффективна. Характеристики программы станут хуже, если правила вычисления общего заработка и расчета налогов будут сложнее. При таких условиях лучше использовать программу ПлатежнаяВедомость2, так как каждый подпроцесс описан в ней подпрограммой и, следовательно, ее легче модифицировать. Отме- тим еще одно достоинство программы ПлатежнаяВедомость2, которое больше связано с программированием на языках будущего, чем с программированием на Паскале и других старых языках. Обработка данных по одному рабочему произ- водится в ней однократным и последовательным применением четырех первых подпрограмм. Это напоминает конвейер. Если бы у нас было четыре процессора и язык, на котором можно описать передачу записи из одного процессора в дру- гой, мы могли бы заставить один процессор читать данные, другой - вычислять общий заработок, третий - считать налог и четвертый - записывать результат. Такие языки и процессоры уже существуют, и опыт написания программ, подоб- ных ПлатежнойВедомости2, - хорошая подготовка к их освоению. 17.4. Многомерные массивы Одномерные массивы нами уже изучены, и в этом разделе мы собираемся в процессе подготовки к созданию нашей следующей программы рассмотреть массивы с большим числом измерений. Если взглянуть на табл. 16.1 (с. 286), выяснится, что синтаксис стандарт- ного Паскаля допускает у типа массив несколько связанных с ним индексных
Гл. 17. Использование структурных переменных 321 типов. Следующий фрагмент, например, является правильным определением типа массив с именем matrixtype (тип ’’матрица”): matrixtype - array [1..4, 1..9] of real} Структуру типа matrixtype можно представить так, как изображено на рис. 17.1. 123456789 Рис. 17.1. Структура типа matrixtype Эта структура состоит из 36 элементов типа real, организованных в строки и столбцы. Такой тип структуры называется двумерным массивом. По соглаше- нию, первый индексный тип определяет диапазон индексов для строк и тем са- мым максимальное количество строк (в данном случае оно равно четырем). Вто- рой индексный тип аналогичным образом определяет диапазон индексов для столбцов. И переменная, и параметр типа многомерный массив могут быть описаны или заданы обычным путем, тогда доступ к ним осуществляется с помощью индексной переменной, состоящей более чем из одного индексного выражения. Соответствующий синтаксис указан на рис. 6.4 (с. 103-104). Пусть thismatrix, thatmatrix и anymatrix - переменные типа matrixtype, тогда индексированные переменные thismatrix [2,5] thatmatrix [г, с] anymatrix [г-1, с+1] корректны при условии, что первое индексное выражение в каждом случае имеет значение в диапазоне 1..4, а второе - в диапазоне 1..9. Данную структуру можно рассматривать и по-другому - как массив масси- вов. В стандартном Паскале каждый многомерный массив эквивалентен одно- мерному массиву, элементами которого являются в свою очередь массивы. Можно, например, представить, что новый тип matrixtype содержит четыре элемента (’’ряда”), каждый из которых является массивом из девяти элементов. В терминах нового типа rowtype тип matrixtype можно описать так: rowtype array [1..9] of real*, matrixtype e array [1..4] of rowtype\ или, не определяя rowtype явно, следующим образом: matrixtype = array [1..4] of array [1..9] of real; Все три метода описания многомерного массива эквивалентны, хотя обычно ис- пользуется первый метод. В свете сказанного, вполне осмысленно употребление переменной массива с меньшим числом индексных выражений, нежели размерность массива. Таким образом, при условии, что выражение г попадает в диапазон 1..4, выражение thismatrixfr] И Заказ№1110
322 Гл. 17. Использование структурных переменных обозначает строку с индексом г в thismatrix и является величиной типа rowtype. Соответственно, выражение thismatrix[r} [с] также допустимо. Оно обозначает элемент с индексом с в массиве thismatrix[r} и в точности эквивалентно выражению thismatrix[r,c\ Так как любой простой тип данных, кроме real, допускается в качестве типа индекса, можно определить многомерные массивы самых разных видов. Например, структура данных puzzletype, определенная как type indextype - 1 ..15; puzzletype e array [indextype, indextype} of char; может применяться для представления кроссворда (или загадки другого рода), построенного в виде сетки с одним символом в каждой ячейке. В данном приме- ре число рядов равно числу колонок и дважды используется один и тот же индексный тип. Другой пример: в театре ряды кресел часто помечаются буквами, а кресла в ряду нумеруются. Структура схемы мест, на которой помечаются проданные билеты, может быть определена так: type rowindextype в ’А’..Т; { тип ’’индекс ряда” } seatindextype e 1..36; { тип ’’номер места” } bookingsheettype e array [rowindextype, seatindextype} of Boolean; а переменная типа bookingsheettype будет состоять из 360 элементов булевского типа. И наконец, число измерений в массиве не ограничивается двумя. С помо- щью следующих определений и описаний: type monthindextype dayindextype rowindextype seatindextype archivetype = 1..12; { тип ’’месяц” } 1..31; { тип ’’день” } ’A’..’J’; 1..36; array [monthindextype, dayindextype, rowindextype, seatindextype} of Boolean; var booked : archivetype; переменная booked становится массивом из 133920 элементов типа Boolean и может фиксировать заказы на билеты в течение всего года. Величина booked [12, 25, ’В’, 17] скажет, заказан ли вам билет на место В17 в день под Рождество, а величина booked [7, 4] даст полную информацию о билетах на 4 июля. Число элементов этого четырех- мерного массива привлекает внимание к одной из главных проблем, возникаю- щих при работе с многомерным массивом: он может потребовать больше памяти, чем имеется в вашем компьютере!
Гл. 17. Использование структурных переменных 323 17.5. Представление шаблонов Большая часть окружающей нас информации имеет графическую форму, и компьютеры все шире применяются для порождения и переработки информации такого рода. В последнем примере данной главы мы собираемся исследовать программу, обрабатывающую очень простые шаблоны. Шаблоны отображаются довольно грубо, с помощью обычных символов. Программа под названием Жизнь и задача, которую она решает, основаны на игре, созданной Джоном Конвеем. Она моделирует историю жизни колонии микроорганизмов. Полная спецификация программы приведена в табл. 17.2. С первого взгляда на шаблоны представляется, что нам понадобится один или два двумерных массива, однако это слишком поспешное предположение. Пе- ред тем, как перейти к выводам о структурах данных, мы должны подумать о том, как они будут обрабатываться. Вообще, типы данных следует конструиро- вать только тогда, когда известно, какие операции над ними потребуются. Теперь, как обычно, мы начнем со схемы предполагаемого алгоритма. Таблица 17.2. Спецификация программы Жизнь Следующие шаблоны отображают последовательные поколения колонии микроорганизмов, где звездочка показывает наличие организма, а точка - его отсутствие. * ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ *.*.*. * *.*... *.**.. ★ *...** **.... ****,★ * , * в * * * * * * ★ ★ ★ * * ...*.. . * * * ..*... Правила преобразования одного поколения в другое: а) существующий организм выживает в следующем поколении, если у него 2, 3 или 4 соседа, иначе он погибает и исчезает; б) новый организм рождается в свободной ячейке, если ровно 3 соседних ячейки заселены; в) все рождения и смерти происходят одновременно. Напишите программу, моделирующую развитие колонии в квадрате, содержащем N * N ячеек. Точные характеристики таковы: Входные данные Первая строка в файле данных содержит число N - целое в диапазоне 1 ..20. Следующие N строк содержат по N символов каждая, как изображено в приведенных шаблонах. Последняя строка содержит положительное целое число, определяющее, сколько новых поколений надо моделировать. Результаты Программа должна напечатать исходный шаблон и шаблоны для каждого из следующих поколений, один под другим. Один из возможных вариантов схемы таков: begin инициализировать нужные переменные; ввести число размер матрицы; ввести исходный шаблон в ’’старую” матрицу; вывести шаблон из старой матрицы; ввести требуемое число новых поколений; 11*
324 Гл. 17. Использование структурных переменных для требуемого числа новых поколений выполнить begin породить ’’новую” матрицу по старой; вывести шаблон из новой матрицы; переслать новую матрицу в старую; end end Наиболее сложным из перечисленных является пункт породить ’’новую” матрицу по старой; и его стоит развернуть первым. Он должен иметь такую общую структуру: begin для каждой строки матрицы выполнить для каждой ячейки в текущей строке выполнить если эта ячейка в старой матрице занята то породить ячейку новой матрицы по правилу а) иначе породить ячейку новой матрицы по правилу б) end где применение правил а) и б) (см. табл. 17.2) включает подсчет числа занятых соседей у данной ячейки. Ячейка, которая не находится на краю матрицы, имеет восемь соседей и проверка каждого из них очевидна. Но как быть с ячейкой в углу или на краю матрицы? Их можно обрабатывать как особые случаи, но лучше привести мат- рицу к такому виду, чтобы все ячейки были одинаковы с точки зрения их распо- ложения. Это можно сделать, если окружить матрицу ’’рамкой” из всегда сво- бодных клеток. Такой прием полезен при программировании многих подобных задач. Теперь у нас достаточно информации для конструирования типа данных, представляющего матрицу ячеек. Его структура определяется следующими сооб- ражениями: а) максимальный размер матрицы - 20 * 20 элементов; б) должны храниться вся матрица и рамка из свободных клеток; в) должен быть известен фактический размер матрицы; г) нам нужно уметь легко выполнять следующие операции: (i) инициализировать матрицу, устанавливая содержимое рамки; (ii) вводить шаблон в матрицу; (iii) выдавать шаблон, хранящийся в матрице; (iv) подсчитывать у данной клетки число занятых соседних клеток. Эти факторы говорят в пользу того, что двумерный массив должен составлять по крайней мере часть требуемого типа данных. Индексный тип должен принадле- жать диапазону 0..21 по обоим измерениям, поскольку места должно хватить на матрицу и на рамку при максимально возможном N. Есть, однако, несколько возможностей для выбора типа элементов массива: Вариант А он может быть типа char, и это будет удобно для считывания шаблона и его печати. Вариант Б он может быть типа 0..1, где 0 обозначает отсутствие организма, а 1 - его присутствие, что удобно при под- счете числа занятых клеток - соседей. Вариант В он может быть типа Boolean, что удобно для принятия решения о занятости клетки. К сожалению, ни один вариант не является идеальным для всех необходимых нам операций. Однако тщательное исследование показывает, что ’наиболее
Гл. 17. Использование структурных переменных 325 очевидный вариант А подходит менее всего, тогда как варианты Б и В более или менее равноценны. Здесь мы используем тип 0..1, а в упр. 17.6 вам будет предложено реализовать программу с использованием типа Boolean, Наши рассуждения оформлены в виде программы Жизнь на листинге 17.5. Определения в строках 5-13 задают соответствующие константы и типы данных, включая тип данных matrixtype для представления одной матрицы. Это тип- запись из двух полей: поле cell содержит двумерный массив с компонентами типа 0..1, а в другом поле, size, должен храниться фактический размер матрицы. Основные операции над матрицами реализуются следующими четырьмя подпрограммами: initmatrix инициализирует матрицу, определяя рамку заданного размера, который берется из параметра anymatrix-, readmatrix вводит шаблон размером anymatrix,size в массив anymatrix,cell\ writematrix выводит шаблон размером anymatrix.size из массива any matrix, cell', numberofneighbors возвращает число занятых соседей для ячейки anymatrix,cell(r,c\, а две схемы, набросанные нами выше, становятся еще двумя процедурами - simulate (’’моделирование”) и newgeneration (’’новое поколение”). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 program Life (input, output) ; { Моделирует жизненный цикл колонии организмов } const maxsize - 20; maxindex -21; type indextypeindex; matrixtype - record cell: array (indextype,indextype) of 0..1 size : \..maxsize end; procedure initmatrix (var anymatrix : matrixtype); var i, sizeplus 1 : indextype; begin { значение anymatrix.size должно быть определено } sizeplus 1 anymatrix. size + 1; with anymatrix do for i 1 to sizeplus 1 do begin ceZZ[O,Z] 0; cell(sizeplusl, /] 0; ctf//[/,0] 0; cell[i, sizeplusl] 0; end end { initmatrix }; procedure readmatrix (var anymatrix : matrixtype) ; var r, c : indextype; ch : char ; begin with anymatrix do for r 1 to size do begin for c 1 to size do begin read (ch); if ch - then cell(r,c\ 1 else cellar,c\ 0 end;
326 Гл. 17. Использование структурных переменных 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 readln end end { readmatrix }; procedure writematrix (var anymatrix : matrixtype); var r, c : indextype; begin with anymatrix do for r 1 to size do begin for c 1 to size do if cell[r,c] - 1 then write (’*’) else write (’.’); writein end; writein end { writematrix }; function numberofneighbors (var anymatrix : matrixtype; r, c : indextype) : integer, begin with anymatrix do numberofneighbors ceZ/[r-l,c-l] + cell[r-l,c] + ce//[r-l,cH] + ce//[r,c-l] +ce//(r,c+l] + cell[r+l ,c-l ] + cellfr+A ,c] + cell\r+\ ,c+l ] end { numberofneighbors }; procedure newgeneration (var oldmatrix, newmatrix : matrixtype)', var r, c : indextype; count: 0..8; begin for r 1 to oldmatrix.size do for c 1 to oldmatrix.size do begin countnumberofneighbors {oldmatrix, r, c); if oldmatrix.cell[r,c] - 1 then if (count >- 2) and (count - 4) then newmatrix. cell[r,c] 1 else newmatrix. cell[r,c] 0 else if count - 3 then newmatrix.cell[r,c] 1 else newmatrix.cell[r,c] 0 end end { newgeneration }; procedure simulate; var matrix : array [1..2] of matrixtype; numberofgenerations : 0..maxint; old, new : 1..2; begin old 1; new 2; readln (matrix (old) .size); matrix[new] .size matrixfold] .size; initmatrix (matrix fold}); initmatrix (matrix f new]); write In ('Начальная колония : ’); writein; readmatrix (matrix fold]); writematrix (matrixfold]); readln (numberofgenerations); writein ('последующие поколения : writein; for numberofgenerations numberofgenerations downto 1 do begin newgeneration (matrixfold], matrix f new]);
Гл. 17. Использование структурных переменных 327 104 105 106 107 108 109 НО 111 112 113 writematrix (matrix [new]); old new; new 3 - old { поменять old и new } end end { simulate}; begin writeln (’♦♦♦♦ РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ ЖИЗНЬ ♦♦♦♦’); writeln; simulate; writeln; writeln (’♦♦** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦♦’); end { Life }. Листинг 17.5. Один из многих способов написания программы Жизнь В окончательном варианте процедуры simulate отразилось одно дополните- льное соображение по сравнению с исходной схемой. Оно касается предложения переслать новую матрицу в старую; которое обычно подразумевает копирование всех элементов из одного массива в другой. В нашем случае оно реализовано несколько иным, более эффективным образом благодаря приему, позволяющему легко производить обмен содержимого старой и новой матриц. Прием состоит в том, что: а) описана переменная-массив из двух матриц; б) переменные old и new установлены так, что каждая индексирует одну матрицу; в) old и new меняются значениями, когда требуется пересылка матриц. Есть много других способов реализации программы Жизнь. Стиль этой программы, как и других программ данной главы, направлен на то, чтобы разви- вать у программистов представление о структурах данных как об отображении объектов реального мира, и создавать новые типы данных, содержащие всю информацию, связанную с данным классом объектов. Обучение такому способу мышления - полезный шаг на пути к созданию больших программ, которые включают не один-единственный, а много видов сложных структур данных. Упражнения 17.1. Слово АБРАКАДАБРА раньше использовалось как заклинание против малярии и других болез- ней. Чтобы оно подействовало наверняка, его писали на бумаге приведенным ниже способом и носили вокруг шеи. Напишите программу Заклинание (Charms) для печати строк таких заклинаний, содержащих слово АБРАКАДАБРА или другое слово не длиннее 12 символов. В каждом ряду программа должна печатать 4 заклинания рядом, разместив их на странице шириной в 120 символов. Данные состоят из целого, указывающего число строк для печати, и следующего за ним слова - заклинания. АБРАКАДАБРА АБРАКАДАБР АБРАКАДАБ АБРАКАДА А Б Р А К А Д АБРАКА АБРАК А 5 Р А А Б Р А Б А
328 Гл. 17. Использование структурных переменных 17.2. Напишите и протестируйте следующие подпрограммы, считая что linetype - тип данных, опре- деленный в листинге 17.2 (с. 313) и модифицированный для обработки строк длиной до 100 символов: а) функция function blank (var anyline : linetype) : Boolean', возвращает величину true, если anyline не содержит других символов, кроме пробелов, и величину false в противном случае. б) процедура procedure convertandremove (var anyline : linetype)', переводит все буквы в anyline в верхний регистр и убирает все остальные символы; в) процедура procedure censor (wordlength : integer, firstletter : char, var anyline : linetype); заполняет в anyline звездочками все слова, которые имеют длину wordlength и начинаются с буквы firstletter. г) функция function palindrome (anyline : linetype) : Boolean', возвращает true, если anyline содержит фразу, являющуюся палиндромом и false в противном случае. Фраза называется палиндромом, если она читается от конца к началу так же, как от начала к концу при игнорировании всех знаков, кроме букв, и в предположении, что большие и маленькие буквы одинаковы. Фразы "Шорох весь сев хорош” или "Аргентина манит негра” - палиндромы. д) функция function anagram (linel, line2 : anagram) : Boolean', возвращает величину true, если фраза в linel является анаграммой фразы из Нпе2, и величину false в противном случае. Одна фраза является анаграммой другой, если она содержит те же буквы, но в другом порядке. Все знаки кроме букв игнорируются, а большие и маленькие буквы считаются одинаковыми. Например, "Харитон Матекин” - анаграмма имени "Антиох Кантемир”. 17.3. Напишите программу под названием Выравнивание (Justify), вводящую текст как последовательность абзацев, а затем выводящую его с выравниванием по краям при заданной ширине. Точные требования таковы: Входные данные Текст хранится в виде последовательности абзацев, разделенных пустыми строками или строками, которые заполнены пробелами. Можно допустить, что каждая строка содержит не менее двух слов, кроме, может быть, последней строки абзаца. Результаты Текст должен быть выведен строками, содержащими ровно по 70 символов и с выравненными левым и правым краями, со следующими исключениями: а) пробелы в начале строки должны быть сохранены; б) последнюю строку абзаца не следует выравнивать. 17.4. Отпечаток пальца может быть представлен кодом в диапазоне 10000..99999 и последовательнос- тью из 12 вещественных чисел, полученных в результате измерений отпечатка. Два действительных числа считаются "равными”, если разница между ними составляет не более 5% от большего числа. Два множества измерений считаются "совпадающими”, если не менее девяти из двенадцати пар "равны”. Входной файл содержит данные по отпечаткам пальцев, найденным на месте преступления, а также данные о множестве известных преступников. Данные о каждом новом отпечатке записываются с новой строки. Все данные допустимы. Напишите программу под названием СличитьОтпечатки (MatchPrints), которая читает файл данных и печатает отчет, в котором перечисляются коды отпечатков, "совпадающих” с новым.
Гл. 17. Использование структурных переменных 329 17.5. Напишите программу под названием Календарь (Calendar), которая читает два целых числа, а именно год и день недели, на который падает первый день года (1 означает воскресенье, 2 - понедельник, и.т.д.) и печатает календарь этого года в формате, изображенном на табл. 17.3. Таблица 17.3 ★★★★★ AikЛАгЖтк AAA A A A AA AA A AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AA AA КАЛЕНДАРЬ HA 1989 ** aa aa aa ★ ЯНВАРЬ ★ ★ ★ вс ПН ВТ СР ЧТ ПТ СБ ★ 1 2 3 4 5 6 7 ★ 8 9 10 11 12 13 14 ★ 15 16 17 18 19 20 21 ★ 22 23 24 25 26 27 28 ★ 29 30 31 ★ ★ ★ ФЕВРАЛЬ аа аа ** ВС ПН ВТ СР ЧТ ПТ СБ ** 1 2 3 4 аа 5 6 7 8 9 10 11 аа 12 13 14 15 16 17 18 аа 19 20 21 22 23 24 25 аа 26 27 28 аа аа аа Замечание. В стандартном Паскале вызов встроенной процедуры page обеспечивает печать результатов с новой страницы. 17.6. Перепишите программу Жизнь (с. 327), используя другое определение типа matrixtype: matrixtype - record cell: array [indextype, indextype] of Boolean', size : \..maxsize end; Измените программу так, чтобы моделирование останавливалось с выдачей соответствующего сообщения в следующих ситуациях: а) два последовательных поколения одинаковы; б) все организмы вымерли.
18 ОБРАБОТКА ПОСЛЕДОВАТЕЛЬ- НОСТЕЙ ОБЪЕКТОВ Многие виды информации могут быть представлены в виде последовательности. или списка объектов, где в* общем случае каждый объект может состоять более чем из одного компонента. В сущности, если элементы в последовательности сами могут быть последовательностями, то почти любая информация представима в таком виде. Обработка последовательностей объектов очень важна в обработке информации и применениях ЭВМ. Последовательность определяется как структура данных, состоящая из нуля или более информационных объектов известного типа данных, располо- женных в заданном порядке. Приведем примеры последовательностей: телефонный справочник; закупочный лист; расписание вылетов в аэропорту. В некоторых случаях между соседними объектами последовательности существу- ет соотношение, определяющее их порядок. Так устроена и телефонная книга, где информация расположена в алфавитном порядке, и расписание вылетов, где рейсы обычно приведены в хронологическом порядке. Иногда порядок располо- жения элементов определяется не информацией, содержащейся в объектах, а другими факторами. Например, объекты покупки скорее всего будут записаны в том порядке, в котором они пришли в голову покупателю. Тем не менее и в этом списке есть своя упорядоченность, и можно говорить о его первом или последнем элементе. Таким образом, поддержание последовательности в опреде- ленном порядке или ее переупорядочение играет важную роль при обработке последовательностей. В данной главе мы намерены обсудить один способ представления последовательности объектов в памяти компьютера, а также рассмотреть, какие операции и как могут быть реализованы применительно к последовательностям. В качестве типичного примера последовательности мы будем рассматривать перечень блюд меню. Для каждого блюда меню указывается условный номер, краткое описание и цена. Ниже приведен пример меню: код описание цена 420 Слоеный пирожок 6.95 73 Мозги с горошком 2.75 361 Сосиски с капустой 6.95 311 Пулярка жареная 3.75 762 Огурец соленый 5.99
Гл. 18. Обработка последовательностей объектов 331 Конечно, полное меню может содержать несколько сотен названий и, хотя в учебных целях мы будем пользоваться короткими последовательностями, следу- ет помнить, что в реальных приложениях последовательности весьма часто бы- вают длинными. Прежде всего мы должны сконструировать тип данных для представления последовательностей объектов, что мы и сделаем в три этапа. 18.1. Строки и новый тип phrase type В качестве первого шага к определению типа данных, представляющего полную последовательность пунктов меню, рассмотрим листинг 18.1. Программа содержит определение нового типа с именем phrasetype и процедуру с именем readphrase, которая читает 25 символов из входного файла и запоминает их в параметре anyphrase типа phrasetype. const maxphrase length - 25 ; type phraseindextype - 1 ..maxphrase length', phrasetype - packed array [phraseindextype} of char, procedure readphrase (var anyphrase : phrasetype)', var i: phraseindextype', begin for i1 to maxphraselength do read (anyphrase [i]) end { readphrase }; Листинг 18.1. Тип данных phrasetype и процедура, читающая фрагмент текста Мы уже обсудили один метод представления последовательности символов, а именно тип данных linetype, см. гл. 17 (с. 313). В этой структуре данных последовательность символов хранится в массиве, а ее длина - в другом поле этой структуры. Тип phrasetype сконструирован для хранения последовательно- сти символов фиксированной длины. При изменении константы maxphraselength фиксированная длина последовательности меняется. Отметим, что phrasetype яв- ляется менее гибкой структурой, чем linetype, но для многих приложений она подходит. Ее удобство обусловлено наличием слова packed (упакованный) перед сло- вом array в определении типа phrasetype. Как следует из определения синтаксиса нового типа данных, см. рис. 16.1 (с. 286), служебное слово packed можно по- местить в стандартном Паскале перед любым новым структурным типом. В об- щем случае использование phrasetype приводит к тому, что элементы нового типа представляются в процессоре максимально компактно, хотя часто такое представление сопровождается увеличением времени обработки. В специальных случаях, когда: а) используется одномерный массив символов и б) индексный тип определен как диапазон 1..л (а не О..п или -n..zi) упакованный массив называется строкой символов и обладает следующими свойствами: а) строка символов может быть выведена целиком с помощью одного опе- ратора вывода - write или writein', б) если строки имеют одинаковую длину, их можно сравнивать с помощью обычных операций сравнения;
332 Гл. 18. Обработка последовательностей объектов в) строковая константа соответствующей длины может быть присвоена строковой переменной или параметру1. Например, если thisphrase и thatphrase - переменные или константы типа phrasetype, допустимыми являются операторы write (’Значение переменной thisphrase : thisphrase)', writeln (thisphrase, thatphrase)', if thisphrase > thatphrase then ... while thisphrase <> ’страница девяносто восемь’ do ... thatphrase :« В последних двух примерах каждая строковая константа (литерал) содержит 25 символов, что соответствует длине типа phrasetype. Сравнение строк основано на порядке символов в наборе и отвечает нашим обычным представлениям об упорядоченном расположении слов, скажем, в словаре. Тот факт, что сравниваться должны лишь строки одинаковой длины, а также возможное наличие пробелов требуют известной осторожности при обра- щении со строками. Например, ’This’ - ’This’ равно true, так как все символы совпадают, ’This’ - ’this’ равно false, так как ’Т’ <>’t’, ’ this’ = ’this’ равно false, так как ’ ’ <> ’t’, ’beard’ < ’heard’ равно true, так как ’b’ < ’h’, ’their’ > ’there’ равно false, так как ’Г < ’г’, ’50000’ < ’FIFTY’ равно true, так как ’5’ < ’F’, ’and’ <= ’AND’ равно false, так как ’а’ < ’А’. В приведенных примерах предполагается, что используется набор символов ASCII (с. 45) Таким образом, описание в листинге 18.1 реализует тип данных для 25-символьных строк, процедура readphrase может применяться для ввода таких строк, а средства для их вывода, сравнения и присваивания имеются в стандарт- ном Паскале. 18.2. Новый тип itemtype Следующий шаг на пути к созданию типа данных для последовательности объектов - определение нового типа для представления одного объекта и реали- зация необходимых над ним операций. Типичная позиция меню выглядит так: 420 Слоеный пирожок 6.95 а соответствующая структура для хранения объектов этого типа имеет вид код описание цена 0..999999 phrasetype real Новый тип данных - itemtype (’’тип объекта”), имеющий соответствующую структуру, определен в листинге 18.2. Это запись с тремя полями. Поля называ- 1 Более точным было бы ввести понятия строкового типа и строковой переменной (аналогично типу массива и переменной массива). Напомним также, что строковые значения (константы) в книге именуются литералами. - Примеч. пер.
Гл. 18. Обработка последовательностей объектов 333 ются - code, description, price (’’код”, ’’описание”, ’’цена”). Для определения поля ’’код” используется новый тип numbertype. Поле ’’описание” имеет тип phrasetype, определенный в разд. 18.1, а поле ’’цена” - тип real. Заметим, что представление денежной величины с помощью типа real мо- жет привести к ошибкам при оперировании с большими величинами. Так, на- пример, на процессоре с шестью значащими цифрами максмальное надежно представимое вещественное число равно 9999.99. Однако в нашем примере цены в меню представлены незначительными величинами, и поэтому они могут быть описаны как real. const maxnumber - 99999; type numbertype - 0.. maxnumber, itemtype - record code : numbertype; description : phrasetype; price : real end; procedure readitem (var anyitem : itemtype); { ввод объекта } begin with anyitem do begin read (code); readphrase (description); readln (price) end end { readitem }; procedure writeitem (var anyitem : itemtype) ; { вывод объекта } begin with anyitem do writein (code : 5, description : 30, price : 10:2) end { writeitem }; Листинг 18.2. Новый тип itemtype и процедуры для ввода и вывода объекта Процедуры readitem и writeitem из листинга 18.2 реализуют ввод элемента из входного файла и вывод элемента целиком в выходной файл. Здесь нам нуж- ны только эти операции, однако в других примерах может понадобиться значи- тельно более широкий спектр операций над отдельными элементами последова- тельностей. 18.3. Новый тип sequencetype Теперь мы готовы к заданию нового типа данных для представления по- следовательности объектов. Он назван sequencetype и определен вместе с тремя процедурами в листинге 18.3. Обратите внимание на константы maxlength и maxindex, а также тип indextype. Как и предполагает ее имя, maxlength содер- жит максимальную длину последовательности, которая может храниться в пере- менной или параметре типа sequencetype. Значение maxindex на единицу боль- ше, чем maxlength, a indextype есть диапазон maxindex (а не maxindex), что вместе гарантирует наличие двух дополнительных записей - в начале и в конце последовательности. Иногда дополнительные записи можно приспособить для де- ла (см. разд. 18.4, 18.6 и 18.7), а расширенный диапазон индексов предотвраща- ет ошибки на границах массива, когда последовательность или пуста, или полна. Новый тип sequencetype представляет собой запись с пятью полями, кото- рые имеют следующее назначение:
334 Гл. 18. Обработка последовательностей объектов item является массивом записей для хранения фактических элементов массива (одного за другим) и начинается с элемента item[l]; в length хранится текущая длина последовательности; cursor 1 и cursor! могут применяться для индексирования одного элемента массива item*, их величины всегда лежат в диапазоне 0. length + 1. Это гарантирует, что каждый индекс всегда указывает либо на элемент вну- три последовательности, либо на дополнительный элемент перед ней, либо на элемент сразу после нее; ОК является величиной типа Boolean, которая может использоваться как ’’красный свет”, указывая, успешно ли прошла определенная операция над последовательностью (см. разд. 18.6). const max length - 500; maxindex - 501; { - maxlength + 1 } type indextype - 0.. maxindex; sequencetype - record item : array [indextype] of itemtype; length : $..maxlength; cursor 1, cursor2 : indextype; OK : Boolean end; procedure initializesequence (var anysequence : sequencetype); { инициализирует последовательность } begin anysequence. length 0 end { initializesequence }; function spaceavailiable (var anysequence : sequencetype) : Boolean; { есть свободное место } begin spaceavailiable anysequence. length < maxlength end { spaceavailiable }; procedure writesequence (var anysequence : sequencetype); { выдает последовательность } var i : indextype; begin with anysequence do for i 1 to length do writeitem (item[i\) end { writesequence }; Листинг 18.3. Новый тип sequencetype и три операции над последовательностью Описанная структура называется непрерывным представлением последова- тельности, так как ее элементы хранятся в массиве (под именем item) в том же порядке, в котором они расположены в последовательности. В табл. 18.1 пока- зана переменная mailorderlist типа sequencetype, которая содержит рассмотрен- ную выше последовательность элементов. Три подпрограммы, описанные в листинге 18.3, открывают обширный на- бор процедур и функций, которые мы создадим в нескольких следующих разде- лах этой главы. Каждая из процедур реализует одну базовую операцию над па- раметром anysequence типа sequencetype. Они работают следующим образом: процедура initializesequence инициализирует последовательность anysequence, просто обнуляя поле length. Всякую последовательность необходимо инициализировать перед тем, как оперировать с ней; функция spaceavailable возвращает величину true, если есть место еще для одного элемента в anysequence, и величину false в противном случае.
Гл. 18. Обработка последовательностей объектов 335 Наличие этой функции существенно, так как свободное место в после- довательности есть необходимое условие для всякой операции, ее увели- чивающей. Функция может служить для проверки данного условия пе- ред каждым выполнением такой операции; процедура writesequence выдает все элементы последовательности anysequence с помощью процедуры writeitem из листинга 18.1. mailorderlist length +----------+ I 5 | cursorl +---------+ I ? i +---------+ cursor2 I з i +---------+ Таблица 18.1. Переменная с именем mailorderlist типа sequencetype, содержащая последо- вательность пунктов из меню item code description price +- 0 1 ? —+- ? -+ I ? 1 I 420 i Слоеный пирожок | 6.95 2 I 73 ! Мозги с горошком j 2.75 з I 361 Сосиски с капустой | 6.95 4 I 311 i Пулярка жареная | 3.75 5 I 762 i Огурец соленый | 5.99 6 I +- ? —+- ? i ? +---------+------------------------+----------+ 501 | ? | ? | ? | +---------+------------------------+----------+ OK Бросается в глаза, что в листинге 18.3 отсутствует процедура чтения последова- тельности. Она опущена потому, что, как мы увидим в разд. 18.9, при построе- нии последовательности часто оказывается удобным сначала инициализировать ее, а потом вставлять элементы по одному. 18.4. Вставка элемента Последовательность является динамической структурой данных: на нее можно смотреть как на пустую вначале, расширяющуюся путем вставки элемен- тов и уменьшающуюся путем их исключения. Список покупок, например, сна- чала представляет собой чистый лист бумаги, который постепенно заполняется названиями товаров. После того как покупка сделана, соответствующее название вычеркивается, и в конце концов список снова может стать пустым. Вставка нового элемента и исключение существующего элемента являются основными операциями применительно к последовательностям. Первую мы рассмотрим в этом разделе, а вторую - в разд. 18.5. Для реализации вставки элементов в последовательность было бы достато- чно одной процедуры, но мы рассмотрим две. Обе приведены в листинге 18.4. Первая процедура называется insertattail. Получая новый элемент в виде пара- метра newitem, она вставляет копию этого элемента в конец последовательности,
336 Гл. 18. Обработка последовательностей объектов заданной параметром anysequence, другими словами - после всех имеющихся элементов последовательности. У этой процедуры есть предусловие, а именно: в anysequence должно хватить места для еще одного элемента. Время исполнения процедуры равно 0(1), поскольку количество необходимых действий не зависит от длины последовательности. procedure insertattaU (var newitem : itemtype] var anysequence: sequencetype)] { Вставляет в данную последовательность новый элемент после последнего } begin with anysequence do begin length length + 1; item [length] newitem end end {insertattaU}; procedure insertatcursor (var newitem : itemtype] var anysequence : sequencetype); { Вставляем новые элементы перед тем элементом, на который указывает cursor2, при этом cursor2 не меняется } var i: indextype] begin with anysequence do begin { есть место и cursor2 из 1..length + 1} for ilength downto cursor2 do item [H-l] item [i]; item [cursor2] newitem] length length + 1 end end {insertatcursor }; Листинг 18.4 Процедуры для вставки элемента в (непрерывную) последовательность Результат вызова процедуры показан в табл. 18.2. Здесь приведено значение переменной mailorderlist после выполнения оператора процедуры insertattaU (thisitem, mailorderlist) причем значение thisitem указано в верхней части таблицы, а начальное значение mailorderlist то же, что и в табл. 18.1. Вторая процедура из листинга 18.5 называется insertatcursor. Она имеет два параметра: newitem и anysequence. Ее назначение - вставлять новый элемент в anysequence в позицию cursor2. Мы должны внимательно обсудить две детали этой процедуры: а) что мы имеем в виду под словами ”в позицию cursor!”*! б) куда указывает курсор после вставки? Ответы на эти вопросы даны в комментарии в начале процедуры. Слова ”в пози- цию cursor!” означают, что новый элемент вставляется непосредственно перед тем элементом, на который указывает курсор. Сам курсор не двигается и при выходе из процедуры указывает на только что вставленный элемент. Результат вызова процедуры insertatcursor (thatitem, mailorderlist) отражен в табл. 18.3. Значение thatitem указано в верхней части таблицы, значение mailorderlist при входе в процедуру то же, что и в табл. 18.2.
Гл. 18. Обработка последовательностей объектов 337 Таблица 18.2. Выполнение оператора процедуры insertattail {thisitem, mailorderlist) со значением mailorderlist из табл. 18.1 приводит mailorderlist к изображен- ному здесь виду. Обратно, исполнение операто- ра deleteattail {mailorderlist) над значением mailorderlist' показанным в таблице, даст значе- ние из табл. 18.1 code description price thisitem I 395 j Сладкий пирожок | 6.50 length item code description price +-------+---------------------+--------+ +--------+ 0 | ? | ? | ? . | | g | +-------+---------------------+--------+ +--------+ 1 | 420 | Слоеный пирожок | 6.95 | сигsoг1 +--------+---------------------+--------+ +--------+ 2 | 73 | Мозги с горошком | 2.75 | | 9 | +--------+---------------------+--------+ +--------+ +—> 3 | 361 | Сосиски с капустой | 6.95 | cursor2 | +-------+---------------------+--------+ +--------+ | 4 | 311 | Пулярка жареная | 3.75 | | з । —+ +--------+--------------------+---------+ +--------+ 5 | 762 | Огурец соленый | 5.99 | 6 | 395 | Сладкий пирожок | 6.50 | Ок +--------+-----------------------+---------+ +---------+ I 7 I +--------+-----------------------+---------+ +---------+ 501 | ? | ? I ? I Так же как и insertattail, процедура insertatcursor имеет предусловие, гласящее, что в параметре anysequence должно быть свободное место по крайней мере для одного элемента. У нее есть еще одно предусловие, а именно: значение cursor2 должно лежать в диапазоне l..length+l. Это обеспечивает вставку нового элемента перед существующими элементами последовательности, между ними или же сразу после них. Как указано сбоку табл. 18.3, в общем случае выполнение insertatcursor включает в себя сдвиг существующих элементов последовательности. Этот сдвиг выполняется в процедуре оператором for и является наиболее продолжительной частью процесса, особенно если обрабатывается длинная последовательность. Наихудшим является случай, когда cursor2 равен 1 и требуется сдвигать все су- ществующие элементы. В наилучшем случае cursor2 изначально равен length+1, элементы не двигаются и процедура insertatcursor эквивалентна процедуре insertattail. В среднем приходится двигать около половины элементов, поэтому среднее время исполнения равно О (АО, где N - длина последовательности. 12 Заказ № 1110
338 Гл. 18. Обработка последовательностей объектов Таблица 18.3. Оператор процедуры insertatcursor (thatitem, mailorder list), выполненный над значением mailorderlist из табл. 18.2, приводит mailorderlist к изображенному здесь виду. Обратно, выполне- ние оператора deleteatcursor {mailorderlist) над значением mailorder list, показанным в таблице, даст значение из табл. 18.2 thatitem code description price +--------+-----------------+------4 | 399 | Редька в меду | 4.15 | +--------+-----------------+------+ length item code description price +--------+--------------------+------+ +---------+ 0 | ? | ? | ? | | 7 | +--------+--------------------+------+ +---------+ 1 | 420 |Слоеный пирожок | 6.95 | cursorl +--------+--------------------+------+ +---------+ 2 | 73 |Мозги с горошком | 2.75 | | ? | +---------------------------------+--------------------+------+ +---------+ +—> 3 | 399 |Редька в меду | 4.15 | cursor2 | +--------+--------------------+------+ +---------+ | 4 | 361 | Сосиски С капустой | 6.95 I Эти элементы | з | —+ +--------+--------------------+------+ +---------+ 5 | 311 |Пулярка жареная | 3.75 | сдвигаются, когда +--------+--------------------+------+ 6 | 762 |0гурец соленый | 5.99 | item[3] вставляется +-----------+--------------------+------+ 7 | 395 |Сладкий пирожок | 6.50 | или удаляется ОК +--------+--------------------+------+ | ? | +---------------------------------+--------------------+------+ +---------+ 501 | ? | ? | ? | 18.5. Исключение элемента Вставку элемента мы обсудили подробно, поэтому его исключение рассмо- трим вкратце. Исключение есть действие, противоположное вставке, и можно ожидать, что процедура исключения выполняет нечто обратное тому, что делает соответствующая процедура вставки. В листинге 18.8 вы найдете две процедуры deleteattail и deleteatcursor, которые соответствуют процедурам insertattail и insertatcursor из листинга 18.5. Процедуры исключения просто уничтожают элемент последовательности; исключенный элемент никуда не копируется. Поэтому у каждой процедуры имеется только один параметр - anysequence. Просмотрев процедуры deleteattail и deleteatcursor, вы можете убедиться сами, что после выполнения оператора deleteatcursor (mailorderlist) последовательность mailorderlist из табл. 18.3 превратится в последовательность из табл. 18.2, а исполнение оператора
Гл. 18. Обработка последовательностей объектов 339 deleteattail (mailorderlist) вернет последовательность в состояние, изображенное в табл. 18.1. Если быть абсолютно точным, окончательное состояние переменной mailorderlist будет от- личаться от первоначального. Из табл. 18.1 видно, что величины item [6] и item[T] не определены. Прослеживая исключение двух элементов, мы увидим, что оба эти элемента в действительности будут содержать копии одного из уда- ленных элементов, но поскольку length равно 5 в табл. 18.1, эти элементы не яв- ляются частью последовательности. Величины, оказавшиеся в них, недоступны. procedure deleteattail (var anysequence : sequencetype); { Исключение последнего элемента в данной последовательности } begin with anysequence do { length > 0 } length length - 1 end { deleteattail}; procedure deleteatcursor (var anysequence : sequencetype) -, { Исключение элемента, на который указывает cursor2 } var i : indextype\ begin with anysequense do begin { length > 0 and cursor2 из 1..length } length := length - 1; for i cursor2 to length do item[i] item[i+l] end end { deleteatcursor }; Листинг 18.5. Процедуры для исключения элемента из (непрерывной) последовательности У обеих процедур исключения есть предусловие: величина length должна быть больше нуля. Если это условие не выполняется, мы не обнаружим исключаемого элемента. Кроме того, в процедуре deleteatcursor величина cursor! должна ле- жать в диапазоне 1..length. Это гарантирует, что cursor! указывает на элемент, лежащий внутри последовательности, а значит, элемент последовательности действительно будет исключен. Время исполнения deleteattail составляет 0(1), а среднее время исполнения процедуры deleteatcursor составляет О (АО, где N - длина последовательности. 18.6. Поиск элемента Процесс нахождения элемента последовательности по значению одного или более чем одного поля называется поиском в последовательности. Мы огра- ничим наше изучение поиска случаем, когда искомый элемент идентифицирует- ся по значению одного из полей, называемого полем поиска. Само это значение называется целевой величиной. Если величины в каком-нибудь поле всех элеме- нтов последовательности отсортированы по возрастанию, это поле называется упорядоченным, в противном случае - неупорядоченным полем. Выбор метода для поиска целевой величины зависит от того, является поле поиска упорядоченным или нет. 18.6.1. Полный поиск Предположим, у вас есть обычный телефонный справочник, и вы хотите найти фамилию и адрес человека по известному вам телефонному номеру. У вас возникнет проблема, если допустить, что такого номера нет: чтобы понять это, 12*
340 Гл. 18. Обработка последовательностей объектов придется пролистать весь справочник. Если же такой номер существует, вам придется просмотреть в среднем только половину справочника. В любом случае это утомительная задача! По возможности избегайте поиска по всей после- довательности, даже с помощью компьютера. Впрочем, иногда он неизбежен. Для иллюстрации поиска целевой величины по неупорядоченному полю обратимся к нашему меню. Рассмотрим поиск элемента с заданным описанием в случае, когда последовательность элементов меню не упорядочена. Процедура, названная completesearch и выполняющая эту задачу, показана в листинге 18.6. Она имеет два параметра: параметр target, содержащий описание, передается по значению, а параметр anysequence, содержащий последовательность, передается по ссылке. procedure completesearch {target : phrasetype\ var anysequence : sequencetype); { Ищет в данной последовательности элемент с данной целевой величиной в поле description. Если это удается, устанавливает cursor2 на этот элемент и тогда ОК равен true. Иначе оставляет ОК - false, a cursor2 - length+\ } begin with anysequence do begin item [length + 1 ] .description target', { поставить часового } cursor2 := 1; while item [cwryor2] .description <> target do cursor2 cursor2 + 1; { item [cursor2] .description - target } OK cursor2 <> length + 1 end end { completesearch }; Листинг 18.6. Поиск в последовательности по неупорядоченному полю Первое, что надо отметить в процедуре completesearch, это способ переда- чи результатов. Используются два поля в параметре anysequence, а именно cursor2 и ОК. Если будет найден элемент с требуемым описанием, то на него укажет cursor2, а ОК будет равен true. Иначе ОК будет равен false, a cursor2 будет равен length+\. Во-вторых, посмотрим на тот способ поиска, который здесь применен. Найти нужное описание можно единственным образом. Нужно начать с одного конца последовательности и просмотреть каждый элемент, пока не будет найден искомый или не будет достигнут другой конец последовательности. Следователь- но, существуют две причины прекращения поиска и условие завершения запи- сывается так: { Нужный элемент найден или конец последовательности } Используя cursor2 для указания на текущий элемент, запишем его в виде { (item [cursor2] .description = target) or (cursor2 = length+\) } и работая обратным методом, получим следующий цикл: cursor2 := 1; while (item [cursor2\ .description > target) and (cursor2 <> length+1) do cursor2 := cursor2 + 1; { (item[cursor2\ .description = target) or (cursor2 = length+\) } Однако в процедуре completesearch поиск запрограммирован по-другому. В гл. 15 (с. 276) мы отмстили, что если цикл исполняется много раз, его можно сделать более эффективным, упростив управляющее логическое выраже-
Гл. 18. Обработка последовательностей объектов 341 ние. Так можно поступить и сейчас. В приведенном операторе while обе состав- ляющие логического выражения являются необходимыми, так как нет уверенно- сти, что искомый элемент будет обнаружен. Если бы мы определенно знали, что найдем нужный элемент, то условное выражение можно было бы упростить и сделать оператор более эффективным. Прием, гарантирующий успешный поиск, заключается в том, что копия искомого значения помещается в поле description элемента, следующего за концом последовательности. Этот прием называется поставить часового. Задача часового - несение охраны, и в данном случае он охраняет нас от опасности выйти при поиске за границу последовательности. Этот прием использован в процедуре completesearch, поскольку даже в последовательности максимальной длины в конце всегда есть запасной элемент (с. 333), и на это место можно поставить часового. Использование часового гарантирует, что при выходе из оператора while значение item [cursor2\ .description всегда равно target. Заключительный оператор OK :e cursor2 <> length + 1 делает ОК истинным, если искомое значение было найдено внутри последовате- льности, и ложным, если мы достигли часового. Если в фактической последова- тельности оказалось больше одного элемента с искомым полем описания, данная процедура находит только первый элемент. Таблица 18.4. Значение переменной mailorderlist после выполнения оператора процедуры completesearch {thisdescription, mailorderlist) thisdescription length t+-------+ I 5 | +-------+ cursorl +-------+ I ? I +-------+ cursor2 +--------------+ +—> | 4 | —+ OK +--------+ I true | +-----------------+ |Пулярка жареная | +-----------------+ item code description price +---------------+---------------------+-------+ 0 I ? I ? I ? I +------------+--------------------+---------+ 1 | 420 | Слоеный пирожок | 6.95 | +--------+---------------------+--------+ 2 | 73 | Мозги с горошком | 2.75 | +--------+---------------------+--------+ 3 | 361 | Сосиски с капустой| 6.95 | +--------+---------------------+--------+ 4 | 311 | Пулярка жареная | 3.75 | +--------+---------------------+--------+ 5 | 762 | Огурец соленый | 3.75 | +--------+---------------------+--------+ 6 | ? | Пулярка жареная | ? |<-часовой +--------+---------------------+--------+ +--------+--------------- ------+-------+ 501 | ? | ? | ? | В табл. 18.4 показан результат оператора процедуры completesearch {thisdescription, mailorderlist)
342 Гл. 18. Обработка последовательностей объектов где переменная thisdescription имеет величину, указанную в верхней части таблицы, а начальное значение mailorderlist взято из табл. 18.1. Так как cursor2 по окончании равен 4, а ОК равно true, искомое значение обнаружено в item [4]. Время исполнения процедуры completesearch явно зависит от того, где найдено искомое значение. В худшем случае, когда поиск останавливается на часовом, строка while item [cursor2\ .description otarget do исполняется 1+jV раз, где N - длина последовательности. В этом случае временная сложность процедуры completesearch становится равной О (АО. Мы обсудили поиск целевой величины в поле description, но точно такой же алгоритм может применяться для поиска по любому другому неупорядочен- ному полю. Например, чтобы переделать процедуру completesearch в процедуру для поиска по полю price, нужно заменить "phrasetype" на "real” в заголовке процедуры и заменить "description” на "price" во всей процедуре. Если обе процедуры включены в одну программу, они, конечно, должны иметь разные имена. 18.6.2. Линейный поиск Искать всегда проще, если объекты, среди которых вы ищете, как-то орга- низованы. Вот почему записи в телефонном справочнике и слова в словаре рас- положены в алфавитном порядке. Самый простой метод организации элементов в последовательности состоит в том, что выбирается некоторое поле и элементы сортируются по возрастанию значений этого поля. В примере, который приводи- тся ниже, элементы отсортированы по возрастанию значений поля code: code description price 108 диван 2.40 228 чемодан 6.95 356 саквояж 14.95 497 картина 13.20 607 корзина 2.25 849 картонка 17.95 996 маленькая собачонка 4.95 Мы будем пользоваться этой отсортированной последовательностью как иллюст- рацией при обсуждении двух методов поиска по упорядоченному полю. Сначала рассмотрим процедуру linearsearch (’’линейный поиск”) из лис- тинга 18.7. У нее два параметра: target, который содержит искомое значение кода и передается по значению, и sorted sequence, который напоминает своим именем, что это упорядоченная последовательность, и передается по ссылке. Здесь, как и в процедуре completesearch, используется часовой, поиск начинает- ся с i'tem[l], a cursor2 и ОК возвращают результат. Она отличается от процеду- ры completesearch следующим: а) Поиск можно закончить, если найдена величина, большая или равная искомой, так как величины в поле code упорядочены по возрастанию. б) Cursor2 будет указывать на часового, только если искомый код превышает наибольший код в последовательности. в) После окончания поиска cursor2 всегда указывает или на элемент, соде- ржащий искомую величину, или на позицию, где этот элемент должен был бы стоять. Это позволяет применять данную процедуру для поиска места вставки нового элемента в последовательность (см. разд. 18.7).
Гл. 18. Обработка последовательностей объектов 343 procedure linearsearch {target: numbertype', var sortedsequence : sequencetype)', { Последовательность отсортирована no полю code. Процедура ищет элемент с заданным значением в этом поле. В случае успеха cursor2 указывает на найденный элемент, а ОК равен true. Иначе cursor2 указывает на элемент, перед которым должен стоять элемент с искомой величиной, а ОК равен false } begin with sortedsequence do begin item [length + \}.code target, { поставить часового } cursor2 1; while item [cursor2].code < target do cursor2 cursor2 + 1; { item[cursor2] .code >- target и cursor2 в пределах 1..length + 1} OK {item[cursor2} .code - target) and {cursor2 <> length+V) end end { linearsearch }; Листинг 18.7. Поиск в последовательности по упорядоченному полю (линейный поиск) Таблица 18.5. Переменная sortedmailorderlist после выполне- ния оператора процедуры linearsearch (thiscode, sortedmailorderlist) thiscode | 356 | +---------+ length item 7 -+ 1 -+ 0 1 I +- cursorl -+ 2 +- ? -+ +—> 3 +- I cursor2 -+ I I 4 +- I 3 1 —+ +- -+ 5 I OK -+ 6 7 +- I true -+ 8 +- 501 code description price ? ? | ? 108 диван | 2.40 228 чемодан 6.95 356 1 саквояж 1 14.95 497 i картина i 13.20 607 корзина 2.25 849 картонка 1 17.95 996 I маленькая собачонка 1 4.95 356 i ? i ? T T 1 ? i ? | <-часовой Результат выполнения оператора процедуры linearsearch {thiscode, sortedmailorderlist)
344 Гл. 18. Обработка последовательностей объектов показан в табл. 18.5, где переменная sortedmailorderlist содержит образец корот- кой последовательности, а переменная thiscode приведена в верхней части таб- лицы. Величины cursor2 и ОК указывают, что искомый код обнаружен в item [3]. Чтобы оценить временную сложность процедуры linearsearch, нам нужно учесть только два факта. В худшем случае, когда целевая величина больше любой величины в последовательности, строка while item \cursor2\ .code < target do исполняется 1+7V раз, где N - длина последовательности. В среднем же просмат- ривается половина последовательности и эта строка исполняется 1+Л72 раз. Отсюда временная сложность процедуры linearsearch равна О(N). Наконец, если рассматриваемая последовательность отсортирована по полю description, а не по полю code, то для поиска по полю description в процедуре linearsearch и процедуре binarysearch, которую мы рассмотрим в следующем разделе, нужно заменить ”numbertype” на ”phrasetype” в заголовке процедуры и заменить идентификатор ”code” на ”description” везде, где он встреча- ется. Измененные процедуры будут работать правильно, так как в стандартном Пас- кале операции сравнения применимы к строкам, а значения типа phrasetype являются строками. 18.6.3. Двоичный поиск Вторая процедура, которая ищет целевую величину в упорядоченном поле поиска, приведена в листинге 18.8. Она называется binarysearch и по результа- там работы эквивалентна процедуре linearsearch, однако основана на другом методе, известном как алгоритм двоичного поиска. procedure binarysearch {target: numbertype-, var sortedsequence : sequencetype); { Данная последовательность сортирована no полю code. Процедура ищет элемент с искомым значением в этом поле. В случае успеха cursor2 указывает на найденный элемент, а ОК равен true. Иначе cursor? указывает на элемент, перед которым должен стоять элемент с искомой величиной, а ОК равен false } var mid : indextype', begin with sortedsequence do begin item [length + l].code target, { поставить сторожа } cursor! 1; cursor? length + 1; while cursor! <> cursor? do begin mid {cursor! + cursor?) div ?; if item[mid] .code < target then cursor! mid + 1 { отвергнуть первую половину } else cursor? mid { отвергнуть вторую половину } end; { item[cursor?].code >- target и cursor? в пределах 1..length + 1} OK {item[cursor?] .code - target) and {cursor? <> length + 1) end end { binarysearch }; Листинг 18.8. Поиск в последовательности по упорядоченному полю
Гл. 18. Обработка последовательностей объектов 345 Принцип, лежащий в основе алгоритма двоичного поиска (и некоторых других важных алгоритмов), состоит в том, что иногда удается последователь- но уменьшать объем задачи до такой степени, что ее решение в конце концов становится тривиальным. Такие алгоритмы обычно относят к алгоритмам сокращения размерности. Центральная проблема поиска величины в последовательности - это длина последовательности, и объем задачи сократится, если мы сможем сократить фак- тический размер последовательности. Главный шаг при двоичном поиске - взять элемент приблизительно в середине последовательности и в зависимости от его значения отбросить ту или другую половину последовательности из дальнейшего рассмотрения. Повторное выполнение этого шага быстро сводит последователь- ность к одному элементу, а поиск среди одного элемента - дело несложное. Впечатляет скорость, с которой сокращается размер задачи. Например, переход от 1000 элементов к 1 происходит за 10 шагов, а от 1000000 элементов (если они помещаются в памяти) к одному - за 20 шагов. В процедуре binarysearch cursorl и cursor2 применяются для индексации первого и последнего элемента из той части последовательности, которая остается в поле зрения. Внутри оператора while то одна, то другая из этих переменных изменяется таким образом, что в конце концов они обязательно ста- новятся равными. Когда это произойдет, цикл завершится и останется только определить, равен ли элемент, на который указывают курсоры, искомому эле- менту. Очевидно, теперь нужен только cursor2, и ситуация становится аналогич- ной той, что возникает при выходе из цикла в процедуре linearsearch. Послед- ний оператор присваивания, одинаковый в обеих процедурах, заносит в ОК нужное значение и этим завершает вычисление. Важной деталью в процедуре binarysearch является использование часово- го. Как обычно, он ставится в соответствующее поле элемента item[length+V\, а начальное значение cursor2 таково, что этот элемент включается в поиск. Это гарантирует правильную работу процедуры в двух специальных случаях: а) когда последовательнось пуста; б) котда искомая величина больше, чем максимальная из имеющихся в последовательности. Лучший способ получить полное представление о процедуре binarysearch - это проверить ее самому на нескольких примерах. Один пример приведен в табл. 18.6. Он показывает сокращение длины перебираемой последовательности при исполнении оператора процедуры binarysearch (thiscode, sortedmailorderlist) где thiscode и sortedmailorderlist те же, что и в табл. 18.5. В каждой записи просматривается только поле code, так как остальные поля не существенны для задачи. Последовательные значения cursorl, cursor2 и локальной переменной mid приведены в таблице. Величина length не участвует в вычислениях, отсюда следует, что фактическая длина последовательности не меняется. Чтобы установить временную сложность процедуры binarysearch, нам надо ответить на один вопрос. Сколько раз будет исполняться тело оператора while для последовательности длины Можно показать, что при использовании тех- ники с часовым, ответом на этот вопрос будет наименьшее целое, большее или равное log2(N+l). Таким образом, временная сложность процедуры binarysearch равна O(log2JV) и не зависит от наличия искомой величины.
346 Гл. 18. Обработка последовательностей объектов Таблица 18.6. Выполнение оператора процедуры binarysearch {thiscode^ sortedmailorderlist) thiscode | 356 | sortedmailorderlist item code +--------+ 0 ? cursorl -> 1 mid -> 4 108 228 356 497 607 849 1 | 108 | 2 | 228 | +--------+ 3 | 356 | +--------+ 4 | 497 | +----------+ +--------+ 3 | 356 | 3 | 356 | +----------+ +--------+ 4 | 497 | +--------+ 996 356 cursor2 -> 8 <-часовой 501 2 3 5 6 7 cursorl mid cursor2 113 3 4 2 3 8 4 4 3 18.6.4. Сравнение линейного и двоичного поиска В предыдущих двух подразделах мы установили, что: а) временная сложность процедуры linearsearch равна О (N); б) временная сложность процедуры binarysearch равна O(log2M. Какой отсюда вывод для программиста-практика? В качестве первого шага при ответе на этот вопрос рассмотрим величины N и log2jV, приведенные в табл. 18.7. Очевидно, что log2N, вообще говоря, гораздо меньшее число, чем N. Во-вторых, приведенные выше оценки времени исполнения означают, что tL. время исполнения linearsearch. и tB. время исполнения binarysearch, имеют вид tL = а + bN и tB = с + dlog2N, где а. Ь. с и d - константы. Мы не можем получить точные значения для этих констант, но детальное рассмотрение процедур показывает, что с больше and больше Ь. Учитывая эти соображения, нарисуем график приблизительной зави-
Гл. 18. Обработка последовательностей объектов 347 симости времени исполнения от N. Из приведенных на рис. 18.1 графиков видно, что процедура linearsearch предпочтительнее при ”маленьких” значениях N, а процедура binarysearch предпочтительнее при ’’больших” значениях N. Таблица 18.7. Значения log2TV N Iog2W 1 0 2 1 4 2 8 3 16 4 32 5 64 6 128 7 256 8 512 9 1024 10 2048 11 4096 12 8192 13 16384 14 32768 15 Точная граница между ’’маленьким” и ’’большим” значениями зависит от сравнительных величин констант b и J, а они в свою очередь зависят от множес- тва факторов, включая то, какие компьютер и компилятор используются. Одна- ко в большинстве случаев точка пересечения находится в диапазоне 10..25. Для программиста-практика это означает, что двоичный поиск предпочтительнее линейного, кроме тех случаев, когда последовательность очень коротка. Рис. 18.1. Приблизительные графики показывающие зависимости времени исполнения процедур linearsearch и binarysearch от длины последовательности N 18.7. Примеры применения вставки, исключения и поиска Различные подпрограммы, приведенные в разд. 18.3-18.6, были аккуратно сконструированы, так что их можно использовать совместно для выполнения многих широко употребительных операций. Рассмотрим в качестве иллюстрации
348 Гл. 18. Обработка последовательностей объектов четыре такие операции над последовательностью элементов нашего закупочного списка. Последовательность уже отсортирована по полю code и хранится в пере- менной thatsequence. Во всех случаях используется тот факт, что процедуры по- иска возвращают результаты через thatsequence.ОК и thatsequence.cursor2. Первое. Предположим, что в соответствующее место thatsequence нужно вставить копию элемента, хранящегося в переменной thisitem. Это выполняют следующие операторы: if spaceavailable (thatsequence) then begin binarysearch (thisitem.code, thatsequence); insertatcursor (thisitem, thatsequence) end else { необходимые действия при нехватке места } причем вместо binarysearch можно применить и linearsearch. Второе. Чтобы найти элемент с описанием, заданным в thisdescription, и скопировать его в thisitem, выполняются следующие операторы: completesearch (thisdescription, thatsequence); with thatsequence do if OK then thisitem item [cursor2\ else { необходимые действия, если искомый элемент отсутствует } В качестве третьего примера приведем задачу поиска элемента с кодом thiscode и увеличения на 10% значения его поля price, что можно запрограмми- ровать так: linearsearch (thiscode, thatsequence); with thatsequence do if OK then item [cursor2\ .price := item \cursor2\ .price *1.1 else { необходимые действия, если искомый элемент отсутствует } В этом случае binarysearch может заменить linearsearch И наконец, приведем фрагмент для поиска, а затем исключения элемента с описанием thatdescription: completesearch (thisdescription, thatsequence); with thatsequence do if OK then deleteatcursor (thatsequence); else { необходимые действия, если искомый элемент отсутствует } 18.8. Сортировка Два алгоритма из трех рассмотренных нами в разд. 18.6 можно применять, только если последовательность элементов предварительно отсортирована по полю поиска. Такие упорядоченные последовательности бывают нужны довольно часто, поэтому в данном разделе мы рассмотрим два метода сортировки неотсор- тированных последовательностей. 18.8.1. Сортировка вставками Один из алгоритмов сортировки последовательностей состоит в том, что по очереди берется каждый элемент и вставляется на соответствующее место среди
Гл. 18. Обработка последовательностей объектов 349 предыдущих. В листинге 18.9 приведена процедура, названная insertionsort, которая воплощает этот метод. Она предназначена для упорядочения элементов нашего списка по возрастанию значений поля code. Чтобы пояснить устройство данной процедуры, отметим четыре ее особен- ности: а) при каждом выполнении тела цикла for. один элемент, а именно item [/], ставится на ’’правильное” место, обычно это происходит после того, как некоторые другие элементы будут подвинуты; б) элементы, которые должны быть подвинуты, если таковые есть, сдвигаются в цикле while'. в) перед входом в оператор while значение элемента item\i\ заносится в Иет[0]. откуда он ставится на нужное место в последовательности при выходе из оператора while', копия, находящаяся в одновременно служит в качестве часового, обеспечивающего правильное завершение оператора цикла while*. г) значение поля code элемента item[i\ посылается в локальную перемен- ную target, для того чтобы не обращаться постоянно к item [0] .code. procedure insertionsort (var anysequence : sequencetype); { Сортирует элементы item[l]..item[length] последовательности anysequence no полю code в восходящем порядке } var i: indextype-, target: numbertype-, begin with anysequence do for i 2 to length do begin item [0] item [ I ]; target item [z] .code, cursor 1 i - 1; cursor2 := z; while item [cursor 1].code > target do begin item [czzrsor2] item [cursor!] -, cursor2 cursor Г, cursor 1 cursor 1 - 1 end; { (item [cursori] .code <- target) и (cursor 1 >- 0) } item[cursor2] :-ztem[0] end end { insertionsort}; Листинг 18.9. Процедура сортировки вставками Ход сортировки, выполняемой процедурой insertionsort, представлен в табл. 18.8. Здесь десять элементов нашего списка приведены в исходном поряд- ке. В результате выполнения очередного этапа алгоритма порядок элементов ме- няется. Эти изменения также показаны в таблице. Поля description и price опущены, так как не участвуют в алгоритме. При движении элемента переме- щаются, разумеется, все его поля. Чаще всего в процедуре insertionsort исполняется строка while item[cursor 1\ .code > target do которая повторяется в точности N - 1 + М раз, где N - длина последовательнос- ти, а М - число элементов, перемещаемых в операторе цикла. Величина М варь- ируется от нуля в случае отсортированной последовательности до 1/2*У(ЛМ), в случае, когда элементы стоят в обратном порядке. Отсюда средняя величина М равна 1/4 N2. и временная сложность процедуры insertionsort составляет OW2).
350 Гл. 18. Обработка последовательностей объектов Таблица 18.8. Сортировка вставками последовательности из десяти элементов. Во второй колонке показан исходный порядок элементов (только поле code), а в последующих -порядок расположения элементов после каждой вставки. Стрелки соот- ветствуют значению переменной z, а выделены те числа, которые были подвинуты индекс 1 42 5 5 1 1 1 1 1 1 1 2 -> 5 42 42 5 5 5 5 5 5 5 3 87 ->87 87 42 42 12 12 12 12 12 4 1 1 -> 1 87 74 42 42 25 25 25 5 74 74 74 ->74 87 74 63 42 42 33 6 12 12 12 12 ->12 87 74 63 58 42 7 63 63 63 63 63 ->63 87 74 63 58 8 25 25 25 25 25 25 ->25 87 74 63 9 z 58 58 58 58 58 58 58 ->58 87 74 10 33 33 33 33 33 33 33 33 ->33 87 18.8.2. Пирамидальная сортировка В качестве второго метода сортировки мы рассмотрим алгоритм, известный как пирамидальная сортировка. Чтобы понять этот алгоритм, надо представить, что элементы в последовательности организованы в двоичное дерево, т.е. в дерево, где каждый ’’родитель” имеет не более двух ’’детей”. Например, десять элементов, которые мы использовали в разд. 18.8.1 для иллюстрации сортировки вставками, приведены на рис. 18.8.2 в виде неупорядоченного двоичного дерева. Поля description и price опять опущены, так как не участвуют в процессе сортировки. Дерево на рис. 18.2 обладает следующими свойствами: а) В корне дерева находится элемент item [ 1 ], а остальные элементы име- ют расположение в ширину, т.е. второй уровень дерева составляют item [2] и item [3], третий уровень - item{4] ..item [7] и т.д. б) Чтобы двигаться вверх и вниз по дереву от элемента с индексом i, надо учитывать следующее: (i) родитель элемента i имеет индекс i div 2, (ii) первый потомок имеет индекс 2 ♦ /, (iii) второй потомок имеет индекс 2 ♦ i + 1. Например, полагая i равным 4, мы получаем, что родителем item [4] является item [2], а потомками - item [8] и item [9]. в) Если в дереве N элементов, последний элемент, обладающий хотя бы одним потомком, имеет индекс N div 2. Поскольку наша последовате- льность содержит 10 элементов, элемент item[5] имеет одного потомка. Если посмотреть на рис. 18.3, сначала покажется, что мы видим то же самое дерево. Однако при ближайшем рассмотрении выясняется, что, несмотря на идентичность формы дерева, элементы в нем и в соответствующей последова- тельности упорядочены по-другому. Произведенная перестановка превращает дерево в пирамиду. Пирамида определяется следующим образом: дерево (поддерево) является пирамидой, если каждый элемент в нем больше или равен элементам, которые являются его потомками (если таковые существуют).
Гл. 18. Обработка последовательностей объектов 351 Дерево, являющееся пирамидой, очень удобно для сортировки, так как корневой элемент дерева является наибольшим. индекс код дерево 1 42 2 5 3 87 4 1 5 74 6 12 7 63 8 25 9 58 10 33 Рис. 18.2. Последовательность элементов и соответствующее двоичное дерево индекс код 1 87 2 74 3 63 4 58 5 33 6 12 7 42 8 ‘ 25 9 1 10 5 дерево Рис. 18.3. Дерево с рис. 18.2, преобразованное в пирамиду ' Три процедуры, названные movecursor 1, makeheap и heapsort на листинге 18.10, реализуют алгоритм пирамидальной сортировки. Первые две обеспечива- ют простые операции над деревом или, вернее, над той совершенно обычной последовательностью, которую мы представляем себе как дерево. Они использу- ются в пирамидальной сортировке, но также могут применяться при реализации других алгоритмов, основанных на понятии пирамиды. Процедура movecursorl имеет один параметр, названный anytree и имею- щий тип sequencetype. Она переставляет anytree.cursorl с того элемента, на который он указывает, на его наибольшего потомка, при условии, что потомки вообще существуют, если же их нет, курсор не передвигается. Процедура считает, что anytree.cursor2 указывает на последний элемент в дереве, который не обязательно, вообще говоря, является последним элементом последователь- ности. Процедура имеет предусловие:
352 Гл. 18. Обработка последовательностей объектов { cursorl в пределах \..cursor2 и cursor2 в пределах 1...length } которое гарантирует, что item[cursorl] входит в дерево, и item[Y\..item[cursor2], составляющие дерево, лежат внутри полной последовательности. procedure movecursor 1 (var anytree : sequencetype); { Если элемент в item[cursorl] в anytree имеет двух потомков, устанавливает cursorl на большего потомка. Если потомок один, устанавливает cursorl на него. Если потомков нет, не меняет cursorl. } begin with anytree do { cursorl в пределах ]..cursor2, cursor2 в пределах 1..length. } if cursorl <- cursor2 div 2 then begin { у item[cursorl] есть хотя бы один потомок } cursorl 2*cursorl\ if cursorl < cursor2 then if item[cursorl + l].code > item[cursorl] .code then cursorl cursorl + 1 end end { movecursor 1 }; procedure makeheap (r : indextype', var anytree : sequencetype); begin with anytree do { левое и правое поддерево у item [г] - пирамиды } begin item [0] item [г]; cursorl r, movecursor 1 (anytree); while item[r].code < item[cursorl].code do begin item[r] item [cursorl] ; item [cursor] item[0] ; r cursorl; movecursor 1 (anytree) end; {item[l].code >- item[cursorl].code } end end { makeheap }; procedure heapsort (var anysequence : sequencetype); { сортирует компоненты item[l]..item[length] из anysequence, используя пирамидальную сортировку } var i : indextype; begin with anysequence do begin { делает размер дерева равным длине последовательности } c ursor2 г- length; { 1-й этап : превращаем дерево в пирамиду } f or ilength div 2 downto 1 do makeheap (i, anysequence) ; { 2-й этап : разрушаем пирамиду и сортируем последовательность } while cursor2 > 1 do begin { переставляем первый и последний элементы в дереве } item[0] item[l]; item[l] item[cursor2] ; item[cursor2] { уменьшаем размер дерева на единицу } c ursor2 cursor2 - 1; { опять превращаем дерево в пирамиду } makeheap (1, anysequence) end end end { heapsort}; Листинг 18.10. Сортировка последовательности с использованием алгоритма пирамидальной сортировки
Гл. 18. Обработка последовательностей объектов 353 Так, например, если anytree - дерево с рис. 18.3, то cursor2 равен 10, и при исполнении movecursor 1 величина cursorl меняется следующим образом: cursorl перед исполнением cursorl после исполнения 1 2 2 4 3 7 4 8 5 10 а для всех больших величин cursorl остается неизменным. индекс код 1 5 2 74 3 63 4 58 5 33 6 12 7 42 8 25 9 1 10 индекс код 1 74 2 58 3 63 4 25 5 33 6 12 7 42 8 5 9 1 10 дерево А дерево Б Рис. 18.4. Выполнение makeheap при г - 1 превращает дерево А в дерево Б Процедура makeheap используется, чтобы превратить дерево в пирамиду. У нее два параметра - индекс г и последовательность anytree, а корнем рассмат- риваемого дерева является элемент последовательности item [г]. У процедуры
354 Гл. 18. Обработка последовательностей объектов есть предусловие: левое и правое поддеревья вершины item [г] должны уже быть пирамидами. Из этого условия следует, что только величина в item 1г] может быть не на своем месте. Если она меньше, чем величина большего потомка, они меняются местами. В результате может разрушиться пирамида, вершиной кото- рой являлся данный потомок, и процесс придется повторить. Общий эффект за- ключается в том, что неправильно расположенный элемент перемещается вниз по уровням дерева пока он строго меньше одного из своих потомков и пока по- томки еще есть. Описанный процесс показан на рис. 18.4, где левое и правое поддеревья дерева А являются пирамидами, а полное дерево не является тако- вым из-за величины 5 в его корне. Выполнение процедуры makeheap при г = 1 сдвигает эту величину вниз, порождая дерево В, являющееся пирамидой. Наконец, мы готовы к анализу процедуры heapsort. После установки пере- менной cursor2 равной длине последовательности, т.е. включив в дерево вначале всю последовательность, выполняются два основных этапа процедуры. Первый этап превращает дерево в пирамиду с помощью вызова makeheap для каждого элемента, имеющего потомков, причем обработка идет от последнего из таких элементов к первому. Этот порядок обеспечивает выполнение предусловия в процедуре makeheap. На втором этапе пирамида разрушается посредством следу- ющих шагов, повторяемых, пока в дереве есть хотя бы один элемент: а) переставить первый и последний элементы в дереве, тем самым переме- щая наибольшую величину на соответствующее место в отсортирован- ной последовательности, а маленькую - в корневую вершину дерева; б) уменьшить размер дерева на единицу; в) снова преобразовать дерево в пирамиду, обращаясь к makeheap с г = 1. Таблица 18.9. Сортировка последовательности из десяти эле- ментов с помощью heapsort. Вторая колонка по- казывает начальную последовательность эле- ментов (только поле code), а в последующих изображена последовательность после каждого из четырнадцати вызовов процедуры makeheap. Соответствующие значения параметра г приве- дены в верхней части таблицы. Стрелки показы- вают текущее положение cursor2 и тем самым последний элемент в дереве. Выделение номера элемента показывает, что он был перемещен < - Этап 1 > < Этап 2 > г 5 4 3 2 1 1 1 1 1 1 1 1 1 1 индекс 1 42 42 42 42 42 87 74 63 58 42 33 25 12 5 ->1 2 5 5 5 5 74 74 58 58 33 33 25 5 5 -> 1 5 3 87 87 87 87 87 63 63 42 42 12 12 12 -> 1 12 12 4 1 1 58 58 58 58 25 25 25 25 1 -> 1 25 25 25 5 74 74 74 74 33 33 33 33 5 5 ->5 33 33 33 33 6 12 12 12 12 12 12 12 12 12 -> 1 42 42 1 42 42 7 63 63 63 63 63 42 42 1 -> 1 58 58 58 58 58 58 8 25 25 25 25 25 25 5 -> 5 63 63 63 63 63 63 63 9 58 58 1 1 1 1 -> 1 74 74 74 74 74 74 74 74 10 >33 >33 >33 >33 -> 5 -> 5 87 87 87 87 87 87 87 87 87
Гл. 18. Обработка последовательностей объектов 355 Выполнение полного процесса сортировки последовательности, изображен- ной на рис. 18.2, показано в табл. 18.9. Чтобы лучше понять происходящее, полезно нарисовать для себя диаграммы соответствующих деревьев. Остается оценить временные параметры этого довольно сложного процесса. Для последовательности длины // heapsort вызывает процедуру makeheap N div 2 раз в части 1 и N-1 раз в части 2. Наиболее часто в процедуре исполняется следующая строка: while item [г].code < item [cursor J] .code do Можно показать, что она исполняется О (АО раз на этапе 1 и O(Mog2A0 раз на этапе 2. Таким образом, общая временная сложность процедуры heapsort равна O(Mog2M. 18.8.3. Сравнение процедур сортировки В предыдущих двух разделах мы рассмотрели два алгоритма сортировки и среди прочих отметили следующие факты: а) временная сложность insertionsort равна OW2); б) временная сложность heapsort равна O(7Vlog27V). Если мы теперь обозначим время исполнения insertionsort как а время исполнения heapsort как /д, то графики на рис. 18.5 покажут приблизительную зависимость этих величин от N. Из него следует, что ситуация для алгоритмов сортировки очень напоминает ситуацию для алгоритмов поиска, а именно: insertionsort лучше работает для ’’маленьких” значений М а heapsort лучше работает для ’’больших” значений N, Рис. 18.5. Приблизительный график, показывающий, как время исполнения O(7V2) алгоритма соотносится с временем O(Mog2M алгоритма причем граница между ’’маленьким” и ’’большим” условна. На практике это означает, что сортировку вставками и другие подобные методы, имеющие время исполнения порядка OW2), следует использовать при сортировке коротких пос- ледовательностей, содержащих менее 20 элементов, а пирамидальная сортировка и другие методы с временем исполнения порядка O(Mog2JV) должны применять- ся для последовательностей большей длины. 18.9. Полная программа В заключение данной главы мы исследуем программу, которая покажет, как различные типы данных и разработанные для них подпрограммы могут быть объединены вместе, образуя завершенную программную единицу. Эта программа называется Сортировка&Контроль и предназначена для того, чтобы считать
356 Гл. 18. Обработка последовательностей объектов последовательность из нашего списка товаров, упорядочить ее по возрастанию поля code и напечатать отсортированный список. Кроме того, элемент, код кото- рого равен коду другого элемента, печатается в списке ’’удвоенных” элементов перед упорядоченным списком. Некоторые образцы входных данных и соответст- вующие им результаты показаны в табл. 18.11. Таблица 18.11. Образец входных данных и результатов для программы Сортировка&Контроль Входные данные 497 картина 13.20 849 картонка 17.95 356 саквояж 14.95 849 диван 2.40 607 корзина 2.25 228 чемодан 6.95 Результаты **** РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ СОРТИРОВКА&КОНТРОЛЬ **** Элементы с повторяющимися номерами (если они есть): 849 диван 2.40 Упорядоченная последовательность элементов: 228 чемодан 6.95 356 саквояж 14.95 497 картина 13.20 607 корзина 2.25 849 картонка 17.95 **** КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ **** Текст программы Сортировка&Контроль (листинг 18.12) приведен с со- кращениями, поскольку он уже был частично показан в листингах данной главы. Новыми являются здесь головная процедура processonesequence и процедура readandtrytoinsert. Процедура processonesequence имеет одну локальную переменную с име- нем mailorderlist, в которой хранится отсортированная последовательность эле- ментов, а в процессе ее обработки используются процедуры initialize sequence, writesequence и readandtrytoinsert из листинга 18.3. Процедура readandtrytoinsert имеет один параметр с именем anysequence и локальную переменную newitem. Процедура считывает новый элемент, и, если в последовательности существует элемент с тем же-номером, новый элемент печа- тается в списке ’’дубликатов”, в противном случае делается попытка вставить его в последовательность. Если места для элемента нет, то элемент печатается с пометкой ’’лишний”. Эти операции используют процедуры readitem и writeitem
Гл. 18. Обработка последовательностей объектов 357 из листинга 18.2, spaceavailable из листинга 18.3, insertatcursor из листинга 18.4 и binarysearch из листинга 18.8. Остальную часть программы составляют определения констант и типов, приведенные в листингах 18.1-18.3 (которые, согласно правилам стандартного Паскаля, должны быть отнесены либо в раздел, где определены константы, либо в раздел, где определены типы), и тексты каждой из подпрограмм в соответству- ющем порядке. program CheckAndSort (input, output); { Читает последовательность элементов, сортирует их и выводит } { Использует непрерывное представление последовательности объектов } const ... { определить константы maxphraselength, maxnumber, maxlength и maxindex } type ... { определить типы phraseindextype, phrasetype, numbertype, itemtype, indextype и sequencetype } procedure ... ... { определить подпрограммы readphrase, readitem, writeitem, initializesequence, spaceavailable, writesequence, insertatcursor и binarysearch. } procedure readandandtrytoinsert (var anysequence : sequencetype); var newitem : itemtype; begin readitem (newitem); binarysearch (newitem.code, anysequence); if anysequence.OK then begin { найден дубликат } writeitem (newitem); writeln end else if spaceavailable (anysequence) then insertatcursor (newitem, anysequence) else begin write (*Лишний элемент:'); writeitem (newitem); writeln end end { readandandtrytoinsert}; procedure processonesequence; var mailorderlist: sequencetype; begin initializesequence (mailorderlist); writeln ('Элементы с повторяющимися номерами (если они есть) :'); writeln; while not eof do readandtrytoinsert (mailorderlist); writeln; writeln ('Отсортированная последовательность:'); writesequence (mailorderlist) end { processonesequence }; begin writeln (’♦♦♦♦ РЕЗУЛЬТАТЫ РАБОТЫ ПРОГРАММЫ СОРТИРОВКА&КОНТРОЛЬ ♦♦♦♦’>; writeln; processonesequence; writeln; writeln (’**♦* КОНЕЦ ВЫДАЧИ РЕЗУЛЬТАТОВ ♦♦♦♦’); end { CheckAndSort}. Листинг 18.12. Текст программы Сорпшровка&Контроль
358 Гл. 18. Обработка последовательностей объектов Полная программа Сортировка&Контроль содержит три главные структу- ры данных, а именно фрагменты текста, объекты и последовательности, а также десять подпрограмм, восемь из которых созданы скорее для демонстрации опера- ций над определенными структурами данных, чем непосредственно для данной программы. Пользуясь теми же структурами данных и подпрограммами, можно быстро и надежно построить множество других программ. Можно также создать обширный набор аналогичных программ, изменив определение типа item и сде- лав необходимые изменения в типах данных и именах полей в подпрограммах. Упражнения 18.1. Считая, что используется набор символов ASCII, установите, допустимы ли приведенные ниже логические выражения в стандартном Паскале, и вычислите те из них, которые правильны. a) ’chalk’ > ’cheese’ г) ’Joseph* >- б) ’fish’ < ’chips’ д) ’.’ < ’,* в) ’250’ < ’TEN* е) ’false’ < ’true’ 18.2. Входной файл содержит следующую последовательность элементов: 113 Кожаный пояс 2.95 105 Старинная карта 1.55 821 Золотой жук 4.50 851 Турецкий кинжал 2.45 Считая, что программа содержит переменную 7 типа itemtype (с. 333), и переменную S типа sequencetype (с. 334), запишите состояние переменной S в точках {А}, {В) и.т.д. следующего программного фрагмента: begin initializesequence (S); {А} readitem (7); insertattaU (I, S) ; {5} readitem (7); S.cursor2 :- 1; insertatcursor (I, S); {C} readitem (7); insertatcursor (I, S); {£>} readitem (7); completesearch (I.description, 5); {E} insertatcursor (I, S); {7} end где используются различные подпрограммы, созданные в данной главе. 18.3. Переменная S типа sequencetype (с. 334) содержит упорядоченную последовательность элемен- тов со следующими значениями в поле code: 37 104 132 179 206 311 368 425 597 634 Взяв за образец табл. 18.6 (с. 346), покажите, как происходит исполнение каждой из следующих процедур: a) binarysearch (179, 5); б) binarysearch (816, 5); в) binarysearch (25, 5); 18.4. Переменная S типа sequencetype (с. 334) содержит последовательность элементов со следующи- ми значениями в поле code: 427 311 104 132 597 368 37 179 634 206 а) Взяв за образец табл. 18.8 (с. 350), покажите, как будет происходить сортировка данной последовательности, если исполняется оператор insertionsort (S). б) Взяв за образец табл. 18.9 (с. 355), покажите, как будет происходить сортировка данной последовательности, если исполняется оператор heapsort (S). 18.5. Перепишите процедуру readphrase в процедуру со следующим заголовком: procedure readphrase (var anyphrase : phrasetype\ terminator : char)',
Гл. 18. Обработка последовательностей объектов 359 которая, пропуская начальные пробелы, вводит в anyphrase символьную строку, оканчивающую- ся символом terminator, и заполняет лишние знаковые позиции пробелами. Если строка слишком длинная, лишние символы читаются, но игнорируются. 18.6. Напишите и протестируйте следующие подпрограммы: a) procedure insertinplace (var anyitem : itemtype; var sortedsequence : sequencetype}', где в предположении, что sortedsequence упорядочена по возрастанию поля code, anyitem вставляется на соответствующее место в sortedsequence (без использования существующих процедур поиска и вставки). б) procedure heapsort2 (var anysequence : sequencetype}; которая сортирует последовательность anysequence в восходящем порядке по полю description. Потребуются модифицированные версии movecursorl и makeheap. в) procedure merge (var sequence 1, sequence2 : sequencetype}; которая, при условии, что последовательности sequence 1 и sequence2 упорядочены в возрастающем порядке по полю code, будет сливать элементы последовательностей из sequence 1 и sequence2, образуя итоговую отсортированную последовательность в sequence 1, а sequence2 оставляя пустой. Если это возможно, sequence!.ОК становится равным true. Если же sequence2 слишком длинна для слияния, то слияние не производится, a sequencel.OK становится равным false. г) procedure split (midprice : real; var sequence 1, sequence2 : sequencetype} ; которая разбивает последовательность элементов из sequencel на две части. Элементы с ценой, большей чем midprice, перемещаются в последовательность sequence2, сохраняя существующий порядок, а остальные остаются в sequencel. Если в sequence2 изначально находились какие-либо элементы, они будут затерты. 18.7. Программа под названием ОбновлениеКаталога (UpdateCatalog} должна делать следующее: а) вводить последовательность элементов из каталога предметов (уже отсортированных по полю code}; б) обновлять каталог, выполняя последовательность операций следующих видов: вставить новый элемент, удалить элемент по его коду, удалить элемент с данным описанием, сменить описание предмета на новое, изменить цену предмета с данным кодом, увеличить все цены на заданный процент. в) выдавать измененный каталог. Создайте необходимую структуру для входного файла и напишите программу. 18.8. Алгоритм пирамидальной сортировки легко модифицируется для работы с таким деревом, каж- дая вершина которого имеет не два, а к потомков. Внесите необходимые изменения и поэкспе- риментируйте с получившейся процедурой.
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ А абстрактная структура данных 315, 316 абсолютная величина, abs (функция) 216 алгоритм 275 Евклида 275, 276 альтернативы при разработке 135 анаграмма 328 анализ программ 263, 266-273 аналитический профиль 263-265, 270-272 апостроф 60, 63, 98, 100, 101, 227 внутри литерала 100 изображение 98, 100, 101 аппаратура 20 см. также оборудование арифметическая прогрессия 130-132, 144-146, 150-155, 176, 185, 186 арифметические выражения 29, 38, 42, 178 знаки 46, 97 операции 27, 62, 178, 179, 216, 230, 232, 234, 236, 265, 279; см. также операции, вещественные числа тип результата 237 функции 216, 236 арктангенс, arctan 238 ASCII 44-46, 217, 310, 311, 332, 358 Б базовый тип 288 биномиальный коэффициент 212 блок 114-118, 213-215, 285-287, 299 программы 114, 116 процедуры 114, 116 функции 114, 116, 214 блочная структура 114, 169 БНФ, Бэкуса-Наура форма 87 Булева алгебра 175 булевские (булевы) выражения 34, 35-38, 42, 49, 52, 108-111, 113, 126, 138, 175, 177-180, 182, 185 значения 34, ПО, 180, 181, 215 константы 180-181 операнды 178 операции 177-182 параметры 227 переменные 175-177, 185, 186 функции 218 булевский тип, Boolean 175 Буль, Джордж 175 буферная переменная inpur 27, 48, 49, 76, 102, 104, 136, 184, 310 В ввод 27, 44, 46-66, 314, 315, 325, 328, 332, 333, 359, 360 булевых значений 180, 181 вещественных чисел 235-236 значений в массив 298 символов 48-51 целых чисел 56-58 вертикальная черта, в РБНФ 87 вещественные числа 99, 229-239, 333; см. также представление вещественных чисел без знака 98 ввод и вывод 235-236 потеря точности 231-234, 254-259 присваивание 237 синтаксис 234-235 сравнение 238 точность представления 149 в управлении циклом 248 усечение и округление 230-231 вещественный тип, real 229 вещественная арифметика, свойства 231-234, 247, 259 Вирт, Никлаус 24 включающее или, ог (булевская операция) 178-180 влияние языка 141 внешние запоминающие устройства, 20, 21 см. также внешняя память внешняя память 21, 25 внутренние элементы данных 19-21, 27, 274 возврат управления 70 восьмеричная система счисления 235 временная сложность 266, 270 вставка в последовательность 335, 336, 342, 347-350 встроенные; см. также предопределенные средства в языке программирования 24, 309 входной файл 48, 49, 51, 274 входные данные 19, 22-25, 30, 31, 38 некорректные 23; см. некорректность выбор (программная структура) 32, 33, 39, 142 выборка поля записи 103-104, 301, 303 вывод 27, 44, 47-66, 195 булевых значений 180-182 вещественных чисел 235-236 записи 304 значений массива 298 на новую строку 52-55 символов 48-51 символьных строк 58-62 целых чисел 56-58 вызов подпрограммы 119, 191-192 процедуры 70, 72, 132, 193 функции 214-215, 226, 253% 310, 311 выполнение 18-20 команд 18-19, 23 оператора присваивания 29 оператора ввода 29 оператора вывода 30, 49 оператора пока (while) 36-37 оператора пока-не (repeat) 244
Предметный указатель 361 оператора цикла с шагом (for) 299-300 программы 18-21, 24-25, 28 в интерактивном режиме 44 процедуры 70-72 условного оператора 34 функции 214 выражение 102-110 арифметическое 29, 38, 42 булевское 34, 35-38, 42, 49, 52, 108-111, ИЗ, 126, 138, 175 простое 102-105 синтаксис 102-104, 106 синтаксический анализ 105-108 выход за границы массива 297, 298 выход из цикла 152, 153, 163, 164, 169, 184-186, 249 выходной файл 48-51, 274 выходные данные 19, 30; см. результаты вычислительная система 173 см. также компьютерная система вычислительный процесс; см. процесс вычитание 27, 62, 232, 234, 236 Г гармонический ряд 284 геометрическая прогрессия 155 гибкость 201, 204 глобальная константа 74-75 переменная 74-76, 150, 169, 170, 203, 216, 310 головная программа 118, 119, 132, 133 процедура 132, 135-137 грамматика 84; см. также синтаксис границы массива 297, 298 д данное, данные 19 двоичная система счисления 235 двоичное дерево 350-351 двоичный поиск 345-348 двумерный массив 321, 323-325 Дейкстра, Эдсгер 172 деление 27, 62, 236 на нуль 62, 234 по модулю (mod) 27, 62 нацело (div) 62 дерево 350-355, 360 расположение в ширину 350 диагностическое тестирование 171-173 диаграмма синтаксическая 84-86, 88, 89, 91-93, 97, 102, 105, 117 диапазон входных данных (границы корректности) 147, 157 значений типа данных 27 индикатор 97 (тип данных) 286, 287-290 целых чисел 27, 100, 148, 150 динамическая структура данных 335 дискриминант 240 дисплей 21, 147 длина последовательности 331, 333, 348 дополнительный компонент, в массиве 295, 296 дополнительные записи, в последовательности 333 доступ к подпрограммам 118-120 к элементам данных 77-80, 118-120, 150, 169, 189-190, 205, 310, 311, 315, 321 отсутствие 189-190; см. недоступность полный 189-190, 205 дубликат (элемента данных) 189-190, 195, 205, 315 Е Евклид 227, 275 естественная структура задачи 32 3 заголовок программы 27 процедуры 70 функции 214 законы (правила) де Моргана 180 закон Кирхгоффа 266 запись (изменение данных) 189-190, 205 см. также доступ запись (перенос информации) 19; см. также вывод в текстовый файл 47-49 запись (структура данных) 291, 300-305, 314, 315, 320; см. также тип, переменная выборка поля 103-104, 301-303 секция 301 затирание 29 зацикливание 38, 145, 163, 251 см. выход из цикла защита управляющей переменной 299 знак равенства, в БНФ 87 знаки препинания 41 И и, and (булевская операция) 178-180 идентификатор 96, 98, 99, 101, 102, 104, 108, 114, 117, 195, 196, 201, 214, 215, 252, 287, 292, 300; см. также имя определение 114 определенный программистом 99 предопределенный 98, 99 изображаемые символы 45, 46, 60, 307 или, ог (включающее или, булевская операция) 178-180 именование (объектов программы) 98, 287 имя константы 31, 99, 102-104, 115-117, 175 параметра 195 переменной 29, 99, 102-104, 108-109, 117, 199 подпрограммы 72 поля 103, 104, 300 программы 27, 99, 117 процедуры 70, 108-110, 117, 133 синтаксического класса 86 типа 115-117, 176, 201, 215, 292
362 Предметный указатель функции 103, 104, 109, 110, 117, 214, 215, 252-253 импликация 181, 182 индекс 291, 292, 296; см. также тип индексированная переменная 104, 293, 295, 297, 310, 321, 334 индексное выражение 297 инициализация 162, 163, 168, 311 информация об управлении 29, 165 исключающее или 181,182 исключение из последовательности 335, 338, 347, 348 исполнение программы (этап) 24, 144 ошибки в ходе 144, 147-149, 157, 162 ход см. трассировка исправление ошибок 156, 173 исходная программа 24, 25, 114 итеративный метод вычисления кубического корня 248-249 квадратного корня 261 К кассеты 21 квадрат, sqr (функция) 216 квадратное уравнение 240 корни 240-241 коэффициенты 240 квадратный корень (функция), sqrt 238, 249 итеративный метод вычисления 261 квадратные скобки 292, 297 в БНФ 88 Кирхгофф, Густав Роберт 266 клавиатура 20 класс см. синтаксический класс комментарий 67, 68, 70, 71, 95, 96, 101, 127, 133, 153, 157, 173, 175, 183, 184, 223, 315, 317, 336 коммуникативность программы 23 компилятор 21, 24, 347 Паскаля 24, 114, 118, 141, 171, 203 компиляция 24, 25, 120, 147, 171 ошибки во время 98, 100, 120, 163, 203 компонент записи 300 компьютерной системы 20 массива 291 программы 172 сложной системы 290 компьютерная система 20 составные части 20 Конвей, Джон 323 конец строки маркер 46, 49, 51, 53, 55-57, 61, 101, 159, 166, 169, 218-221 (функция), eoln 218 конец файла, eof (функция) 218 константа 30, 190, 205, 235, 312, 314, 315, 325, 331-333, 347, 348, 357 без знака 102-104 булевская 175-176, 180-181 косинус, cos 238 круглые скобки, в БНФ 88 кубический корень 244 итеративный метод вычисления 248-249 контроль границ массива 297 Л лексема 84-89, 91-93, 95-102, 108, 110, ИЗ, 114, 116, 121, 125, 227, 234 типы 96-100 линейный поиск 342 литерал 59, 60, 63, 96-98, 100-105, 115, 116, 121, 332 логарифм натуральный, In 238 логическая ошибка 144-146 логическое выражение 175; см. булевское выражение локальный элемент данных 132, 133, 214 переменная 72, 74-78, 168, 191, 195, 205, 214, 299, 320 константа 72, 74, 77, 78, 214 М магнитная лента см. накопители мантисса 229-231, 233, 235, 236, 238 маркер конца строки 46, 49, 51, 53, 55-57, 61, 101, 159, 166, 169, 218-221 массив 291, 294, 298, 306, 309, 310 упакованный 298, 331 матрица 321, 323-325, 327 машинный код 20, 21, 23, 24 язык 20, 23 метод Виета 261 многомерные массивы 320-322 множитель 102-108, 214 мобильность программ 24 моделирование 325, 329 модификация программы 149-150, 168, 173, 281; см. также исправление де Морган, Аугустус 180 Н набор операций 27, 288 символов 44; см. также ASCII наглядность; см. также расположение процедур 189, 203-204 наибольший общий делитель 275, 276 накладные расходы 276-277 « накопители на гибких дисках 21 на магнитных дисках 21 на магнитных лентах 21, 147 натуральный логарифм, In 238 начальное значение переменной 29, 163 управляющей переменной 299 недоступность локальных объектов 78, 194 объектов программы 78, 80 переменной 168 процедуры 119 элемента последовательности 339 независимость
Предметный указатель 363 частей программы 80, 203-204, 208 некорректность (входных) данных 23, 144-147, 149, 154, 159, 160, 172 нелад 23, 164, 168, 174, 252, 290 ненадежность цифр 232, 234, 242-244, 251 результатов 238, 248 неоднозначность в языке программирования 112 условного оператора 111 неопределенное значение переменной 29, 51-52, 55, 176, 194, 295 переменной триГ 61 функции eoln 218 неопределенность спецификации программы 157 процесса тестирования 171,173 непрерывное представление последовательности 334, 336, 339 неупорядоченное двоичное дерево 350 поле, в последовательности 339, 340, 342 неэффективность 281 неявное определение типа 285, 287, 292, 300 переменной 118 нисходящее проектирование 124 новый тип 285, 286 нулевой вариант 145, 146, 166, 167, 246 о область действия 114, 117-120, 171, 190 обнаружение ошибок 156, 158-173, 289; см. также тестирование методы 158 оборудование 20, 21, 46, 97, 98, 101, 146, 175 ограничения 144, 147-149 обращение, см. также вызов к процедуре 70 к функции 214 объектная программа 24, 25, 114 объектный язык 141, 142 объем памяти 149, 274 объемная сложность 274 odd (булевская функция) 218 одномерный массив 291, 297, 321, 331 округление 230-232, 237 погрешность 231-234, 247, 248, 253-258 операнд 62, 121, 177, 178, 232, 237, 238 оператор 26, 28, 108-110 ввода (чтения), read, readln 29, 55, 61, ПО, 190, 199, 235 вывода, write, writeln 30, 49, 53, 58, 61, 110, 119, 181, 191, 235-236 присваивания 29, 97, 108, ПО, 162, 177, 214, 215, 235, 237, 298, 299, 304 синтаксический анализ 111 присоединения, with НО, 305, 306 простой 108, ПО процедуры 70, 108-110, 191-192, 199, 204 пустой ПО, 111, 112 составной 32, 36, 39, 108, 110-112, 114, 116, 125, 267 синтаксический анализ 112 структурный 32, 108, ПО, 267-269 условный 33, 108, ПО, 113-114, 267-268 синтаксический анализ 113 цикла пока, while 36, 108-110, 126, 132, 163, 183-187, 244, 268 обратный способ построения 183-184 синтаксический анализ 111 цикла пока-не, repeat ПО, 244, 269 цикла с шагом, for 110, 298-300, 320 операции 275, 276, 310 арифметические 27, 62, 178, 179, 216, 230, 232, 234, 236, 265, 279 булевские 177-180 ввода-вывода 44, 56, 147 деления 247, 252 над диапазонным типом 288 над последовательностью 332-335, 344, 348, 351 над (структурными) типами 288, 309, 312, 314, 319, 323, 332, 358 над целыми числами 27, 61-62 процессора 124 сложения 102, 104, 178, 179, 276 сравнения 34, 102, 104, 106, 182, 238, 276, 331, 332, 344 старшинство 38, 106, 179 умножения 102, 104, 106, 178, 179, 276 операционная система 21 описание переменных 27, 214 процедуры 70, 115-116, 199 переменной-записи 300 переменной-массива 292 функции 115-116,213-214 определение идентификатора 114 константы 30-31 типа 285, 287, 292, 300, 301 типа-записи 300, 301 типа-массива 292 организация цикла 299 \основная (оперативная) память 20; см. память отказ 52, 171, 186, 221, 226, 227 отладка 23 отрицание, not (булевская операция) 177, 180, 184, 185, 187 отрицательное переполнение порядка 233, 249 отсечение 204 отступы 40 отсутствие доступа 189, 190 ошибки 23, 144-174 исправление 156, 173 обнаружение 156, 158-173, 289 предотвращение 157 этапа исполнения 52, 144, 147-149, 157, 162 этапа компиляции 24, 98, 100, 120, 163 п палиндром 328 память 149, 263, 274-275, 279, 281, 286, 293, 310-312, 315, 322, 330, 345
364 Предметный указатель основная (оперативная) 20, 21, 27, 31 внешняя 21, 25 параллельные процессы 32 параллельное программирование 123 параметр 49, 56-58, 60, 61, 103-104, 109-110, 115-116, 189-210, 214-217, 226, 227, 238; см. также передача параметров оператора write 200-201 Паскаль 24 блочная структура 169 реализация 24 Паскаль-процессор 24 Паскаль-система 25 передача параметров по значению 190-192, 194-196, 199, 204-206, 315 по ссылке 190, 195, 196, 197, 199, 201, 202, 204-207, 216, 299, 315 переменная 26, 27 глобальная 74-76, 150, 169, 170 инициализация 162, 163, 168, 311 локальная 72, 74-78, 168, 191 описание 27 разрушение 75, 76, 80 создание 75 существование 75 цикла (управляющая) ПО, 298-299 переменная-запись 104,110,291 переменная-массив 104, 291, 292 переполнение 185, 206-208, 226, 227 порядка 233, 234, 249 целочисленное 148, 149, 152-154 памяти 286, 293 перфокарты 20 пирамида 350-354 пирамидальная сортировка 350-352, 355, 359 плавающая запятая, формат; см. представление повторение (программная структура) 32, 36, 39, 142 погрешность округления 231-234, 247, 248, 253-255, 257, 258 усечения 230, 231 поддерево 351 подпрограмма 69, 72, 74, 75, 78, 80, 132, 309, 314, 358 подпроцесс 32, 36, 67, 69, 70, 80, 86, 132, 219, 221, 275 поиск, в последовательности 339-49, 356, 359 двоичный 345 линейный 342 полный 339 показательная функция, ехр 238 Показу шка 85-92 поле записи 300 поиска 339 полный доступ 189, 190, 205, 315 полный поиск 339 пользователь 19-21, 31, 44, 58, 135, 146, 150, 160, 170 порядковые функции, ord, chr, succ, pred 216-217 порядковый номер символа 45 порядковый тип 286 порядок (вещественного числа) 230, 231, 233, 236 переполнение 233-234, 249 порядок, в последовательности 330, 349, 350 последовательное программирование язык 32, 40, 123 последовательное выполнение (программная структура) 32, 39, 142 последовательность (структура данных) 330, 331-334, 335, 338-352 порядок, упорядоченность 330, 339-344, 349, 350; см. сортировка примеры 330 последовательность двоичных цифр 100 действий 18 команд 19 лексем 96 операторов 28 последовательный ввод из файла 46 последовательный процесс 32 ’’посмертные выдачи” 172 построение программ 122-143; см. также разработка, проектирование, составление построчная запись программы 95 организация данных 46 постусловие 187 потеря точности 231-233, 254-259 поток управления 266 потомок 351-354, 360 поэтапное (пошаговое) уточнение 123, 124, 128, 130, 137, 157 методы 132 предварительное вычисление 279 предопределенные (встроенные) буферная переменная inpur 136 идентификаторы 98-99, 116, 118, 119, 121, 176 константа maxint 100, 154 константы false и true 175 процедуры 98-99, 199, 329 текстовые файлы input, output 48 функции 98-99, 213, 216-219, 236-238, 249 ширина поля вывода 58 предотвращение ошибок 157 представление вещественных чисел 229 внутреннее 235 диапазон 233 с плавающей запятой 229-236, 247 с фиксированной запятой 230-236 точность 149 последовательности 331, 333 строки текста 312 целых чисел 100 шаблонов 323 предусловие 187, 210, 335, 337, 339, 352, 354 присваивание 29, 193, 235, 237, 264, 265, 275, 299, 304 см. оператор присваивания
Предметный указатель 365 присоединения оператор, with ПО, 305, 306 пробел (символ) 44 проверка корректности данных 23 программа 19 анализ 263, 266-273; см. также профиль заголовок 27 и процессор 122-123 имя 27, 99, 117 исходная 24, 25, 114 каркас 119 модификация, внесение изменений 149-150, 168, 173, 281 объектная 24, 25, 114 отладка 23 построение, проектирование, разработка, составление 23, 122-143, 146, 154, 157, 160, 161, 165, 173, 219 сложность 275, 342, 344, 347, 350, 355 сопровождение 149 спецификация 123, 124, 127, 128, 130, 132, 135-137, 141, 147, 155, 157-159, 160, 168, 175, 287, 316, 323 тестирование 158-160, 171-173 трансляция 23 устойчивость 149-156, 290 программирование систем реального времени 147 фундаментальные структуры 32, 142 функциональное 216 цикла 245 программист 19 программное обеспечение 20, 21 прикладное 21 системное 21, 24, 25 прогрессия арифметическая 130-132, 144, 146, 150-155, 176, 185, 186 геометрическая 155 проектирование 146, 161, 165 нисходящее 124 прописная буква 45, 93, 96, 176, 217 прослеживание 28; см. трассировка профилирования средства 172 профиль 263, 264 аналитический 263-265, 270-272 процедура 69, 114, 132, 135, 140 блок 114, 116 вызов 132, 135 головная 132, 136, 137 заголовок 70, 114-117 имя 108, 110, 117 оператор 70, 108-110 описание 70, 115-116, 135 с параметрами 189 как способ уточнения 132, 133 тело 119 процесс 18, 32, 275 процессор 18-20, 24, 122-124 пунктуация; см. знаки препинания пустая строка в файле 46 в программе 40 пустой оператор 111, 112 файл 46 "пустоты” в программе 40, 101-102 Р раздел операторов 28, 116 описания переменных 27 описания процедур и функций 70 определения типов 285 определения констант 30, 31, 33 разделители 101 размер массива 291 размещение результатов 58 см. также расположение разработка программы 125-127, 157, 173 альтернативная 135 большой 154, 157 ранние стадии 160 разрушающее тестирование 171, 172 разрушение элементов данных 30 переменных 75, 76, 80 распечатка 98, 263 расположение текста программы 40 выходных данных 58 наглядное 58, 102, 150, 153, 157 расширенная Бэкуса-Наура форма, РБНФ 87-92, 97, 102, 109, 110 реализация алгоритма 312, 351 типа (структуры) данных 309, 312, 314, 319, 323, 332 редактор (системная программа) 21, 25 результат функции 213 результаты (выходные данные) 19, 20, 22, 24, 25, 46, 48, 51, 122 рекуррентное соотношение 254, 259 рекурсия 254 родитель 351 ручная проверка 158-171 С сбой 52, 172, 259 секция формальных параметров 199 секция записи 286, 301 семантика 84, 95, 109, 120, 126 встроенная в синтаксис 104, 109 символы 44 изображаемые 45-46, 60 набор 44 строковый 98 управляющие 46 символьная строка 59; см. литерал ср. строка символов символьный тип, char 48 симметрия 279 синтаксис 84, 178, 199, 201, 215, 234, 236 вещественных чисел 234 выражения 102 идентификатора 99 множителя 106
366 Предметный указатель оператора 108-110 определение 87 параметров 199, 200 переменной 102 программ, подпрограмм и блоков 114-116 функций 213, 215 целых чисел 99 синтаксическая правильность 84, 86-93, 187, 234 синтаксические диаграммы 84-86, 88, 89, 91-93, 97, 102, 105, 117 классы 85-89, 91-93, 96, 98, 99, 102-104, 108, 109, ИЗ, 117, 121, 178 описания 84, 92, 179 определения 93, 97 ошибки 95, 120, 144, 147, 171, 187 правила 86 синтаксический анализ (разбор) 84, 89-93, 104-108, 111, 112, 187 дерево 89-91 синус, sin 238 синус (сумма ряда) 278-280 система счисления 235 системное программное обеспечение 21, 24, 25 системное сообщение об ошибке 149 скобки 46, 97 в выражениях 106, 107, 179, 180 квадратные 88, 292, 297 (комментарии в программе) 68, 173 круглые 27, 38, 88 фигурные 68, 88 скорость 277 сложение 27, 38, 62, 102, 104, 106, 148, 153, 232, 233, 236 сложность программы 275, 342, 344, 347, 350, 355 служебные слова Паскаля 98, 99, 101, 108, 116, 121, 126, 244, 292, 301, 331 смысл 84 совершенные числа 223-227 сокращение размерности 345, 346 сопровождение программы 149 сортировка 195, 349-353, 355-357, 359, 360 вставками 349, 350, 355 пирамидальная 350-352, 355, 359 составной оператор 32, 36, 39, 108, 110-112, 114, 116, 125, 267 составные символы 34, 68 составление (под)программы 127, 157, 173, 219, 226, 234 сочетание программных конструкций 39 структур данных 306 спецификация программы 123, 124, 127, 128, 130, 132, 135-137, 141, 147, 155, 157-159, 160, 168, 175, 287, 316, 323 неопределенность 157 список (структура данных) 330 список полей 286, 301 фактических параметров 195 формальных параметров 191,195 справочная таблица 309, 312, 339, 340, 342 сравнение строк 331-332, 344 сравнение линейного и двоичного поиска 347 процедур сортировки 355 средства разработки программ 172 ссылочный параметр см. передача параметров стандартный Паскаль 24, 95-120, 320; см. синтаксис старшинство операций 38, 62, 106-108, 179, 180 строка программы 27, 40 текстового файла 46-48, 50-52, 55, 56 строка символов; ср. символьная строка (тип данных) 331,332 (константа) см. литерал строковый символ 98 строковая константа 150 строчные буквы 46, 93, 96, 181, 217 структура в программировании 32 данных 32 абстрактная 315, 316 динамическая 335 программы 114, 170 языка 84 структурное программирование 32 структурный тип данных 290, 309, 314, 319, 323, 332, 358 оператор 32, 108 суммирование ряда 278, 279 схема взаимных вызовов 72, 135, 169, 223, 274 сходимость итеративного процесса 249 Т таблица истинности 178 разработки программ 128 текстовые файлы 46, 48 текущая позиция 47, 49, 55, 56, 135, 218, 235 телетайпы 21 тело оператора цикла пока 36 оператора цикла пока-не 244, 245 оператора цикла с шагом 298 оператора процедуры 70 терм 102-108, 321 тест на отсечение 203 тестирование 158-160, 169, 171-173, 180, 234, 261, 284 тип данных 26, 27 базовый 288 диапазона 286, 287 записи 286, 291 индекса 286, 291, 292, 296 компонента 286, 291-293, 301 массива 286, 291-293 новый 285, 286 порядковый 286
Предметный указатель 367 реализация 309, 315, 332 результата 214 структурный 290, 314, 332 точка, в БНФ 87 точка выполнения 266 определения 116, 119, 171, 190 точность 240, 248, 252, 259, 278, 322 представления вещественных чисел 149 трансляция 23, 24, 114 транслятор 147 трассировка 28, 31, 34, 37, 40, 49, 51-53, 56, 67, 75, 76, 78, 145, 165, 172, 296, 304 средства 172 У удобочитаемость 58, 157, 204 см. наглядность умножение 27, 38, 62, 102, 104, 106, 107, 146, 233, 236 упакованный массив 298, 331 упорядоченное поле, в последовательности 339, 342-344 управление циклом 153, 184, 248 управляющая информация см. информация об управлении управляющая переменная 298-299 управляющие символы 46 усечение 230, 231 условия 34 условный оператор 33 неоднозначность 111-112 метод записи 113-114 устойчивость программы 149-156, 290 устройства 147, 349 ввода 20, 21, 34, 44, 46 внешние запоминающие 20, 21 вывода 20, 21, 34, 44 компьютерной системы 20-21 построчной печати 20-21 утверждение 175, 184-187 уточнение (этап проектирования) 124, 128, 130, 132, 137, 157 Ф файл 315-318, 323, 328, 331, 333, 358, 360 текстовый 46, 48 фигурные скобки, в БНФ 88 фиксированная запятая, формат см. представление форма Бэкуса-Наура (БНФ) 87 фундаментальные структуры программирования 32, 142 функциональное программирование 216 функция 69, 213-228, 310 арифметическая 216, 236 блок 114,116,214 булевская 218 введенная программистом 213 встроенная 213, 216-219, 236-238 обращение к 214 описание 115-116, 213-214 результат 213 рекурсивная 254 синтаксис 213, 215 ц целевая величина 339, 340, 342, 344, 345 целое без знака 98, 101 целочисленное переполнение 148 целочисленный тип, integer 27 центральный процессор 20 цикл (программная структура) 32, 36, 38, 142, 145, 162, 163, 173, 182 выход из 152, 153, 163, 164, 169, 184-186, 250 организация 299 см. оператор цикла цикл в синтаксической диаграмме 86, 88 Ч часовой 340, 341-344, 346, 349 численный анализ 240 число без знака 97, 98, 102-104, 115, 116 чтение (выборка данных) 189-190, 205 см. также доступ чтение (перенос информации) 19, 46, 48 см. также ввод чисел 235 Ш шаблон 307, 323-325 шестнадцатеричная система счисления 235 ширина поля вывода 58, 235, 236 э эквивалентность 182 экспоненциальный формат 229 см. представление с плавающей запятой этап исполнения 24, 144 компиляции 24, 25, 120, 147, 171 эффективность 141, 263, 274, 279, 281, 306, 309, 311 вычислений 278 Я явное определением типа 285 язык машинный 20, 23 объектный 141, 142 программирования 23, 141 высокого уровня 23 последовательного программирования 32, 40, 123 с блочной структурой 114
Научное издание Говард Джонстон УЧИТЕСЬ ПРОГРАММИРОВАТЬ Книга одобрена на заседании секции редсовета по информатике 28.10.86 Зав. редакцией К. В. Коробов Редактор А. И. Павловская Худож. редактор Ю. И. Артюхов Техн, редактор Г. А. Полякова Корректор Г.В.Хлопцева Переплет художника М. К. Гурова ИБ № 2254 Подписано в печать 31.10.89. Формат 70x1001/16. Гарнитура "Таймс” Печать офсетная. Усл.п.л. 29,9 Усл.кр.-отт. 29,9. Уч.-изд. л. 30, 45 Тираж 30 000 экз. Заказ 1110. Цена 2 руб. 4 0 коп. Издательство "Финансы и статистика”, 101000, Москва, ул. Чернышевского, 7 Книга подготовлена к печати на ППЭВМ в текстовом процессоре MS Word Типография им. Котлякова издательства "Финансы и статистика" Государственного комитета СССР по печати 195273, Ленинград, ул. Руставели, 13.
ЗА СТРАНИЦАМИ УЧЕБНИКА SHEBA.SPBPU/ZA Хочу всё знать (теория) ЮНЫЙ ТЕХНИК (ПРАКТИКА) ДОМОВОДСТВО (УСЛОВИЯ)