Вы находитесь на странице: 1из 465

.

1
ББК 32.973.2-018.2я7
УДК 004.43(075)
П12

Рецензенты:
Сергеев М. Б., завкафедрой вычислительных систем и сетей Санкт-Петербургского
государственного университета аэрокосмического приборостроения,
доктор технических наук, профессор;
Фомичев В. С., доктор технических наук, профессор кафедры вычислительной
техники СПбГЭТУ «ЛЭТИ».

Павловская Т. А.
П12 Паскаль. Программирование на языке высокого уровня: Учебник для вузов.
2-е изд. — СПб.: Питер, 2010. — 464 с.: ил.

ISBN 978-5-49807-772-7
В учебнике рассматриваются структурная и объектно-ориентированная технологии программи-
рования, методы проектирования и отладки программ и основные структуры данных. Книга со-
держит последовательное изложение основ программирования на примере языка Паскаль, практи-
кум по всем разделам курса, полную справочную информацию, 260 индивидуальных заданий для
лабораторных работ и полностью соответствует Государственному образовательному стандарту.
Проверить правильность выполнения основных лабораторных работ, изучить электронный кон-
спект лекций и пройти тесты на знание синтаксиса языка можно на сайте http://ips.ifmo.ru.
Допущено Министерством образования и науки Российской Федерации в качестве учебника для
студентов высших учебных заведений, обучающихся по направлению подготовки дипломирован-
ных специалистов «Информатика и вычислительная техника».

ББК 32.973.2-018.2я7
УДК 004.43(075)

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

ISBN 978-5-49807-772-7 © ООО Издательство «Питер», 2010

2
Краткое оглавление

Предисловие ............................................................ 9

Интернет-поддержка книги ........................................10

Часть I. Основы программирования ............................ 11


Глава 1. Основные понятия языка ........................................................................................... 12
Глава 2. Управляющие операторы языка ............................................................................... 33
Глава 3. Типы данных, определяемые программистом ..................................................... 49
Глава 4. Модульное программирование ................................................................................. 75
Глава 5. Работа с динамической памятью ............................................................................104
Глава 6. Технология структурного программирования ..................................................130
Глава 7. Введение в объектно-ориентированное программирование ........................140
Глава 8. Иерархии объектов .....................................................................................................149
Глава 9. Объекты в динамической памяти ..........................................................................161

Часть II. Практикум ................................................. 171


Семинар 1. Линейные программы .........................................................................................172
Семинар 2. Разветвляющиеся вычислительные процессы ...........................................185
Семинар 3. Организация циклов ............................................................................................197
Семинар 4. Одномерные массивы ..........................................................................................206
Семинар 5. Двумерные массивы и подпрограммы ...........................................................223
Семинар 6. Строки, записи, модуль Crt ...............................................................................248
Семинар 7. Программирование в графическом режиме ................................................275
Семинар 8. Динамические структуры данных ...................................................................285

3
4 Краткое оглавление

Семинар 9. Объекты..................................................................................................................311
Семинар 10. Наследование .......................................................................................................334

Послесловие ......................................................... 353

Справочная информация ......................................... 355


Приложение 1. Зарезервированные слова и стандартные директивы ......................356
Приложение 2. Стандартные модули Паскаля ..................................................................357
Приложение 3. Директивы компилятора ............................................................................415
Приложение 4. Сообщения об ошибках...............................................................................424
Приложение 5. Таблица кодов ASCII ...................................................................................442
Приложение 6. Расширенные коды клавиатуры ..............................................................443
Приложение 7. Интегрированная среда Turbo Pascal 7.0 ...............................................444

Литература............................................................ 451

Алфавитный указатель ............................................ 452

4
Оглавление

Предисловие ............................................................ 9

Интернет-поддержка книги ........................................10

Часть I. Основы программирования ............................ 11


Глава 1. Основные понятия языка .................................................................12
Состав языка ................................................................................................................................ 12
Типы данных ................................................................................................................................ 16
Стандартные типы данных ..................................................................................................... 17
Линейные программы ............................................................................................................... 23
Глава 2. Управляющие операторы языка .......................................................33
Операторы ветвления ............................................................................................................... 34
Операторы цикла........................................................................................................................ 39
Процедуры передачи управления......................................................................................... 46
Оператор перехода goto ........................................................................................................... 48
Глава 3. Типы данных, определяемые программистом ....................................49
Перечисляемый тип данных ................................................................................................... 49
Интервальный тип данных ..................................................................................................... 50
Массивы......................................................................................................................................... 50
Двумерные массивы .................................................................................................................. 54
Строки ............................................................................................................................................ 58
Записи ............................................................................................................................................ 61
Множества .................................................................................................................................... 64
Файлы ............................................................................................................................................ 67
Совместимость типов ............................................................................................................... 73
Совместимость по присваиванию ........................................................................................ 74
Глава 4. Модульное программирование .........................................................75
Подпрограммы ............................................................................................................................ 75
Модули........................................................................................................................................... 89

5
6 Оглавление

Глава 5. Работа с динамической памятью .................................................... 104


Указатели ....................................................................................................................................104
Динамические структуры данных ......................................................................................110
Глава 6. Технология структурного программирования .................................. 130
Критерии качества программы ............................................................................................130
Этапы создания структурной программы .......................................................................131
Правила программирования ................................................................................................135
Глава 7. Введение в объектно-ориентированное программирование .............. 140
Описание объектов ..................................................................................................................142
Экземпляры объектов .............................................................................................................146
Глава 8. Иерархии объектов ....................................................................... 149
Наследование.............................................................................................................................149
Раннее связывание ...................................................................................................................152
Совместимость типов объектов...........................................................................................153
Позднее связывание. Виртуальные методы ....................................................................155
Глава 9. Объекты в динамической памяти .................................................... 161
Динамические объекты. Деструкторы ..............................................................................161
Организация объектов во время проектирования
и выполнения программы .....................................................................................................165

Часть II. Практикум ................................................. 171


Семинар 1. Линейные программы ............................................................... 172
Задача С1.1. Валютные операции .......................................................................................172
Работа в интегрированной среде ........................................................................................175
Ошибки компиляции ..............................................................................................................177
Задача С1.2. Временной интервал ......................................................................................178
Задача С1.3. Расчет по формуле..........................................................................................180
Ошибки времени выполнения .............................................................................................181
Итоги ............................................................................................................................................182
Задания ........................................................................................................................................182
Семинар 2. Разветвляющиеся вычислительные процессы............................. 185
Задача С2.1. Выстрел по мишени .......................................................................................185
Задача С2.2. Определение времени года ..........................................................................187
Задача С2.3. Простейший калькулятор ............................................................................187
Итоги ............................................................................................................................................188
Задания ........................................................................................................................................189
Семинар 3. Организация циклов ................................................................. 197
Задача С3.1. Вычисление суммы ряда ..............................................................................197
Задача С3.2. Нахождение корня нелинейного уравнения .........................................200
Задача С3.3. Количество квадратов в прямоугольнике ..............................................201
Задача С3.4. Пифагоровы числа .........................................................................................202
Итоги ............................................................................................................................................203
Задания ........................................................................................................................................203

6
Оглавление 7

Семинар 4. Одномерные массивы ............................................................... 206


Задача С4.1. Количество элементов между минимумом и максимумом ..............206
Задача С4.2. Сумма элементов правее последнего отрицательного .......................210
Задача С4.3. Сжатие массива ...............................................................................................214
Задача С4.4. Быстрая сортировка массива ......................................................................216
Итоги ............................................................................................................................................219
Задания ........................................................................................................................................219
Семинар 5. Двумерные массивы и подпрограммы ........................................ 223
Задача С5.1. Минимальный по модулю элемент ..........................................................223
Задача С5.2. Номер первого элемента, равного нулю .................................................226
Задача С5.3. Упорядочивание строк матрицы ...............................................................230
Задача С5.4. Процедура преобразования массива ........................................................234
Задача С5.5. Функция подсчета количества положительных элементов ............240
Итоги ............................................................................................................................................242
Задания ........................................................................................................................................243
Семинар 6. Строки, записи, модуль Crt ....................................................... 248
Задача С6.1. Поиск подстроки .............................................................................................248
Задача С6.2. Подсчет количества вхождений слова в текст ......................................250
Задача С6.3. Отдел кадров (поиск в массиве записей) ...............................................255
Задача С6.4. База моделей сотовых телефонов .............................................................258
Итоги ............................................................................................................................................269
Задания ........................................................................................................................................269
Семинар 7. Программирование в графическом режиме ................................ 275
Задача С7.1. Вывод диаграммы ...........................................................................................275
Итоги ............................................................................................................................................280
Задания ........................................................................................................................................280
Семинар 8. Динамические структуры данных .............................................. 285
Задача С8.1. Быстрая сортировка динамического массива
с использованием стека ..........................................................................................................285
Задача С8.2. Отдел кадров (линейный список).............................................................288
Задача С8.3. Очередь в автосервисе...................................................................................297
Итоги ............................................................................................................................................304
Задания ........................................................................................................................................305
Семинар 9. Объекты .................................................................................. 311
Задача С9.1. Поиск произвольной подстроки (объекты) ..........................................312
Задача С9.2. Очередь объектов в автосервисе................................................................318
Итоги ............................................................................................................................................328
Задания ........................................................................................................................................329
Семинар 10. Наследование ......................................................................... 334
Задача С10.1. Слияние файлов............................................................................................334
Задача С10.2. Очередь к врачу.............................................................................................339
Задача С10.3. Модификация очереди в автосервисе ...................................................344
Итоги ............................................................................................................................................347
Задания ........................................................................................................................................348

7
8 Оглавление

Послесловие ......................................................... 353

Справочная информация ......................................... 355


Приложение 1. Зарезервированные слова и стандартные директивы ............. 356
Приложение 2. Стандартные модули Паскаля ............................................. 357
Модуль Crt .................................................................................................................................357
Модуль Dos ................................................................................................................................362
Модуль Graph ............................................................................................................................370
Модуль Strings ..........................................................................................................................390
Модуль System ..........................................................................................................................392
Модуль WinDos ........................................................................................................................409
Приложение 3. Директивы компилятора ..................................................... 415
Приложение 4. Сообщения об ошибках ...................................................... 424
Сообщения компилятора об ошибках...............................................................................424
Ошибки этапа выполнения ...................................................................................................437
Приложение 5. Таблица кодов ASCII .......................................................... 442
Приложение 6. Расширенные коды клавиатуры ........................................... 443
Приложение 7. Интегрированная среда Turbo Pascal 7.0 .............................. 444
Запуск TP ....................................................................................................................................444
Работа с меню ............................................................................................................................445

Литература............................................................ 451

Алфавитный указатель ............................................ 452

8
Предисловие

Существует достаточно света для тех, кто хочет видеть,


и достаточно мрака для тех, кто не хочет.

Блез Паскаль (1623–1662)

Эта книга — для тех, кто по зову сердца или по необходимости изучает основы про-
граммирования. Но почему же Паскаль? Ведь серьезные программы пишут, как
правило, на других языках, так не лучше ли сразу изучать именно то, что пригодит-
ся в дальнейшем?
Сторонникам такой утилитарной точки зрения могу рекомендовать, например,
свои книги по С++ и C# [9, 12], по структуре аналогичные данной. Но следует
иметь в виду, что, начав обучение программированию с «промышленных» языков,
легко увязнуть в их хитросплетениях и так и не приобрести необходимый любому
программисту хороший стиль. Никлаус Вирт создал Паскаль именно для обучения;
язык получился настолько удачным и ясным, что и теперь, спустя десятки лет, и он,
и его потомки используются очень широко.
Язык Паскаль прост, но при этом обладает ключевыми свойствами более сложных
и современных языков высокого уровня. Строгий синтаксис обеспечивает хоро-
шую диагностику ошибок. Наиболее распространенные среды программирования
Borland Pascal with Objects и Turbo Pascal 7.0 при фантастической по современным
меркам компактности обладают достаточно удобными средствами написания и от-
ладки программ. Нельзя не упомянуть и о том, что в профессиональной среде про-
граммирования Delphi используется язык, базирующийся на Паскале.
Эта книга написана на основе учебника и практикума [10, 11], которые выпуска-
лись издательством «Питер» на протяжении ряда последних лет и имеют грифы
Министерства образования. В книге рассматриваются конструкции языка, базо-
вые алгоритмы, методы и приемы написания программ, основные структуры дан-
ных, основы объектно-ориентированного программирования, типичные ошибки,

9
10 Предисловие

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


сы качества и стиля. Большое внимание уделено процессу отладки и тестирования
программ.
Книга состоит из трех частей. Первая часть посвящена изложению основ про-
граммирования, включая процедурную и объектно-ориентированную технологию,
а также реализацию основных структур данных: стека, списка, очереди и бинарного
дерева.
Вторая часть — практикум, который начинается с разбора самой простой програм-
мы, какую только можно себе представить, и заканчивается списком полиморфных
объектов в динамической памяти. Практикум состоит из 10 семинаров, по каждой
теме приведено минимум по 20 однотипных вариантов лабораторных работ в рас-
чете на учебную группу студентов.
Ленивый студент найдет здесь массу готовых программ, а любознательный — мно-
жество советов, пояснений и соображений, как написать программу так, чтобы она
получилась красивой и надежной, а процесс ее создания доставил удовольствие.
Третья часть книги — справочная. В семи приложениях приводятся все сведения,
необходимые для выполнения лабораторных работ. Для облегчения поиска инфор-
мации все использованные в книге ключевые слова, типы, стандартные константы,
переменные, функции и процедуры описаны в предметном указателе.

Интернет-поддержка книги
Важная особенность этого учебника состоит в его интернет-поддержке: зарегис-
трировавшись на сайте http://ips.ifmo.ru, можно в интерактивном режиме пройти
тесты на знание синтаксиса языка, понимание алгоритмов, знание основных по-
ложений объектно-ориентированного программирования, а главное, проверить
правильность выполнения лабораторных работ, задания для которых приведены
в книге. Каждая программа, посылаемая на сайт, проходит полный набор эталон-
ных тестов. Кроме того, на сайте имеются аналогичные материалы по основам ал-
горитмов и по другим языкам и аспектам программирования.
Таким образом обеспечивается единый для всех высокий уровень качества обуче-
ния основам программирования в соответствии с государственным образователь-
ным стандартом: где бы вы ни обучались, с помощью первой части этого учебника
можно освоить теоретический материал, с помощью второй, практической, части
научиться создавать надежные и эффективные программы, а помогут в этом при-
веденная в третьей части справочная информация и поддерживающий сайт.
В основу книги положен курс, на протяжении многих лет читавшийся автором
в Санкт-Петербургском государственном университете информационных техно-
логий, механики и оптики (СПбГУ ИТМО). Как показала практика, первую часть
этого курса успешно усваивают и школьники старших классов.
Ваши замечания, пожелания, дополнения, а также замеченные ошибки и опечатки
не ленитесь присылать по адресу pta-ipm@yandex.ru — и тогда благодаря вам сле-
дующее издание этой книги станет еще лучше.

10
Часть I. Основы программирования

Основы программирования, обсуждаемые в этой части, охва-


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

11
Глава 1. Основные понятия языка
В этой главе описано то, что необходимо для создания простейших программ: эле-
ментарные строительные блоки языка, стандартные типы данных, структура про-
граммы, переменные, операции, выражения и процедуры ввода-вывода.

Состав языка
Язык программирования можно уподобить очень примитивному иностранному
языку с жесткими правилами, не имеющими исключений. Изучение иностранного
языка обычно начинают с алфавита, затем переходят к простым словам, далее рас-
сматривают законы построения фраз, и только в результате длительной практики
становится возможным свободно выражать на этом языке свои мысли. Примерно
так же поступим и мы при изучении языка Паскаль, а сначала определим термино-
логию.
Для решения задачи на компьютере требуется написать программу. Программа
состоит из исполняемых операторов и операторов описания. Исполняемый опе-
ратор задает законченное действие, выполняемое над данными. Примеры испол-
няемых операторов: вывод на экран, занесение числа в память, выход из програм-
мы.
Оператор описания, как и следует из его названия, описывает данные, над которы-
ми в программе выполняются действия. Примером описания (конечно, не на Па-
скале, а на естественном языке) может служить предложение «В памяти следует
отвести место для хранения целого числа, и это место мы будем обозначать А».
Исполняемые операторы для краткости часто называют просто операторами, а опе-
раторы описания — описаниями. Описания должны предшествовать операторам,
в которых используются соответствующие данные. Операторы исполняются по-
следовательно, один за другим, если явным образом не задан иной порядок.
Рассмотрим простейшую программу на Паскале. Все, что она делает, — вычисляет
и выводит на экран сумму двух целых чисел, введенных с клавиатуры:
var a, b, sum : integer; { 1 }
begin { 2 }
readln(a, b); { 3 }
sum := a + b; { 4 }
writeln('Cумма чисел ', a, ' и ', b, ' равна ', sum); { 5 }
end. { 6 }

12
Глава 1. Основные понятия языка 13

В программе шесть строк, каждая из них для удобства рассмотрения помечена ком-
ментарием с номером (внутри фигурных скобок можно писать все, что угодно, но
правильнее, когда там находятся пояснения к программе).
В строке 1 расположен оператор описания используемых в программе величин.
Для каждой из них задается имя, по которому к ней будут обращаться, и ее тип.
«Волшебным словом» var обозначается тот факт, что a, b и sum — переменные, то
есть величины, которые во время работы программы могут менять свои значения.
Для всех переменных задан целый тип, он обозначается integer. Тип необходим для
того, чтобы переменным в памяти было отведено соответствующее место.
Исполняемые операторы программы располагаются между служебными словами
begin и end, которые предназначены для объединения операторов и сами оператора-
ми не являются. Операторы отделяются друг от друга точкой с запятой.
Ввод с клавиатуры выполняется в строке 3 с помощью стандартной процедуры
с именем readln. В скобках после имени указывается, каким именно переменным
будут присвоены значения. Для вывода результатов работы программы в строке 5
используется стандартная процедура writeln. В скобках через запятую перечисля-
ется все, что мы хотим вывести на экран, при этом пояснительный текст заключа-
ется в апострофы. Например, если ввести в программу числа 2 и 3, результат будет
выглядеть так:
Cумма чисел 2 и 3 равна 5
В строке 4 выполняется вычисление суммы и присваивание ее значения перемен-
ной sum. Справа от знака операции присваивания, обозначаемой символами :=, на-
ходится так называемое выражение — правило вычисления значения. Выражения
являются частью операторов.
Чтобы выполнить программу, требуется перевести ее на язык, понятный процес-
сору, — в машинные коды. Этим занимается компилятор. Каждый оператор языка
переводится в последовательность машинных команд, которая может быть весьма
длинной, поэтому Паскаль и называется языком высокого уровня. В языках низ-
кого уровня, например в ассемблере, каждая команда переводится в одну или не-
сколько машинных команд.
Компилятор планирует размещение данных в оперативной памяти в соответствии
с операторами описания. Попутно он ищет синтаксические ошибки, то есть ошибки
записи операторов. Кроме этого, в Паскале на компилятор возложена еще одна обя-
занность — подключение к программе стандартных подпрограмм (например, ввода
данных или вычисления синуса угла).

Алфавит и лексемы
Все тексты на языке пишутся с помощью его алфавита. Алфавит Паскаля вклю-
чает в себя:
 прописные и строчные латинские1 буквы, знак подчеркивания _;
 цифры от 0 до 9;
1
Заметьте: русских букв в алфавите языка Паскаль нет. К сожалению, хотя Никлаус Вирт
и знает русский язык, но в алфавит Паскаля русские буквы не включил.

13
14 Часть I. Основы программирования

 специальные символы, например +, *, { и @;


 пробельные символы: пробел, табуляция и переход на новую строку.
Из символов составляются лексемы (tokens), то есть минимальные единицы языка,
имеющие самостоятельный смысл:
 константы;
 имена (идентификаторы);
 ключевые слова;
 знаки операций;
 разделители (скобки, точка, запятая, пробельные символы).
Лексемы языка программирования аналогичны словам естественного языка. На-
пример, лексемами являются число 128, имя Vasia, ключевое слово goto и знак опе-
рации сложения +. Компилятор при синтаксическом разборе текста программы
определяет границы одних лексем по другим, например разделителям или знакам
операций. Из лексем строятся выражения и операторы. Рассмотрим каждый вид
лексем подробнее.

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

Таблица 1.1. Классификация констант Паскаля

Целые Вещественные Символьные Строковые


Десятичные Шестнадцате- С плавающей С порядком
ричные точкой
–0.26 ’k’
2 $0101 1.2e4 ’абырвалг’
.005 #186
15 $FFA4 0.1E–5 ’I’’m fine’
21. ^M

Как видно из таблицы, десятичные целые константы представляются в естествен-


ной форме. Шестнадцатеричная константа состоит из шестнадцатеричных цифр (0,
1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F), предваряемых знаком $. В табл. 1.1 представле-
ны в шестнадцатеричном виде числа 257 и 65 444.
Вещественные константы записываются с точкой перед дробной частью. Либо це-
лая, либо дробная часть могут отсутствовать. Вещественная константа с порядком
представляется в виде мантиссы и порядка. Мантисса записывается слева от зна-
ка E или e, порядок — справа от знака. Значение константы равно произведению
мантиссы и возведенного в указанную в порядке степень числа 10. В табл. 1.1 пред-
ставлены числа 1,2×104 и 0,1×10–5. Пробелы внутри числа не допускаются.
Символьные константы служат для представления любого символа из набора, ис-
пользуемого в данном компьютере. Так как под каждый символ отводится 1 байт,
всего используется 256 символов. Каждому символу соответствует свой код. В опе-
рационной системе MS-DOS для кодировки символов используется стандарт ASCII,

14
Глава 1. Основные понятия языка 15

являющийся международным только в первой половине кодов (от 0 до 127), вто-


рая половина кодов (от 128 до 255) является национальной и различна для разных
стран. Более того, в нашей стране есть несколько видов кодировок русских букв.
Кодовая таблица MS-DOS, используемая в Паскале, приведена в приложении 5.
Первые 32 символа являются управляющими: хотя многие из них имеют графи-
ческое представление, предназначены они для передачи управляющих сигналов
внешним устройствам, например монитору, принтеру или модему. Символьные
константы записываются в одной из трех форм:
1. Символ, заключенный в апострофы.
2. Десятичный код символа, предваряемый знаком #. Применяется для представле-
ния символов, отсутствующих на клавиатуре (в табл. 1.1 в виде #186 приведено
представление символа ║).
3. Буква, предваряемая знаком ^. Используется для представления управляющих
символов. Код буквы должен быть на 64 больше, чем код представляемого таким
образом символа (в табл. 1.1 в виде ^M представлен символ с кодом 13, по которо-
му при выводе выполняется переход к началу строки).
Строковая константа — это последовательность любых ASCII-символов, распо-
ложенная на одной строке и заключенная в апострофы. Если требуется предста-
вить сам апостроф, он дублируется. Максимальная длина строковой константы —
126 символов.

Имена, ключевые слова и знаки операций


Имена в программах служат той же цели, что и имена людей, — чтобы обращаться
к программным объектам и различать их, то есть идентифицировать. Поэтому име-
на также называют идентификаторами.
Как уже говорилось, данные, с которыми работает программа, надо описывать. Для
этого служат операторы описания, которые связывают данные с именами. Имена
дает программист, при этом следует соблюдать следующие правила:
 имя должно начинаться с буквы (или знака подчеркивания, что, вообще говоря,
не рекомендуется );
 имя должно содержать только буквы, знак подчеркивания и цифры;
 прописные и строчные буквы не различаются;
 длина имени практически не ограничена1.
Например, правильными именами будут Vasia, A, A13, A_and_B и _____, а неправиль-
ными — 2late, Big gig и Sюр (первое начинается с цифры, второе содержит недо-
пустимый символ «пробел», третье — недопустимый символ ю). Имена даются
элементам программы, к которым требуется обращаться: переменным, константам,
процедурам, функциям, меткам и т. д.
Ключевые (зарезервированные) слова — это идентификаторы, имеющие специаль-
ное значение для компилятора. Их можно использовать только в том смысле, в ко-
тором они определены. Например, для оператора перехода определено ключевое
1
Значащими для компилятора будут являться только первые 63 символа, но попробуйте
придумать, а потом выговорить такое имечко!

15
16 Часть I. Основы программирования

слово goto, а для описания переменных — var. Имена, создаваемые программистом,


не должны совпадать с ключевыми словами. Полный список ключевых слов Паска-
ля приведен в приложении 1.
Знак операции — это один или более символов, определяющих действие над операн-
дами. Внутри знака операции пробелы не допускаются. Например, операция срав-
нения «меньше или равно» обозначается <=, а целочисленное деление записывается
как div. Операции делятся на унарные (с одним операндом) и бинарные (с двумя).
Чаще всего знаки операций состоят из одного символа. Например, сложение обо-
значается символом +, а вычитание — символом –.

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

Классификация типов
Любая информация легче усваивается, если она «разложена по полочкам». Поэто-
му, прежде чем перейти к изучению конкретных типов языка Паскаль, давайте рас-
смотрим их классификацию в табл. 1.2.

Таблица 1.2. Типы данных Паскаля1

Определяемые программистом1
Стандартные
Простые Составные
Логические Перечисляемый Массивы Файлы
Целые Интервальный Строки Процедурные типы
Вещественные Адресные Записи Объекты
Символьный Множества
Строковый
Адресный
Файловые

1
В литературе чаще встречается термин «типы, определяемые пользователем». При этом
имеется в виду пользователь языка, то есть программист.

16
Глава 1. Основные понятия языка 17

Стандартные типы не требуют предварительного определения. Для каждого типа


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

ПРИМЕЧАНИЕ
Типы, выделенные в табл. 1.2 полужирным шрифтом, объединяются термином «по-
рядковые». Этот термин рассмотрен на с. 22.

Стандартные типы данных


В этом разделе рассмотрены логические, целые, вещественные и символьный
типы. Стандартные строки описаны вместе со строками, определяемыми програм-
мистом, на с. 58, адресный тип — в разделе «Указатели» на с. 104, а файловые —
на с. 67.

Логические типы
Внутреннее представление. Основной логический тип данных Паскаля называет-
ся boolean. Величины этого типа занимают в памяти 1 байт и могут принимать всего
два значения: true (истина) или false (ложь). Внутреннее представление значения
false — 0 (нуль), значения true — 1.
Для совместимости с другими языками в Паскале определены и другие логические
типы данных: ByteBool, WordBool и LongBool длиной 1, 2 и 4 байта соответственно. Ис-
тинным в них считается любое отличное от нуля значение.
Операции. К величинам логического типа применяются логические операции and,
or, xor и not (табл. 1.3). Для наглядности вместо значения false в таблице использу-
ется 0, а вместо true — 1.

Таблица 1.3. Логические операции

a b a and b a or b a xor b not a


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

В табл. 1.3 приведены все возможные сочетания значений аргументов и соответ-


ствующие им значения результата. Такая таблица называется таблицей истинно-
сти.
Операция and называется логическое И, или логическое умножение. Ее результат
имеет значение true, только если оба операнда имеют значение true.

17
18 Часть I. Основы программирования

Результат операции or (логическое ИЛИ, логическое сложение) имеет значение true,


если хотя бы один из операндов имеет значение true. Например, false or true →
true, true or true → true.
Операция xor — так называемое исключающее ИЛИ, или операция неравнозначно-
сти. Ее результат истинен, когда значения операндов не совпадают.
Логическое отрицание not является унарной операцией, то есть имеет один операнд,
который и инвертирует. Например, not true даст в результате false.
Кроме того, величины логического типа можно сравнивать между собой с помощью
операций отношения, перечисленных в табл. 1.4. Результат этих операций имеет ло-
гический тип. Например, результат проверки false < true — значение true (истина),
а проверки false = true — значение false (ложь).

Таблица 1.4. Операции отношения

Операция Знак операции Операция Знак операции


Больше > Меньше или равно <=
Больше или равно >= Равно =
Меньше < Не равно <>

Целые типы
Внутреннее представление. Целые числа представляются в компьютере в двоич-
ной системе счисления (отрицательные числа — в дополнительном коде, но для нас
это не принципиально). В Паскале определены несколько целых типов данных, раз-
личающихся длиной и наличием знака: старший двоичный разряд либо восприни-
мается как знаковый, либо является обычным разрядом числа (табл. 1.5). Внутрен-
нее представление определяет диапазоны допустимых значений величин (от нулей
до единиц во всех двоичных разрядах).

Таблица 1.5. Целые типы данных

Тип Название Размер Знак Диапазон значений


integer Целое 2 байта Есть –32 768 ... 32 767 (–215 ... 215–1)
Короткое
shortint 1 байт Есть –128 ... 127 (–27 ... 27–1)
целое
byte Байт 1 байт Нет 0 ... 255 (0 ... 28–1)
word Слово 2 байта Нет 0 ... 65 535 (0 ... 216–1)
Длинное
longint 4 байта Есть –2 147 483 648 ... 2 147 483 647 (–231 ... 231–1)
целое

Первоначально в Паскале был всего один целый тип — integer, остальные добавле-
ны впоследствии для представления больших величин или для экономии памяти.
Например, нет смысла отводить 4 байта под величину, про которую известно, что
все ее значения находятся в диапазоне от 0 до 100.
Операции. С целыми величинами можно выполнять арифметические операции
(табл. 1.6). Результат их выполнения всегда целый (при делении дробная часть от-
брасывается).

18
Глава 1. Основные понятия языка 19

Таблица 1.6. Арифметические операции для целых величин

Операция Знак операции Операция Знак операции


Сложение + Деление div
Вычитание – Остаток от деления mod
Умножение *

К целым величинам можно также применять операции отношения, перечисленные


в табл. 1.4 (см. с. 18). Результат этих операций имеет логический тип, например
результатом сравнения 3 < 8 будет значение true.
Кроме того, к целым величинам можно применять поразрядные операции and, or,
xor и not. При выполнении этих операций каждая величина представляется как со-
вокупность двоичных разрядов. Действие выполняется над каждой парой соответ-
ствующих разрядов операндов: первый разряд с первым, второй — со вторым, и т. д.
Таблицы истинности операций приведены в разделе «Логические типы» на с. 17.
Например, результатом операции 3 and 2 будет 2, поскольку двоичное представле-
ние числа 3 — 11, числа 2 — 10.
Для работы с целыми величинами предназначены также операции сдвига влево shl
и вправо shr. Слева от знака операции указывается, с какой величиной будет вы-
полняться операция, а справа — на какое количество двоичных разрядов требуется
сдвинуть величину. Например, результатом операции 12 shr 2 будет значение 3, по-
скольку двоичное представление числа 12 — 1100. Выполнив операцию 12 shl 1, то
есть сдвинув это число влево на 1 разряд, получим 24. Освободившиеся при сдвиге
влево разряды заполняются нулями, а при сдвиге вправо — знаковым разрядом.
Стандартные функции и процедуры. К целым величинам можно применять стан-
дартные функции и процедуры, перечисленные в табл. 1.7 (в тригонометрических
функциях угол задается в радианах).

Таблица 1.7. Стандартные функции и процедуры для целых величин

Имя Описание Результат Пояснения


функции
abs Модуль Целый |x| записывается abs(x)
arctan Арктангенс угла Вещественный arctg x записывается arctan(x)
cos Косинус угла Вещественный cos x записывается cos(x)
exp Экспонента Вещественный ex записывается exp(x)
ln Натуральный логарифм Вещественный logex записывается ln(x)
odd Проверка на четность Логический odd(3) даст в результате true
pred Предыдущее значение Целый pred(3) даст в результате 2
sin Синус угла Вещественный sin x записывается sin(x)
sqr Квадрат Целый x2 записывается sqr(x)

sqrt Квадратный корень Вещественный x записывается sqrt(x)


succ Следующее значение Целый succ(3) даст в результате 4
продолжение 

19
20 Часть I. Основы программирования

Таблица 1.7 (продолжение)


Имя Описание Результат Пояснения
процедуры
inc(x) — увеличить х на 1
inc Инкремент
inc(x, 3) — увеличить х на 3
dec(x) — уменьшить х на 1
dec Декремент
dec (x, 3) — уменьшить х на 3

ПРИМЕЧАНИЕ
Различие между функцией и процедурой проявляется в способе вызова. Процедура
вызывается отдельным оператором, а функция — в составе выражения. На с. 12 был
приведен пример вызова процедур readln и writeln.

Вещественные типы
Внутреннее представление. Вещественные типы данных хранятся в памяти ком-
пьютера иначе, чем целые. Внутреннее представление вещественного числа состоит
из двух частей — мантиссы и порядка, и каждая часть имеет знак. Например, число
0,087 представляется в виде 0,87×10–1, и в памяти хранится мантисса 87 и порядок
–1 (для наглядности мы пренебрегли тем, что данные на самом деле представляют-
ся в двоичной системе счисления и несколько сложнее).
Существует несколько вещественных типов, различающихся точностью и диапазо-
ном представления данных (табл. 1.8). Точность числа определяется длиной ман-
тиссы, а диапазон — длиной порядка.

Таблица 1.8. Вещественные типы данных

Значащих деся- Диапазон значений


Тип Название Размер тичных цифр
real Вещественный 6 байт 11–12 2.9e–39 ... 1.7e+38
Одинарной
single 4 байта 7–8 1.5e–45 ... 3.4e+38
точности
Двойной точ-
double 8 байт 15–16 5.0e–324 ... 1.7e+308
ности
extended Расширенный 10 байт 19–20 3.4e–4932 ... 1.1e+4923
comp Большое целое 8 байт 19–20 –9.22e18 ... 9.22e18 (–263 ... 263–1)

ПРИМЕЧАНИЕ
Для первых четырех типов в табл. 1.8 приведены абсолютные величины минимальных
и максимальных значений в виде констант Паскаля.

Автор языка Никлаус Вирт определил всего один вещественный тип — real и отвел
под него разумное количество памяти. Однако аппаратно этот тип в компьютерах

20
Глава 1. Основные понятия языка 21

семейства IBM PC не поддерживается1, поэтому впоследствии в язык были вве-


дены типы single и double, а также тип extended для работы с большими числами
и с высокой точностью.
Тип comp на самом деле представляет собой длинные целые числа. Величины этого
типа хранятся таким же образом, как целые, но отнести его к целым мешает то, что
по области применимости он несколько отличается от остальных: тип comp не от-
носится к порядковым типам (они рассматриваются на с. 22).
Рассмотрим теперь, что же можно делать с величинами вещественных типов, то
есть какие к ним применяются операции и функции.
Операции. С вещественными величинами можно выполнять арифметические опе-
рации, перечисленные в табл. 1.9. Результат их выполнения — вещественный.

Таблица 1.9. Арифметические операции для вещественных величин

Операция Знак операции Операция Знак операции


Сложение + Умножение *
Вычитание – Деление /

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

ПРИМЕЧАНИЕ
Обратите внимание, что целочисленное и вещественное деление записываются с по-
мощью разных знаков операций. Если требуется получить вещественный результат
деления двух целых величин, нужно использовать операцию /, если целый — опера-
цию div.

К вещественным величинам можно также применять операции отношения, пере-


численные в табл. 1.4 (см. с. 18). Результат этих операций имеет логический тип.
Стандартные функции. К вещественным величинам можно применять стандарт-
ные функции, приведенные в табл. 1.10 (в тригонометрических функциях угол за-
дается в радианах).

Таблица 1.10. Стандартные функции и процедуры для вещественных величин

Имя Описание Результат Пояснения


abs Модуль Вещественный |x| записывается abs(x)
arctan Арктангенс угла Вещественный arctg x записывается arctan(x)
cos Косинус угла Вещественный cos x записывается cos(x)
exp Экспонента Вещественный ex записывается exp(x)
продолжение 
1
Аппаратная поддержка означает наличие в процессоре специальных трактов для обработки
данных. При отсутствии аппаратной поддержки необходимые операции эмулируются про-
граммно, что выполняется медленнее.

21
22 Часть I. Основы программирования

Таблица 1.10 (продолжение)

Имя Описание Результат Пояснения


Дробная часть аргу-
frac Вещественный frac(3.1) даст в результате 0,1
мента
int Целая часть аргумента Вещественный int(3.1) даст в результате 3,0
Натуральный лога-
ln Вещественный logex записывается ln(x)
рифм
pi Значение числа π Вещественный 3,1415926536
round(3.1) даст в результате 3
round Округление до целого Целый
round (3.8) даст в результате 4
sin Синус угла Вещественный sin x записывается sin(x)
sqr Квадрат Вещественный x2 записывается sqr(x)
sqrt Квадратный корень Вещественный √x записывается sqrt(x)
trunc Целая часть аргумента Целый trunc(3.1) даст в результате 3

Символьный тип
Этот тип данных, обозначаемый ключевым словом char, служит для представления
любого символа из набора допустимых символов. Под каждый символ отводится
1 байт. К символам можно применять операции отношения (<, <=, >, >=, =, <>), при
этом сравниваются коды символов. Меньшим окажется символ, код которого мень-
ше. Других операций с символами нет, да они и не имеют смысла. Стандартных
подпрограмм для работы с символами тоже немного (табл. 1.11).

Таблица 1.11. Стандартные функции для символьных величин

Имя Описание Результат Пояснения


Порядковый номер сим- ord('b') даст в результате 98
ord Целый
вола ord('ю') даст в результате 238
chr(98) даст в результате 'b'
chr Преобразование в символ Символьный
chr(238) даст в результате 'ю'
pred Предыдущий символ Символьный pred('b') даст в результате 'a'
succ Последующий символ Символьный succ('b') даст в результате 'c'
Перевод в верхний регистр
upcase (только для символов из Символьный upcase('b') даст в результате 'B'
диапазона 'a' ... 'z')

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

22
Глава 1. Основные понятия языка 23

 к любому порядковому типу может быть применена стандартная функция Ord,


которая в качестве результата возвращает порядковый номер конкретного зна-
чения в данном типе;
 к любому порядковому типу могут быть применены стандартные функции Pred
и Succ, которые возвращают предыдущее и последующее значения соответственно;
 к любому порядковому типу могут быть применены стандартные функции Low
и High, которые возвращают наименьшее и наибольшее значения величин данного
типа.
При изучении операторов Паскаля мы увидим, что в некоторых из них допускается
использовать только величины порядковых типов.

Приведение типов
Иногда при программировании требуется явным образом преобразовывать вели-
чину одного типа в величины другого. Для этого служит операция приведения типа,
которая записывается так:
имя_типа (преобразуемая_величина)1
Например:
integer ('A')
byte(500)
Размер преобразуемой величины должен быть равен числу байтов, отводимых под
величины типа, в который она преобразуется. Исключение составляют преобразо-
вания более длинных целых типов в более короткие: в этом случае лишние биты
просто отбрасываются. Приведение типа изменяет только точку зрения компиля-
тора на содержимое ячеек памяти, никакие преобразования внутреннего представ-
ления при этом не выполняются.

Линейные программы
Линейной называется программа, все операторы которой выполняются в том поряд-
ке, в котором они записаны. Это самый простой вид программ.

Переменные
Переменная — это величина, которая во время работы программы может менять
свое значение. Все переменные, используемые в программе, должны быть описа-
ны в разделе описания переменных, начинающемся со служебного слова var (от
слова variable — переменная). Для каждой переменной задается ее имя и тип, на-
пример:
var number : integer;
x, y : real;
option : char;

1
Здесь и далее русскими словами обозначены элементы, на место которых должны быть
подставлены конкретные значения. Символ подчеркивания между словами используется
для указания на тот факт, что на это место подставляется одно значениe, а не два.

23
24 Часть I. Основы программирования

Имя переменной определяет место в памяти, по которому находится значение пере-


менной. Имя дает программист. Оно должно отражать смысл хранимой величины
и быть легко распознаваемым. Например, если в программе вычисляется количе-
ство каких-либо предметов, лучше назвать соответствующую переменную quantity
или, на худой конец, kolich, но не, скажем, A, t17_xw или prikol.

СОВЕТ
Желательно, чтобы имя не содержало символов, которые можно перепутать друг
с другом, например: l (строчная L) или I (прописная i).

Тип переменных выбирается исходя из диапазона и требуемой точности представ-


ления данных. Например, не следует заводить переменную вещественного типа для
хранения величины, которая может принимать только целые значения, — хотя бы
потому, что целочисленные операции выполняются гораздо быстрее.
При объявлении можно присвоить переменной некоторое начальное значение, то
есть инициализировать ее. Под инициализацией понимается задание значения, вы-
полняемое до начала работы программы. Инициализированные переменные опи-
сываются после ключевого слова const.
const number : integer = 100;
x : real = 0.02;
option : char = 'ю';
По умолчанию все переменные, описанные в главной программе, обнуляются.

Абсолютные переменные
Обычно при описании переменных решение об их конкретном расположении в па-
мяти возлагается на компилятор. Однако в Паскале есть возможность явным об-
разом задавать адреса, по которым должны находиться переменные. Существует
два способа это сделать: либо записать адрес явным образом, либо дать указание
компилятору расположить переменную начиная с того же адреса, что и некоторая
другая переменная.
var имя : тип absolute адрес;
var имя : тип absolute имя_существующей_переменной;
Например:
var scr : byte absolute $B800:0;
x : integer;
b : word absolute x;
Здесь дается указание расположить переменную scr по адресу $B800:0. Адрес за-
дается в виде сегмента и смещения (см. с. 78). Этот способ может использоваться,
например, для непосредственного доступа к видеопамяти или порту и является не-
желательным, так как снижает переносимость программы.
Переменная b размещена «поверх» переменной х. Этот способ применяется для
того, чтобы иметь возможность рассматривать один и тот же участок памяти как
величины разных типов.

24
Глава 1. Основные понятия языка 25

Выражения
Выражение — это правило вычисления значения. В выражении участвуют опе-
ранды, объединенные знаками операций. Операндами выражения могут быть кон-
станты, переменные и вызовы функций. Операции выполняются в определенном
порядке в соответствии с приоритетами, как и в математике. Для изменения по-
рядка выполнения операций используются круглые скобки, уровень их вложенно-
сти практически не ограничен.
Результатом выражения всегда является значение определенного типа, который
определяется типами операндов. Величины, участвующие в выражении, должны
быть совместимых типов (см. с. 73). Например, допускается использовать в одном
выражении величины целых и вещественных типов. Результат такого выражения
будет вещественным.
Ниже приведены операции Паскаля, упорядоченные по убыванию приоритетов.
1. Унарная операция not, унарный минус –, взятие адреса @1.
2. Операции типа умножения: *, /, div, mod, and, shl, shr.
3. Операции типа сложения: +, –, or, xor.
4. Операции отношения: =, <, >, <>, <=, >=, in2.
Функции, используемые в выражении, вычисляются в первую очередь.

ВНИМАНИЕ
Константа и переменная являются частными случаями выражения.

Примеры выражений:
 t + sin(x)/2 * x — результат имеет вещественный тип;
 a <= b + 2 — результат имеет логический тип;
 (x > 0) and (y < 0) — результат имеет логический тип.
Порядок вычисления первого выражения такой: сначала выполняется обращение
к стандартной функции sin и результат делится на 2, затем получившееся чис-
ло умножается на x, и только после этого выполняется сложение с переменной t.
Скобки в третьем выражении необходимы по той причине, что приоритет операций
отношения ниже, чем логической операции and.

Структура программы
Программа на Паскале состоит из заголовка, разделов описаний и раздела опера-
торов.
program имя; { заголовок – не обязателен }
разделы описаний
begin
раздел операторов
end. (* программа заканчивается точкой *)

1
Эта операция рассмотрена на с. 105.
2
in — операция проверки принадлежности множеству (с. 65).

25
26 Часть I. Основы программирования

Программа может содержать комментарии, заключенные в фигурные скобки { }


или в скобки вида (* *). Комментарии служат для документирования программы —
компилятор их игнорирует, поэтому на их содержимое никаких ограничений не на-
кладывается. Операторы отделяются друг от друга символом «точка с запятой».
В разделе операторов записываются исполняемые операторы программы. Ключе-
вые слова begin и end не являются операторами1, а служат для их объединения в так
называемый составной оператор, или блок. Блок может записываться в любом ме-
сте программы, где допустим обычный оператор.
Разделы описаний бывают нескольких видов: описание модулей, констант, типов,
переменных, меток, процедур и функций. Модуль — это подключаемая к программе
библиотека ресурсов (подпрограмм, констант и т. п.).
Раздел описания модулей, если он присутствует, должен быть первым. Описание
начинается с ключевого слова uses, за которым через запятую перечисляются все
подключаемые к программе модули, как стандартные, так и собственного изготов-
ления, например:
uses crt, graph, my_module;
Возможности стандартных модулей мы рассмотрим в разделе «Стандартные моду-
ли Паскаля» (с. 93), а создание собственных — на с. 90.
Количество и порядок следования остальных разделов произвольны, ограничение
только одно: любая величина должна быть описана до ее использования. Призна-
ком конца раздела описания является начало следующего раздела. В программе мо-
жет быть несколько однотипных разделов описаний, но для упрощения структуры
программы рекомендуется группировать все однотипные описания в один раздел.
В разделе описания переменных необходимо определить все переменные, которые
будут использоваться в основной программе. Раздел описания констант служит
для того, чтобы вместо значений констант можно было использовать в программе
их имена. Такие константы называют именованными, например:
const MaxLen = 100; g = 9.8;
koeff = 5;
Применение именованных констант при осмысленном выборе имен улучшает чи-
табельность программы и облегчает внесение в нее изменений. А еще в разделе опи-
сания констант описываются переменные, которым требуется присвоить значение
до начала работы программы:
const weight : real = 61.5;
Синтаксически такая переменная отличается от константы наличием типа. Впо-
следствии ею можно пользоваться так же, как и другими переменными.
Раздел описания меток начинается с ключевого слова label, за которым через за-
пятую следует перечисление всех меток, встречающихся в программе. Метки слу-
жат для организации перехода на конкретный оператор с помощью оператора
безусловного перехода goto (он рассматривается на с. 48). Метка — это либо имя,
1
Поэтому между оператором и ключевым словом end точка с запятой ни к чему.

26
Глава 1. Основные понятия языка 27

либо положительное число, не превышающее 9999. Метка ставится перед любым


исполняемым оператором и отделяется от него двоеточием:
label 1, 2, error;
Разделы описания типов, процедур и функций будут рассмотрены позже, по мере
изучения материала.

Оператор присваивания
Присваивание — это занесение значения в память. В общем виде оператор присваи-
вания записывается так:
переменная := выражение
Здесь символами := обозначена операция присваивания. Внутри знака операции
пробелы не допускаются.
Механизм выполнения оператора присваивания такой: вычисляется выражение, и его
результат заносится в память по адресу, который определяется именем переменной, на-
ходящейся слева от знака операции. Схематично это полезно представить себе так:
переменная ← выражение
Напомню, что константа и переменная являются частными случаями выражения.
Примеры операторов присваивания:
a := b + c / 2; b := a; a := b; x := 1; x := x + 0.5;
Обратите внимание: b := a и a := b — это совершенно разные действия!

ПРИМЕЧАНИЕ
Чтобы не перепутать, что чему присваивается, запомните мнемоническое правило:
присваивание — это передача данных «налево».

Начинающие часто делают ошибку, воспринимая присваивание как аналог равен-


ства в математике. Чтобы избежать этой ошибки, надо понимать механизм работы
оператора присваивания. Рассмотрим для этого последний пример (x := x + 0.5).
Сначала из ячейки памяти, в которой хранится значение переменной x, выбирается
это значение. Затем к нему прибавляется 0,5, после чего получившийся результат
записывается в ту же самую ячейку. При этом то, что хранилось там ранее, теряет-
ся безвозвратно. Операторы такого вида применяются в программировании очень
широко.
Правая и левая части оператора присваивания должны быть, как правило, одного
типа. Говоря более точно, они должны быть совместимы по присваиванию. Напри-
мер, выражение целого типа можно присвоить вещественной переменной, потому
что целые числа являются подмножеством вещественных, и информация при та-
ком присваивании не теряется.
вещественная переменная := целое выражение;
Правила совместимости перечислены в разделе «Совместимость по присваива-
нию» на с. 74.

27
28 Часть I. Основы программирования

Процедуры ввода-вывода
Любая программа при вводе исходных данных и выводе результатов взаимодей-
ствует с внешними устройствами. Совокупность стандартных устройств ввода
и вывода, то есть клавиатуры и экрана дисплея, называется консолью. Обмен дан-
ными с консолью является частным случаем обмена с внешними устройствами, ко-
торый будет подробно рассмотрен в разделе «Текстовые файлы» (с. 69), а кратко —
в следующем разделе.

Ввод с клавиатуры
Для ввода с клавиатуры определены процедуры read и readln:
read(список);
readln[(список)];
В скобках указывается список имен переменных через запятую. Квадратные скобки
указывают на то, что список может отсутствовать. Например:
read(a, b, c); readln(y);
readln;
Вводить можно целые, вещественные, символьные и строковые величины. Вво-
димые значения должны разделяться любым количеством пробельных символов
(пробел, табуляция, перевод строки).
Ввод значения каждой переменной выполняется так:
1. Значение переменной выделяется как группа символов, расположенных между
разделителями.
2. Эти символы преобразуются во внутреннюю форму представления, соответ-
ствующую типу переменной.
3. Значение записывается в ячейку памяти, определяемую именем переменной.
Например, при вводе вещественного числа 3.78 в переменную типа real оно преоб-
разуется из четырех символов (3, «точка», 7 и 8) в шестибайтовое представление
в виде мантиссы и порядка.
Процедура readln после ввода всех значений выполняет переход на следующую
строку исходных данных. Иными словами, если в следующей части программы
есть ввод, он будет выполняться из следующей строки исходных данных. При ис-
пользовании процедуры read очередные исходные данные будут взяты из той же
строки. Процедура readln без параметров просто ожидает нажатия клавиши Enter.
Особенность ввода символов и строк состоит в том, что пробельные символы в них
ничем не отличаются от всех остальных, поэтому разделителями являться не могут.
Например, пусть определены переменные
var a : integer;
b : real;
d : char;
и в программе есть процедура ввода read(a, b, c). Допустим, переменной а надо
задать значение, равное 2, переменной b — 3,78, а в d записать символ #. Любой

28
Глава 1. Основные понятия языка 29

вариант расположения исходных данных приведет к неверному результату, потому


что после второго числа требуется поставить пробельный символ для того, чтобы
его можно было распознать, и этот же символ будет воспринят как значение пере-
менной d:
2 3.78#
2 3.78→#
2 3.78 #
Символом → обозначено нажатие клавиши Tab. В первом случае будет выдана
ошибка времени выполнения, а в двух оставшихся переменной d будет присвоено
значение символа табуляции и символа пробела соответственно. Правильным ре-
шением является ввод чисел и символов в разных процедурах и размещение симво-
лов в отдельной строке, например:
readln(a, b); readln(d);
Ввод данных выполняется через буфер — специальную область оперативной па-
мяти. Фактически данные сначала заносятся в буфер, а затем считываются оттуда
процедурами ввода. Занесение в буфер выполняется по нажатию клавиши Enter
вместе с ее кодом (#13#10). Процедура read, в отличие от readln, не очищает буфер,
поэтому следующий после нее ввод будет выполняться с того места, на котором за-
кончился предыдущий, то есть начиная с символа конца строки:
read(a); { считывается целое }
write(' Продолжить? (y/n) ');
readln(d); { вместо ожидания ввода символа считывается символ #13 из предыдущего
ввода }
Чтобы избежать подобной ситуации, следует вместо read использовать readln.

Вывод на экран
При выводе выполняется обратное преобразование: из внутреннего представления
в символы, выводимые на экран. Для этого в языке определены стандартные про-
цедуры write и writeln.
write(список);
writeln[(список)];
Как вы уже догадались, процедура write выводит указанные в списке величины на
экран, а writeln вдобавок к этому переводит курсор на следующую строку. Процеду-
ра writeln без параметров просто переводит курсор на следующую строку.
Выводить можно величины логических, целых, вещественных, символьного и строко-
вого типов. В списке могут присутствовать не только имена переменных, но и вы-
ражения, а также их частный случай — константы. Кроме того, для каждого выво-
димого значения можно задавать его формат, например:
writeln('Значение a = ', a:4, ' b = ', b:6:2, sin(a) + b);
Рассмотрим этот оператор подробно (переменные a и b описаны выше). В спи-
ске вывода пять элементов, разделенных запятыми. В начале записана строковая

29
30 Часть I. Основы программирования

константа в апострофах, которая выводится без изменений, со всеми пробелами.


В непосредственной близости от нее будет выведено значение целой переменной a.
После имени переменной через двоеточие указано количество отводимых под нее
позиций, внутри которых значение выравнивается по правому краю.
Третьим элементом списка является строковая константа, поясняющая располо-
женное после нее значение переменной b. Для b указаны две форматные специфи-
кации, означающие, что под эту переменную отводится всего шесть позиций, при-
чем две из них — под дробную часть (еще одна позиция будет занята десятичной
точкой, итого на целую часть остается три позиции).
Последний элемент списка вывода — выражение, значение которого будет выведе-
но в форме по умолчанию (с порядком).
Значение a = 2 b = 3.78 4.6892974268E+00
Теперь, когда мы познакомились с примером, можно сформулировать общие пра-
вила записи процедур вывода.
 Список вывода разделяется запятыми.
 Список содержит выражения, а также их частные случаи — переменные и кон-
станты логических, целых, вещественных, символьного и строкового типов.
 После любого значения можно через двоеточие указать формат, то есть количе-
ство отводимых под него позиций. Если значение короче, оно «прижимается»
к правому краю отведенного поля, если длиннее, поле «раздвигается» до необ-
ходимых размеров.
 Для вещественных чисел можно указать второй формат, указывающий, сколько
позиций из общего количества отводится под дробную часть числа. Необходимо
учитывать, что десятичная точка также занимает одну позицию. Если второй или
оба формата не указаны, вещественное число выводится в форме с порядком.
 Если форматы не указаны, под целое число, символ и строку отводится мини-
мально необходимое для их представления количество позиций. Под веществен-
ное число всегда отводится 17 позиций, причем 10 из них — под его дробную
часть.
 Форматы могут быть выражениями целого типа.

СОВЕТ
При выводе всегда сопровождайте выводимые значения понятными комментариями
и указывайте форматы. Это экономит время осмысления результатов.

Теперь наконец-то мы изучили достаточно материала, чтобы с полным пониманием


написать первую законченную программу.
Пример. Программа, которая переводит температуру в градусах по Фаренгейту
в градусы Цельсия по формуле С = 5/9 (F – 32), где C — температура по Цельсию,
а F — температура по Фаренгейту.
program temperature;
var fahr, cels : real; { 1 }
begin

30
Глава 1. Основные понятия языка 31

writeln('Введите температуру по Фаренгейту'); { 2 }


readln(fahr); { 3 }
cels := 5 / 9 * (fahr – 32); { 4 }
writeln('По Фаренгейту: ', fahr:6:2, ' в градусах Цельсия: ', cels:6:2); { 5 }
end.
Для хранения исходных данных и результатов требуется выделить место в памя-
ти. Это сделано в операторе 1. Поскольку температура может принимать не только
целые значения, для переменных fahr и cels выбран вещественный тип real. Опе-
ратор 2 представляет собой приглашение ко вводу данных. В приглашении обычно
поясняют, сколько и каких величин требуется ввести.
Ввод выполняется в операторе 3 с помощью процедуры readln, ее использовать
предпочтительнее, чем read. В операторе 4 вычисляется выражение, записанное
справа от операции присваивания, и результат присваивается переменной cels,
то есть заносится в отведенную этой переменной память. При вычислении целые
константы преобразуются компилятором в вещественную форму. В пятом опера-
торе выводятся исходное и рассчитанное значение с соответствующими поясне-
ниями.

Ввод и вывод для текстовых файлов


При отладке даже небольших программ может потребоваться выполнить их не раз,
не два и даже не десять. При этом ввод исходных данных может утомить и испо-
ртить все удовольствие от процесса1. Удобно заранее подготовить исходные данные
в текстовом файле и считывать их в программе. Кроме того, это дает возможность
не торопясь продумать, какие исходные данные требуется ввести для полной про-
верки программы, и заранее рассчитать, что должно получиться в результате.
Вывод результатов тоже бывает полезно выполнить в текстовый файл для последу-
ющего неспешного анализа. Работа с файлами подробно рассматривается в разделе
«Файлы» (с. 67), а здесь даются лишь самые необходимые сведения. Вот версия
программы из предыдущего раздела, использующая файлы:
program temperature;
var fahr, cels : real;
f_in, f_out : text; { 1 }
begin
assign(f_in, 'D:\pascal\input.txt'); { 2 }
reset(f_in); { 3 }
assign(f_out, 'D:\pascal\output.txt'); { 4 }
rewrite(f_out); { 5 }
readln(f_in, fahr); { 6 }
cels := 5 / 9 * (fahr – 32);
writeln(f_out, 'По Фаренгейту: ', fahr:6:2,
' в градусах Цельсия: ', cels:6:2); { 7 }
close(f_out); { 8 }
end.

1
А удовольствие — необходимое условие для написания хорошей программы!

31
32 Часть I. Основы программирования

Для того чтобы использовать в программе файлы, необходимо:


 объявить файловую переменную (оператор 1);
 связать ее с файлом на диске (операторы 2 и 4);
 открыть файл для чтения или записи (операторы 3 и 5);
 выполнить операции ввода-вывода (операторы 6 и 7);
 закрыть файл (оператор 8).
В этой программе объявлены две переменные, f_in и f_out, стандартного типа «тек-
стовый файл». Процедура assign связывает эти переменные с файлами на диске,
путь к которым задается с помощью строк символов. Если полный путь не указан,
файл располагается в текущем каталоге. Процедура reset открывает файл для чте-
ния, а rewrite — для записи. Если файл, который требуется открыть для записи,
существует, он стирается и создается заново.
При вводе и выводе в файлы используются уже известные нам процедуры read,
readln, write и writeln с единственным различием: первым аргументом в них пере-
дается файловая переменная. Файл, в который выполняется запись, обязательно
закрывать с помощью процедуры close, иначе информация может быть потеряна.
Вот и все, что пока достаточно знать!

СОВЕТ
При отладке программы бывает удобно выводить одну и ту же информацию и на экран,
и в текстовый файл.

32
Глава 2. Управляющие операторы языка
В теории программирования доказано, что программу для решения задачи любой
сложности можно составить только из трех структур, называемых следованием,
ветвлением и циклом. Следованием называется конструкция, представляющая со-
бой последовательное выполнение двух или более операторов (простых или состав-
ных). Ветвление задает выполнение либо одного, либо другого оператора в зависи-
мости от выполнения какого-либо условия. Цикл задает многократное выполнение
оператора (рис. 2.1).

Рис. 2.1. Базовые конструкции структурного программирования

Следование, ветвление и цикл называют базовыми конструкциями структурного


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

33
34 Часть I. Основы программирования

Операторы ветвления
Операторы ветвления if и варианта case применяются для того, чтобы в зависи-
мости от конкретных значений исходных данных обеспечить выполнение разных
последовательностей операторов. Оператор if обеспечивает передачу управления
на одну из двух ветвей вычислений, а оператор case — на одну из произвольного
числа ветвей.

Условный оператор if
Условный оператор if используется для разветвления процесса вычислений на два
направления. Структурная схема оператора приведена на рис. 2.2. Формат оператора:
if выражение then оператор_1 [else оператор_2;]
Сначала вычисляется выражение, которое должно иметь логический тип. Если оно
имеет значение true, выполняется первый оператор, иначе — второй. После этого
управление передается на оператор, следующий за условным.

Рис. 2.2. Структурная схема условного оператора

Операторы, входящие в состав условного оператора, могут быть простыми или со-
ставными. Составной оператор (блок) обрамляется ключевыми словами begin и end.
Блок применяют в том случае, когда по какой-либо ветви требуется выполнить не-
сколько операторов: ведь иначе компилятор не сможет понять, где заканчивается
ветвь и начинается следующая часть программы.
ВНИМАНИЕ
Отсутствие операторных скобок (ключевых слов begin и end) в ветви else компилятор
как ошибку не распознает!

Одна из ветвей может отсутствовать. Логичнее опускать вторую ветвь вместе


с ключевым словом else.
Примеры условных операторов:
if a < 0 then b := 1; { 1 }
if (a < b) and ((a > d) or (a = 0)) then inc(b)
else begin
b := b * a; a := 0
end; { 2 }

34
Глава 2. Управляющие операторы языка 35

if a < b then
if a < c then m := a else m := c
else
if b < c then m := b else m := c; { 3 }
В примере 1 отсутствует ветвь else. Такая конструкция называется «пропуск опе-
ратора», поскольку присваивание значения переменной b либо выполняется, либо
пропускается в зависимости от выполнения условия a < 0, после чего управление
всегда передается следующему оператору.
Если требуется проверить несколько условий, их объединяют знаками логических
операций. Так, выражение в примере 2 будет истинно в том случае, если выполнит-
ся одновременно условие a < b и хотя бы одно из условий a > d и a = 0. Если опустить
скобки, в которые взята операция ИЛИ, будет выполнено сначала логическое И,
а потом — ИЛИ, поскольку его приоритет ниже. Скобки, в которые заключены
операции отношения, обязательны, потому что приоритет у логических операций
выше, чем у операций отношения. Поскольку по ветви else требуется выполнить
два оператора, они заключены в блок.
В примере 3 вычисляется наименьшее из значений трех переменных: a, b и с.

ВНИМАНИЕ
Частая ошибка при программировании условных операторов — неверная запись про-
верки на принадлежность диапазону. Например, условие 0 < x < 1 нельзя записать
непосредственно. Правильный способ: if(0 < x) and (x < 1) then…, поскольку факти-
чески требуется задать проверку выполнения одновременно двух условий: x > 0 и x < 1.
Вторая ошибка — отсутствие блока после else, если на самом деле по этой ветви требу-
ется выполнить более одного действия. Эта ошибка не может быть обнаружена компи-
лятором, поскольку является не синтаксической, а семантической, то есть смысловой.

Пример. Программа, которая по введенному значению аргумента вычисляет значе-


ние функции, заданной в виде графика (рис. 2.3).

Сначала составим описание алгоритма в неформальном словесном виде.


1. Ввести значение аргумента х.
2. Определить, какому интервалу из области определения функции оно принад-
лежит, и вычислить значение функции y по соответствующей формуле.
3. Вывести значения х и y.
Второй пункт алгоритма следует детализировать. Сначала запишем определение
функции в виде формул:

⎧0, x < − 2⎫
⎪− x − 2, − 2 ≤ x < −1⎪
⎪⎪ ⎪⎪
y = ⎨ x, − 1 ≤ x < 1⎬ .
⎪ − x + 2, 1≤ x < 2⎪
⎪ ⎪
⎪⎩0, x ≥ 2 ⎪⎭

35
36 Часть I. Основы программирования

Теперь в соответствии с формулами опишем словами последовательность действий


второго пункта алгоритма:
2.1. Если x < –2, присвоить переменной y значение 0.
2.2. Если –2 ≤ x < –1, присвоить переменной y значение –x – 2.
2.3. Если –1 ≤ x < 1, присвоить переменной y значение x.
И так далее.

Рис. 2.3. Функция, заданная в виде графика

Мы представили шаги алгоритма максимально подробно, поэтому можно присту-


пать к написанию программы (листинг 2.1). Поверьте, что писать программу по хо-
рошо продуманному детальному алгоритму — сплошное удовольствие!

Листинг 2.1. Вычисление функции – вариант 1


program calc_function_1;
var x, y : real;
begin
writeln(' Введите значение аргумента'); readln(x);
if x < –2 then y := 0;
if (x >= –2) and (x < –1) then y := –x – 2;
if (x >= –1) and (x < 1) then y := x;
if (x >= 1) and (x < 2) then y := –x + 2;
if x >= 2 then y := 0;
writeln('Для x = ', x:6:2, ' значение функции y = ', y:6:2);
end.
Тестовые примеры для этой программы должны содержать по крайней мере по
одному значению аргумента из каждого интервала, а для проверки граничных
условий — еще и все точки перегиба (если это кажется вам излишним, попробуйте
в предпоследнем условии «забыть» знак =, а затем ввести значение х, равное 1).
Можно записать эту программу и по-другому, сократив количество проверок с по-
мощью вложенных условных операторов (листинг 2.2).

36
Глава 2. Управляющие операторы языка 37

Листинг 2.2. Вычисление функции – вариант 2


program calc_function_2;
var x, y : real;
begin
writeln(' Введите значение аргумента'); readln(x);
if x < –2 then y := 0
else if x < –1 then y := –x – 2
else if x < 1 then y := x
else if x < 2 then y := –x + 2
else y := 0;
writeln('Для x = ', x:6:2, ' значение функции y = ', y:6:2);
end.
В этом варианте проверка на принадлежность аргумента очередному интервалу вы-
полняется только в том случае, если x не входит в предыдущий интервал. В пред-
ыдущей же программе все пять условных операторов всегда выполняются один за
другим, при этом истинным оказывается только одно условие.
Какой вариант лучше? В современной иерархии критериев качества программы на
первом месте стоят ее надежность, простота поддержки и модификации, а эффек-
тивность и компактность отходят на второй план. Поэтому в общем случае, если
нет специальных требований к быстродействию, лучше более наглядный вариант.
Первый вариант понятен с первого взгляда, поскольку он фактически представляет
собой «кальку» с формулы, задающей поведение функции.
Следует избегать проверки вещественных величин на равенство, вместо этого луч-
ше сравнивать модуль их разности с некоторым малым числом. Это связано с по-
грешностью представления вещественных значений в памяти. Значение величины,
с которой сравнивается модуль разности, следует выбирать в зависимости от ре-
шаемой задачи и точности переменных, участвующих в выражении. Самая малая
величина для типа real составляет примерно 3e–39. В большинстве случаев такая
высокая точность не требуется. Пример:
const eps = 1e-6; { Требуемая точность вычислений }
var x, y : real;
...
if (x = y) then writeln('Величины x и y равны'); { Плохо! Ненадежно! }
if (abs(x - y) < eps) then writeln('Величины x и y равны'); { Рекомендуется }
Большого количества вложенных условных операторов также следует избегать, по-
тому что они делают программу совершенно нечитабельной.

Оператор варианта case


Оператор варианта (выбора) предназначен для разветвления процесса вычислений
на несколько направлений. Структурная схема оператора приведена на рис. 2.4.
Формат оператора:
case выражение of
константы_1 : оператор_1;
константы_2 : оператор_2;
продолжение 

37
38 Часть I. Основы программирования


константы_n : оператор_n;
[ else : оператор ]
end;

Рис. 2.4. Структурная схема оператора выбора

Выполнение оператора выбора начинается с вычисления выражения. Затем управ-


ление передается на оператор, помеченный константами, значение одной из ко-
торых совпало с результатом вычисления выражения. После этого выполняется
выход из оператора. Если совпадения не произошло, выполняются операторы, рас-
положенные после слова else, а при его отсутствии управление передается опера-
тору, следующему за case.
Выражение после ключевого слова case должно быть порядкового типа (см. с. 22),
константы — того же типа, что и выражение. Чаще всего после case использует-
ся имя переменной (напомню, что это частный случай выражения). Перед каждой
ветвью оператора можно записать одну или несколько констант через запятую или
операцию диапазона, обозначаемую двумя идущими подряд точками, например:
case a of
4 : writeln('4');
5, 6 : writeln('5 или 6');
7..12 : writeln('от 7 до 12');
end;

ВНИМАНИЕ
Если по какой-либо ветви требуется записать не один, а несколько операторов, они
заключаются в блок с помощью ключевых слов begin и end.

Пример. Программа, определяющая, какая из клавиш управления курсором была на-


жата.
Для объяснения этой программы надо забежать немного вперед и рассказать о том,
что в состав оболочек Паскаля входят так называемые модули — библиотеки по-
лезных при программировании ресурсов. В модуле Crt есть функция readkey, позво-
ляющая получить код нажатой клавиши. Модуль описан на с. 93 и в приложении 2
(с. 357).

38
Глава 2. Управляющие операторы языка 39

Функция readkey работает так1: если нажата алфавитно-цифровая клавиша, функ-


ция возвращает соответствующий символ. Если нажата клавиша курсора, возвра-
щается символ с кодом 0, а при повторном вызове можно получить так называемый
расширенный код клавиши. Для простоты можно считать, что расширенный код —
это номер клавиши на клавиатуре. Функция ord позволяет получить числовой код
символа.

Листинг 2.3. Клавиши курсора


program cursor_keys;
uses Crt;
var key : char;
begin
writeln('Нажмите одну из курсорных клавиш ');
key := readkey;
if ord(key) <> 0 then writeln('обычная клавиша')
else begin
key := readkey;
case ord(key) of
77: writeln('стрелка вправо');
75: writeln('стрелка влево');
72: writeln('стрелка вверх');
80: writeln('стрелка вниз');
else writeln('не стрелка');
end;
end;
end.

СОВЕТ
Хотя наличие слова else не обязательно, рекомендуется всегда описывать случай,
когда значение выражения не совпадает ни с одной из констант. Это облегчает поиск
ошибок при отладке программы.

Оператор case предпочтительнее оператора if в тех случаях, когда в программе


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

Операторы цикла
Операторы цикла используются для вычислений, повторяющихся многократно.
В Паскале имеется три вида циклов: цикл с предусловием while, цикл с постусло-
вием repeat и цикл с параметром for. Каждый из них состоит из определенной по-
следовательности операторов.

1
Приведено упрощенное описание. Более подробно работа с клавиатурой рассматривается
в разделе «Модуль Crt» на с. 93.

39
40 Часть I. Основы программирования

Блок, ради выполнения которого и организуется цикл, называется телом цикла.


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

Рис. 2.5. Структурные схемы операторов цикла

Начальные установки служат для того, чтобы до входа в цикл задать значения пере-
менных, которые в нем используются.
Проверка условия продолжения цикла (ромб «Выражение») выполняется на каждой
итерации либо до тела цикла (тогда говорят о цикле с предусловием, см. рис. 2.5, а),
либо после тела цикла (цикл с постусловием, см. рис. 2.5, б). Разница между ними
состоит в том, что тело цикла с постусловием всегда выполняется хотя бы один раз,
после чего проверяется, надо ли его выполнять еще раз. Проверка необходимости
выполнения цикла с предусловием делается до тела цикла, поэтому возможно, что
он не выполнится ни разу.
Параметром цикла называется переменная, которая используется при проверке
условия цикла и принудительно изменяется на каждой итерации, причем, как пра-
вило, на одну и ту же величину. Если параметр цикла целочисленный, он называется

40
Глава 2. Управляющие операторы языка 41

счетчиком цикла. Количество повторений такого цикла можно определить заранее.


Параметр есть не у всякого цикла. В так называемом итеративном цикле условие
продолжения содержит переменные, значения которых изменяются в цикле по ре-
куррентным формулам1.
Цикл завершается, если условие его продолжения не выполняется. Возможно при-
нудительное завершение как текущей итерации, так и цикла в целом. Для этого
служат операторы break, continue (см. раздел «Процедуры передачи управления»,
с. 46) и goto. Передавать управление извне внутрь цикла не рекомендуется, потому
что при этом могут не выполниться начальные установки.

Цикл с предусловием while


Формат оператора прост:
while выражение do оператор
Выражение должно быть логического типа. Например, это может быть операция отно-
шения или просто логическая переменная. Если результат вычисления выражения
равен true, выполняется расположенный после служебного слова do простой или
составной оператор (напомню, что составной оператор заключается между begin
и end). Эти действия повторяются до того момента, пока результатом выражения не
станет значение false. После окончания цикла управление передается на следую-
щий за ним оператор.

ВНИМАНИЕ
Если в теле цикла требуется выполнить более одного оператора, необходимо заключить
их в блок с помощью ключевых слов begin и end.

Пример. Программа, печатающая таблицу значений функции

⎧t , x < 0⎫
⎪ ⎪
y = ⎨ tx, 0 ≤ x < 10 ⎬
⎪2t , x ≥ 10 ⎪⎭

для аргумента, изменяющегося в заданных пределах с заданным шагом.
Опишем алгоритм в словесной форме.
1. Ввести исходные данные.
2. Взять первое значение аргумента.
3. Определить, какому из интервалов оно принадлежит.
4. Вычислить значение функции по соответствующей формуле.
5. Вывести строку таблицы.
6. Перейти к следующему значению аргумента.
7. Если оно не превышает конечное значение, повторить шаги 3–6, иначе закон-
чить.
1
Рекуррентной называется формула, в которой новое значение переменной вычисляется
с использованием ее предыдущего значения.

41
42 Часть I. Основы программирования

Шаги 3–6 повторяются многократно, поэтому для их выполнения надо органи-


зовать цикл. Назовем необходимые нам переменные так: начальное значение
аргумента — Xn, конечное значение аргумента — Xk, шаг изменения аргумента —
dX, параметр — t. Все величины вещественные. Программа выводит таблицу, со-
стоящую из двух столбцов — значений аргумента и соответствующих им значений
функции (листинг 2.4)1.

Листинг 2.4. Таблица значений функции (оператор while)


program tabl_fun;
var
Xn, Xk : real; { начальное и конечное значение аргумента }
dX : real; { шаг изменения аргумента }
x, y : real; { текущие значения аргумента и функции }
t : real; { параметр }
begin
writeln('Введите Xn, Xk, dX, t'); { приглашение ко вводу данных }
readln(Xn, Xk, dX, t); { ввод исходных данных – шаг 1 }
writeln(' --------------------------- '); { заголовок таблицы }
writeln('| X | Y |');
writeln(' --------------------------- ');
x := Xn; { первое значение аргумента = Xn – шаг 2 }
while x <= Xk do begin { заголовок цикла – шаг 7 }
if x < 0 then y := t; { вычисление значения функции - шаг 4 }
if (x >= 0) and (x < 10) then y := t * x; { шаг 4 }
if x >= 10 then y := 2 * t; { шаг 4 }
writeln('|', x:9:2,' |', y:9:2,' |');{ вывод строки табл. – шаг 5 }
x := x + dX; { переход к следующему значению аргумента - шаг 6 }
end;
writeln(' --------------------------- ');
end.

ПРИМЕЧАНИЕ
Еще один пример использования цикла while приведен на с. 47.

Цифры в комментариях соответствуют шагам алгоритма. Обратите внимание,


что условие продолжения цикла записано в его заголовке и проверяется до входа
в цикл. Таким образом, если задать конечное значение аргумента, меньшее началь-
ного, даже при отрицательном шаге цикл не будет выполнен ни разу.
Параметром этого цикла, то есть переменной, управляющей его выполнением, явля-
ется х. Для правильной работы цикла необходимо присвоить параметру начальное
значение до входа в цикл, поэтому блок начальных установок цикла присутствует
1
Из эстетических соображений можно заменить символы, которыми выполняется графле-
ние таблицы, на псевдографические. Их коды приведены в приложении 5. Чтобы ввести
символ с помощью его кода, надо нажать левую клавишу Alt и, не отпуская ее, ввести код
символа на дополнительной клавиатуре. При отпускании клавиши Alt символ отобразится
на экране.

42
Глава 2. Управляющие операторы языка 43

в явном виде (шаг 2). Начинающие часто забывают про этот блок. В данном слу-
чае при отсутствии этого оператора переменной х будет присвоено значение 0, по-
скольку в Паскале глобальные переменные обнуляются автоматически.
Блок модификации параметра цикла представлен оператором, выполняющимся на
шаге 6. Для перехода к следующему значению аргумента текущее значение нара-
щивается на величину шага и заносится в ту же переменную. Начинающие часто
забывают про модификацию параметра, в результате чего программа «зациклива-
ется». Если с вами произошла такая неприятность, попробуйте для завершения
программы нажать клавиши Ctrl+Break, а впредь перед запуском программы для
параметра цикла проверяйте:
 присвоено ли ему начальное значение;
 изменяется ли он на каждой итерации цикла;
 верно ли записано условие продолжения цикла.

Цикл с постусловием repeat


Тело цикла с постусловием заключено между служебными словами repeat и until,
поэтому заключать его в блок не требуется:
repeat
тело цикла
until выражение
В отличие от цикла while, этот цикл будет выполняться, пока логическое выраже-
ние после слова until ложно. Как только результат выражения станет истинным,
произойдет выход из цикла. Вычисление выражения выполняется в конце каждой
итерации цикла.
Этот вид цикла применяется в тех случаях, когда тело цикла необходимо обяза-
тельно выполнить хотя бы один раз: например, если в цикле вводятся данные и вы-
полняется их проверка. Если же такой необходимости нет, предпочтительнее поль-
зоваться циклом с предусловием.
Пример. Программа, вычисляющая квадратный корень вещественного аргумента X
с заданной точностью eps по итерационной формуле

1⎛ x ⎞
yn = y + ,
2 ⎜⎝ n − 1 yn − 1 ⎟⎠

где yn –1 — предыдущее приближение к корню (в начале вычислений выбирается про-


извольно), yn — последующее приближение. Процесс вычислений прекращается, когда
приближения станут отличаться друг от друга по абсолютной величине менее, чем
на eps — величину заданной точности (листинг 2.5).

Листинг 2.5. Вычисление квадратного корня


program square_root;
var X, eps, { аргумент и точность }
Yp, Y : real; { предыдущее и последующее приближение }
продолжение 

43
44 Часть I. Основы программирования

Листинг 2.5 (продолжение)


begin
repeat
writeln('Введите аргумент и точность (больше нуля): ');
readln(X, eps);
until (X > 0) and (eps > 0);
Y := 1;
repeat
Yp := Y;
Y := (Yp + X / Yp) / 2;
until abs(Y - Yp) < eps;
writeln('Корень из ', X:6:3, ' с точноcтью ', eps:7:5,
'равен ', Y:9:5);
end.

Цикл с параметром for


Этот оператор применяется, если требуется выполнить тело цикла заранее задан-
ное количество раз. Параметр порядкового типа на каждом проходе цикла автома-
тически либо увеличивается, либо уменьшается на единицу.
for параметр := выражение_1 to выражение_2 do оператор
for параметр := выражение_2 downto выражение_1 do оператор
Выражения должны быть того же типа, что и переменная цикла1, оператор — простым
или составным. Циклы с параметром обычно применяются при работе с массивами.
Пример 1. Программа выводит на экран в столбик числа от 1 до 10:
var i : integer;
begin
for i := 1 to 10 do writeln(i)
end.
Цикл будет выполнен 10 раз, на каждом проходе счетчик цикла — переменная i
увеличивается на 1.
Пример 2. Программа выводит на экран числа от 10 до 1 и подсчитывает их сум-
му:
var i, sum : integer;
begin
sum := 0;
for i := 10 downto 1 do begin
writeln(i); inc(sum, i)
end;
writeln('Сумма чисел: ', sum);
end.
В этом цикле переменная i автоматически уменьшается на 1.
1
Говоря более точно, они должны быть совместимы с переменной цикла по присваиванию.
Правила совместимости по присваиванию приведены на с. 74.

44
Глава 2. Управляющие операторы языка 45

Пример 3. Программа выводит на экран символы от 'a' до 'z':


var ch : char;
begin
for ch := 'a' to 'z' do write(ch:2)
end.
Здесь счетчик цикла ch символьного типа поочередно принимает значение каждого
символа от 'a' до 'z'.

ВНИМАНИЕ
Если в теле цикла требуется выполнить более одного оператора, необходимо заключить
их в блок с помощью ключевых слов begin и end.

Выражения, определяющие начальное и конечное значения счетчика, вычисляются


один раз до входа в цикл. Цикл for реализован в Паскале как цикл с предусловием,
то есть его можно представить в виде эквивалентного оператора while. Это означа-
ет, что, если условие продолжения цикла не выполняется при первом же значении
счетчика, тело цикла не будет пройдено ни разу. Так, цикл из первого примера мож-
но записать в виде
i := 1;
while i <= 10 do begin
writeln(i);
inc(i)
end;
После нормального завершения цикла значение счетчика не определено. Факти-
чески оно равно первому значению, для которого выполняется условие выхода из
цикла, но использовать это в программах не рекомендуется. Также не следует из-
менять значение счетчика внутри цикла вручную, например:
for i := 1 to 10 do begin inc(i,3); ... end; { плохо! }
Это может привести к зацикливанию программы.

Рекомендации по использованию циклов


Часто встречающимися ошибками при программировании циклов являются исполь-
зование в теле цикла переменных, которым не были присвоены начальные значе-
ния, а также неверная запись условия продолжения цикла. Нужно помнить и о том,
что в операторе while истинным должно являться условие повторения вычислений,
а в операторе repeat — условие их окончания.
Чтобы избежать ошибок, рекомендуется:
 не забывать о том, что, если в теле циклов while и for требуется выполнить более
одного оператора, нужно заключать их в блок;
 убедиться, что всем переменным, встречающимся в правой части операторов
присваивания в теле цикла, до этого присвоены значения, а также проверить,
возможно ли выполнение других операторов;

45
46 Часть I. Основы программирования

 проверить, изменяется ли в теле цикла хотя бы одна переменная, входящая


в условие продолжения цикла;
 предусматривать аварийный выход из итеративного цикла по достижении не-
которого предельно допустимого количества итераций (пример см. в следующем
разделе).

Процедуры передачи управления


В Паскале есть несколько стандартных процедур, изменяющих последовательность
выполнения операторов:
 break — завершает выполнение цикла, внутри которого записана;
 continue — выполняет переход к следующей итерации цикла;
 exit — выполняет выход из программы или подпрограммы, внутри которой за-
писана;
 halt — немедленно завершает выполнение программы1.
Кроме того, для передачи управления используется оператор перехода goto (см.
с. 48).
Рассмотрим пример применения процедур передачи управления.
Пример. Программа вычисления значения функции Сh x (гиперболический косинус)
с помощью бесконечного ряда Тейлора с точностью ε по формуле

x2 x4 x6 x 2n
y =1+ + + + ... + + ...
2! 4! 6! 2n !
Этот ряд сходится при любых значениях аргумента. При увеличении номера n мо-
дуль члена ряда Cn стремится к нулю. При некотором n неравенство |Cn| ≥ ε пере-
стает выполняться, и вычисления прекращаются.
Общий алгоритм прост: задать начальное значение суммы ряда, а затем многократ-
но вычислять очередной член ряда и добавлять его к ранее найденной сумме, пока
абсолютная величина очередного члена ряда не станет меньше заданной точности.
До выполнения программы предсказать, сколько членов ряда потребуется просум-
мировать, невозможно. В цикле такого рода есть опасность, что он никогда не за-
вершится — как из-за возможных ошибок в вычислениях, так и из-за ограниченной
области сходимости ряда (данный ряд сходится на всей числовой оси, но существу-
ют ряды Тейлора, которые сходятся только для определенного интервала значений
аргумента). Кроме того, в данном случае значения функции при увеличении абсо-
лютной величины аргумента сильно возрастают и могут переполнить разрядную
сетку. Поэтому для надежности программы необходимо предусмотреть аварийный
выход из цикла с печатью предупреждающего сообщения по достижении некоторо-
го максимально допустимого количества итераций.

1
Есть и еще одна процедура, runerror, вызывающая завершение программы с ошибкой вре-
мени выполнения (run-time error), но обычно этого же результата добиваются совершенно
непроизвольно.

46
Глава 2. Управляющие операторы языка 47

Прямое вычисление члена ряда по приведенной выше общей формуле, когда х воз-
водится в степень, вычисляется факториал, а затем числитель делится на знаме-
натель, имеет два недостатка, которые делают этот способ непригодным. Первый
недостаток — большая погрешность вычислений. При возведении в степень и вы-
числении факториала можно получить очень большие числа, при делении которых
друг на друга произойдет потеря точности, поскольку количество значащих цифр,
хранимых в ячейке памяти, ограничено; кроме того, большие числа могут перепол-
нить разрядную сетку. Второй недостаток связан с эффективностью вычислений:
легко заметить, что при вычислении очередного члена ряда предыдущий уже из-
вестен, поэтому вычислять каждый член ряда «от печки» нерационально.
Для уменьшения количества выполняемых действий следует воспользоваться ре-
куррентной формулой получения последующего члена ряда через предыдущий:
Cn+1 = Cn × T, где T — некоторый множитель. Подставив в эту формулу Cn и Cn+1, по-
лучим выражение для вычисления Т:

Cn +1 2n ! x 2( n +1) x2 .
T = = 2n =
Cn x (2(n + 1))! (2n + 1)(2n + 2)
Текст программы с комментариями приведен в листинге 2.6.

Листинг 2.6. Вычисление суммы бесконечного ряда


program ch;
const MaxIter = 500; { максимальное количество итераций }
var x, eps : double; { аргумент и точность }
c, y : double; { член ряда и его сумма }
n : integer; { номер члена ряда }
done : boolean; { признак достижения точности }
begin
writeln('Введите аргумент и точность:');
readln(x, eps);
done := true;
c := 1; y := c; { первый член ряда и нач. значение суммы }
n := 0;
while abs(c) > eps do begin
c := c * sqr(x) /(2 * n + 1)/(2 * n + 2); { очередной член ряда }
y := y + c; { добавление члена ряда к сумме }
inc(n);
if n > MaxIter then begin { аварийный выход из цикла }
writeln('Ряд расходится!');
done := false; break
end
end;
if done then
writeln('Для аргумента ', x, ' значение функции: ', y, #13#10,
'вычислено с точностью', eps, ' за ', n, ' итераций');
readln;
end.

47
48 Часть I. Основы программирования

Ниже приведен пример использования процедуры continue (вместо оператора, по-


меченного комментарием «аварийный выход из цикла»):
if n <= MaxIter then continue;
writeln('Ряд расходится!');
done := false; break;
Два последних оператора будут выполнены только один раз — при превышении
максимально допустимого количества итераций.
Получение суммы бесконечного ряда — пример вычислений, которые принци-
пиально невозможно выполнить точно. В данном случае мы задавали желаемую
погрешность вычислений с помощью ε. Ее величина не может быть меньше, чем
самое малое число, представимое с помощью переменной типа double, но при за-
дании такого значения точность результата фактически будет гораздо ниже из-за
погрешностей, возникающих про вычислениях. Они связаны с конечностью раз-
рядной сетки.
В общем случае погрешность результата складывается из нескольких частей:
 погрешность постановки задачи (возникает при упрощении задачи);
 начальная погрешность (точность представления исходных данных);
 погрешность метода (при использовании приближенных методов решения за-
дачи);
 погрешности округления и вычислений (поскольку величины хранятся в огра-
ниченном количестве разрядов).
Специфика машинных вычислений состоит в том, что алгоритм, безупречный
с точки зрения математики, при реализации без учета возможных погрешностей
может привести к получению результатов, не содержащих ни одной верной знача-
щей цифры! Это происходит, например, при вычитании двух близких значений или
при работе с очень большими или малыми числами.

Оператор перехода goto


Этот оператор имеет простой синтаксис: в точке программы, из которой требуется
организовать переход, после слова goto через пробел записывается имя метки, на-
пример goto 1 или goto error. При программировании на Паскале необходимость
в применении оператора перехода возникает в очень ограниченном количестве си-
туаций, в большинстве же случаев используются операторы циклов вместе с про-
цедурами передачи управления.
Использование оператора безусловного перехода оправданно, как правило, в двух
случаях:
 принудительный выход вниз по тексту программы из нескольких вложенных
циклов или операторов выбора;
 переход из нескольких мест программы в одно (например, если перед выходом
из программы необходимо всегда выполнять какие-либо действия).
Во всех остальных случаях следует привести алгоритм к структурному виду, то есть
преобразовать его так, чтобы он мог быть записан с помощью базовых конструкций.

48
Глава 3. Типы данных, определяемые
программистом
Информация, которую требуется обрабатывать в программе, имеет различную
структуру. Для ее адекватного представления используются типы данных, которые
программист определяет сам в разделе описания типов type. Типу дается произ-
вольное имя, которое можно затем использовать для описания программных объ-
ектов точно так же, как и стандартные имена типов.
type имя_типа = описание_типа
...
var имя_переменной : имя_типа
Можно задать тип и непосредственно при описании переменных:
var имя_переменной : описание_типа
В этом случае, как видите, имя типу не дается. Этот способ удобно применять, если
тип используется только в одном месте программы.

ПРИМЕЧАНИЕ
Компилятор не сопоставляет различные описания переменных. Это означает, что, если
две переменные описаны в разных операторах с помощью идентичных описаний, их
типы будут считаться различными.

Программист может определить простые и составные типы (см. классифика-


цию на с. 16). Рассмотрим сначала простые типы — перечисляемый и интер-
вальный.

Перечисляемый тип данных


При написании программ часто возникает потребность определить несколько свя-
занных между собой именованных констант, имеющих различные значения. Для
этого удобно воспользоваться перечисляемым типом данных, все возможные зна-
чения которого задаются списком констант.
type имя_типа = (список имен констант)
Константы в списке перечисляются через запятую, например:
type Menu = (READ, WRITE, EDIT, QUIT)

49
50 Часть I. Основы программирования

Переменным перечисляемого типа можно присвоить либо значение одной из пере-


численных констант, либо значение другой переменной того же типа, например:
var m, n : Menu;

m := READ; n := m;
Перечисляемый тип относится к порядковым типам данных. Это означает, что
к величинам этого типа можно применять функции получения номера значения
в списке Ord, предыдущего значения Pred и последующего значения Succ. Константы
в списке нумеруются с нуля. Например, Ord(READ) даст в результате 0, Succ(EDIT) —
QUIT. Попытка получения значения, следующего за последним, приведет к ошибке.
Использовать перечисляемый тип в операциях ввода-вывода нельзя. Имена кон-
стант в пределах области их описания (программы или подпрограммы) должны
быть уникальными. Иными словами, в двух перечисляемых типах имена констант
совпадать не должны.

Интервальный тип данных


С помощью интервального типа задается диапазон значений какого-либо типа.
type имя_типа = константа_1 .. константа_2
Константы должны быть одного и того же порядкового типа. Тип, на котором стро-
ится интервал, называется базовым. Константа_1 должна быть меньше константы_2
или равна ей. Примеры описания интервальных типов:
type Hour = 0 .. 23;
Range = –100 .. 100;
Letters = 'a' .. 'z';
Actions = READ .. EDIT;
Как и для других типов, определяемых программистом, интервальный тип можно
задать прямо при описании переменной, например:
var r : –100 .. 100;
С переменной интервального типа можно делать все, что допустимо для ее базо-
вого типа. Единственное ограничение — ее значение должно находиться в ука-
занном диапазоне, в противном случае произойдет ошибка времени выполнения
«Constant out of range», то есть «Выход за пределы диапазона». Поэтому, если
заранее известно, что переменная должна принимать только значения из опреде-
ленного диапазона, лучше описать ее как интервальную, это может помочь при
отладке программы.
Интервальный тип используется в программах как самостоятельно, так и внутри
определения составного типа — массива, который мы рассмотрим ниже.

Массивы
При использовании простых переменных, которые мы рассматривали до этого
момента, каждой области памяти, предназначенной для хранения какого-либо

50
Глава 3. Типы данных, определяемые программистом 51

значения, соответствует свое имя. Если с группой величин одинакового типа тре-
буется выполнять однообразные действия, им дают одно имя, а различают по по-
рядковому номеру (индексу). Это позволяет компактно записывать множество
операций с помощью циклов.
Конечная именованная последовательность однотипных величин называется мас-
сивом. Чтобы описать массив, надо определить, к какому типу принадлежат его эле-
менты и каким образом они пронумерованы (какого типа его индекс).
type имя_типа = array [тип_индекса] of тип_элемента
Здесь array и of — ключевые слова, тип индекса задается в квадратных скобках.
Примеры описания типа:
type mas = array [1 .. 10] of real;
Color = array [byte] of mas;
Active = array [Menu] of boolean;
В первом операторе описан тип массива из вещественных элементов, которые ну-
меруются от 1 до 10. Во втором операторе элементами массива являются масси-
вы типа mas, а нумеруются они в пределах, допустимых для типа byte, то есть от 0
до 255. Еще более экзотический пример — третий, где в качестве индекса исполь-
зовано имя типа из раздела «Перечисляемый тип данных» (с. 49), а сами элементы
могут принимать значения true или false.
Итак, тип элементов массива может быть любым, кроме файлового, тип индексов —
интервальным, перечисляемым или byte. Чаще всего для описания индекса исполь-
зуется интервальный тип данных.

ВНИМАНИЕ
Размещение массива в памяти происходит до выполнения программы, поэтому при
описании индекса можно применять только константы или константные выражения.
Переменные использовать нельзя!

Обычно при описании массива верхняя граница его индекса задается в виде имено-
ванной константы, например:
const n = 6;
type intmas = array [1 .. n] of integer;
После задания типа массива переменные этого типа описываются обычным обра-
зом:
var a, b : intmas;
Если требуемый тип массива используется только в одном месте программы, мож-
но описать тип прямо при определении переменных:
var a, b : array [1 .. n] of integer;
С массивами в целом можно выполнять только одну операцию: присваивание. При
этом массивы должны быть одного типа:
b := a;

51
52 Часть I. Основы программирования

Все остальные действия выполняются с отдельными элементами массива. Для об-


ращения к элементу массива после имени массива указывается номер элемента
в квадратных скобках:
a[4] b[i]
С элементом массива можно делать все, что допустимо для переменных того же
типа.
При обращении к элементу массива автоматический контроль выхода индекса за
границу массива не производится. Для включения режима автоматического контро-
ля необходимо добавить в любое место программы, предшествующее обращениям
к элементу, ключ компиляции {$R+} или установить соответствующий режим в обо-
лочке. Рекомендуется включать этот режим на период отладки, а после ее заверше-
ния выключать (более подробно об этой директиве см. приложение 4).
Инициализация массивов. Элементам массива можно присвоить значения до начала
выполнения программы. Это делается так же, как и для простых переменных, —
в разделе описания констант, например:
const a : intmas = (0, 5, –7, 100, 15, 1);
Количество констант должно точно соответствовать числу элементов массива. Мас-
сивы, описанные в разделе var главной программы, обнуляются автоматически.
Рассмотрим задачу поиска максимального элемента массива. Очевидно, что для
отыскания самого большого элемента нужно сравнить между собой все элементы
массива. Поскольку компьютер может сравнивать одновременно только два числа,
элементы выбираются попарно. Например, сначала первый элемент сравнивается
со вторым, затем тот из них, который оказался больше, — с третьим, тот, который
оказался больше, — с четвертым, и так далее до последнего элемента.
Иными словами, при каждом сравнении из двух чисел выбирается наибольшее.
Поскольку его надо где-то хранить, в программе описывается переменная того же
типа, что и элементы массива. После окончания просмотра массива в ней окажет-
ся самый большой элемент. Для того чтобы все элементы сравнивались единоо-
бразно, перед началом просмотра в эту переменную заносится какой-либо элемент
массива.
Сформулируем алгоритм поиска максимума.
1. Принять за максимальный первый элемент массива.
2. Просмотреть массив, начиная со второго элемента.
3. Если очередной элемент оказывается больше максимального, принять его за
максимальный.
Можно просматривать массив не со второго, а с первого элемента. В этом случае
за начальное значение максимума можно принять любой элемент. Программа при-
ведена в листинге 3.1.

Листинг 3.1. Максимальный элемент массива из 20 вещественных элементов


program max_elem;
const n = 20;

52
Глава 3. Типы данных, определяемые программистом 53

var a : array [1 .. n] of real;


i : integer;
max : real;
begin
writeln('Введите ', n, ' элементов массива');
for i := 1 to n do read(a[i]);
max := a[1]; { принять за максимальный первый элемент массива }
for i := 2 to n do { просмотреть массив, начиная со второго элемента }
if a[i] > max then max := a[i]; { при необходимости обновить максимум }
writeln('Максимальный элемент: ', max:6:2)
end.
Еще один простой пример работы с массивом приведен в листинге 3.2.

Листинг 3.2. Сумма и количество отрицательных элементов


целочисленного массива
program sum_num;
const n = 10;
var a : array [1 .. n] of integer;
i, sum, num : integer;
begin
writeln('Введите ', n, ' элементов массива');
for i := 1 to n do read(a[i]);
sum := 0;
num := 0;
for i := 1 to n do begin
sum := sum + a[i];
if a[i] < 0 then inc(num);
end;
writeln('Сумма элементов: ', sum);
writeln('Отрицательных элементов: ', num);
end.
Рассмотрим теперь задачу сортировки массива. Будем использовать метод выбора.
Он состоит в том, что сначала выбирается наименьший элемент массива и меняется
местами с первым элементом, затем просматриваются элементы, начиная со второ-
го, и наименьший из них меняется местами со вторым элементом, и так далее n – 1
раз. На последнем проходе цикла при необходимости меняются местами предпо-
следний и последний элементы массива.

Листинг 3.3. Сортировка выбором


program sort;
const n = 20;
var a : array [1 .. n] of integer;
i, j, nmin, buf : integer;
begin
writeln('Введите ', n, ' элементов массива');
for i := 1 to n do read(a[i]);
for i := 1 to n – 1 do begin { просмотр массива n–1 раз }
продолжение 

53
54 Часть I. Основы программирования

Листинг 3.3 (продолжение)


nmin := i;
for j := i + 1 to n do { поиск минимума }
if a[j] < a[nmin] then nmin := j;
buf := a[i]; { перестановка }
a[i] := a[nmin]; { двух }
a[nmin] := buf; { элементов массива }
end;
writeln('Упорядоченный массив:');
for i := 1 to n do write(a[i]:5)
end.
Процесс обмена значениями между элементами массива с номерами i и nmin через
буферную переменную buf иллюстрирует рис. 3.1. Цифры около стрелок обознача-
ют порядок действий.

Рис. 3.1. Обмен значениями между двумя элементами массива

Двумерные массивы
Элемент массива может быть любого типа, кроме файлового, следовательно, он мо-
жет быть и массивом, например:
const n = 4; m = 3;
type mas = array [1 .. n] of integer;
mas2 = array [1 .. m] of mas;
Более компактно это можно записать так:
type mas2 = array [1 .. m, 1 .. n] of integer;
Здесь описан тип массива, состоящего из m массивов, каждый из которых содержит
n целых чисел. Иными словами, это матрица из m строк и n столбцов (рис. 3.2). Обе
размерности массива должны быть константами или константными выражениями,
поскольку инструкции по выделению памяти формируются компилятором до вы-
полнения программы. Имя типа указывается при описании переменных, например:
var a, b : mas2;
В памяти двумерный массив располагается по строкам:
a11 a12 a13 a14 a21 a22 a23 a24 a31 a32 a33 a34
| – 1–я строка – | – 2–я строка – | – 3–я строка – |

54
Глава 3. Типы данных, определяемые программистом 55

Строки массива ничем не отделены одна от другой, то есть прямоугольной матри-


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

Рис. 3.2. Матрица из m строк и n столбцов (m = 3, n = 4)

К элементу двумерного массива обращаются, указывая номер строки и номер столб-


ца, на пересечении которых он расположен, например:
a[1, 4] b[i, j] b[j, i]

ВНИМАНИЕ
Необходимо помнить, что компилятор воспринимает как номер строки первый индекс,
как бы он ни был обозначен в программе.

При инициализации двумерных массивов каждая строка заключается в дополни-


тельную пару круглых скобок, например:
const a : mas2 = ( ( 2, 3, 1, 0),
( 1, 9, 1, 3),
( 3, 5, 7, 0) );
С массивами в целом определена только одна операция — присваивание массивов
одного типа (например, b := a) . Все остальные действия выполняются с отдельны-
ми элементами. Например, чтобы ввести с клавиатуры двумерный массив, необхо-
димо организовать вложенные циклы:
for i := 1 to m do
for j := 1 to n do read(a[i, j]);
В соответствии с приведенным здесь порядком следования циклов элементы мас-
сива должны вводиться по строкам (при этом неважно, как будут располагаться
элементы массива — на одной строке исходных данных, на разных строках или во-
обще по одному элементу на строке, — важен только порядок следования элементов
друг относительно друга).
Пример 1. Программа, которая для целочисленной матрицы 3 × 4 определяет сред-
нее арифметическое ее элементов и количество положительных элементов в каждой
строке.
Для нахождения среднего арифметического элементов массива требуется найти их
общую сумму, после чего разделить ее на количество элементов. Порядок перебора

55
56 Часть I. Основы программирования

элементов массива (по строкам или по столбцам) роли не играет. Нахождение ко-
личества положительных элементов каждой строки требует просмотра матрицы по
строкам. Схема алгоритма приведена на рис. 3.3, программа — в листинге 3.4.

Рис. 3.3. Структурная схема алгоритма для примера 1

56
Глава 3. Типы данных, определяемые программистом 57

Листинг 3.4. Среднее арифметическое и количество положительных элементов


program sred_n;
const m = 3; n = 4;
var a : array [1 .. m, 1 .. n] of integer;
i, j, n_pos_el : integer;
sred : real;
begin
for i := 1 to m do
for j := 1 to n do read(a[i, j]);
sred := 0;
for i := 1 to m do begin
n_pos_el := 0;
for j := 1 to n do begin
sred := sred + a[i, j];
if a[i, j] > 0 then inc(n_pos_el);
end;
writeln('В ', i, '–й строке ', n_pos_el, ' положительных элементов');
end;
sred := sred / m / n;
writeln('Среднее арифметическое: ', sred:6:2);
end.
Обратите внимание на то, что переменная sred, в которой накапливается сумма эле-
ментов, обнуляется перед циклом просмотра всей матрицы, а количество положи-
тельных элементов — перед циклом просмотра очередной строки, поскольку для
каждой строки его вычисление начинается заново.

СОВЕТ
Записывайте операторы инициализации накапливаемых в цикле величин непосред-
ственно перед циклом, в котором они вычисляются.

Пример 2. Программа, которая для прямоугольной целочисленной матрицы 3 × 4


определяет номер самого левого столбца, содержащего только положительные эле-
менты. Если такого столбца нет, выводится сообщение.

Для решения этой задачи матрицу необходимо просматривать по столбцам. Сде-


лать вывод о том, что какой-либо столбец содержит только положительные эле-
менты, можно только после просмотра столбца целиком; зато, если в процессе про-
смотра встретился отрицательный элемент, можно сразу переходить к следующему
столбцу.
Эта логика реализуется с помощью переменной-флага all_posit, которая перед на-
чалом просмотра каждого столбца устанавливается в значение true, а при нахожде-
нии отрицательного элемента «опрокидывается» в false. Если все элементы столб-
ца положительны, флаг не опрокинется и останется истинным, что будет являться
признаком наличия искомого столбца в матрице.
Если столбец найден, просматривать матрицу дальше не имеет смысла, поэтому
выполняется выход из цикла и вывод результата (листинг 3.5).

57
58 Часть I. Основы программирования

Листинг 3.5. Поиск в матрице


program num_posit;
const m = 3; n = 4;
var a: array [1 .. m, 1 .. n] of integer;
i, j, num : integer;
all_posit : boolean;
begin
for i := 1 to m do
for j := 1 to n do read(a[i, j]);
num := 0;
for j := 1 to n do begin
all_posit := true;
for i := 1 to m do
if a[i, j] < 0 then begin
all_posit := false; break; end;
if all_posit then begin
num := j; break; end;
end;
if num = 0 then writeln('Таких столбцов нет')
else writeln('Номер столбца: ', num);
end.
В программе предусмотрен случай, когда ни один столбец не удовлетворяет
условию. Для этого переменной num, в которой будет находиться номер искомого
столбца, присваивается начальное значение, не входящее в множество значений,
допустимых для индекса, например нуль. Перед выводом результата его значение
анализируется. Если оно после просмотра матрицы сохранилось неизменным, то
есть осталось равным нулю, значит, столбцов, удовлетворяющих заданному усло-
вию, в матрице нет.

Строки
Строки используются для хранения последовательностей символов. В Паскале су-
ществует три типа строк:
 стандартные (string);
 определяемые программистом на основе string;
 строки в динамической памяти (рассматриваются на с. 101).
Строка типа string может содержать до 255 символов. Под каждый символ отво-
дится по одному байту, в котором хранится код символа. Еще один байт отводит-
ся под фактическую длину строки. Таким образом, в памяти под одну переменную
типа string всегда отводится 256 байт.
Для коротких строк использовать стандартную строку неэффективно, поэтому есть
возможность самостоятельно задавать максимальную длину строки. Например,
ниже описан собственный тип данных с именем str4:
type str4 = string [4]; { переменная такого типа занимает в памяти 5 байтов }

58
Глава 3. Типы данных, определяемые программистом 59

ВНИМАНИЕ
Длина строки должна быть константой или константным выражением, потому что
инструкции по выделению памяти формируются компилятором до начала выполнения
программы.

Примеры описания строк:


const n = 15;
var s : string; { строка стандартого типа }
s1 : str4; { строка типа str4, описанного выше }
s2 : string [n]; { описание типа задано при описании переменной }
Инициализация строк, как и переменных других типов, выполняется в разделе опи-
сания констант:
const s3 : string [15] = 'shooshpanchik';
Внутреннее представление строки s3 представлено на рис. 3.4.

Рис. 3.4. Внутреннее представление строки s3

Мы познакомились с описанием строк и их внутренним представлением. Те-


перь в соответствии с определением типа данных (см. с. 16) надо изучить, что
можно делать со строками, то есть какие операции и функции для них приме-
няются.

Операции
Строки можно присваивать друг другу. Если максимальная длина результирующей
строки меньше длины исходной, лишние символы справа отбрасываются:
s2 := 'shooshpanchik';
s1 := s2; { в s1 будут помещены символы "shoo" }
Строки можно склеивать (сцеплять) между собой с помощью операции конкатена-
ции, которая обозначается знаком +, например:
s1 := 'ком';
s2 := s1 + 'пот'; { результат — "компот" }
Строки можно сравнивать друг с другом с помощью операций отношения. При
сравнении строки рассматриваются посимвольно слева направо, при этом срав-
ниваются коды соответствующих пар символов. Строки равны, если они име-
ют одинаковую длину и посимвольно эквивалентны. В строках разной длины

59
60 Часть I. Основы программирования

существующий символ всегда больше соответствующего ему отсутствующего


символа:
'abc' > 'ab' 'abc' = 'abc' 'abc' < 'abc '
Имя строки может использоваться в процедурах ввода-вывода:
readln (s1, s2); write (s1);
При вводе в строку считывается из входного потока количество символов, равное
длине строки или меньшее, если символ перевода строки (который вводится нажа-
тием клавиши Enter) встретится раньше. При выводе под строку отводится количе-
ство позиций, равное ее фактической длине.
К отдельному символу строки можно обращаться как к элементу массива симво-
лов, например s1[4]. Символ строки совместим с типом char, их можно использо-
вать в выражениях одновременно, например:
s1[4] := 'x'; writeln (s2[3] + s2[5] + 'r');

Процедуры и функции для работы со строками


При работе со строками, как правило, возникает необходимость выполнять опре-
деленный набор действий со строкой и ее фрагментами, например копирование,
вставку, удаление или поиск. Для эффективной реализации этих действий в Паска-
ле предусмотрены стандартные процедуры и функции. Они кратко описаны ниже.
Функция Concat(s1, s2, ..., sn) возвращает строку, являющуюся слиянием строк
s1, s2, ..., sn. Ее действие аналогично операции конкатенации.
Функция Copy(s, start, len) возвращает подстроку длиной len, начинающуюся
с позиции start строки s. Параметры len и start должны быть целого типа.
Процедура Delete(s, start, len) удаляет из строки s, начиная с позиции start, под-
строку длиной len.
Процедура Insert(subs, s, start) вставляет в строку s подстроку subs, начиная с по-
зиции start.
Функция Length(s) возвращает фактическую длину строки s, результат имеет тип
byte.
Функция Pos(subs, s) ищет вхождение подстроки subs в строку s и возвращает но-
мер первого символа subs в s или нуль, если subs не содержится в s.
Процедура Str(x, s) преобразует числовое значение x в строку s, при этом для x
может быть задан формат, как в процедурах вывода write и writeln, например
Str(x:6:2, s).
Процедура Val(s, x, errcode) преобразует строку s в значение числовой перемен-
ной x, при этом строка s должна содержать символьное представление числа. В слу-
чае успешного преобразования переменная errcode равна нулю. Если же обнаруже-
на ошибка, то errcode будет содержать номер позиции первого ошибочного симво-
ла, а значение x не определено.
Пример. Программа читает текст из файла и выводит его на экран, заменяя задан-
ную с клавиатуры последовательность символов на многоточие (листинг 3.6).

60
Глава 3. Типы данных, определяемые программистом 61

Листинг 3.6. Поиск и замена в строке


program censor;
var s, str : string[10];
f : text;
i, dl : integer;
begin
assign(f, 'primer.txt'); reset(f);
writeln('Какую последовательность заменять?'); readln(s);
dl := length(s);
while not Eof(f) do begin
readln(f, str);
i := 1;
while i <> 0 do begin
i := Pos(s, str);
if i <> 0 then begin
Delete(str, i, dl); Insert('...', str, i);
end;
end;
writeln(str);
end;
close(f)
end.

Записи
В программах часто возникает необходимость логического объединения данных.
Например, база данных предприятия содержит для каждого сотрудника его фами-
лию, дату рождения, должность, оклад и т. д.; программа моделирования движения
поездов — пункты отправления и прибытия, время, количество вагонов и многое
другое. Однотипные данные, как вы уже знаете, организуются в массивы, а для
объединения разнотипных данных предназначен тип «запись». Он вводится с по-
мощью ключевого слова record1. Элементы записи называются полями.
type имя_типа = record
описание 1-го поля записи;
описание 2-го поля записи;
...
описание n-го поля записи;
end;
Поля записи могут быть любого типа, кроме файлового. Например, для каждого
товара на складе требуется хранить его наименование, цену и количество единиц:
type goods = record
name : string[20];
price : real;
number : integer;
end;
1
Обратите внимание, что запись наряду с массивом является типом, определяемым програм-
мистом, а не стандартным типом языка.

61
62 Часть I. Основы программирования

Переменные типа «запись» описываются обычным образом. Можно задавать опи-


сание типа при описании переменной, создавать массивы из записей, записи из
массивов, и т. д.
var g1, g2 : goods;
stock : array [1 .. 100] of goods;
student : record
name : string [30];
group : byte;
marks : array [1 .. 4] of byte;
end;
С записями целиком можно делать то же, что и с массивами: присваивать одну за-
пись другой, если они одного типа, например:
g1 := g2; g2 := stock[3];
Все остальные действия выполняются с отдельными полями записи. Есть два спо-
соба доступа к полю записи: либо с помощью конструкции имя_записи.имя_поля, либо
с использованием оператора присоединения with, например:
g1.price := 200;
with g1 do begin
price := 200; number := 10
end;
Оператор with удобнее использовать, если требуется обращаться к нескольким по-
лям одной и той же записи.
Пример. Сведения о товарах на складе хранятся в текстовом файле. Для каждого
товара отводится одна строка, в первых 20 позициях которой записано наименова-
ние товара, а затем через произвольное количество пробелов — его цена и количество
единиц. Программа по запросу выдает сведения о товаре или сообщение о том, что
товар не найден (листинг 3.7).

Листинг 3.7. Поиск в массиве записей


program store;
const Max_n = 100;
type str20 = string [20];
goods = record
name : str20;
price : real;
number : integer;
end;
var stock : array[1 .. Max_n] of goods;
i, j, len : integer;
name : str20;
found : boolean;
f : text;
begin
assign(f, 'stock.txt'); reset(f);
i := 1;

62
Глава 3. Типы данных, определяемые программистом 63

while not Eof(f) do begin


with stock[i] do readln(f, name, price, number);
inc(i);
if i > Max_n then begin { 1 }
writeln('Переполнение массива'); exit end;
end;
while true do begin { 2 }
writeln('Введите наименование'); Readln(name);
len := length(name);
if len = 0 then break; { 3 }
for j := len + 1 to 20 do name := name + ' '; { 4 }
found := false;
for j := 1 to i – 1 do begin { 5 }
if name <> stock[j].name then continue;
with stock[j] do writeln (name:22, price:7:2, number:5);
found := true;
break;
end;
if not found then writeln ('Товар не найден'); { 6 }
end;
end.
Инициализация записей выполняется в разделе констант, при этом для каждого
поля задается его имя, после которого через двоеточие указывается значение:
const g : goods = (name : 'boots'; price : 200; number : 10);

Записи с вариантной частью


Этот вид записей применяется для экономии памяти. Представьте себе массив за-
писей, состоящих из многих полей; в каждой записи заполнена только часть полей,
причем в разных записях разная. Например, в телефонной книге для каждого або-
нента хранится его фамилия и телефонный номер, при этом для служебных кон-
тактов указывается должность, а для личных — дата рождения и код на входной
двери в дом. Изменяющаяся часть записи называется вариантной. Все варианты
располагаются в памяти на одном и том же месте в конце записи.
type contact = record
name : string [40];
tel : string [15];
case i : integer of
0 : (post : string [20]);
1 : (date : string [10]; code : word);
end;
Вариантная часть может быть только одна, ее длина равна длине наибольшего из
вариантов. В этом примере адрес начала поля post совпадает с началом поля date.
Записи с вариантной частью применяются только тогда, когда известно, что любая
конкретная запись может содержать набор полей только из одного варианта. Какой
именно вариант используется, можно определить по значению поля i, о формиро-
вании значения которого должен позаботиться сам программист.

63
64 Часть I. Основы программирования

Поле варианта должно быть порядкового типа. Часто его делают перечисляемым.
Например, пусть требуется хранить в памяти характеристики геометрических фи-
гур — прямоугольника, треугольника и круга. Для каждой из них задается местопо-
ложение (это будет общая часть записи), а также длины сторон для прямоугольни-
ка, координаты двух вершин для треугольника и радиус для круга:
type figure = (rect, triangle, circle);
shape = record
x, y : real;
case kind : figure of
rect : (height, width : real);
triangle : (x2, y2, x3, y3 : real);
circle : (radius : real);
end;
Можно не хранить в записи поле варианта, в этом случае его имя не указывается:
type shape = record
x, y : real;
case integer of
0 : (height, width : real);
1 : (x2, y2, x3, y3 : real);
2 : (radius : real);
end;
Доступ к вариантным полям остается на совести программиста, то есть ничто не
мешает записать в вариантную часть одни поля, а обратиться к ним через другие:
type my_string = record
case integer of
0 : (s : string [10]);
1 : (x : array [1 .. 11] of byte);
2 : (z : record
len : byte; c : array [1 .. 10] of char;
end);
end;
var a : my_string;
...
a.s := 'shooshpanchiki';
writeln(length(a.s), a.x[1], a.z.len );
В последней строке приведено три способа доступа к одному и тому же байту, хра-
нящему длину строки s.

Множества
Множественный тип данных в языке Паскаль соответствует математическому
представлению о множествах: это ограниченная совокупность различных элемен-
тов. Множество создается на основе элементов базового типа — это может быть
перечисляемый тип, интервальный или byte. В множестве не может быть более
256 элементов, а их порядковые номера должны лежать в пределах от 0 до 255.

64
Глава 3. Типы данных, определяемые программистом 65

Множество описывается с помощью служебных слов set of:


type имя_типа = set of базовый_тип;
Примеры описания множественных типов:
type Caps = set of 'A' .. 'Z';
Colors = set of (RED, GREEN, BLUE);
Numbers = set of byte;
Принадлежность переменных к множественному типу может быть определена пря-
мо в разделе описания переменных, например:
var oct : set of 0 .. 7;
Тип «множество» задает набор всех возможных подмножеств его элементов, вклю-
чая пустое. Если базовый тип, на котором строится множество, имеет k элементов,
то количество подмножеств, входящих в это множество, равно 2k.
Константы множественного типа записываются в виде заключенной в квадратные
скобки последовательности элементов или интервалов базового типа, разделенных
запятыми, например:
['A', 'D'] [1, 3, 6] [2, 3, 10 .. 13].
Порядок перечисления элементов базового типа в константах не имеет значения.
Константа вида [ ] означает пустое подмножество. Переменная типа «множество»
содержит одно конкретное подмножество значений множества. Пусть имеется пе-
ременная b интервального типа:
var b : 1 .. 3; { переменная может принимать три различных значения: 1, 2 или 3 }
Переменная m типа «множество»
var m : set of 1 .. 3;
может принимать восемь различных значений:
[ ] [1] [2] [3] [1, 2] [1, 3] [2, 3] [1, 2, 3]

Операции над множествами


Величины множественного типа не могут быть элементами списка ввода-вывода.
Допустимые операции над множествами перечислены в табл. 3.1.

Таблица 3.1. Операции над множествами

Знак Название Математическая запись Результат


:= Присваивание Множество
+ Объединение ∪ Множество
* Пересечение ∩ Множество
– Дополнение \ Множество
= Тождественность = Логический
<> Нетождественность ≠ Логический
<= Содержится в ⊆ Логический
>= Содержит ⊇ Логический
in Принадлежность Логический

65
66 Часть I. Основы программирования

Операции над множествами в основном соответствуют операциям, определенным


в теории множеств. Объединение множеств А и В — это множество, состоящее из
всех элементов, принадлежащих А и В. Пересечение множеств А и В — множество,
состоящее из элементов, принадлежащих одновременно А и В. Дополнение мно-
жеств А и В — множество, состоящее из элементов множества А, не принадлежа-
щих В.
Тождественность множеств А и В означает, что эти множества совпадают, нетож-
дественность — не совпадают. Множество А содержится в множестве В, если все
элементы А принадлежат В. Множество А содержит множество В, если все элемен-
ты В принадлежат А.
В операциях могут участвовать переменные и константы совместимых множествен-
ных типов. Исключение составляет операция in: ее первый операнд должен при-
надлежать базовому типу элементов множества, записанного вторым операндом.
Рассмотрим примеры применения операций. Пусть задано множество, основанное
на значениях прописных латинских букв.
type Caps = set of 'A' .. 'Z';
var a, b, c : Caps;
begin
a := ['A', 'U' .. 'Z'];
b := [ 'M' .. 'Z'];
c := a; { присваивание }
c := a + b; { объединение, результат ['A', 'M' .. 'Z'] }
c := a * b; { пересечение, результат ['U' .. 'Z'] }
c := b – a; { вычитание, результат ['M' .. 'T'] }
c := a – b; { вычитание, результат ['A'] }
if a = b then writeln ('тождественны'); { не выполнится }
if a <> b then writeln ('не тождественны'); { выполнится }
if c <= a then writeln ('c содержится в а'); { выполнится }
if 'N' in b then writeln ('в b есть N'); { выполнится }
end.
С помощью констант-множеств часто проверяют, входит ли символ в заданный
диапазон. Например, чтобы проверить, является ли введенный символ цифрой,
можно написать:
var c : char;
...
if c in ['0' .. '9'] then ...
Пример. В автомобильных гонках участвуют 30 машин со стартовыми номерами
от 101 до 130. Проводится 5 заездов по 6 машин. Программа формирует состав за-
ездов случайным образом (листинг 3.8).

Листинг 3.8. Применение множества


program race;
var cast, lap : set of 100 .. 130;
i, j, n : byte;

66
Глава 3. Типы данных, определяемые программистом 67

begin
randomize;
cast := [];
for i := 1 to 5 do begin
write ( i, '–й заезд: ');
lap := [];
for j := 1 to 6 do begin
repeat
n := random(30) + 101;
until not (n in lap) and not (n in cast);
lap := lap + [n];
write (n:4);
end;
cast := cast + lap;
writeln;
end;
end.

Файлы
Файловые типы данных введены в язык для работы с внешними устройствами —
файлами на диске, портами, принтерами и т. д. Передача данных с внешнего устрой-
ства в оперативную память называется чтением, или вводом, обратный процесс —
записью, или выводом.
Файловые типы языка Паскаль бывают стандартные и определяемые программи-
стом. Стандартными являются текстовый файл (text) и бестиповой файл (file).
Они описываются в программе, например, так:
var ft : text;
fb : file;
Программист может определить файл, состоящий из элементов определенного
типа. Такой файл называется компонентным, или типизированным:
var fc : file of <тип_компонентов>;
Компоненты могут быть любого типа, кроме файлового. Любой файл, в отличие от
массива и записи, может содержать неограниченное количество элементов.
Текстовые файлы предназначены для хранения информации в виде строк симво-
лов. При выводе в текстовый файл данные преобразуются из внутренней формы
представления в символьную, понятную человеку, при вводе выполняется обрат-
ное преобразование.
Бестиповые и компонентные файлы хранят данные в том же виде, в котором они
представлены в оперативной памяти, то есть при обмене с файлом происходит по-
битовое копирование информации.
Доступ к файлам может быть последовательным, когда очередной элемент можно
прочитать (записать) только после аналогичной операции с предыдущим элемен-
том, и прямым, при котором выполняется чтение (запись) произвольного элемента
по заданному адресу. Текстовые файлы позволяют выполнять только последова-
тельный доступ, в бестиповых и компонентных можно использовать оба метода.

67
68 Часть I. Основы программирования

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

Таблица 3.2. Классификация файлов Паскаля

Файл: текстовый бестиповой компонентный


Преобразование + – –
Прямой доступ – + +

Чтобы не путать файлы в программе и файлы на диске, переменные файлового типа


называют логическими файлами, а реальные устройства и файлы на диске — физиче-
скими файлами. Их имена задаются с помощью строк символов, например:
 'primer.pas' — имя файла в текущем каталоге;
 'd:\pascal\input.txt' — полное имя файла;
 'CON' 'NUL' 'COM1' 'PRN' — имена устройств.
Для организации ввода-вывода необходимо выполнить следующие действия:
1. Объявить файловую переменную.
2. Связать ее с физическим файлом.
3. Открыть файл для чтения и (или) записи.
4. Выполнить операции ввода-вывода.
5. Закрыть файл.
Все стандартные процедуры и функции Паскаля, обеспечивающие ввод-вывод дан-
ных, работают только с логическими файлами, то есть с файловыми переменными.
Перед выполнением операций файловая переменная связывается с физическим
файлом, после чего он в тексте программы не упоминается.
Ввод-вывод выполняется не непосредственно между внешним устройством и пере-
менными программы, а через так называемый буфер — специальную область опе-
ративной памяти. Буфер выделяется для каждого открытого файла. При записи
в файл вся информация сначала направляется в буфер и там накапливается до тех
пор, пока весь буфер не заполнится. Только после этого или после специальной ко-
манды сброса происходит передача данных на внешнее устройство. При чтении из
файла данные вначале считываются в буфер, причем данных считывается не столь-
ко, сколько запрашивается, а сколько поместится в буфер.
Механизм буферизации позволяет более быстро и эффективно обмениваться ин-
формацией с внешними устройствами.

68
Глава 3. Типы данных, определяемые программистом 69

В Паскале есть подпрограммы, применяемые для файлов любого типа, а также под-
программы для работы только с определенными типами файлов. Подпрограммы
для работы со всеми типами файлов: assign, close, erase, rename, reset, rewrite, eof
и IOresult. Их описание приведено в приложении 2.

Текстовые файлы
Текстовый файл представляет собой последовательность строк символов перемен-
ной длины. Каждая строка заканчивается символами перевода строки и возврата
каретки (их коды — 13 и 10). Эти символы вставляются в физический файл при
нажатии клавиши Enter. При чтении файла эти символы не вводятся в переменные
в программе, а воспринимаются как разделитель.
Текстовый файл можно открыть не только для чтения или записи с помощью про-
цедур reset и rewrite, но и для добавления информации в конец. Для этого служит
процедура append. Для чтения из текстового файла применяются процедуры read(f,
список) и readln(f, [список]). Они отличаются от известных вам процедур ввода
с клавиатуры (см. с. 28) только наличием первого параметра — имени логического
файла. Это неудивительно, поскольку консольный ввод-вывод является частным
случаем обмена с текстовым файлом.
Для ввода с клавиатуры определено стандартное имя файла INPUT, а для вывода на
экран — OUTPUT. Эти файлы отличаются от обычных тем, что их нельзя открывать
и закрывать, поскольку это делается автоматически, и их имена можно не указы-
вать при обращении к процедурам ввода и вывода.
Процедуры записи в текстовый файл — write(f, список) и writeln(f, [список]). При
записи в текстовый файл происходит преобразование из внутренней формы пред-
ставления выводимых величин в символьные строки. Описание процедур приве-
дено на с. 29. Чтение и запись выполняются последовательно, то есть записать или
считать очередной символ можно только после предыдущего.
В Паскале есть несколько стандартных подпрограмм, которые предназначены толь-
ко для работы с текстовыми файлами: flush, settextbuf, seekEof и seekEoln. Все эти
подпрограммы описаны в приложении 2.

Бестиповые файлы
Бестиповые файлы предназначены для хранения участков оперативной памяти на
внешних носителях. После описания файловой переменной
var имя : file;
ее требуется связать с физическим файлом с помощью процедуры assign. Чтение
и запись производится через буфер «порциями», равными размеру буфера. Размер
буфера, отличающийся от стандартного (128 байт), можно задать с помощью вто-
рого параметра процедур reset и rewrite при открытии файла:
reset(var f : file; bufsize : word)
rewrite(var f : file; bufsize : word)
Размер буфера должен находиться в пределах от 1 байта до 64 Кбайт.

69
70 Часть I. Основы программирования

Собственно чтение и запись выполняются с помощью процедур blockread и blockwrite:


blockread(var f : file; var x; count : word; var num : word);
blockwrite(var f : file; var x; count : word; var num : word);
Процедура blockread считывает в переменную x количество блоков count. Длина
блока равна размеру буфера. Значение count должно быть больше или равно 1, за
одно обращение нельзя ввести больше 64 Кбайт. Поскольку при вводе никаких пре-
образований данных не выполняется, имеет значение только адрес начала области
памяти и ее длина, поэтому тип переменной x не указывается.
Необязательный параметр num возвращает количество прочитанных блоков. В слу-
чае успешного завершения операции чтения оно равно запрошенному, в случае
аварийной ситуации параметр num будет содержать число успешно прочитанных
блоков. Следовательно, с помощью параметра num можно контролировать правиль-
ность выполнения операции чтения.
Процедура blockwrite записывает в файл количество блоков, равное count, начиная
с адреса, заданного переменной x. Длина блока равна длине буфера. Необязатель-
ный параметр num возвращает число успешно записанных блоков (неудача может
произойти, например, из-за переполнения или сбоя диска).
Для бестиповых файлов применяется как последовательный, так и прямой доступ.
Ниже приведен пример последовательного заполнения файла, а прямому доступу
и его преимуществам посвящен раздел «Прямой доступ» (с. 72).
Пример. Программа выполняет ввод вещественных чисел из текстового файла и за-
пись их в бестиповой файл блоками по четыре числа (листинг 3.9).

Листинг 3.9. Запись в бестиповой файл


program create_bfile;
var buf : array[1 .. 4] of real;
f_in : text;
f_out : file;
i, k : integer;
name_in, name_out : string;
begin
{$I–}
writeln('Введите имя входного файла'); readln(name_in);
assign(f_in, name_in); reset(f_in);
if IOResult <> 0 then begin
writeln('Файл ', name_in,' не найден'); exit end;
writeln('Введите имя выходного файла'); readln(name_out);
assign(f_out, name_out); rewrite(f_out, sizeof(real) * 4);
{$I+}
i := 0;
while not eof(f_in) do begin
inc(i);
read(f_in, buf[i]);
if i = 4 then begin
blockwrite(f_out, buf, 1); i := 0; end;
end;

70
Глава 3. Типы данных, определяемые программистом 71

if i <> 0 then begin


for k := i + 1 to 4 do buf[k] := 0;
blockwrite(f_out, buf, 1);
end;
close(f_in); close(f_out);
end.

Компонентные файлы
Компонентные файлы применяются для хранения однотипных элементов в их вну-
тренней форме представления. Тип компонентов задается после ключевых слов file of.
var имя : file of тип_компонентов;
Компоненты могут быть любого типа, кроме файлового, например вещественным
числом, массивом, множеством, строкой, записью или массивом записей. В опера-
циях ввода-вывода могут участвовать только величины того же типа, что и компо-
ненты файла, например:
type mas = array [1 .. 100] of real;
var a, b : mas;
f : file of mas;
begin
assign(f, 'some_file.dat'); rewrite(f);
...
write(f, a, b);
close(f)
end.
Обратите внимание, что компонентом этого файла является массив целиком. В та-
кой файл нельзя записать отдельный элемент массива или простую переменную
вещественного типа. За одну операцию записывается или считывается столько
компонентов, сколько перечислено в процедурах write или read.
Пример. Из текстового файла прочитать пары вещественных чисел, считая первое
вещественной, а второе — мнимой составляющей комплексного числа, и записать их
в файл комплексных чисел (листинг 3.10).

Листинг 3.10. Запись в компонентный файл


program tip_file;
type Complex = record
d, m : real;
end;
var x : Complex;
f1 : text;
f2 : file of Complex;
i : byte;
begin
assign(f1, 'tipfile.dat'); reset(f1);
assign(f2, 'tipfile.res'); rewrite(f2);
while not eof(f1) do begin
продолжение 

71
72 Часть I. Основы программирования

Листинг 3.10 (продолжение)


read(f1, x.d, x.m); write(f2, x);
end;
close(f1); close(f2);
end.
Компонентные файлы, как и бестиповые, применяются не для просмотра их чело-
веком, а для использования в программах. Рассмотрим, как выполняется прямой
доступ к элементам этих файлов.

Прямой доступ
При последовательном доступе чтение или запись очередного элемента файла воз-
можны только после аналогичной операции с предыдущим элементом. Например,
чтобы получить n-й элемент файла, требуется считать n – 1 элементов. Бестипо-
вые и компонентные файлы состоят из блоков одинакового размера. В бестиповом
файле размер блока равен длине буфера, а в компонентном — длине компонента.
Это позволяет применить к таким файлам прямой доступ, при котором операции
выполняются с заданным блоком. Прямой доступ применяется только для физиче-
ских файлов, расположенных на дисках.
С помощью стандартной процедуры seek производится установка текущей позиции
в файле на начало заданного блока, и следующая операция чтения-записи выпол-
няется, начиная с этой позиции. Первый блок файла имеет номер 0.
Ниже описаны стандартные подпрограммы для реализации прямого доступа:
filepos(var f) : longint
Функция возвращает текущую позицию в файле f. Для только что открытого фай-
ла это будет 0. После чтения или записи первого блока текущая позиция равна 1.
После прочтения последней записи функция возвратит количество блоков в файле:
filesize(var f) : longint
Функция возвращает количество блоков в открытом файле f:
seek(var f; n: longint)
Процедура выполняет установку текущей позиции в файле (позиционирование).
В параметре n задается номер блока, к которому будет выполняться обращение.
Блоки нумеруются с нуля. Например, чтобы работать с четвертым блоком, необхо-
димо задать значение n, равное 3. Процедура seek работает с открытыми файлами.
truncate(var f)
Процедура устанавливает в текущей позиции признак конца файла и удаляет все
последующие блоки.
Пример. Программа выводит на экран заданную по номеру запись из файла, сфор-
мированного в листинге 3.9.

Листинг 3.11. Чтение из бестипового файла


program get_bfile;
var buf : array[1 .. 4] of real;

72
Глава 3. Типы данных, определяемые программистом 73

f : file;
i, k : integer;
filename : string;
begin
{$I–}
writeln('Введите имя входного файла'); readln(filename);
assign(f, filename);
reset(f, sizeof(real) * 4);
if IOResult <> 0 then begin
writeln('Файл ', filename, ' не найден'); exit end;
{$I+}
while true do begin
writeln('Введите номер записи или –1 для окончания');
readln(k);
if (k > filesize(f)) or (k < 0) then begin
writeln('Такой записи в файле нет',); exit end;
seek(f, k);
blockread(f, buf, 1);
for i:= 1 to 4 do write(buf[i]:6:1);
end;
close(f);
end.
Таким же образом можно изменять заданную запись в файле. Файл при этом может
быть открыт как для чтения, так и для записи. Попытка чтения-записи несуществу-
ющего блока приводит к ошибке времени выполнения.

ПРИМЕЧАНИЕ
Стандартный модуль Dos содержит подпрограммы, с помощью которых можно об-
ращаться к некоторым функциям операционной системы, например получать и уста-
навливать время создания и атрибуты файла, выполнять поиск в заданных каталогах
по имени файла, получать объем свободного дискового пространства и т. д. Восполь-
зоваться этими подпрограммами можно, подключив к своей программе модуль Dos
в разделе uses. Модуль Dos описан в приложении 2 на с. 362.

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

73
74 Часть I. Основы программирования

 Один тип является строковым типом, другой — строковым типом или типом
pchar.
 Один тип — pointer, другой — любой тип указателя.
 Один тип — p c h a r , другой — символьный массив с нулевой базой вида
array [0 .. X] of char (только при разрешении расширенного синтаксиса дирек-
тивой {$X+}).
 Оба типа являются указателями идентичных типов (только при разрешении
расширенного синтаксиса директивой {$X+}).
 Оба типа являются процедурными с идентичными типами результатов, одина-
ковым числом параметров и соответствием между параметрами.

Совместимость по присваиванию
Этот вид совместимости требуется при присваивании значений, например в опера-
торе присваивания или при передаче значений в подпрограмму.
Значение типа T1 является совместимым по присваиванию с типом T2 (то есть до-
пустим оператор T1 := T2), если выполняется одно из следующих условий.
 T1 и T2 — тождественные типы (кроме файловых или типов, содержащих элемен-
ты файлового типа).
 T1 и T2 — совместимые порядковые типы, при этом значения типа T2 попадают
в диапазон возможных значений T1.
 T1 и T2 — вещественные типы, при этом значения типа T2 попадают в диапазон
возможных значений T1.
 T1 — вещественный тип, а T2 — целочисленный.
 T1 и T2 — строковые типы.
 T1 — строковый тип, а T2 — символьный (char).
 T1 и T2 — совместимые множественные типы, при этом все значения типа T2 по-
падают в диапазон возможных значений T1.
 T1 и T2 — совместимые типы указателей.
 T1 — тип pchar, а T2 — строковая константа (только при разрешении расширенного
синтаксиса директивой {$X+})1.
 T1 — тип pchar, а T2 — символьный массив с нулевой базой вида array [0 .. n]
of char (только при разрешении расширенного синтаксиса директивой {$X+}).
 T1 и T2 — совместимые процедурные типы.
 T1 представляет собой процедурный тип, а T2 — процедура или функция с иден-
тичным типом результата и соответствующими параметрами.
На этапе компиляции и выполнения выдается сообщение об ошибке, если совме-
стимость по присваиванию необходима, а ни одно из условий предыдущего списка
не выполнено.

1
Директивы компилятора приведены в приложении 3.

74
Глава 4. Модульное программирование
С увеличением объема программы становится невозможным удерживать в памяти
все детали. Естественным способом борьбы со сложностью любой задачи является
ее разбиение на части. В Паскале задача может быть разделена на более простые
и понятные фрагменты — подпрограммы, после чего программу можно рассматри-
вать в более укрупненном виде — на уровне взаимодействия подпрограмм.
Использование подпрограмм является первым шагом к повышению степени аб-
стракции программы и ведет к упрощению ее структуры. Разделение программы на
подпрограммы позволяет также избежать избыточности кода, поскольку подпро-
грамму записывают один раз, а вызывать ее на выполнение можно многократно из
разных точек программы.
Следующим шагом в повышении уровня абстракции программы является группи-
ровка подпрограмм и связанных с ними данных в отдельные файлы (модули), ком-
пилируемые раздельно. Разбиение на модули уменьшает время перекомпиляции и
облегчает процесс отладки, скрывая несущественные детали за интерфейсом моду-
ля и позволяя отлаживать программу по частям (при этом, возможно, разным про-
граммистам). Интерфейсом модуля являются заголовки всех подпрограмм и опи-
сания доступных извне типов, переменных и констант.

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

75
76 Часть I. Основы программирования

вызывается по имени, за которым следует список аргументов1 в круглых скобках.


Если аргументов нет, скобки не нужны. Список аргументов при вызове как бы на-
кладывается на список параметров, поэтому они должны попарно соответствовать
друг другу. Правила соответствия рассматриваются далее.
Процедура вызывается с помощью отдельного оператора, а функция — в правой
части оператора присваивания, например:
inc(i); writeln(a, b, c); { вызовы процедур }
y := sin(x) + 1; { вызов функции }
Внутри подпрограмм можно описывать другие подпрограммы. Они доступны толь-
ко из той подпрограммы, в которой описаны.

Процедуры
Структура процедуры аналогична структуре основной программы:
procedure имя [(список параметров)]; { заголовок }
разделы описаний
begin
раздел операторов
end;
Квадратные скобки в данном случае не являются элементом синтаксиса, а означа-
ют, что список параметров может отсутствовать. Рассмотрим простой пример.
Пример. Найти разность средних арифметических значений двух вещественных мас-
сивов из 10 элементов.
Как видно из условия, для двух массивов требуется найти одну и ту же величину —
среднее арифметическое. Следовательно, логичным будет оформить его нахождение
в виде подпрограммы, которая сможет работать с разными массивами (листинг 4.1).

Листинг 4.1. Разность средних арифметических значений массивов (процедура)


program dif_average;
const n = 10;
type mas = array[1 .. n] of real;
var a, b : mas;
i : integer;
dif, av_a, av_b : real;
procedure average(x : mas; var av : real); { 1 }
var i : integer;
begin
av := 0;
for i := 1 to n do av := av + x[i];
av := av / n;
end; { 2 }
begin
for i := 1 to n do read(a[i]);
for i := 1 to n do read(b[i]);
1
Часто аргументы называют фактическими параметрами.

76
Глава 4. Модульное программирование 77

average(a, av_a); { 3 }
average(b, av_b); { 4 }
dif := av_a – av_b;
writeln('Разность значений ', dif:6:2);
end.
Описание процедуры average расположено в строках с {1} по {2}. В строках {3} и {4}
эта процедура вызывается сначала для обработки массива а, затем — массива b.
Массивы передаются в качестве аргументов. Результат вычислений возвращается
в главную программу через второй параметр процедуры.
Пока от вас не требуется понимать все детали синтаксиса, главное — уловить общий
смысл использования подпрограмм. На таком простом алгоритме мы, естественно,
не получили выигрыша в длине программы, но этот вариант по сравнению с вы-
числением двух средних арифметических «в лоб» имеет ряд важных преимуществ.
Во-первых, мы получили более простую главную программу. Во-вторых, у нас те-
перь есть подпрограмма, с помощью которой можно вычислить среднее арифме-
тическое элементов любого вещественного массива из n элементов. Мы можем ис-
пользовать ее многократно и в этой, и в других программах, причем для этого нам не
требуется помнить, как она работает, — достаточно взглянуть на ее заголовок. Мы
можем оформить подпрограмму в виде модуля и передать коллегам. И наконец, мы
можем вносить в эту подпрограмму изменения и дополнения, будучи уверенными,
что это не отразится на главной программе.

Функции
Описание функции отличается от описания процедуры незначительно:
function имя [(список параметров)] : тип; { заголовок }
разделы описаний
begin
раздел операторов
имя := выражение;
end;
Квадратные скобки в данном случае не являются элементом синтаксиса, а означа-
ют, что список параметров может отсутствовать. Функция вычисляет одно значе-
ние, которое передается через ее имя. Следовательно, в заголовке должен быть опи-
сан тип этого значения, а в теле функции — оператор, присваивающий вычисленное
значение ее имени. Он не обязательно должен находиться в конце функции. Более
того, таких операторов может быть несколько — это определяется алгоритмом. Рас-
смотрим пример применения функции для программы, приведенной в предыду-
щем разделе.
Пример. Найти разность средних арифметических значений двух вещественных мас-
сивов из 10 элементов (листинг 4.2).

Листинг 4.2. Разность средних арифметических значений массивов (функция)


program dif_average1;
const n = 3;
продолжение 

77
78 Часть I. Основы программирования

Листинг 4.2 (продолжение)


type mas = array[1 .. n] of real;
var a, b : mas;
i : integer;
dif : real;
function average(x : mas) : real; { 1 }
var i : integer; { 2 }
av : real;
begin
av := 0;
for i := 1 to n do av := av + x[i];
average := av / n; { 3 }
end;
begin
for i := 1 to n do read(a[i]);
for i := 1 to n do read(b[i]);
dif := average(a) – average(b); { 4 }
writeln('Разность значений ', dif:6:2)
end.
Оператор 1 представляет собой заголовок функции. Тип функции определен как
вещественный, потому что к такому типу относится среднее арифметическое эле-
ментов вещественного массива. Оператор 3 присваивает имени функции вычис-
ленное значение. В операторе 4 функция вызывается дважды: сначала для одного
массива, затем для другого.
Как видите, приведенный пример записывается с помощью функции короче и яс-
нее, поскольку, во-первых, интерфейс подпрограммы стал более лаконичным, а во-
вторых, в одном операторе можно записать несколько вызовов функции.

ПРИМЕЧАНИЕ
Ничто не мешает вычислять в функции не одно значение, а несколько. В этом случае
одно, «главное», значение передается через имя функции, а остальные — через список
параметров по адресу (о способах передачи параметров будет рассказано далее). Но
в таком случае чаще всего лучше использовать не функцию, а процедуру. И наоборот:
если подпрограмма формирует только одно значение, предпочтительно оформить ее
в виде функции.

Глобальные и локальные переменные


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

78
Глава 4. Модульное программирование 79

хранятся во время выполнения программы в сегментных регистрах: CS — адрес сег-


мента кода, DS — адрес сегмента данных, SS — адрес сегмента стека.
Глобальными называются переменные, описанные в главной программе. Перемен-
ные, которые не были инициализированы явным образом1, перед началом выпол-
нения программы обнуляются. Время жизни глобальных переменных — с начала
программы и до ее завершения.
Внутри подпрограмм описываются локальные переменные. Они располагаются
в сегменте стека, причем распределение памяти происходит в момент вызова под-
программы, а ее освобождение — по завершении подпрограммы. Таким образом,
время жизни локальных переменных — с начала работы подпрограммы и до ее
окончания. Значения локальных переменных между двумя вызовами одной и той
же подпрограммы не сохраняются, и эти переменные предварительно не обнуляют-
ся, то есть в соответствующих ячейках памяти находятся произвольные значения.

ПРИМЕЧАНИЕ
Если переменная внутри подпрограммы определена в разделе описания констант,
память под нее выделяется не в сегменте стека, а в сегменте данных, и начальное
значение ей присваивается один раз до начала работы программы, а не при входе
в подпрограмму. Время жизни такой переменной — вся программа, то есть значение
этой переменной сохраняется между вызовами подпрограммы. Область действия
переменной — подпрограмма, в которой она описана, то есть вне подпрограммы к этой
переменной обратиться нельзя.

Глобальные переменные доступны в любом месте программы или подпрограммы,


кроме тех подпрограмм, в которых описаны локальные переменные с такими же
именами. Локальные переменные могут использоваться только в подпрограмме,
в которой они описаны, и всех вложенных в нее.
Понятно, что никаких дополнительных усилий по передаче глобальных перемен-
ных в подпрограмму не требуется: они видны в ней естественным образом. Этот
способ обмена информацией между главной программой и подпрограммой — са-
мый простой, но он же и самый плохой.
Чтобы понять, чем он плох, представьте себе программу размером в несколько ты-
сяч строк (это совсем не большая программа), состоящую из сотен подпрограмм.
Допустим, что в процессе отладки обнаружилось неверное значение некой пере-
менной. Если она может беспрепятственно измениться в любой подпрограмме, то,
чтобы определить, какой именно фрагмент кода привел к ошибке, может потребо-
ваться просмотреть весь текст целиком.
Кроме того, использование глобальных переменных сужает возможности примене-
ния подпрограммы: если в ней используется имя глобальной переменной, подпро-
грамма может работать только с ней, а не с любой переменной того же типа, как это
происходит при передаче данных через параметры. Представьте, как неудобно было
бы пользоваться стандартными функциями, если бы для их вызова требовалось за-
водить и инициализировать переменные с заданными именами!
1
То есть им не были присвоены значения как типизированным константам, см. 224.

79
80 Часть I. Основы программирования

Поэтому в подавляющем большинстве случаев для обмена данными между вызы-


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

ВНИМАНИЕ
Подпрограмму надо писать таким образом, чтобы вся необходимая для ее использо-
вания информация содержалась в ее заголовке.

Обратите внимание на раздел описания переменных в функции из листинга 4.2.


В этом разделе должны быть описаны все служебные переменные, которые требу-
ются этой подпрограмме для вычислений. Несмотря на то что в главной программе
есть переменная i, которая также служит для просмотра массива, в подпрограмме
лучше описать собственную переменную. Это позволит избежать возможных оши-
бок (например, если подпрограмма будет вызываться внутри цикла по i), а также
позволит «развязать» подпрограмму с вызывающей программой.
Локализовывать переменные, то есть писать подпрограммы так, чтобы все данные
либо передавались им извне через параметры, либо были описаны внутри подпро-
граммы, очень важно. Как мы увидим позже, этот принцип лежит в основе инкапсу-
ляции — одного из краеугольных камней объектно-ориентированного программи-
рования. Рассмотрим виды параметров и механизм их передачи.

Виды параметров подпрограмм


Список параметров, то есть величин, передаваемых в подпрограмму и обратно, со-
держится в ее заголовке. Для каждого параметра обычно задаются его имя, тип
и способ передачи. Либо тип, либо способ передачи могут не указываться.
Запомните, что в заголовке подпрограммы нельзя вводить описание нового типа —
там должны использоваться либо имена стандартных типов, либо имена типов,
описанных программистом ранее в разделе type.
В Паскале существует четыре вида параметров: значения, переменные, константы
и нетипизированные параметры.
Кроме того, по другим критериям можно выделить особые виды параметров:
 открытые массивы и строки;
 процедурные и функциональные параметры;
 объекты.
Параметры-значения
Параметр-значение описывается в заголовке подпрограммы следующим образом:
имя : тип;
Например, передача величины целого типа в процедуру Р записывается так:
procedure P(x : integer);

80
Глава 4. Модульное программирование 81

Имя параметра может быть произвольным. Параметр х можно представить себе как
локальную переменную, которая получает свое значение из главной программы при
вызове подпрограммы. В подпрограмму передается копия значения аргумента.
Механизм передачи следующий: из ячейки памяти, в которой хранится перемен-
ная, передаваемая в подпрограмму, берется ее значение и копируется в область
сегмента стека, называемую областью параметров. Подпрограмма работает с этой
копией, следовательно, доступа к ячейке, где хранится сама переменная, не имеет.
По завершении работы подпрограммы стек освобождается. Такой способ называ-
ется передачей по значению. Ясно, что им можно пользоваться только для величин,
которые не должны измениться после выполнения подпрограммы, то есть для ее
исходных данных.
При вызове подпрограммы на месте параметра, передаваемого по значению, может
находиться выражение (а также, конечно, его частные случаи — переменная или
константа). Тип выражения должен быть совместим по присваиванию с типом па-
раметра, то есть выражение должно быть таким, чтобы его можно было присвоить
параметру по правилам Паскаля (о совместимости типов см. с. 73).
Например, если в вызывающей программе описаны переменные
var x : integer; c : byte; y : longint;
то следующие вызовы подпрограммы Р, заголовок которой описан выше, будут син-
таксически правильными:
P(x); P(c); P(y); P(200); P(x div 4 + 1);

ПРИМЕЧАНИЕ
Если передаваемое в подпрограмму целое значение не соответствует допустимому для
типа параметра диапазону, оно усекается. Для вещественных значений в аналогичном
случае возникает ошибка переполнения.

Недостатками передачи по значению являются затраты времени на копирование


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

Параметры-переменные
Признаком параметра-переменной является ключевое слово var перед описанием
параметра:
var имя : тип;
Например, параметр-переменная целого типа в процедуре Р записывается так:
procedure P(var x : integer);
При вызове подпрограммы в область параметров копируется не значение перемен-
ной, а ее адрес, и подпрограмма через него имеет доступ к ячейке, в которой хранит-
ся переменная. Этот способ передачи параметров называется передачей по адресу.

81
82 Часть I. Основы программирования

Подпрограмма работает непосредственно с переменной из вызывающей програм-


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

Листинг 4.3. Параметры-значения и параметры-переменные


var a, b, c, d, e : word;
procedure X(a, b, c : word; var d : word);
var e : word;
begin
c := a + b; d := c; e := c;
writeln ('Значения переменных в подпрограмме:');
writeln ('c = ', c, ' d = ', d, ' e = ', e);
end;
begin
a := 3; b := 5;
x(a, b, c, d);
writeln ('Значения переменных в главной программе:');
writeln ('c = ', c, ' d = ', d, ' e = ', e);
end.
Результаты работы этой программы приведены ниже:
Значения переменных в подпрограмме:
c = 8 d = 8 e = 8
Значения переменных в главной программе:
c = 0 d = 8 e = 0
Как видите, значение переменной с в главной программе не изменилось, посколь-
ку переменная передавалась по значению, а значение переменной е не изменилось
потому, что в подпрограмме была описана локальная переменная с тем же име-
нем.

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

82
Глава 4. Модульное программирование 83

параметры-константы передаются по адресу, но доступ к ним обеспечивается толь-


ко для чтения. Поэтому опасность переполнения стека и затраты, связанные с ко-
пированием и размещением параметров, исключаются.
Например, параметр-константа целого типа в процедуре Р записывается так:
procedure P(const x : integer);
Подведем итоги. Если данные передаются в подпрограмму по значению, их можно
изменять, но эти изменения затронут только копию в области параметров и не от-
разятся на значении аргумента в вызывающей программе. Если данные передают-
ся как параметры-константы, изменять их в подпрограмме нельзя. Следовательно,
эти два способа передачи должны использоваться для передачи в подпрограмму
исходных данных.
Параметры составных типов (массивы, записи, строки) предпочтительнее пере-
давать как константы, потому что при этом не расходуется время на копирование
и место в стеке (размер стека не может превышать 64 Кбайт, а по умолчанию уста-
навливается равным 16 Кбайт).
Результаты работы процедуры следует передавать через параметры-переменные,
результат функции — через ее имя.

СОВЕТ
В списке параметров записывайте сначала все входные параметры, затем — все вы-
ходные. Давайте параметрам имена, по которым можно получить представление об
их назначении.

Паскаль — язык со строгой типизацией, и к параметрам подпрограмм и аргументам


он предъявляет жесткие правила соответствия типов. Что же делать, если хочется
написать универсальную подпрограмму, которая работала бы с параметрами лю-
бых типов? Оказывается, такая возможность есть: для этого нужно использовать
нетипизированные параметры, которые мы сейчас и рассмотрим.

Нетипизированные параметры
Как можно догадаться из названия, при описании нетипизированных параметров
не указывается тип. Передаются они всегда по адресу — либо как константы, либо
как переменные, например:
procedure P(const a, b; var y);
Казалось бы, все прекрасно, однако есть одно «но»: передать-то их можно, а вот де-
лать с ними в подпрограмме что-либо до тех пор, пока они не приведены к какому-
либо определенному типу, все равно нельзя! Более того, раз тип параметров не
указан, компилятор не может проверить допустимость действий, выполняемых
с ними в подпрограмме, и ответственность за эти действия ложится на плечи про-
граммиста.
Впрочем, в этом нет ничего удивительного: ведь вся жизнь устроена так, что чем
больше свободы действий, тем больше ответственность! Но довольно о грустном,
давайте лучше рассмотрим пример применения этих параметров (листинг 4.4).

83
84 Часть I. Основы программирования

Листинг 4.4. Функция сравнения на равенство двух произвольных величин


Function EQ(const x, y; size : word) : boolean;
type mas_byte = array[0 .. MaxInt] of byte;
var n : integer;
begin
n := 0;
while (n < size) and (mas_byte(x)[n] = mas_byte(y)[n]) do inc(n);
EQ := n = size;
end;
В эту функцию фактически передаются только адреса начала расположения в па-
мяти двух переменных, поэтому необходим еще один параметр: длина сравнивае-
мых величин в байтах (параметр size). Единственный способ выяснить, равны ли
две величины, размер которых заранее не известен, — их побайтовое сравнение,
поэтому оба параметра приводятся к типу mas_byte, объявленному в функции. При
описании массива используется стандартная константа MaxInt, в которой хранится
максимальное значение для величин типа integer, то есть 32 767.
Организуется цикл, выход из которого будет выполнен либо при несовпадении
очередной пары соответствующих байтов, либо после просмотра всех байтов. В по-
следнем случае переменная цикла n окажется равной длине сравниваемых значе-
ний, и имени функции будет присвоено значение true.
Вместо явного приведения типа, которое приходится выполнять каждый раз при
использовании параметра, можно использовать наложение на параметры абсолют-
ных переменных (они рассматривались на с. 24):
Function EQ(const x, y; size : word) : boolean;
type mas_byte = array[0 .. MaxInt] of byte;
var xb : mas_byte absolute x;
yb : mas_byte absolute y;
n : integer;
begin
n := 0;
while (n < size) and (xb[n] = yb[n]) do inc(n);
EQ := n = size;
end;
С помощью функции EQ можно сравнить две любые величины. Пусть, например,
в программе описаны переменные:
var a, b : array [1 .. 10] of byte; x: real; c: string;
Следующие обращения к функции EQ будут корректны:
EQ(a, b, sizeof(a)) { сравнение двух массивов }
EQ(a[2], b[5], 4) { сравнение 2–5 элементов массива "a" с 5–8 элементами
массива "b" соответственно }
EQ(c, x, sizeof(real)) { сравнение первых 6 байт строки с с переменной x }
В общем случае для применения нетипизированных параметров должны быть
достаточно веские причины, чтобы выигрыш от обобщения подпрограммы на

84
Глава 4. Модульное программирование 85

параметры произвольного типа перевесил отсутствие контроля параметров компи-


лятором и ухудшение читабельности программы.

Открытые массивы и строки


Чтобы передать в подпрограмму массив, нужно предварительно определить его
в разделе описания типов, а значит, явно задать количество его элементов и их тип
(см. листинг 4.1). Следовательно, подпрограмма, параметром которой является
массив из десяти целых чисел, не сможет работать с массивом из семи или пятнад-
цати элементов. Это неудобно, поэтому в списке параметров подпрограммы разре-
шается определять открытый массив, например:
procedure P(a : array of real);
Открытый массив может быть только одномерным и состоять из элементов любого
типа, кроме файлового. На место открытого массива можно передавать одномер-
ный массив любой размерности, состоящий из элементов такого же типа. Переда-
вать открытый массив можно как значение, переменную или константу.
Поскольку тип индексов массива не указывается, используется соглашение, по ко-
торому его элементы нумеруются с нуля. Номер максимального элемента в массиве
можно определить с помощью функции High. Иными словами, диапазон индексов
массива, передаваемого в подпрограмму в качестве аргумента, отображается в ней
на диапазон [0 .. High(<имя массива>)]. Рассмотрите пример (листинг 4.5).

Листинг 4.5. Максимальный элемент любого целочисленного массива


function max_el(const mas : array of integer) : integer;
var i, max : integer;
begin
max := mas[0];
for i := 0 to High(mas) do
if mas[i] > max then max := mas[i];
max_el := max;
end;
Для передачи в подпрограмму по адресу строк любой длины используется либо
специальный тип OpenString, называемый открытой строкой, либо тип string при
включенном режиме {$P+} (по умолчанию этот режим выключен).
Напомню, что, если параметр передается в подпрограмму как значение или кон-
станта, от него не требуется точного совпадения с типом аргумента — достаточно
соответствия по присваиванию. Поскольку присваивать друг другу строки разной
длины разрешено, их можно использовать и в качестве параметров, то есть на место
параметра-значения или параметра-константы типа string можно передавать стро-
ку любой длины без использования открытых строк.
Пример передачи строк в подпрограмму:
type s20 = string[20];
var s1 : string[40];
s2 : string[10];
продолжение 

85
86 Часть I. Основы программирования

procedure P(const x : s20; y : string; var z : openstring);


...
begin
... P(s2, s1, s1); ...
end.

Параметры процедурного типа


Все рассмотренные параметры подпрограмм позволяли выполнять один и тот же
алгоритм с различными данными. В Паскале есть и другая возможность — пара-
метризовать алгоритм функциями и процедурами. Это может пригодиться, если
требуется выполнить одну и ту же последовательность действий, внутри которой
выполняется обращение к разным функциям или процедурам.
Простой пример — подпрограмма, вычисляющая среднее значение функции на за-
данном интервале. Ясно, что алгоритмы вычисления среднего для различных функ-
ций будут различаться только их именами, поэтому логично передать имя функции
подпрограмме вычисления среднего значения в качестве параметра.
Как же это сделать? Описание параметра подпрограммы в большинстве случаев
состоит из имени и типа. Имя функции является константой процедурного (функ-
ционального) типа, который требуется описать в разделе type, например:
type fun = function(x : real) : real;
pr = procedure;
proc = procedure(a, b : word; var c : word);
Здесь вводится описание трех типов. Первый из них соответствует любой функции
с одним аргументом вещественного типа, возвращающей вещественное значение,
второй — процедуре без параметров, а третий — процедуре с тремя параметрами
типа word. Как видно из примеров, описание процедурного (функционального)
типа соответствует заголовку подпрограммы без имени. Имя типа используется за-
тем в списке параметров подпрограммы аналогично другим типам.

ПРИМЕЧАНИЕ
Процедурные типы применяются не только для передачи имен подпрограмм в под-
программу. Можно описать переменную такого типа и присваивать ей имя конкретной
подпрограммы соответствующего типа или значение другой переменной того же типа.
Это удобно для организации вызова различных подпрограмм из одного и того же места
программы в зависимости от условий. Пример использования приведен на с. 109.

Пример. Программа, вычисляющая определенные интегралы методом прямоуголь-


ников для двух функций

2x
q= , r = cos x − 0, 2 x
1 − sin 2 x
на интервале [a, b] с заданным количеством его разбиений (листинг 4.6).

86
Глава 4. Модульное программирование 87

Листинг 4.6. Вычисление определенного интеграла методом прямоугольников


program integrals;
type fun = function(x : real) : real; { 1 }
var a, b : real;
n : integer;
{$F+}
function Q(x : real) : real;
begin
Q := 2 * x / sqrt(1 – sin(2 * x));
end;
function R(x : real) : real;
begin
R := cos(x) – 0.2 * x;
end;
{$F–}
function integr(f : fun; a, b : real; n : integer) : real;
var sum, x, h : real;
i : integer;
begin
h := (b – a) / n; sum := 0; x := a;
for i := 1 to n do begin
sum := sum + f(x); x := x + h;
end;
integr := sum * h;
end;
begin
writeLn('Введите интервал и количество шагов');
readln(a, b, n);
writeln('Интеграл для первой функции: ', integr(Q, a, b, n):8:3);
writeln(' Интеграл для второй функции: ', integr(R, a, b, n):8:3);
end.
Вычисление определенного интеграла методом прямоугольников состоит в при-
ближенном подсчете площади, ограниченной осью абсцисс, графиком функции
и границами интервала. Интервал разбивается на заданное количество промежут-
ков, и площади получившихся фигур заменяются площадями прямоугольников
(рис. 4.1). Погрешность метода на рисунке заштрихована (о видах погрешностей
рассказывалось на с. 48). Этот алгоритм реализован в функции integr. Количество
разбиений интервала хранится в переменной n.
Итак, чтобы передать имя функции или процедуры в подпрограмму, необходимо:
1. Определить соответствующий процедурный тип.
2. Задать для функций и процедур, предназначенных для передачи в подпро-
грамму, ключ компилятора {$F+} , определяющий дальнюю адресацию. При
этом компилятор формирует полный адрес, состоящий из сегмента и смещения.
Альтернативный способ — указать в заголовке каждой функции директиву far:
function Q(x : real) : real; far;

87
88 Часть I. Основы программирования

Рис. 4.1. Метод прямоугольников

Рекурсивные подпрограммы
Рекурсивной называется подпрограмма, в которой содержится обращение к самой
себе. Такая рекурсия называется прямой. Есть также косвенная рекурсия, когда две
или более подпрограммы вызывают друг друга.
При обращении подпрограммы к самой себе происходит то же самое, что и при
обращении к любой другой функции или процедуре: в стек записывается адрес
возврата, резервируется место под локальные переменные, происходит передача
параметров, после чего управление передается первому исполняемому оператору
подпрограммы. При повторном вызове этот процесс повторяется. Для завершения
вычислений каждая рекурсивная подпрограмма должна содержать хотя бы одну
нерекурсивную ветвь, заканчивающуюся возвратом в вызывающую программу.
При завершении подпрограммы область ее локальных переменных освобождается,
а управление передается на оператор, следующий за рекурсивным вызовом.
Простой пример рекурсивной функции — вычисление факториала (это не означает,
что факториал следует вычислять именно так). Чтобы получить факториал числа n,
требуется умножить на n факториал (n – 1)!. Известно также, что 0! = 1 и 1! = 1.
function fact(n : byte) : longint;
begin
if (n = 0) or (n = 1) then fact := 1
else fact := n * fact(n – 1);
end;
Рассмотрим, что происходит при вызове этой функции при n = 3. В стеке отводит-
ся место под параметр n, ему присваивается значение 3, и начинается выполнение
функции. Условие в операторе if ложно, поэтому управление передается на ветвь
else. Для вычисления выражения n * fact(n – 1) требуется повторно вызвать функ-
цию fact. Для этого в стеке отводится новое место под параметр n, ему присваива-
ется значение 2, и выполнение функции начинается сначала. В третий раз функция
вызывается со значением параметра, равным 1, и вот тут-то становится истинным

88
Глава 4. Модульное программирование 89

выражение (n = 0) or (n = 1), поэтому происходит возврат из подпрограммы в точ-


ку вызова, то есть на выражение n * fact(n – 1) для n = 2. Результат выражения
присваивается имени функции и передается в точку ее вызова, то есть в то же вы-
ражение, только теперь происходит обращение к параметру n, равному 3.
Понимание механизма рекурсии помогает осознать ее достоинства, недостатки
и область применения. Рекурсивные подпрограммы чаще всего применяют для
компактной записи рекурсивных алгоритмов, а также для работы со структура-
ми данных, описанными рекурсивно, например с двоичными деревьями (с. 120).
Любую рекурсивную функцию можно реализовать без применения рекурсии: для
этого программист должен сам обеспечить распределение памяти под необходимое
количество копий параметров.
Достоинством рекурсии является компактная запись. К недостаткам относятся
расход времени и памяти на повторные вызовы функции и передачу ей параметров,
а главное, опасность переполнения стека.
При отладке рекурсивных алгоритмов полезно отслеживать глубину рекурсии
либо визуально, вставив оператор вывода в начало подпрограммы, либо с помощью
типизированной константы, которая увеличивается на единицу при каждом вызове
подпрограммы, например:
function fact(n : byte) : longint;
const num : word = 0;
begin
inc(num); writeln(num); { отладочная печать }
if (n = 0) or (n = 1) then fact := 1
else fact := n * fact(n – 1);
end;
Напомню, что локальная типизированная константа, в отличие от локальной пере-
менной, размещается в сегменте данных и сохраняет свое значение между вызова-
ми подпрограммы. В случае переполнения стека программа завершится с соответ-
ствующим сообщением об ошибке. Проверка переполнения стека задается ключом
компилятора {$S+}. По умолчанию он включен.

Модули
Модуль — это подключаемая к программе библиотека ресурсов. Он может содер-
жать описания типов, констант, переменных и подпрограмм. В модуль обычно
объединяют связанные между собой ресурсы: например, в составе оболочки есть
модуль Graph для работы с экраном в графическом режиме. Модули применяются
как библиотеки, которые могут использоваться различными программами, и для
разбиения сложной программы на составные части.
Чтобы использовать модуль, достаточно знать только его интерфейс: детали реали-
зации модуля скрыты от его пользователя. Это позволяет успешно создавать про-
граммы большого объема, поскольку мозг человека1 может хранить одновременно

1
А тем более программиста.

89
90 Часть I. Основы программирования

довольно ограниченный объем информации. Кроме того, если программа разбита


на модули, возрастает скорость ее компиляции, поскольку они хранятся в скомпи-
лированном виде и перекомпилируются только при наличии изменений в их ис-
ходном тексте.
Задача разбиения программы на максимально обособленные части, спецификации
их интерфейсов и оформления этих частей в виде модулей должна решаться на
этапе проектирования программы. Мы поговорим об этом в разделе «Технология
структурного программирования» на с. 130.
Использование модулей имеет еще одно преимущество: оно позволяет преодолеть
ограничение в один сегмент на объем кода исполняемой программы, поскольку код
каждого подключаемого к программе модуля содержится в отдельном сегменте.
Модули можно разделить на стандартные, которые входят в состав системы про-
граммирования, и пользовательские, то есть создаваемые программистом. Чтобы
подключить модуль к программе, его требуется предварительно скомпилировать.
Результат компиляции каждого модуля хранится на диске в отдельном файле с рас-
ширением .tpu.

Описание модулей
Исходный текст каждого модуля хранится в отдельном файле с расширением .pas.
Модуль состоит из секций (разделов). Общая структура модуля:
unit имя; { заголовок модуля }
interface { ------------- интерфейсная секция модуля }
{ описание глобальных элементов модуля (видимых извне) }
implementation { --------------- секция реализации модуля }
{ описание локальных (внутренних) элементов модуля }
begin { ------------------- секция инициализации }
{ может отсутствовать }
end.

ВНИМАНИЕ
Имя файла, в котором хранится модуль, должно совпадать с именем, заданным после
ключевого слова unit.

Модуль может использовать другие модули, для этого их надо перечислить в опе-
раторе uses, который может находиться только непосредственно после ключевых
слов interface или implementation. Если модули подключаются к интерфейсной ча-
сти, все константы и типы данных, описанные в интерфейсной секции этих моду-
лей, могут использоваться в любом описании в интерфейсной части данного моду-
ля. Если модули подключаются к части реализации, все описания из этих модулей
могут использоваться только в секции реализации.
В интерфейсной секции модуля определяют константы, типы данных, переменные,
а также заголовки процедур и функций. Полностью же подпрограммы описыва-
ются в секции реализации, скрытой от пользователя модуля. Это естественно, по-
скольку для применения подпрограммы требуется знать только информацию, кото-
рая содержится в ее заголовке (при условии, что подпрограмма написана грамотно).

90
Глава 4. Модульное программирование 91

В секции реализации описываются подпрограммы, заголовки которых приведены


в интерфейсной части. Заголовок подпрограммы должен или быть идентичным
указанному в секции интерфейса, или состоять только из ключевого слова procedure
или function и имени подпрограммы. Для функции также указывается ее тип.
Кроме того, в этой секции можно определять константы, типы данных, переменные
и внутренние подпрограммы. Они используются внешними элементами модуля
и видны только в части реализации, то есть из программы, к которой подключен
этот модуль, обратиться к ним нельзя.
Секция инициализации предназначена для присваивания начальных значений
переменным, используемым в модуле или в программе, к которой он подключен.
Операторы, расположенные в секции инициализации модуля, выполняются перед
операторами основной программы. Если к программе подключено более одного мо-
дуля, их секции инициализации вызываются на выполнение в порядке, указанном
в операторе uses.
В оболочках Borland Pascal и Turbo Pascal результат компиляции по умолчанию
размещается в оперативной памяти и на диск не записывается. Поэтому для сохра-
нения скомпилированного модуля на диске требуется установить значение пункта
CompileDestination в значение Disk. Компилятор создаст файл с расширением .tpu,
который надо переместить в специальный каталог, путь к которому указан в пункте
меню OptionsDirectories в поле Unit Directories.

ПРИМЕЧАНИЕ
Кроме того, откомпилированный модуль может находиться в том же каталоге, что и ис-
пользующие его программы, а также в библиотеке исполняющей системы. Поместить
модуль в библиотеку исполняющей системы можно с помощью утилиты tpumover.
exe, которая входит в состав системы программирования. Этот способ применяется
для часто используемых и хорошо отлаженных модулей.

В качестве примера оформим в виде модуля подпрограмму вычисления среднего


арифметического значения элементов массива из листинга 4.1 (листинг 4.7).

Листинг 4.7. Пример оформления модуля


unit Average;
interface
const n = 10;
type mas = array[1 .. n] of real;
procedure average(x : mas; var av : real);
implementation
procedure average(x : mas; var av : real);
var i : integer;
begin
av := 0;
for i := 1 to n do av := av + x[i];
av := av / n;
end;
end.

91
92 Часть I. Основы программирования

Список параметров подпрограммы в разделе реализации указывать не обяза-


тельно.

Использование модулей
Чтобы использовать в программе величины, описанные в интерфейсной части
модуля, имя модуля следует указать в разделе uses (напомню, что он должен рас-
полагаться перед всеми остальными разделами). Можно записать несколько имен
модулей через запятую, например:
program example;
uses Average, Graph, Crt;
После этого все описания, расположенные в интерфейсных секциях модулей, ста-
новятся известными в программе, и ими можно пользоваться точно так же, как
и величинами, определенными в ней непосредственно. Поиск модулей выполняет-
ся сначала в библиотеке исполняющей системы, затем в текущем каталоге, а после
этого — в каталогах, заданных в диалоговом окне OptionsDirectories.
Если в программе описана величина с тем же именем, что и в модуле, для обраще-
ния к величине из модуля требуется перед ее именем указать через точку имя моду-
ля. Например, пусть в программе определена процедура Circle, а в разделе uses упо-
минается модуль Graph, который также содержит процедуру с таким именем. Для
обращения к процедуре Circle из модуля Graph следует записать ее имя в виде Graph.
Circle. Имя модуля с точкой может предшествовать любому идентификатору: кон-
станте, типу данных, переменной или подпрограмме.

ПРИМЕЧАНИЕ
К любой программе автоматически подключается стандартный модуль System, ко-
торый содержит библиотеку исполняющей системы Паскаля.

Пример использования модуля из листинга 4.7 приведен в листинге 4.8. Програм-


ма находит разность средних арифметических значений двух вещественных мас-
сивов.

Листинг 4.8. Разность средних арифметических значений массивов (модуль)


program dif_average;
uses Average;
var a, b : mas;
i : integer;
dif, av_a, av_b : real;
begin
for i := 1 to n do read(a[i]);
for i := 1 to n do read(b[i]);
average(a, av_a);
average(b, av_b);
dif := av_a – av_b;
writeln('Разность значений ', dif:6:2);
end.

92
Глава 4. Модульное программирование 93

Стандартные модули Паскаля


В Паскале имеется ряд стандартных модулей, в которых описано большое количество
встроенных констант, типов, переменных и подпрограмм. Каждый модуль содержит
связанные между собой ресурсы. Ниже приводится краткая характеристика моду-
лей Паскаля. Полные описания основных модулей можно найти в приложении 2.

Модуль System
Модуль System содержит базовые средства языка, которые поддерживают ввод-
вывод, работу со строками, операции с плавающей точкой и динамическое распре-
деление памяти. Этот модуль автоматически используется во всех программах, его
не требуется указывать в операторе uses. Он содержит все стандартные и встро-
енные процедуры, функции, константы и переменные Паскаля. Полное описание
модуля приведено в приложении 2 (с. 392).

Модуль Crt
Модуль Crt предназначен для организации эффективной работы с экраном, кла-
виатурой и встроенным динамиком. Программы, не использующие модуль Crt,
выполняют вывод на экран с помощью средств операционной системы DOS, что
является весьма медленным способом. При подключении модуля Crt выводимая
информация посылается в базовую систему ввода-вывода (ВIОS) или непосред-
ственно в видеопамять. При этом ввод-вывод выполняется гораздо быстрее, кроме
того, появляется возможность управлять цветом и размещением на экране. Полное
описание модуля приведено в приложении 2 (с. 357).

ПРИМЕЧАНИЕ
Модуль Crt предназначен для использования только на IBM PC-совместимых ком-
пьютерах.

В текстовом режиме экран представляется как совокупность строк и столбцов.


Каждый символ располагается на так называемом знакоместе на пересечении стро-
ки и столбца. Символы хранятся в специальной части оперативной памяти, назы-
ваемой видеопамятью. Ее содержимое отображается на экране.
Под каждый символ отводится два байта: один байт занимает ASCII-код символа,
другой байт хранит атрибуты символа — его цвет, цвет фона и признак мерцания
(рис. 4.2). Изображение символа по пикселам содержится в специальной матрице,
а цвет формируется из трех составляющих — синей, зеленой и красной, наличие кото-
рых задается установкой соответствующего бита в единицу. Под цвет фона отводится
три бита, а под цвет символа — четыре (четвертый бит управляет яркостью цвета). Та-
ким образом, можно получить восемь различных цветов фона и 16 цветов символов.
Для каждого цвета в модуле Crt определена соответствующая константа (см. с. 358).
Модуль Crt позволяет:
 выполнять вывод в заданное место экрана заданным цветом символа и фона;
 открывать на экране окна прямоугольной формы и выполнять вывод в пределах
этих окон;

93
94 Часть I. Основы программирования

 очищать экран, окно, строку и ее часть;


 обрабатывать ввод с клавиатуры;
 управлять встроенным динамиком.

Рис. 4.2. Текстовый режим монитора

Работа с экраном
Текущие цвета символа и фона задаются с помощью процедур TextColor
и TextBackGround и действуют на следующие за ними процедуры вывода. Текущие
атрибуты хранятся в младшем байте переменной TextAttr, ее значение можно уста-
новить и непосредственно. Вывод выполняется в текущую позицию курсора. Для
ее изменения служит процедура GotoXY.
Окно определяется с помощью процедуры Window. Оно задается координатами ле-
вого верхнего и правого нижнего угла. После определения окна позиционирование
курсора выполняется относительно него. Если окно не задано, им считается весь
экран.
Очистка текущего окна выполняется с помощью процедуры ClrScr, которая запол-
няет его пробелами с текущим цветом фона и устанавливает курсор в левый верх-
ний угол.
Пример. Программа «Угадай число» (листинг 4.9).

94
Глава 4. Модульное программирование 95

Листинг 4.9. Пример использования модуля Crt


program luck;
uses crt;
const max = 10;
var i, k, n : integer;
begin
clrscr; { очистить экран }
randomize; { инициализировать генератор }
i := random(max); { загадать число }
window(20, 5, 60, 20); { определить окно }
TextBackGround(Blue); { цвет фона – синий }
clrscr; { залить окно фоном }
TextColor(LightGray); { цвет символов – серый }
k := –1; { счетчик попыток }
GotoXY(12, 5); writeln(' Введите число : ');
repeat { цикл ввода ответа }
GotoXY(20, 9); { установить курсор }
readln(n); { ввести число }
inc(k);
until i = n;
window(20, 22, 60, 24); { определить окно результата }
TextAttr := 2 shl 4 + 14; { желтые символы за зеленом фоне }
clrscr; { залить окно фоном }
GotoXY(6, 2); { установить курсор }
writeln(' Коэффициент невезучести : ', k / max :5:1);
readkey; { ждать нажатия любой клавиши }
TextAttr := 15; { белые символы на черном фоне }
clrscr; { очистить после себя экран }
end.
Примерный вид экрана приведен на рис. 4.3. Генератор случайных чисел формиру-
ет число, находящееся в диапазоне от нуля до max – 1. Пользователь вводит числа
в одну и ту же позицию на экране до тех пор, пока не угадает это число. При угады-
вании с первого раза коэффициент невезучести равен нулю.

Рис. 4.3. Примерный вид экрана для программы luck

95
96 Часть I. Основы программирования

Работа с клавиатурой
Стандартные процедуры read и readln воспринимают только алфавитно-цифровые
символы и конец строки (символы с кодами #13 и #10). Модуль Crt позволяет рабо-
тать с управляющими клавишами и комбинациями клавиш.
Нажатие каждой клавиши преобразуется либо в ее ASCII-код, либо в так называе-
мый расширенный код (скан-код) и записывается в буфер клавиатуры, из которого
затем и выбирается процедурами ввода. Под каждый код отводится два байта. Если
нажатие клавиш соответствует символу из набора ASCII, в первый байт заносит-
ся код символа. Если нажата, например, клавиша управления курсором, функцио-
нальная клавиша или комбинация клавиш с Ctrl или Alt, то первый байт равен нулю,
а во втором находится расширенный код, соответствующий этой комбинации. Рас-
ширенные коды приведены в приложении 6 на с. 443.
Для работы с клавиатурой модуль Crt содержит функции ReadKey и KeyPressed.
Функция ReadKey : сhar считывает символ с клавиатуры, но не отображает его на
экране. При нажатии специальной клавиши или комбинации функция возвращает
символ с кодом 0, а при повторном вызове — расширенный код клавиши.
Функция KeyPressed : boolean возвращает значение truе, если на клавиатуре нажата
клавиша, и false в противном случае. Символ (или символы) остаются в буфере
клавиатуры.
Пример работы с расширенными кодами приведен на с. 39.

Модули Dos и WinDos


Модули Dos и WinDos содержат подпрограммы, реализующие возможности операци-
онной системы MS-DOS, такие как переименование, поиск и удаление файлов, по-
лучение и установка системного времени, выполнение программных прерываний
и т. д. Эти подпрограммы в стандартном Паскале не определены. Для поддержки
подпрограмм в модулях определены константы и типы данных.
Модуль Dos использует строки Паскаля, а WinDos — строки с завершающим нулем
(см. с. 101). Есть и другие различия между этими модулями, они подробно описаны
в приложении 2 на с. 362 и 410.
Пример. Программа определяет, сколько русских букв находится во всех текстовых
файлах текущего каталога (листинг 4.10).

Листинг 4.10. Пример использования модуля Dos


program count_rus_letters;
uses Dos;
var Dir : SearchRec;
code : integer;
n : longint;
c : char;
f : text;
begin
n := 0;
FindFirst('*.txt', AnyFile, Info);

96
Глава 4. Модульное программирование 97

while DosError = 0 do begin


assign(f, Info.Name); reset(f);
while not EOF(f) do begin
read(f, c);
code := ord(c);
if (code > $7F) and (code < $B0)
or (code > $DF) and (code < $F2) then inc(n);
end;
close(f);
FindNext(Info);
end;
writeln('Русских букв в текущем каталоге – ', n)
end.
Функция FindFirst заносит в запись Info типа SearchRec, определенного в модуле
Dos, информацию о первом найденном файле, соответствующем заданному ша-
блону. Первым параметром функции задается путь к каталогу (с метасимволами *
и (или) ?), вторым — атрибуты в виде константы, определенной в том же модуле.
Имя файла можно получить из поля Name записи Info.
Найденный файл открывается и просматривается посимвольно. Функция FindNext
выполняет поиск следующего файла по тому же шаблону. Если файлы исчерпаны,
переменная DosError принимает значение, не равное нулю, и цикл завершается.

Модуль Graph
Модуль обеспечивает работу с экраном в графическом режиме. Полное описание
ресурсов, входящих в модуль Graph, приведено в приложении 2 (с. 370).
Экран в графическом режиме представляется в виде совокупности точек — пик-
селов (pixel, сокращение от picture element). Цвет каждого пиксела можно задавать
отдельно. Начало координат находится в левом верхнем углу экрана и имеет коор-
динаты (0, 0). Количество точек по горизонтали и вертикали (разрешение экрана)
и количество доступных цветов зависят от графического режима. Графический ре-
жим устанавливается с помощью служебной программы — графического драйвера.
В состав оболочки входят несколько драйверов, каждый из которых может работать
в нескольких режимах. Режим устанавливается при инициализации графики либо
автоматически, либо программистом. Самый «мощный» режим, поддерживаемый
модулем Graph, — 640 × 480 точек, 16 цветов. Модуль Graph обеспечивает:
 вывод линий и геометрических фигур заданным цветом и стилем;
 закрашивание областей заданным цветом и шаблоном;
 вывод текста различным шрифтом, заданного размера и направления;
 определение окон и отсечение по их границе;
 использование графических спрайтов и работу с графическими страницами.
В графическом режиме (в отличие от текстового) курсор невидим, однако его мож-
но переместить в любую точку экрана. Текущее положение курсора используют
многие процедуры вывода изображений. При определении на экране окна началом
координат считается левый верхний угол этого окна.

97
98 Часть I. Основы программирования

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

ПРИМЕЧАНИЕ
Если программист не задал стиль, при выводе изображений используются параметры,
заданные по умолчанию. Например, линия выводится белым цветом, нормальной
толщины, сплошная.

Структура графической программы


Программа, использующая графический режим, должна содержать:
 подключение модуля Graph;
 перевод экрана в графический режим;
 установку параметров изображения;
 вывод изображения;
 возврат в текстовый режим.
Пример. Программа выводит на экран серию приятных глазу разноцветных линий,
движущийся смайлик и текст «The end» (листинг 4.11 и рис. 4.4).

Листинг 4.11. Пример использования модуля Graph


program lines;
uses Graph, Crt; { 1 }
const grDriver : integer = Detect;
size = 40;
s2 = size div 2;
s4 = size div 4;
margin = 40;
var grMode : integer;
ErrCode : integer;
HalfX, HalfY : integer;
x, y, x1, y1, x2, y2, i : integer;
image : pointer;
begin
randomize;
{ ---------------------------инициализация графики ------------ }
InitGraph(grDriver, grMode, 'd:\tp\bgi'); { 2 }
ErrCode := GraphResult; { 3 }
if ErrCode <> GrOK then begin
writeln('Ошибка графики: ', GraphErrorMsg(ErrCode));
exit end;
{ ------------------------------------ вывод линий ------------ }

98
Глава 4. Модульное программирование 99

HalfX := GetMaxX div 2; { 4 }


HalfY := GetMaxY div 2;
x := HalfX; x1 := x;
y := HalfY; y1 := y;
for i := 1 to 450 do begin { 5 }
x2 := round(cos(0.05 * i) * HalfY) + HalfX;
y2 := round(sin(0.02 * i) * HalfY) + HalfY;
if (i mod 10) = 0 then SetColor(random(15) + 1);
Line(x1, y1, x2, y2);
Line(x, y, x2, y2);
x1 := x2; y1 := y2;
delay(5);
end;
{ --------------------------- формирование спрайта ------------ }
SetColor(Cyan);
x := margin; y := x;
Circle(x + s2, y + s2, s2);
SetFillStyle(InterLeaveFill, Green); { 6 }
FillEllipse(x + s4, y + s4, s4, s4 div 2);
FillEllipse(x + 3 * s4, y + s4, s4, s4 div 2);
SetLineStyle(SolidLn, 0, ThickWidth); { 7 }
Line(x + s2, y + s4, x + s2, y + s2);
SetColor(Red);
Arc(x + s2, y + s2, 200, 340, s4);
Getmem(image, imagesize(x, y, x + size, y + size)); { 8 }
GetImage(x, y, x + size, y + size, image^); { 9 }
PutImage(x, y, image^, XorPut);
{ ------------------ вывод движущегося изображения ------------ }
while x < GetMaxX - margin - size do begin { 10 }
PutImage(x, y, image^, XorPut); delay(20);
PutImage(x, y, image^, XorPut);
inc(x, 5);
end;
PutImage(x, y, image^, XorPut);
{ ----------------------------------- вывод текста ------------ }
SetColor(Cyan);
SetTextStyle(GothicFont, HorizDir, 4); { 11 }
OuttextXY(HalfX + margin, HalfY – margin, 'The end');
readln;
CloseGraph { 12 }
end.
Графическая библиотека подключается в операторе 1. В графический режим экран
переводится в операторе 2 вызовом процедуры InitGraph. Ей надо передать три па-
раметра: номер графического драйвера (grDriver), его режим (grMode) и путь к ка-
талогу, в котором находятся драйверы. Если третий параметр представляет собой
пустую строку, поиск драйверов ведется в текущем каталоге.
Если параметр grDriver равен константе Detect, заданной в модуле Graph, выбор ре-
жима выполняется автоматически. При этом устанавливается максимально высо-
кий для данной аппаратуры режим (из поддерживаемых).

99
100 Часть I. Основы программирования

Если переменная grDriver не равна нулю, ее значение рассматривается как номер


драйвера. Этот драйвер загружается, и система переводится в режим, определяе-
мый параметром grMode. Список драйверов и их режимов приведен на с. 372.
Успешность установки графического режима обязательно нужно проверять. Это
делается с помощью функции GraphResult, которая возвращает код ошибки послед-
ней графической операции. Поскольку функция при повторном обращении дает
нуль, ее результат запоминается в переменной ErrCode (оператор 3), которая затем
используется для получения с помощью функции GraphErrorMsg текстового сообще-
ния об ошибке по ее номеру. Переменная ErrCode сравнивается с константой GrOK,
означающей нормальное завершение графической операции. Значение константы
равно нулю.

Рис. 4.4. Примерный вид экрана для программы lines

Если графический режим выбирался автоматически, для позиционирования изо-


бражений на экране необходимо получить доступное количество точек по осям X и Y
с помощью функций GetMaxX и GetMaxY (оператор 4). В данной программе с помощью
этих функций формируются координаты центра экрана.
В цикле (оператор 5) выводится серия линий с небольшой задержкой. Цвет линий
изменяется случайным образом через каждые 10 итераций.
Следующий фрагмент программы демонстрирует работу с графическими спрайта-
ми, которые применяются для вывода движущихся изображений. Для увеличения

100
Глава 4. Модульное программирование 101

скорости отрисовки изображение формируется один раз, после чего заносится в па-
мять с помощью процедуры GetImage (оператор 9). Объем памяти, необходимый для
размещения спрайта, определяется с помощью процедуры ImageSize, выделение па-
мяти выполняет процедура GetMem (оператор 8).

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

При формировании изображения использована процедура установки стиля закра-


ски SetFillStyle (оператор 6). Он используется процедурой рисования закрашен-
ного эллипса FillEllipse. Стиль линии, задающий повышенную толщину линии,
устанавливается процедурой SetLineStyle (оператор 7). Этот стиль действует при
выводе отрезка (Line) и дуги (Arc).
Для вывода спрайта используется процедура PutImage. Ее четвертый параметр за-
дает способ сочетания выводимого изображения и фона. Операция исключающего
ИЛИ (она задается константой XorPut), примененная дважды, позволяет оставить
неизменным фон, по которому движется изображение (цикл 10).
Перед выводом текста устанавливается его стиль (оператор 11). Стиль текста
состоит из имени шрифта, его расположения (горизонтальное или вертикальное)
и масштаба. В модуле Graph имеется один растровый шрифт и несколько векторных.
Каждый символ растрового шрифта определяется точечной матрицей 8 × 8, сим-
вол векторного шрифта задается набором кривых. Векторные шрифты дают более
качественное изображение символов большого размера. Каждый шрифт хранится
в отдельном файле на диске. Программист может воспользоваться и собственными
шрифтами (см. с. 381).
В конце программы восстанавливается исходный режим экрана (оператор 12).

Модули Printer и Overlay


Модуль Printer позволяет посылать стандартный вывод Паскаля на принтер с по-
мощью процедур write и writeln. Модуль Overlay позволяет разбивать программу на
неперекрывающиеся сегменты и загружать их в оперативную память по очереди,
что дает возможность выполнять программы, превышающие объем доступной па-
мяти реального режима. В настоящее время эти модули практически не использу-
ются.

Модуль Strings
Модуль Strings предназначен для работы со строками, заканчивающимися нуль-
символом, то есть символом с кодом 0 (их часто называют ASCIIZ-строками). Этот
вид строк введен в Паскаль специально для работы с длинными строками и про-
граммирования под Windows. Модуль Strings содержит функции копирования,

101
102 Часть I. Основы программирования

сравнения, слияния строк, преобразования их в строки типа string, поиска под-


строк и символов. Полное описание модуля приведено в приложении 2 (с. 390).
В модуле System определен тип pChar, представляющий собой указатель на символ
(^Char). В режиме расширенного синтаксиса, включенном по умолчанию (дирек-
тивой {$X+}), этот тип можно использовать для работы со строками, заканчиваю-
щимися символом #0. Эти строки располагаются в динамической памяти, и про-
граммист должен сам заниматься ее распределением с помощью процедур GetMem
или StrNew1.
Кроме того, для хранения ASCIIZ-строк используются массивы символов с нуле-
вой базой (элементы таких массивов нумеруются с нуля), например:
var str : array[0 .. 4000] of char;
p : pChar;
Массивы символов с нулевой базой и указатели на символы совместимы:
str := 'shooshpanchik'; p := str;
Стандартные процедуры Паскаля read, readln, str и val работают с массивами сим-
волов с нулевой базой как со строками, а процедуры write, writeln, assign и rename
вдобавок к этому «понимают» и указатели на символы. Необходимо учитывать, что
при работе с типом pChar контроль выхода за границу строки не выполняется.
Пример. Программа находит количество повторений последовательности сим-
волов, заданной с клавиатуры, в тексте, хранящемся в файле. Поиск ведется без
учета регистра, искомая последовательность располагается на одной строке (ли-
стинг 4.12).

Листинг 4.12. Пример использования модуля Strings


program count_word;
uses Strings;
var str, p, text : pchar;
buf : string;
f : file of char;
i, num, len : integer;
begin
assign(f, '...'); reset(f); { 1 }
len := Filesize(f); Getmem(text, len + 1);
i := 0;
while not Eof(f) do begin
read(f, text[i]); inc(i);
end;
text[i] := #0;
close(f);
strlower(text);
writeln('Какую подстроку искать?'); readln(buf); { 2 }
getmem(str, length(buf) + 1); strpcopy(str, buf);

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

102
Глава 4. Модульное программирование 103

strlower(str);
p := text; { 3 }
num := 0;
while true do begin
p := strpos(p, str);
if p = nil then break else inc(num);
inc(p);
end;
writeln(' Количество повторений: ', num)
end.
В первой части программы открывается компонентный файл, определяется его
длина, выделяется соответствующий объем динамической памяти, после чего туда
посимвольно считывается содержимое файла и переводится в нижний регистр.
Операторы, помеченные комментарием 2, выполняют чтение в буфер образца для
поиска. Затем буфер копируется в динамическую память. Это необходимо, потому
что процедура readln не умеет считывать строки в переменные типа pChar.
Цикл 3 выполняет поиск подстроки str в тексте text. Функция strpos возвращает
указатель на начало найденного фрагмента или nil, если он не найден. Для того
чтобы при каждом проходе цикла выполнялся поиск очередного вхождения образ-
ца, начальный адрес поиска сдвигается на единицу.

103
Глава 5. Работа с динамической памятью
Напомню (см. с. 78), что в PC-совместимых компьютерах память условно разделе-
на на сегменты. Компилятор формирует сегменты кода, данных и стека, а остальная
доступная программе память называется динамической (хипом, кучей). Ее можно
использовать во время выполнения программы.
В программах, приведенных ранее, для хранения данных использовались про-
стые переменные, массивы или записи. По их описаниям в разделе var компилятор
определяет, сколько места в памяти необходимо для хранения каждой величины.
Такие переменные можно назвать статическими. Распределением памяти под них
занимается компилятор, а обращение к этим переменным выполняется по имени.
Динамические переменные создаются в хипе во время выполнения программы. Об-
ращение к ним осуществляется через указатели.
С помощью динамических переменных можно обрабатывать данные, объем кото-
рых до начала выполнения программы не известен. Память под такие данные выде-
ляется порциями, или блоками, которые связываются друг с другом. Такой способ
хранения данных называется динамическими структурами, поскольку их размеры
изменяются в процессе выполнения программы.

Указатели
Имя переменной служит для обращения к области памяти, которую занимает ее
значение. Каждый раз, когда в исполняемых операторах программы упоминается
какое-либо имя, компилятор подставляет на его место обращение к соответствую-
щей ячейке памяти.
Программист может определить собственные переменные для хранения адресов
областей памяти. Такие переменные называются указателями. В указателе можно
хранить адрес данных или программного кода (например, адрес точки входа в про-
цедуру). Адрес занимает четыре байта и хранится в виде двух слов, одно из которых
определяет сегмент, второе — смещение.
Указатели в Паскале можно разделить на два вида: стандартные и определяемые
программистом. Величины стандартного типа pointer предназначены для хране-
ния адресов данных произвольного типа, например:
var p : pointer;

104
Глава 5. Работа с динамической памятью 105

Программист может определить указатель на данные или подпрограмму конкрет-


ного типа. Как и для других нестандартных типов, это делается в разделе type:
type pword = ^word; { читается как "указатель на word" }
...
var pw : pword;
Здесь определяется тип pword как указатель на величины типа word. В переменной
pw можно хранить только адреса величин указанного типа. Такие указатели назы-
ваются типизированными. Можно описать указатель на любой тип данных, кроме
файловых.
Аналогично другим типам, тип указателя на данные можно описать и непосред-
ственно при описании переменной, например:
var pw : ^word;
Указатели на подпрограммы будут рассмотрены далее (с. 109).

Операции с указателями
Для указателей определены только операции присваивания и проверки на равен-
ство и неравенство. В Паскале, в отличие от других языков, запрещаются любые
арифметические операции с указателями, их ввод-вывод и сравнение на больше-
меньше. Рассмотрим правила присваивания указателей.
Любому указателю можно присвоить стандартную константу nil, которая означает,
что указатель не ссылается на какую-либо конкретную ячейку памяти.
 Указатели стандартного типа pointer совместимы с указателями любого типа.
 Указателю на конкретный тип данных можно присвоить только значение указа-
теля того же или стандартного типа.
Операция @ и функция addr позволяют получить адрес переменной1, например:
var x : word; { переменная }
pw : ^word; { указатель на величины типа word }
...
pw := @w; { или pw := addr(w); }
Для обращения к значению переменной, адрес которой хранится в указателе, при-
меняется операция разадресации (разыменования), обозначаемая с помощью сим-
вола ^ справа от имени указателя, например:
pw^ := 2;
inc(pw^);
writeln(pw^);
В первом операторе в ячейку памяти, адрес которой хранится в переменной pw,
заносится число 2. При выполнении оператора вывода на экране появится чис-
ло 3.
1
В режиме {$T-}, принятом по умолчанию, тип результата операции @ совместим со всеми
типами указателей.

105
106 Часть I. Основы программирования

С величинами, адрес которых хранится в указателе, можно выполнять любые дей-


ствия, допустимые для значений этого типа. Для простоты восприятия можно счи-
тать имя указателя со следующей за ним «крышкой»1 просто именем переменной,
на которую он указывает, хотя на самом деле это, конечно, не так.

ПРИМЕЧАНИЕ
Указатели стандартного типа разыменовывать нельзя.

Указатели можно сравнивать на равенство и неравенство, например:


if p1 = p2 then ...
if p <> nil then ...
В Паскале определены стандартные функции для работы с указателями:
 addr(x) : pointer — возвращает адрес х (аналогично операции @), где х — имя пере-
менной или подпрограммы;
 seg(x) : word — возвращает адрес сегмента для х;
 ofs(x) : word — возвращает смещение для х;
 cseg : word — возвращает значение регистра сегмента кода CS;
 dseg : word — возвращает значение регистра сегмента данных DS;
 ptr(seg, ofs : word) : pointer — по заданному сегменту и смещению формирует
адрес типа pointer.
Динамические переменные
Динамические переменные создаются в хипе во время выполнения программы с по-
мощью подпрограмм new или getmem. Динамические переменные не имеют собствен-
ных имен — к ним обращаются через указатели.
Процедура new(var p : тип_указателя) выделяет в динамической памяти участок
размера, достаточного для размещения переменной того типа, на который ссылает-
ся указатель p, и заносит в него адрес начала этого участка.
Функция new(тип_указателя) : pointer выделяет в динамической памяти участок
размера, достаточного для размещения переменной базового типа для заданного
типа указателя, и возвращает адрес начала этого участка.
Если в new использовать указатель стандартного типа pointer, память фактически
не выделяется, а при попытке работать с ней выдается ошибка. Поэтому процедура
и функция new обычно применяются для типизированных указателей.
Процедура getmem(var p : pointer; size : word) выделяет в динамической памяти
участок размером в size байт и присваивает адрес его начала указателю p. Эту про-
цедуру можно применять и для указателей типа pointer, поскольку количество вы-
деляемой памяти задается в явном виде.
Если выделить требуемый объем памяти не удалось, программа аварийно заверша-
ется.
1
В незапамятные времена этот символ назывался гораздо импозантнее: циркумфлекс. Кста-
ти, и «собака» @ тогда была совсем не собакой!

106
Глава 5. Работа с динамической памятью 107

ПРИМЕЧАНИЕ
Эту реакцию можно изменить, задав собственную функцию обработки ошибки вы-
деления памяти.

Рассмотрим пример работы с динамическими переменными. Определим в разделе


описания переменных главной программы три указателя — p1, p2 и p3.
type rec = record
d : word;
s : string;
end;
pword = ^word;
var p1, p2 : pword;
p3 : ^rec;
Это — обычные статические переменные, компилятор выделяет под них в сегменте
данных по четыре байта и обнуляет их (рис. 5.1).

Рис. 5.1. Размещение указателей в памяти

В разделе исполняемых операторов программы запишем операторы


new(p1); p2 := new(pword); new(p3);
В результате выполнения процедуры new(p1) в хипе выделяется объем памяти, до-
статочный для размещения переменной типа word, и адрес начала этого участка
памяти записывается в переменную p1. Второй оператор выполняет аналогичные
действия, но используется функция new. При вызове процедуры new с параметром p3
в динамической памяти будет выделено количество байтов, достаточное для раз-
мещения записи типа rec.
Доступ к выделенным областям осуществляется с помощью операции разадреса-
ции:
p1^ := 2; p2^ := 4; p3^.d := p1^; p3^.s := 'Вася';
В этих операторах в выделенную память заносятся значения (рис. 5.2).
Динамические переменные можно использовать в операциях, допустимых для ве-
личин соответствующего типа, например:
inc(p1^); p2^ := p1^ + p3^.d;
with p3^ do writeln (d, s);

107
108 Часть I. Основы программирования

Рис. 5.2. Выделение и заполнение динамической памяти

ВНИМАНИЕ
При присваивании указателю другого значения старое значение теряется (рис. 5.3). Это
приводит к появлению так называемого мусора (на рисунке обозначен овалом), когда
доступа к участку динамической памяти нет, а сам он помечен как занятый.

Рис. 5.3. Мусор

Для освобождения динамической памяти используются процедуры Dispose и Freemem,


причем если память выделялась с помощью new, следует применять Dispose, в про-
тивном случае — Freemem.
Процедура Dispose(var p : pointer) освобождает участок памяти, выделенный для
размещения динамической переменной процедурой или функцией New, и значение
указателя p становится неопределенным.
Процедура Freemem(var p : pointer; size : word) освобождает участок памяти раз-
мером size, начиная с адреса, находящегося в p. Значение указателя становится
неопределенным.
Если требуется освободить память из-под нескольких переменных одновременно,
можно применять процедуры Mark и Release.
Процедура Mark(var p : pointer) записывает в указатель p адрес начала участка сво-
бодной динамической памяти на момент ее вызова. Этот адрес хранится в стандарт-
ной переменной HeapPtr1. Процедура mark вызывается до начала выделения памяти,
которую затем потребуется освободить.
1
Определены еще две стандартные переменные: HeapOrg хранит указатель на начало хипа,
а HeapEnd — на его конец.

108
Глава 5. Работа с динамической памятью 109

Процедура Release(var p : pointer) освобождает участок динамической памяти,


начиная с адреса, записанного в указатель p процедурой mark, то есть очищает ту
динамическую память, которая была занята после вызова Mark.
При завершении программы используемая ею динамическая память освобождает-
ся автоматически, поэтому явным образом освобождать ненужную память необхо-
димо только в том случае, если она может потребоваться при дальнейшем выпол-
нении программы.
При работе с динамической памятью часто применяются вспомогательные функ-
ции Maxavail, Memavail и Sizeof.
Функция Maxavail : longint возвращает длину в байтах самого длинного свободно-
го участка динамической памяти.
Функция Memavail : longint возвращает полный объем свободной динамической
памяти в байтах.
Вспомогательная функция Sizeof(x) : word возвращает объем в байтах, занимае-
мый x, причем x может быть либо именем переменной любого типа, либо именем типа.
Рассмотрим пример, в котором для выделения памяти используется процедура
Getmem и функции, описанные выше:
program demo_memo;
type
mas_int = array[1 .. maxint] of integer;
var p : ^mas_int;
i, n : integer;
begin
writeln(' Введите размер массива: '); readln(n);
if Maxavail < n * Sizeof(integer) then begin
writeln(' Недостаточно памяти'); halt end;
Getmem(p, n * Sizeof(integer));
for i := 1 to n do read(p^[i]);
...
end.
С помощью этой программы можно работать с массивом целых чисел, размер кото-
рого на стадии компиляции не известен — он запрашивается во время выполнения
программы. Перед выделением памяти с помощью функции Maxavail проверяется
наличие свободного места. Недостатком такого способа является то, что програм-
мист должен сам отслеживать правильность работы с массивом, ведь компилятор
не может проверить соответствие между размером выделенной памяти и обраще-
нием к элементам массива.

Указатели на процедуры и функции


Указатель на подпрограмму определяется как переменная процедурного (функци-
онального) типа (типы рассматривались на с. 86):
type fun = function(x : real) : real; { функциональный тип }
var pf : fun; { указатель на функции типа fun }

109
110 Часть I. Основы программирования

Указателю на подпрограмму можно присвоить nil, значение другого указателя


того же типа или имя конкретной подпрограммы. Присваивание имени выполня-
ется без использования операции взятия адреса @, поскольку имя подпрограммы
представляет собой адрес точки входа, то есть является константой процедурного
(функционального) типа:
function f(x : real) : real; far; { конкретная функция }
begin
тело функции
end;
...
pf := f; { в переменной pf будет храниться адрес точки входа в функцию f }
y := pf(x);{ теперь функцию f можно вызвать через переменную pf обычным образом }
Функция, адрес которой присваивается переменной, должна компилироваться в ре-
жиме дальней адресации. Для этого в ее заголовке указывается директива far или
задается ключ компиляции {$F+}. Это требование связано с тем, что переменная-
указатель должна содержать полный адрес, состоящий из сегмента и смещения,
а по умолчанию адрес подпрограммы содержит только смещение, потому что ком-
пилятор формирует всего один сегмент кода.
Пример. Шаблон программы, использующей массив указателей на функции (ли-
стинг 5.1). Такие массивы применяют при создании меню.

Листинг 5.1. Использование указателей на функции


program mas_fun;
type fun = function(x : real) : real;
function f1(x : real) : real; far;
begin
f1 := sin(x);
end;
function f2(x : real) : real; far;
begin
f2 := cos(x);
end;
function f3(x : real) : real; far;
begin
f3 := arctan(x);
end;
const pf : array[1 .. 3] of fun = (f1, f2, f3);
var y : real; i : integer;
begin
for i := 1 to 3 do
writeln(' Результат функции ', i, ' : ', pf[i](1));
end.

Динамические структуры данных


Если до начала работы с данными невозможно определить, сколько памяти по-
требуется для их хранения, память следует распределять во время выполнения

110
Глава 5. Работа с динамической памятью 111

программы по мере необходимости отдельными блоками. Блоки связываются друг


с другом с помощью указателей. Такой способ организации данных называется ди-
намической структурой данных, поскольку она размещается в динамической памя-
ти и ее размер изменяется во время выполнения программы.
Из динамических структур в программах чаще всего используются линейные спи-
ски, стеки, очереди и бинарные деревья. Они различаются способами связи отдель-
ных элементов и допустимыми операциями. Динамическая структура, в отличие от
массива или записи, может занимать несмежные участки оперативной памяти.
Динамические структуры широко применяют и для более эффективной работы
с данными, размер которых известен, особенно для решения задач сортировки, по-
скольку упорядочивание динамических структур не требует перестановки элемен-
тов, а сводится к изменению указателей на эти элементы. Например, если в процес-
се выполнения программы требуется многократно упорядочивать большой массив
данных, имеет смысл организовать его в виде линейного списка. При решении за-
дач поиска элемента в тех случаях, когда важна скорость, данные лучше всего пред-
ставить в виде бинарного дерева.
Элемент динамической структуры состоит из двух частей: информационной, ради
хранения которой и создается структура, и указателей, обеспечивающих связь эле-
ментов друг с другом. Элемент описывается в виде записи, например:
type
pnode = ^node;
node = record
d : word; { информационная }
s : string; { часть }
p : pnode; { указатель на следующий элемент }
end;

ПРИМЕЧАНИЕ
Обратите внимание, что тип указателя pnode на запись node определен раньше, чем
сама запись. Это не противоречит принципу «использование только после описания»,
поскольку для описания переменной типа pnode информации вполне достаточно.

Рассмотрим принципы работы с основными динамическими структурами.

Стеки
Стек является простейшей динамической структурой. Добавление элементов
в стек и выборка из него выполняются из одного конца, называемого вершиной сте-
ка. Другие операции со стеком не определены. При выборке элемент исключается
из стека.
Говорят, что стек реализует принцип обслуживания LIFO (last in — first out, послед-
ним пришел — первым обслужен). Стек можно представить себе как узкое дупло,
в которое засовывают, скажем, яблоки1. Достать первое яблоко можно только после

1
Почему яблоки? Ну, придумайте что-нибудь получше!

111
112 Часть I. Основы программирования

того, как вынуты все остальные. Кстати, сегмент стека назван так именно потому,
что память под локальные переменные выделяется по принципу LIFO. Стеки ши-
роко применяются в системном программном обеспечении, компиляторах, в раз-
личных рекурсивных алгоритмах.
Для работы со стеком используются две статические переменные: указатель на вер-
шину стека и вспомогательный указатель.
var top, p : pnode;
Тип указателей должен соответствовать типу элементов стека. Мы будем строить
стек из элементов, тип которых описан в предыдущем разделе.
Занесение первого элемента в стек выполняется в два приема: сначала выделяется
место в памяти и адрес его начала заносится в указатель на вершину стека (опера-
тор 1), а затем заполняются все поля элемента стека (операторы 2):
new(top); { 1 }
top^.d := 100; top^.s := 'Вася'; top^.p := nil; { 2 }
Значение nil в поле указателя на следующий элемент говорит о том, что этот эле-
мент в стеке является последним (рис. 5.4).

Рис. 5.4. Создание первого элемента стека

При добавлении элемента в стек, кроме создания элемента и заполнения его инфор-
мационной части (операторы 1 и 2), требуется связать его с предыдущим элемен-
том (оператор 3) и обновить указатель на вершину стека (оператор 4), потому что
теперь в вершине стека находится новый элемент:
new(p); { 1 }
p^.d := 10; p^.s := 'Петя'; { 2 }
p^.p := top; { 3 }
top := p; { 4 }
Стек, состоящий из трех элементов, изображен на рис. 5.5.
Выборка из стека состоит в получении информационной части элемента (опера-
тор 1), переносе указателя на вершину стека на следующий элемент (оператор 2)
и освобождении памяти из-под элемента (оператор 3):
with top^ do writeln (d, s); { 1 }
p := top; top := top^.p; { 2 }
dispose(p); { 3 }

112
Глава 5. Работа с динамической памятью 113

Рассмотренные операции удобно оформить в виде отдельных функций. В листин-


ге 5.2 приведена программа, которая формирует стек из пяти целых чисел и их тек-
стового представления и выводит его на экран. Функция занесения в стек по тради-
ции называется push, а функция выборки — pop.

Рис. 5.5. Стек из трех элементов

Листинг 5.2. Использование стека


program stack;
const n = 5;
type pnode = ^node;
node = record { элемент стека }
d : word;
s : string;
p : pnode;
end;
var top : pnode; { указатель на вершину стека }
i : word;
s : string;
const text : array [1 .. n] of string = ('one', 'two', 'three', 'four', 'five');
{ ------------------------------ занесение в стек --------------------------- }
function push(top : pnode; d : word; const s : string) : pnode;
var p : pnode;
begin
new(p);
p^.d := d; p^.s := s; p^.p := top;
push := p;
end;
{ ------------------------------ выборка из стека --------------------------- }
function pop(top : pnode; var d : word; var s : string) : pnode;
var p : pnode;
begin
d := top^.d; s := top^.s;
pop := top^.p;
dispose(top);
end;
{ ------------------------------- главная программа ----------------------------- }
begin
продолжение 

113
114 Часть I. Основы программирования

Листинг 5.2 (продолжение)


top := nil;
for i := 1 to n do top := push(top, i, text[i]); { занесение в стек: }
while top <> nil do begin { выборка из стека: }
top := pop(top, i, s); writeln(i:2, s);
end;
end.
Обратите внимание, что вспомогательный указатель объявлен внутри функций,
а их интерфейс содержит все необходимые сведения: с каким стеком идет работа
и что в него заносится или из него выбирается. Функции возвращают указатель на
вершину стека.

Очереди
Очередь — это динамическая структура данных, добавление элементов в которую
выполняется в один конец, а выборка — из другого конца. Другие операции с оче-
редью не определены. При выборке элемент исключается из очереди. Говорят, что
очередь реализует принцип обслуживания FIFO (first in — first out, первым при-
шел — первым обслужен)1. В программировании очереди применяются очень ши-
роко — например, при моделировании, буферизованном вводе-выводе или диспет-
черизации задач в операционной системе.
Для работы с очередью используются указатели на ее начало и конец, а также вспо-
могательный указатель:
var beg, fin, p : pnode;
Тип указателей должен соответствовать типу элементов, из которых состоит оче-
редь. Мы будем создавать очередь из тех же элементов, что и стек (см. с. 111).
Начальное формирование очереди — это создание ее первого элемента и установка
на него обоих указателей (рис. 5.6):
new(beg);
beg^.d := 100; beg^.s := 'Вася'; beg^.p := nil;
fin := beg;

Рис. 5.6. Создание первого элемента очереди

1
От очереди в кассу одноименная структура данных отличается тем, что без очереди влезть
невозможно.

114
Глава 5. Работа с динамической памятью 115

Добавление элемента в конец очереди выполняется с помощью вспомогательного


указателя:
new(p); { 1 }
p^.d := 10; p^.s := 'Петя'; p^.p := nil; { 2 }
fin^.p := p; { 3 }
fin := p; { 4 }
В операторах 1 и 2 создается новый элемент и заполняется его информационная
часть. Оператор 3 добавляет в последний элемент очереди ссылку на новый эле-
мент, а оператор 4 устанавливает новое значение указателя на конец очереди. Оче-
редь, состоящая из трех элементов, изображена на рис. 5.7.

Рис. 5.7. Очередь из трех элементов

Выборка элемента выполняется из начала очереди (оператор 1), при этом выбран-
ный элемент удаляется (оператор 3), а указатель на начало очереди сдвигается на
следующий элемент (оператор 2):
with beg^ do writeln (d, s); { 1 }
p := beg; beg := beg^.p; { 2 }
dispose(p); { 3 }
В листинге 5.3 приведена программа, которая формирует очередь из пяти целых
чисел и их текстового представления и выводит еe на экран. Для разнообразия опе-
рации с очередью оформлены в виде процедур. Процедура начального формирова-
ния называется first, помещения в конец очереди — add, а выборки — get.

Листинг 5.3. Использование очереди


program queue;
const n = 5;
type pnode = ^node;
node = record { элемент очереди }
d : word;
s : string;
p : pnode;
end;
var beg, fin : pnode; { указатели на начало и конец очереди }
i : word;
s : string;
продолжение 

115
116 Часть I. Основы программирования

Листинг 5.3 (продолжение)


const text : array [1 .. n] of string = ('one', 'two', 'three', 'four', 'five');
{ ------------------ начальное формирование очереди --------------------------- }
procedure first(var beg, fin : pnode; d : word; const s : string);
begin
new(beg);
beg^.d := d; beg^.s := s; beg^.p := nil;
fin := beg;
end;
{ --------------------- добавление элемента в конец --------------------------- }
procedure add(var fin : pnode; d : word; const s : string);
var p : pnode;
begin
new(p);
p^.d := d; p^.s := s; p^.p := nil;
fin^.p := p;
fin := p;
end;
{ ---------------------- выборка элемента из начала --------------------------- }
procedure get(var beg : pnode; var d : word; var s : string);
var p : pnode;
begin
d := beg^.d; s := beg^.s;
p := beg; beg := beg^.p;
dispose(p);
end;
{ ------------------------------- главная программа --------------------------- }
begin
{ занесение в очередь: }
first(beg, fin, 1, text[1]);
for i := 2 to 5 do add(fin, i, text[i]);
{ выборка из очереди: }
while beg <> nil do begin
get(beg, i, s);
writeln(i:2, s);
end;
end.

Линейные списки
В линейном списке каждый элемент связан со следующим и, возможно, с предыду-
щим. В первом случае список называется односвязным, во втором — двусвязным.
Также применяются термины «однонаправленный» и «двунаправленный». Если
последний элемент связать указателем с первым, получится кольцевой список.
Каждый элемент списка содержит ключ, идентифицирующий этот элемент. Ключ
обычно бывает либо целым числом, либо строкой и является частью поля данных.
В качестве ключа в процессе работы со списком могут выступать разные части
поля данных. Например, если создается линейный список из записей, содержащих
фамилию, год рождения, стаж работы и пол, любая часть записи может выступать

116
Глава 5. Работа с динамической памятью 117

в качестве ключа. При упорядочивании списка по алфавиту ключом будет фами-


лия, а при поиске, например, ветеранов труда — стаж. Ключи разных элементов
списка могут совпадать.
Над списками можно выполнять следующие операции:
 начальное формирование списка (создание первого элемента);
 добавление элемента в конец списка;
 чтение элемента с заданным ключом;
 вставка элемента в заданное место списка (до или после элемента с заданным
ключом);
 удаление элемента с заданным ключом;
 упорядочивание списка по ключу.
Как можно заметить, стек и очередь представляют собой частные случаи линейного
списка с ограниченным набором допустимых операций.
При чтении элемент списка не удаляется, в отличие от элемента, выбираемого из
стека и очереди. Для работы со списком в программе требуется определить указа-
тель на его начало. Чтобы упростить добавление новых элементов в конец списка,
можно также завести указатель на конец списка.
Рассмотрим программу (листинг 5.4), которая формирует односвязный список из
пяти элементов, содержащих число и его текстовое представление, а затем выпол-
няет вставку и удаление заданного элемента. В качестве ключа используется число.

Листинг 5.4. Использование списка


program linked_list;
const n = 5;
type pnode = ^node;
node = record { элемент списка }
d : word;
s : string;
p : pnode;
end;
var beg : pnode; { указатель на начало списка }
i, key : word;
s : string;
option : word;
const text: array [1 .. n] of string = ('one', 'two', 'three', 'four', 'five');
{ -------------- добавление элемента в конец списка --------------------------- }
procedure add(var beg : pnode; d : word; const s : string);
var p : pnode; { указатель на создаваемый элемент }
t : pnode; { указатель для просмотра списка }
begin
new(p); { создание элемента }
p^.d := d; p^.s := s; { заполнение элемента }
p^.p := nil;
if beg = nil then beg := p { список был пуст }
else begin { список не пуст }
продолжение 

117
118 Часть I. Основы программирования

Листинг 5.4 (продолжение)


t := beg;
while t^.p <> nil do { проход по списку до конца }
t := t^.p;
t^.p := p; { привязка нового элемента к последнему }
end
end;
{ ------------------------- поиск элемента по ключу --------------------------- }
function find(beg : pnode; key : word; var p, pp : pnode) : boolean;
begin
p := beg;
while p <> nil do begin { 1 }
if p^.d = key then begin { 2 }
find := true; exit end;
pp := p; { 3 }
p := p^.p; { 4 }
end;
find := false;
end;

{ -------------------------------- вставка элемента --------------------------- }


procedure insert(beg : pnode; key, d : word; const s : string);
var p : pnode; { указатель на создаваемый элемент }
pkey : pnode; { указатель на искомый элемент }
pp : pnode; { указатель на предыдущий элемент }
begin
if not find(beg, key, pkey, pp) then begin
writeln(' вставка не выполнена'); exit; end;
new(p); { 1 }
p^.d := d; p^.s := s; { 2 }
p^.p := pkey^.p; { 3 }
pkey^.p := p; { 4 }
end;
{ ------------------------------- удаление элемента --------------------------- }
procedure del(var beg : pnode; key : word);
var p : pnode; { указатель на удаляемый элемент }
pp : pnode; { указатель на предыдущий элемент }
begin
if not find(beg, key, p, pp) then begin
writeln(' удаление не выполнено'); exit; end;
if p = beg then beg := beg^.p { удаление первого элемента }
else pp^.p := p^.p;
dispose(p);
end;
{ ------------------------------------ вывод списка --------------------------- }
procedure print(beg : pnode);
var p : pnode; { указатель для просмотра списка }
begin
p := beg;
while p <> nil do begin { цикл по списку }

118
Глава 5. Работа с динамической памятью 119

writeln(p^.d:3, p^.s); { вывод элемента }


p := p^.p { переход к следующему элементу списка }
end;
end;
{ ------------------------------- главная программа --------------------------- }
begin
for i := 1 to 5 do add(beg, i, text[i]);
while true do begin
writeln('1 - вставка, 2 - удаление, 3 - вывод, 4 - выход');
readln(option);
case option of
1: begin { вставка }
writeln('Ключ для вставки?');
readln(key);
writeln('Вставляемый элемент?');
readln(i); readln(s);
insert(beg, key, i, s);
end;
2: begin { удаление }
writeln('Ключ для удаления?');
readln(key);
del(beg, key);
end;
3: begin { вывод }
writeln('Вывод списка:');
print(beg);
end;
4: exit; { выход }
end
writeln;
end
end.
Функция поиска элемента find возвращает true, если искомый элемент найден,
и false в противном случае. Поскольку одного факта отыскания элемента недо-
статочно, функция также возвращает через список параметров два указателя: на
найденный элемент p и на предшествующий ему pp. Последний требуется при уда-
лении элемента из списка, поскольку при этом необходимо связывать предыдущий
и последующий по отношению к удаляемому элементы.
Сначала указатель p устанавливается на начало списка, и организуется цикл про-
смотра списка (оператор 1). Если поле данных очередного элемента совпало с за-
данным ключом (оператор 2), формируется признак успешного поиска и функция
завершается. В противном случае перед переносом указателя на следующий эле-
мент списка (он хранится в поле p текущего элемента, оператор 4) его значение за-
поминается в переменной pp (оператор 3) для того, чтобы при следующем проходе
цикла в ней находился указатель на предыдущий элемент.
Если элемента с заданным ключом в списке нет, цикл завершится естественным
образом, поскольку последний элемент списка содержит nil в поле p указателя на
следующий элемент.

119
120 Часть I. Основы программирования

Вставка элемента выполняется после элемента с заданным ключом (процедура


insert). Если с помощью функции find место вставки определить не удалось, вы-
водится сообщение и процедура завершается (в этом случае можно было исполь-
зовать и другой алгоритм — добавлять элемент к концу списка, это определяется
конкретным предназначением программы). Если элемент найден, указатель на него
заносится в переменную pkey.
Под новый элемент выделяется место в динамической памяти (оператор 1), и ин-
формационные поля элемента заполняются переданными в процедуру значениями
(оператор 2). Новый элемент p вставляется между элементами pkey и следующим за
ним (его адрес хранится в pkey^.p). Для этого в операторах 3 и 4 устанавливаются
две связи (рис. 5.8).

Рис. 5.8. Вставка элемента в список

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

Бинарные деревья
Бинарное дерево — это динамическая структура данных, состоящая из узлов, каж-
дый из которых содержит кроме данных не более двух ссылок на различные бинар-
ные деревья. На каждый узел имеется ровно одна ссылка. Начальный узел называ-
ется корнем дерева.
Пример бинарного дерева приведен на рис. 5.9 (корень обычно изображается
сверху). Узел, не имеющий поддеревьев, называется листом. Исходящие узлы на-
зываются предками, входящие — потомками. Высота дерева определяется количе-
ством уровней, на которых располагаются его узлы.
Если дерево организовано таким образом, что для каждого узла все ключи его лево-
го поддерева меньше ключа этого узла, а все ключи его правого поддерева — боль-
ше, оно называется деревом поиска. Одинаковые ключи не допускаются. В дереве
поиска можно найти элемент, двигаясь от корня и переходя на левое или правое
поддерево в зависимости от значения ключа в каждом узле. Такой поиск гораздо

120
Глава 5. Работа с динамической памятью 121

эффективнее поиска по списку, поскольку время поиска определяется высотой де-


рева, а она пропорциональна двоичному логарифму количества узлов.

Рис. 5.9. Пример бинарного дерева

ПРИМЕЧАНИЕ
Для так называемого сбалансированного дерева, в котором количество узлов справа
и слева различается не более чем на единицу, высота дерева равна двоичному логарифму
количества узлов. Линейный список можно представить как вырожденное бинарное
дерево, в котором каждый узел имеет не более одной ссылки. Для списка среднее время
поиска равно половине длины списка.

Дерево является рекурсивной структурой данных, поскольку каждое поддерево


также является деревом. Действия с такими структурами изящнее всего описыва-
ются с помощью рекурсивных алгоритмов. Например, процедуру обхода всех узлов
дерева можно в общем виде описать так:
procedure print_tree( дерево );
begin
print_tree( левое_поддерево )
посещение корня
продолжение 

121
122 Часть I. Основы программирования

print_tree( правое_поддерево )
end;
Можно обходить дерево и в другом порядке, например: сначала корень, потом под-
деревья, но приведенная функция позволяет получить последовательность клю-
чей, отсортированную по возрастанию, поскольку сначала посещаются вершины
с меньшими ключами, расположенные в левом поддереве. Результат обхода дерева,
изображенного на рис. 5.9:
1, 6, 8, 10, 20, 21, 25, 30
Если в функции обхода первое обращение идет к правому поддереву, результат об-
хода будет другим:
30, 25, 21, 20, 10, 8, 6, 1
Таким образом, деревья поиска можно применять для сортировки значений. При
обходе дерева узлы не удаляются.
Для бинарных деревьев определены операции:
 включения узла в дерево;
 поиска по дереву;
 обхода дерева;
 удаления узла.
Для простоты будем рассматривать дерево, каждый элемент которого содержит
только целочисленный ключ и два указателя.
type pnode = ^node;
node = record
data : word; { ключ }
left : pnode; { указатель на левое поддерево }
right : pnode { указатель на правое поддерево }
end;
Доступ к дереву в программе осуществляется через указатель на его корень:
var root : pnode;
Рассмотрим сначала функцию поиска по дереву, так как она используется и при
включении, и при удалении элемента (листинг 5.5).

Листинг 5.5. Функция поиска по бинарному дереву


function find(root : pnode; key : word; var p, parent : pnode) : boolean;
begin
p := root; { поиск начинается от корня }
while p <> nil do begin
if key = p^.data then { узел с таким ключом есть }
begin find := true; exit end;
parent := p; { запомнить указатель перед спуском }
if key < p^.data
then p := p^.left { спуститься влево }
else p := p^.right; { спуститься вправо }
end;

122
Глава 5. Работа с динамической памятью 123

find := false;
end;
Функция возвращает булев признак успешности поиска. Ей передаются указатель
на корень дерева, в котором выполняется поиск (root), и искомый ключ (key). Вы-
ходными параметрами функции являются указатели на найденный элемент (p)
и его предка (parent).

ПРИМЕЧАНИЕ
Указатель на предка используется при удалении и вставке элемента. Для упрощения
алгоритмов можно добавить этот указатель в каждый элемент дерева.

Удаление элемента — более сложная задача, поскольку при этом необходимо сохра-
нить свойства дерева поиска. Ее можно разбить на четыре этапа.
1. Найти узел, который будет поставлен на место удаляемого.
2. Реорганизовать дерево так, чтобы не нарушились его свойства.
3. Присоединить новый узел к узлу-предку удаляемого узла.
4. Освободить память из-под удаляемого узла.
Удаление узла происходит по-разному в зависимости от его расположения в дереве.
Если узел является листом, то есть не имеет потомков, достаточно обнулить со-
ответствующий указатель узла-предка (рис. 5.10). Если узел имеет только одного
потомка, этот потомок ставится на место удаляемого узла, а в остальном дерево не
изменяется (рис. 5.11).

Рис. 5.10. Удаление узла, не имеющего потомков

Рис. 5.11. Удаление узла с одним потомком

123
124 Часть I. Основы программирования

Сложнее всего случай, когда у узла есть оба потомка, но и здесь есть простой осо-
бый случай: если у его правого потомка нет левого потомка, удаляемый узел заме-
няется на своего правого потомка, а левый потомок удаляемого узла подключается
вместо отсутствующего левого потомка. Звучит не очень-то понятно, поэтому рас-
смотрите этот случай на рис. 5.12.

Рис. 5.12. Удаление узла с двумя потомками

В общем же случае на место удаляемого узла ставится самый левый лист его право-
го поддерева (или наоборот — самый правый лист его левого поддерева). Это не
нарушает свойств дерева поиска. Этот случай иллюстрируется рис. 5.13.

Рис. 5.13. Удаление узла (общий случай)

124
Глава 5. Работа с динамической памятью 125

Корень дерева удаляется аналогичным образом за исключением того, что заме-


няющий его узел не требуется подсоединять к узлу-предку. Вместо этого обнов-
ляется указатель на корень дерева. Рассмотрим реализацию алгоритма удаления
(листинг 5.6).

Листинг 5.6. Процедура удаления узла из бинарного дерева


procedure del(var root : pnode; key : word);
var p : pnode; { удаляемый узел }
parent : pnode; { предок удаляемого узла }
y : pnode; { узел, заменяющий удаляемый }
function descent(p : pnode): pnode; { спуск по дереву }
var y : pnode; { узел, заменяющий удаляемый }
prev : pnode; { предок узла "y" }
begin
y := p^.right;
if y^.left = nil then y^.left := p^.left { 1 }
else begin { 2 }
repeat
prev := y; y := y^.left;
until y^.left = nil;
y^.left := p^.left; { 3 }
prev^. left := y^.right; { 4 }
y^.right := p^.right; { 5 }
end;
descent := y;
end;
begin
if not find(root, key, p, parent) then begin { 6 }
writeln(' такого элемента нет'); exit; end;
if p^.left = nil then y := p^.right { 7 }
else if p^.right = nil then y := p^.left { 8 }
else y := descent(p); { 9 }
if p = root then root := y { 10 }
else { 11 }
if key < parent^.data
then parent^.left := y
else parent^.right := y;
dispose(p); { 12 }
end;

В функцию del передаются указатель root на корень дерева, из которого требуется


удалить элемент, и ключ key удаляемого элемента. С помощью функции find опре-
деляются указатели на удаляемый элемент p и на его предка parent. Если искомого
элемента в дереве нет, выдается сообщение.
В операторах 7–9 определяется указатель на узел y, который должен заменить уда-
ляемый. Если у узла p нет левого поддерева, на его место будет поставлена вершина
(возможно, пустая) его правого поддерева (оператор 7, см. рис. 5.10, 5.11).

125
126 Часть I. Основы программирования

Иначе, если у узла p нет правого поддерева, на его место будет поставлена вершина
его левого поддерева (оператор 8). В противном случае оба поддерева узла суще-
ствуют, и для определения заменяющего узла вызывается вспомогательная функ-
ция descent, выполняющая спуск по дереву (оператор 9).
В этой функции первым делом проверяется особый случай, описанный выше (опе-
ратор 1, см. рис. 5.12). Если же условие отсутствия левого потомка у правого по-
томка удаляемого узла не выполняется, организуется цикл (оператор 2), на каждой
итерации которого указатель на текущий узел запоминается в переменной prev, а
указатель y смещается вниз и влево по дереву до того момента, пока не станет ссы-
латься на узел, не имеющий левого потомка — он-то нам и нужен!
В операторе 3 к этой пустующей ссылке присоединяется левое поддерево удаляе-
мого узла. Перед тем как присоединять к этому узлу правое поддерево удаляемо-
го узла (оператор 5), требуется «пристроить» его собственное правое поддерево.
Мы присоединяем его к левому поддереву предка узла y, заменяющего удаляемый
(оператор 4), поскольку этот узел перейдет на новое место.
Функция descent возвращает указатель на узел, заменяющий удаляемый. Если мы
удаляем корень дерева, надо обновить указатель на корень (оператор 10), иначе —
присоединить этот указатель к соответствующему поддереву предка удаляемого
узла (оператор 11). После того как узел удален из дерева, освобождается занимае-
мая им память (оператор 12).
В листинге 5.7 приведен пример работы с бинарным деревом.

Листинг 5.7. Использование бинарного дерева


program bintree;
uses crt;
type pnode = ^node;
node = record
data : word; { ключ }
left : pnode; { указатель на левое поддерево }
right : pnode { указатель на правое поддерево }
end;
var root : pnode;
key : word;
option : word;
{ ------------------------------------ вывод дерева --------------------------- }
procedure print_tree(p : pnode; level : integer);
var i : integer;
begin
if p = nil then exit;
with p^ do begin
print_tree(right, level + 1);
for i := 1 to level do write(' ');
writeln(data);
print_tree(left, level + 1);
end
end;

126
Глава 5. Работа с динамической памятью 127

{ ------------------------------- поиск по дереву – см. листинг 5.5 ----------- }


function find(root : pnode; key : word; var p, parent : pnode) : boolean;
{ ---------------------------- включение в дерево ----------------------------- }
procedure insert(var root : pnode; key : word);
var p, parent : pnode;
begin
if find(root, key, p, parent) then begin
writeln(' такой элемент уже есть'); exit; end;
new(p); { создание нового элемента }
p^.data := key;
p^.left := nil;
p^.right := nil;
if root = nil then root := p { первый элемент }
else { присоединение нового элемента к дереву}
if key < parent^.data
then parent^.left := p
else parent^.right := p;
end;
{ ------------------------------ удаление из дерева - см. листинг 5.6 --------- }
procedure del(var root : pnode; key : word);
{ ------------------------------- главная программа --------------------------- }
begin
root := nil;
while true do begin
writeln('1 - вставка, 2 - удаление, 3 - вывод, 4 - выход');
readln(option);
case option of
1: begin { вставка }
writeln('Введите ключ для вставки: '); readln(key);
insert(root, key);
end;
2: begin { удаление }
writeln('Введите ключ для удаления: '); readln(key);
del(root, key);
end;
3: begin { вывод }
clrscr;
if root = nil then writeln ('дерево пустое')
else print_tree(root, 0);
end;
4: exit; { выход }
end;
writeln;
end
end.
Рассмотрим функцию обхода дерева print_tree. Вторым параметром в нее передает-
ся целая переменная, определяющая, на каком уровне находится узел. Корень на-
ходится на уровне 0. Дерево печатается по горизонтали так, что корень находится
слева. Для дерева, изображенного на рис. 5.9, вывод выглядит так:

127
128 Часть I. Основы программирования

30
25
21
20
10
8
6
1
Для имитации структуры дерева перед значением узла выводится количество про-
белов, пропорциональное уровню узла. Если закомментировать цикл печати пробе-
лов, отсортированный по убыванию массив будет выведен в столбик. Заметьте, что
функция обхода дерева длиной всего в несколько строк может напечатать дерево
любого размера — ограничением является лишь размер стека.

Реализация динамических структур с помощью массивов


Динамические структуры данных не обязательно реализовывать в программе опи-
санным выше «классическим» способом. Если максимальный размер данных мож-
но определить до начала их использования, более эффективным может оказаться
однократное выделение непрерывной области памяти. Это можно сделать либо
в динамической памяти, либо с помощью обычных массивов. Связь элементов при
этом осуществляется не через указатели, а с помощью вспомогательных массивов
и переменных, в которых хранятся номера элементов.
Рассмотрим реализацию стека. Пуcть известно, что количество его элементов не
превышает n. Кроме массива элементов, соответствующих типу данных стека, до-
статочно иметь одну переменную целого типа для хранения индекса элемента мас-
сива, являющегося вершиной стека. При помещении в стек индекс увеличивает-
ся на единицу, а при выборке — уменьшается. Например, стек, рассмотренный на
с. 111, можно описать так:
const n = 100;
type node = record
d : word; { информационная }
s : string; { часть }
end;
stack = array [1 .. n] of node;
var st : stack;
top : word;
В начале работы со стеком переменная top обнуляется. Занесение в стек выглядит
примерно так:
inc(top);
if top > n then begin writeln('переполнение стека'); exit end;
st[top].d := d;
st[top].s := s;
При использовании вместо массива динамической памяти описывается указатель
на массив и под стек выделяется память:

128
Глава 5. Работа с динамической памятью 129

var st : ^stack;
new(st);
Обращение к элементу стека будет содержать операцию разадресации:
st^[top].d := d;
Для реализации очереди требуются две переменные целого типа — для хранения
индекса элементов массива, являющихся началом и концом очереди.
Линейный список реализуется с помощью вспомогательного массива целых чисел
и переменной, хранящей номер первого элемента, например:
10 25 20 6 21 8 1 30 — массив данных
2 3 4 5 6 7 8 –1 — вспомогательный массив
1 — индекс первого элемента в списке
i-й элемент вспомогательного массива содержит для каждого i-го элемента масси-
ва данных индекс следующего за ним элемента. Отрицательное число или нуль ис-
пользуется как признак конца списка (в принципе, для этого можно использовать
любое число, не входящее в множество значений индекса массива). Массив после
сортировки выглядит так:
10 25 20 6 21 8 1 30 — массив данных
3 8 5 6 2 1 4 –1 — вспомогательный массив
7 — индекс первого элемента в списке
Для создания бинарного дерева используются два вспомогательных массива (ин-
дексы вершин его правого и левого поддерева) и переменная, в которой хранится
индекс корня. Признак пустой ссылки — отрицательное число или нуль. Например,
дерево, приведенное на рис. 5.9, можно представить следующим образом:
10 25 20 6 21 8 1 30 — массив данных
4 3 –1 7 –1 –1 –1 –1 — левая ссылка
2 8 5 6 –1 –1 –1 –1 — правая ссылка
1 — индекс корневого элемента дерева

ВНИМАНИЕ
При работе с подобными структурами необходимо контролировать возможный выход
индексов за границы массива.

Рассмотренный способ реализации позволяет использовать преимущества дина-


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

129
Глава 6. Технология структурного
программирования
Мы рассмотрели множество тем: типы данных и операторы Паскаля, использова-
ние подпрограмм и модулей, работу с динамической памятью и файлами. Такой ба-
гаж позволяет писать довольно сложные программы, но коммерческий программ-
ный продукт можно создать, только следуя определенной дисциплине. Рассмотрим
свойства, отличающие кустарные поделки от профессиональных продуктов.

Критерии качества программы


Основное требование, предъявляемое в настоящее время к программе, — надеж-
ность. Под надежностью подразумевается способность программы работать в пол-
ном соответствии со спецификацией и адекватно реагировать на любые действия
пользователя.
Программа должна также обладать расширяемостью, то есть допускать оператив-
ное внесение необходимых изменений и дополнений. Может показаться странным,
зачем с самого начала думать о будущих изменениях программы, но ведь для любо-
го сколько-нибудь удачного коммерческого продукта выход новых версий — един-
ственный способ не потерять популярность среди пользователей.
Кроме того, программа должна быть выпущена к заявленному сроку. Это значит,
что весь процесс производства программы должен четко планироваться и контро-
лироваться.
В результате быстрого развития отрасли и жесткой конкуренции отходят на второй
план такие критерии качества программы, как эффективность и требуемые ресур-
сы, например объем внешней и оперативной памяти. Однако это не означает, что
этим критериям вообще не следует уделять внимания.
Программирование интенсивно развивается уже более полувека, огромная армия
программистов наступала за это время на миллионы граблей, садилась в лужи и на-
бивала шишки. Одним из результатов классификации граблей, луж и шишек стала
технология структурного программирования, которая охватывает все этапы разра-
ботки программы: спецификацию, проектирование, собственно программирование
и тестирование.
Структурное программирование — это способ создания программ, позволяющий
путем соблюдения определенных правил уменьшить время разработки и облегчить
возможность модификации программы.

130
Глава 6. Технология структурного программирования 131

Как следует из названия, технология структурного программирования позволяет


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

Этапы создания структурной программы


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

Постановка задачи
Создание любой программы начинается с постановки задачи. Изначально задача
формулируется в терминах предметной области, и необходимо перевести ее на
язык понятий, более близких к программированию. Поскольку программист редко
досконально разбирается в предметной области, а заказчик — в программировании
(простой пример: требуется написать бухгалтерскую программу), постановка за-
дачи может стать весьма непростым итерационным процессом. Кроме того, при по-
становке задачи заказчик зачастую не может четко и полно сформулировать свои
требования и критерии.
На этом этапе также определяется среда, в которой будет выполняться программа:
требования к аппаратуре, используемая операционная система и другое программ-
ное обеспечение.
Постановка задачи завершается созданием технического задания, а затем внешней
спецификации программы, включающей в себя:
 описание исходных данных и результатов (типы, форматы, точность, способ
передачи, ограничения)1;
 описание задачи, реализуемой программой;
 способ обращения к программе;
 описание возможных аварийных ситуаций и ошибок пользователя.
Таким образом, программа рассматривается как черный ящик, для которого опре-
делена функция и входные и выходные данные.

Выбор модели и метода решения задачи


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

1
Под типами и форматами не имеются в виду типы языка программирования.

131
132 Часть I. Основы программирования

постановка задачи формализуется, и на этой основе определяется общий метод ее


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

Разработка внутренних структур данных


Большинство алгоритмов зависит от того, каким образом организованы данные,
поэтому интуитивно ясно, что начинать проектирование программы надо не с алго-
ритмов, а с разработки структур, необходимых для представления входных, выход-
ных и промежуточных данных. При этом принимаются во внимание многие факто-
ры, например: ограничения на размер данных, необходимая точность, требования
к быстродействию программы. Структуры данных могут быть статическими или
динамическими.
При решении вопроса о том, как будут организованы данные в программе, полезно
задать себе следующие вопросы.
 Какая точность представления данных необходима?
 В каком диапазоне лежат значения данных?
 Ограничено ли максимальное количество данных?
 Обязательно ли хранить их в программе одновременно?
 Какие действия потребуется выполнять над данными?
Например, если максимальное количество однотипных данных, которые требуется
обработать, известно и невелико, проще всего завести для их хранения статический
массив. Если таких массивов много, сегментов данных и стека может оказаться не-
достаточно, и придется отвести под эти массивы место в динамической памяти.
Если максимальное количество данных неизвестно и постоянно изменяется во вре-
мя работы программы, для их хранения используют динамические структуры. Вы-
бор вида структуры зависит от требуемых операций над данными. Например, для
быстрого поиска элементов лучше всего подходит бинарное дерево, а если данные
требуется обрабатывать в порядке поступления, применяется очередь. В некото-
рых случаях удобно моделировать динамические структуры с помощью массивов
(см. с. 128).

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

132
Глава 6. Технология структурного программирования 133

Для каждой подзадачи составляется внешняя спецификация, аналогичная приве-


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

ВНИМАНИЕ
На этапе проектирования следует учитывать возможность будущих модификаций
программы и стремиться проектировать программу таким образом, чтобы вносить
изменения было как можно проще.

Поскольку не известно, какие изменения придется выполнить, это пожелание напо-


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

Структурное программирование
Программирование здесь рассматривается «в узком смысле», то есть понимается
как запись программы на языке программирования по готовому алгоритму. Этот
процесс часто называют кодированием, чтобы отличить его от полного цикла раз-
работки программы.
Кодирование также организуется по принципу «сверху вниз»: вначале кодируются
модули самого верхнего уровня и составляются тестовые примеры для их отладки,
при этом на месте еще не написанных модулей следующего уровня ставятся так
называемые заглушки — временные программы. Заглушки в простейшем случае
просто выдают сообщение о том, что им передано управление, а затем возвращают
его в вызывающий модуль. В других случаях заглушка может выдавать значения,
заданные заранее или вычисленные по упрощенному алгоритму.
Таким образом, сначала создается логический скелет программы, который затем
обрастает плотью кода. Казалось бы, более логично применять к процессу програм-
мирования восходящую технологию: написать и отладить сначала модули нижнего
уровня, а затем объединять их в более крупные фрагменты, но этот подход имеет
ряд недостатков.

133
134 Часть I. Основы программирования

Во-первых, в процессе кодирования верхнего уровня могут быть вскрыты те или


иные трудности проектирования более низких уровней программы (просто пото-
му, что при написании программы ее логика продумывается более тщательно, чем
при проектировании). Если подобная ошибка обнаруживается в последнюю оче-
редь, требуются дополнительные затраты на переделку готовых модулей нижнего
уровня.
Во-вторых, для отладки каждого модуля, а затем более крупных фрагментов про-
граммы требуется каждый раз составлять свои тестовые примеры, и программист
часто вынужден имитировать то окружение, в котором должен работать модуль.
Нисходящая же технология программирования обеспечивает естественный по-
рядок создания тестов — возможность нисходящей отладки, которая рассмотрена
ниже.
Этапы проектирования и программирования совмещены во времени: в идеале
сначала проектируется и кодируется верхний уровень, затем — следующий, и т. д.
Такая стратегия применяется потому, что она снижает цену ошибки, поскольку
в процессе кодирования может возникнуть необходимость внести изменения, от-
ражающиеся на модулях нижнего уровня.
О кодировании программы хочется поговорить более подробно, поэтому ему по-
священ весь следующий раздел. А пока рассмотрим последнюю, очень важную со-
ставляющую структурной технологии — тестирование.

Нисходящее тестирование
Этот этап записан последним, но это не значит, что тестирование не должно прово-
диться на предыдущих этапах. И проектирование, и программирование обязательно
должно сопровождать (а лучше предшествовать) написание набора тестов — про-
верочных исходных данных и соответствующих им наборов эталонных реакций.
Необходимо различать процессы тестирования и отладки программы. Тестирова-
ние — это процесс, посредством которого проверяется правильность программы.
Тестирование носит позитивный характер, его цель — показать, что программа
работает правильно и удовлетворяет всем проектным спецификациям. Отлад-
ка — процесс исправления ошибок в программе, при котором цель исправить все
ошибки не ставится. Исправляют ошибки, обнаруженные при тестировании. При
планировании следует учитывать, что процесс обнаружения ошибок подчиняется
закону насыщения, то есть большинство ошибок обнаруживается на ранних стади-
ях тестирования, и чем меньше в программе осталось ошибок, тем дольше искать
каждую из них.
Существует две стратегии тестирования: «черный ящик» и «белый ящик». При ис-
пользовании первой внутренняя структура программы во внимание не принимает-
ся и тесты составляются так, чтобы полностью проверить функционирование про-
граммы на корректных и некорректных входных воздействиях.
Стратегия «белого ящика» предполагает проверку всех операторов, ветвей или
условий алгоритма. Общее число ветвей определяется комбинацией всех альтерна-
тив на каждом этапе. Это конечное число, но оно может быть очень большим, поэ-
тому программа разбивается на фрагменты, после исчерпывающего тестирования

134
Глава 6. Технология структурного программирования 135

которых они рассматриваются как элементарные узлы более длинных ветвей. Кро-
ме данных, обеспечивающих выполнение операторов в требуемой последователь-
ности, тесты должны содержать проверку граничных условий (например, переход по
условию х > 10 должен проверяться для значений, бˆольших, меньших и равных 10).
Отдельно проверяется реакция программы на ошибочные исходные данные.
Недостатком стратегии «белого ящика» является то, что обнаружить с помощью
нее отсутствующую ветвь невозможно, а стратегия «черного ящика» требует боль-
шого количества вариантов входных воздействий, поэтому на практике применяют
сочетание обеих стратегий.

ВНИМАНИЕ
Идея нисходящего тестирования предполагает, что к тестированию программы при-
ступают еще до того, как завершено ее проектирование. Это позволяет раньше опро-
бовать основные межмодульные интерфейсы, а также убедиться в том, что программа
в основном удовлетворяет требованиям пользователя. Только после того, как логиче-
ское ядро испытано настолько, что появляется уверенность в правильности реализа-
ции основных интерфейсов, приступают к кодированию и тестированию следующего
уровня программы.

Естественно, полное тестирование программы, пока она представлена в виде ске-


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

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

ВНИМАНИЕ
Главная цель, к которой нужно стремиться, — получить легко читаемую программу воз-
можно более простой структуры. В конечном счете все технологии программирования
направлены на достижение именно этой цели, поскольку только так можно добиться
надежности программы и легкости ее модификации.

135
136 Часть I. Основы программирования

 Программа должна состоять из максимально обособленных частей, связанных


друг с другом только через интерфейсы. Следует четко отделять интерфейс под-
программы или модуля от их реализации и ограничивать доступ к информации,
ненужной для их использования.
 Каждое законченное действие оформляется в виде подпрограммы. Размер подпро-
граммы может быть разным, это зависит от конкретного действия, но желательно,
чтобы ее тело помещалось на один-два экрана: одинаково сложно разбираться
в программе, содержащей несколько необъятных функций, и в россыпи из сотен
подпрограмм по несколько строк каждая. Если какая-либо последовательность
операторов используется хотя бы дважды, ее также нужно оформить в виде под-
программы.
 Все величины, которыми подпрограмма обменивается с вызывающей программой,
должны передаваться ей через параметры. Входные параметры предпочтительнее
передавать как константы. Обычно в списке параметров сначала записывают все
входные параметры, а затем — все выходные. Если подпрограмма возвращает
одно значение, лучше оформить ее в виде функции, если несколько — в виде
процедуры.
 В подпрограмме полезно предусматривать реакцию на неверные входные пара-
метры и аварийное завершение. Это может быть или печать сообщения, или,
что более предпочтительно, формирование признака результата. Этот признак
необходимо анализировать в вызывающей программе. Сообщение об ошибке
должно быть информативным и подсказывать пользователю, как ее исправить.
Например, при вводе неверного значения в сообщении должен быть указан до-
пустимый диапазон.
 Величины, используемые только в подпрограмме, следует описывать внутри нее
как локальные переменные. Это упрощает отладку программы. Использовать гло-
бальные переменные в подпрограммах нежелательно, потому что их изменение
трудно отследить.
 Имена переменных должны отражать их смысл. Правильно выбранные имена мо-
гут сделать программу в некоторой степени самодокументированной. Неудачные
имена, наоборот, служат источником проблем. Сокращения ухудшают читае-
мость, и часто можно забыть, как именно было сокращено то или иное слово.
Общая тенденция состоит в том, что чем больше область видимости переменной,
тем более длинное у нее имя. Перед таким именем можно поставить префикс
типа (одну или несколько букв, по которым можно определить тип переменной).
Для счетчиков коротких циклов, напротив, лучше обойтись однобуквенными
именами типа i или k.
 Следует избегать использования в программе чисел в явном виде. Константы долж-
ны иметь осмысленные имена, заданные в разделе описания const. Символическое
имя делает программу более понятной, а кроме того, при необходимости изме-
нить значение константы потребуется изменить программу только в одном месте.
Конечно, этот совет не относится к константам 0 и 1.
 Для записи каждого фрагмента алгоритма необходимо использовать наиболее
подходящие средства языка. Например, ветвление на несколько направлений
по значению целой переменной более красиво записать с помощью оператора

136
Глава 6. Технология структурного программирования 137

case, а не нескольких if. Для просмотра массива лучше пользоваться циклом for.
Оператор goto применяют весьма редко, например, для принудительного выхода
из нескольких вложенных циклов, а в большинстве других ситуаций лучше ис-
пользовать другие средства языка, такие как процедуры break или exit.
 Программа должна быть «прозрачна». Если какое-либо действие можно за-
программировать разными способами, то предпочтение должно отдаваться не
наиболее компактному и даже не наиболее эффективному, а такому, который
легче для понимания. Особенно это важно тогда, когда пишут программу одни,
а сопровождают другие, что является широко распространенной практикой.
«Непрозрачное» программирование может повлечь огромные затраты на поиск
ошибок при отладке.
 Не следует размещать в одной строке много операторов. Как и в русском языке,
после знаков препинания должны использоваться пробелы:
f=a+b; { плохо! Лучше f = a + b; }
 Вложенные блоки должны иметь отступ в 3–4 символа, причем блоки одного
уровня вложенности должны быть выровнены по вертикали. Форматируйте
текст по столбцам везде, где это возможно.
var p : pnode; { удаляемый узел }
parent : pnode; { предок удаляемого узла }
y : pnode; { узел, заменяющий удаляемый }
...
if p^.left = nil then y := p^.right
else if p^.right = nil then y := p^.left
else y := descent(p);
В последних трех строках показано, что иногда большей ясности можно до-
биться, если не следовать правилу отступов буквально.
 Помечайте конец длинного составного оператора, например:
while true do begin
while not eof(f) do begin
for i := 1 to 10 do begin
for j := 1 to 10 do begin
{ две страницы кода }
end { for j := 1 to 10 }
end { for i := 1 to 10 }
end { while not eof(f) )
end { while true }
 Для организации циклов пользуйтесь наиболее подходящим оператором. Цикл
repeat применяется только в тех случаях, когда тело в любом случае потребуется
выполнить хотя бы один раз, например при проверке ввода. Цикл for использу-
ется, если число повторений известно заранее и параметр имеет порядковый тип,
цикл while — во всех остальных случаях. При записи итеративных циклов (в ко-
торых для проверки условия выхода используются соотношения переменных,
формируемых в теле цикла) необходимо предусматривать аварийный выход
по достижении заранее заданного максимального количества итераций. Чтобы

137
138 Часть I. Основы программирования

цикл легче читался, стремитесь объединять инициализацию, проверку условия


выхода и модификацию параметров цикла в одном месте.
Далее приведено несколько советов по записи операторов ветвления.
 Более короткую ветвь if лучше поместить вверху, иначе вся управляющая струк-
тура может не поместиться на экране, что затруднит отладку.
 Бессмысленно использовать проверку на равенство true или false:
var busy : boolean;

if busy = true then … { плохо! Лучше if busy then }
if busy = false then … { плохо! Лучше if not busy then }
 Следует избегать лишних проверок условий. Например:
if a < b then c := 1;
else if a > b then c := 2;
else if a = b then c := 3; { лишняя проверка }
Лучше написать так:
if a < b then c := 1;
else if a > b then c := 2;
else c := 3; { лучше }
Или даже так:
c := 3;
if a < b then c := 1;
if a > b then c := 2;
 Если первая ветвь оператора if содержит передачу управления, использовать
else нет необходимости:
if i > 0 then exit;
{ здесь i <= 0 }
 Необходимо предусматривать печать сообщений в тех точках программы, куда
управление при нормальной работе программы передаваться не должно. Именно
это сообщение вы с большой вероятностью получите при первом же запуске про-
граммы. Например, оператор case должен иметь слово else с обработкой ситуа-
ции по умолчанию, особенно если в нем перечислены все возможные значения
переключателя.

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

138
Глава 6. Технология структурного программирования 139

В идеале комментарии должны создаваться до написания программы — ими явля-


ется подробный алгоритм, изложенный на естественном языке. Очень полезно до
начала кодирования подробно записать, что и как должна делать программа (под-
программа). Это помогает в деталях продумать алгоритм и интерфейсы, найти на
самой ранней стадии серьезные ошибки и обеспечить содержательные коммента-
рии. После такой работы программирование сводится к вставке фрагментов кода
между комментариями. Подходить к написанию программы нужно таким образом,
чтобы ее можно было в любой момент передать другому программисту.
Комментарии должны представлять собой правильные предложения со знаками
препинания и без сокращений1, они не должны подтверждать очевидное (коммен-
тарии в этой книге не могут служить образцом, поскольку они предназначены для
обучения, а не для сопровождения). Например, бессмысленны фразы типа «вызов
функции f» или «описание переменных».
Если комментарий к фрагменту программы занимает несколько строк, лучше раз-
местить его до фрагмента, чем справа от него, и выровнять по вертикали. Абзацный
отступ комментария должен соответствовать отступу комментируемого блока.
{ Комментарий, описывающий,
что происходит в следующем ниже
блоке программы }
Непонятный
блок
программы
Для разделения подпрограмм и других логически законченных фрагментов поль-
зуйтесь пустыми строками или комментарием вида
{ ----------------------------------------------------------------------------- }
Конечно, все усилия по комментированию программы пропадут втуне, если сама
она написана путано, неряшливо и непродуманно. Поэтому любую программу после
написания необходимо тщательно отредактировать: убрать ненужные фрагмен-
ты, сгруппировать описания, отформатировать текст, оптимизировать проверки
условий и циклы, проверить, оптимально ли разбиение на функции, и т. д. С перво-
го раза без помарок хороший текст не напишешь, будь то сочинение, статья или
программа.
Однако не следует пытаться оптимизировать все, что попадается под руку, посколь-
ку главный принцип программиста тот же, что и врача: «не навреди!». Если про-
грамма работает недостаточно эффективно, надо в первую очередь подумать о том,
какие алгоритмы в нее заложены: например, пузырьковая сортировка всегда будет
работать медленно, как бы тщательно она ни была написана. Полезно также проа-
нализировать программу с помощью профайлера, выявить узкие места и оптими-
зировать только их.
В заключение порекомендую тем, кто предпочитает учиться программированию не
только на своих ошибках, очень полезную книгу Фредерика Брукса [3].

1
Совсем хорошо, если они при этом не будут содержать орфографических ошибок.

139
Глава 7. Введение в объектно-ориентированное
программирование

Объектно-ориентированное программирование (ООП) представляет собой даль-


нейшее развитие идей структурного программирования, основной целью которого
является создание программ простой структуры. Это достигается за счет разбиения
программы на максимально обособленные части.
Структурная программа состоит из совокупности подпрограмм, связанных с по-
мощью интерфейсов. Подпрограмма работает с данными, которые либо являются
локальными, либо передаются ей в качестве параметров. Каждая подпрограмма
предназначена для работы с определенными типами данных. Ошибки часто связа-
ны с тем, что в подпрограмму передаются неверные данные. Естественный путь из-
бежать таких ошибок — связать в одно целое данные и все подпрограммы, которые
предназначены для их обработки. Эта идея лежит в основе ООП: из предметной
области выделяются объекты, поведение и взаимодействие которых моделируются
с помощью программы.
Принципы ООП проще всего понять на примере программ моделирования. В ре-
альном мире каждый предмет или процесс обладает набором статических и дина-
мических характеристик, иными словами, свойствами и поведением. Поведение
объекта зависит от его состояния и внешних воздействий. Например, если в объ-
екте «автомобиль» повернуть руль, изменится положение колес, а если в баке нет
бензина, автомобиль никуда не поедет.
Понятие объекта в программе совпадает с обыденным смыслом этого слова: объект
представляется как совокупность данных, характеризующих его состояние, и про-
цедур их обработки, моделирующих его поведение. Вызов процедуры часто называ-
ют посылкой сообщения объекту.
При создании объектно-ориентированной программы предметная область пред-
ставляется не в виде данных и обрабатывающих эти данные подпрограмм, как при
процедурном подходе, а в виде совокупности объектов. Выполнение программы
состоит в том, что объекты обмениваются сообщениями. Это позволяет использо-
вать при программировании понятия, более адекватно отражающие предметную
область.
При представлении реального объекта с помощью программного необходимо выде-
лить в первом его существенные особенности. Их список зависит от цели моделиро-
вания. Например, объект «крыса» с точки зрения ветеринара, дачника или, скажем,

140
Глава 7. Введение в объектно-ориентированное программирование 141

повара будет иметь совершенно разные характеристики. Выделение существенных


с точки зрения рассмотрения свойств называется абстрагированием. Таким обра-
зом, программный объект — это абстракция.
Важным свойством объекта является его обособленность. Чтобы пользоваться под-
программой, достаточно знать только ее заголовок (интерфейс); объект тоже ис-
пользуется только через интерфейс — обычно это совокупность интерфейсов его
подпрограмм. Детали реализации объекта, то есть внутренние структуры данных
и алгоритмы их обработки, скрыты от пользователя и недоступны для непредна-
меренных изменений.
Скрытие деталей реализации называется инкапсуляцией (от слова «капсула»). Ни-
чего сложного в этом понятии нет: ведь и в обычной жизни мы пользуемся объек-
тами через их интерфейсы. Сколько информации пришлось бы держать в голове,
если бы для просмотра новостей надо было знать устройство телевизора!
Таким образом, объект является «черным ящиком», закрытым по отношению к внеш-
нему миру. Это позволяет представить программу в более укрупненном виде — на
уровне объектов и их взаимосвязей, а следовательно, дает возможность управлять
большим объемом информации и успешно отлаживать более сложные программы.
Сказанное выше можно сформулировать более кратко и строго: объект — это ин-
капсулированная абстракция с четко определенным интерфейсом.
Инкапсуляция позволяет изменить реализацию объекта без модификации основ-
ной части программы, если его интерфейс остался прежним (например, при необ-
ходимости изменить способ хранения данных с массива на стек). Простота моди-
фикации, как уже отмечалось, является важным критерием качества программы.
Кроме того, инкапсуляция позволяет использовать объект в другом окружении
и быть уверенным, что он не испортит не принадлежащие ему области памяти,
а также создавать библиотеки объектов для применения во многих программах.
Каждый год в мире пишется огромное количество новых программ, и важнейшее
значение приобретает возможность повторного использования кода. Для струк-
турных программ она сильно ограничена: любую подпрограмму либо можно ис-
пользовать без изменений, либо надо переписывать. Преимущество объектно-
ориентированного программирования состоит в том, что для любого объекта можно
определить наследников, корректирующих или дополняющих его поведение. При
этом нет необходимости иметь доступ к исходному коду родительского объекта.
Наследование позволяет создавать иерархии объектов. Иерархия представляется
в виде дерева, в котором более общие объекты располагаются ближе к корню, а бо-
лее специализированные — на ветвях и листьях. Наследование облегчает использо-
вание библиотек объектов, поскольку программист может взять за основу объекты
из библиотеки и создать их наследников с требуемыми свойствами. В Паскале лю-
бой объект может иметь одного предка и произвольное количество потомков.
Применение ООП позволяет писать гибкие, расширяемые, хорошо читаемые про-
граммы. Во многом это обеспечивается благодаря полиморфизму, под которым по-
нимается возможность во время выполнения программы с помощью одного и того
же имени выполнять разные действия или обращаться к объектам разного типа.

141
142 Часть I. Основы программирования

Чаще всего понятие полиморфизма связывают с механизмом виртуальных методов


(они будут рассмотрены на с. 155).
Подводя итог сказанному выше, сформулирую преимущества ООП:
 использование в программах понятий, более близких к предметной области;
 более простая структура программы в результате инкапсуляции, то есть объеди-
нения свойств и поведения объекта и скрытия деталей его реализации;
 возможность повторного использования кода за счет наследования;
 сравнительно простая возможность модификации программы;
 возможность создания библиотек объектов.
Эти преимущества проявляются при создании программ большого объема и классов
программ. Однако создание объектно-ориентированной программы представляет
собой весьма непростую задачу, поскольку в процесс добавляется этап разработ-
ки иерархии объектов, а плохо спроектированная иерархия приводит к появлению
сложных и запутанных программ.
Чтобы эффективно использовать готовые объекты, необходимо освоить большой
объем достаточно сложной информации, да и сами идеи ООП непросто применить
грамотно. Неграмотное использование ООП способно привести к созданию про-
грамм, сочетающих недостатки и структурного, и объектно-ориентированного под-
хода и лишенных их преимуществ. Впрочем, в этом нет ничего удивительного: ведь
известно, что если делать дело, то надо делать его хорошо!
Нашу задачу облегчает то, что объектная модель Паскаля по сравнению с другими
языками является упрощенной. Тем не менее на ее примере удобно изучить основ-
ные положения ООП, не увязнув в многообразии возможностей и деталей.

Описание объектов
Объект — это тип данных, поэтому он определяется в разделе описания типов.
В других языках объектный тип называют классом. Объект похож на тип record, но
кроме полей данных в нем можно описывать методы. Методами называются под-
программы, предназначенные для работы с полями объекта. Внутри объекта опи-
сываются только заголовки методов:
type имя = object
[ private ]
описание полей
[ public ]
заголовки методов
end;
Поля и методы называются элементами объекта. Их видимостью управляют дирек-
тивы private и public. Ключевое слово private (закрытые) ограничивает видимость
перечисленных после него элементов файлом, в котором описан объект. Действие
директивы распространяется до другой директивы или до конца объекта. В объекте
может быть произвольное количество разделов private и public. По умолчанию все

142
Глава 7. Введение в объектно-ориентированное программирование 143

элементы объекта считаются видимыми извне, то есть являются public (открыты-


ми). Этим Паскаль отличается от других, более современных языков программи-
рования.
Поля объекта описываются аналогично обычным переменным: для каждого поля
задается его имя и тип. Тип может быть любым, кроме типа того же объекта, но мо-
жет быть указателем на этот тип. Значения полей определяют состояние объекта.
При определении состава методов исходят из требуемого поведения объекта. Каж-
дое действие, которое должен выполнять объект, оформляется в виде отдельной
процедуры или функции. Рассмотрим в качестве примера объект, моделирующий
персонаж компьютерной игры:
type monster = object
procedure init(x_, y_, health_, ammo_ : word);
procedure attack;
procedure draw;
procedure erase;
procedure hit;
procedure move(x_, y_ : word);
private
x, y : word;
health, ammo : word;
color : word;
end;
В этом объекте пять полей данных. Поля x и y представляют собой местоположение
объекта на экране, health хранит состояние здоровья, ammo — боезапас, а color — цвет.
Поля описаны в разделе private, так как эта информация относится к внутренней
структуре объекта.
Допустим, что нашему герою придется перемещаться по экрану, атаковать другие
объекты и терять при этом здоровье и оружие. Каждому действию соответствует
свой метод. Методы описаны в разделе public (по умолчанию), потому что они со-
ставляют интерфейс объекта.

ПРИМЕЧАНИЕ
В объекте monster перечислены только те методы, которые необходимы для демон-
страции синтаксиса объектов и возможностей ООП. При написании реальной игры
элементов было бы гораздо больше.

Вся внешняя информация, необходимая для выполнения действий с объектом,


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

143
144 Часть I. Основы программирования

procedure monster.init(x_, y_, health_, ammo_ : word);


begin
x := x_; y := y_;
health := health_;
ammo := ammo_;
color := yellow;
end;
procedure monster.draw;
begin
setcolor(color); outtextXY(x, y, '@');
end;
Перед использованием объект требуется инициализировать, то есть присвоить на-
чальные значения его полям и, возможно, выполнить другие подготовительные
действия. В документации Borland Pascal процедуре инициализации объекта ре-
комендуется давать имя init. Обратите внимание, что поля объекта используются
внутри методов непосредственно, без указания имени объекта.
Процедура draw, как легко догадаться, предназначена для вывода изображения объ-
екта на экран. Чтобы не отвлекаться на детали, предположим, что изображение
монстра представляет собой просто символ «собаки» @1.
Методы представляют собой разновидность подпрограмм, поэтому внутри них мож-
но описывать локальные переменные. Принцип здесь точно такой же, как и при напи-
сании обычных подпрограмм: если переменная используется для временного хране-
ния данных только внутри метода, ее следует описывать в этом методе как локальную.
Как правило, поля объекта объявляют как private, а методы — как public, однако
это не догма. Если поле объекта представляет собой свойство, которое концепту-
ально входит в интерфейс объекта и пользователь должен иметь право устанавли-
вать и получать его без каких-либо ограничений, логично поместить его в раздел
public. И наоборот: если метод предназначен только для вызова из других методов,
его лучше скрыть от посторонних глаз в разделе private.

ПРИМЕЧАНИЕ
Во многих учебниках по ООП каждому полю сопоставлены два метода: один для
установки значения поля, другой для его получения, что на первый взгляд вызывает
недоумение, потому что увеличивает объем программы и не дает никаких преимуществ.
Дело в том, что применять ООП имеет смысл в основном для сложных объектов,
а в книгах обычно приводятся примеры простых, потому что сложный объект займет
много места, а пояснение основных принципов ООП станет не таким прозрачным.
Поэтому вы должны понимать, что поля реальных объектов часто представляют собой
сложные структуры данных, доступ к которым исключительно с помощью методов
оправдан и необходим. В этом случае можно не только изменять метод доступа к полю,
но и при необходимости вносить в структуру поля изменения, не отражающиеся на ин-
терфейсе объекта. Для полей, представляющих собой простые переменные, часто требу-
ется контролировать допустимость устанавливаемых значений. Кроме того, обращение
ко всем полям с помощью методов обеспечивает единообразие доступа к объекту.

1
Впрочем, с помощью модуля Graph при всем желании не удастся нарисовать ничего пора-
жающего воображение.

144
Глава 7. Введение в объектно-ориентированное программирование 145

Для реализации принципа инкапсуляции, то есть ограничения видимости элемен-


тов, объекты обычно описывают в модулях. Тип объекта определяется в интерфейс-
ном разделе модуля, а тексты методов объекта — в разделе реализации.
Объекты можно описать и в разделе реализации модуля. Они подчиняются тем же
ограничениям, что и любые другие типы, определенные в этом разделе. Объект,
определенный в интерфейсном разделе модуля, может иметь потомков, опреде-
ленных в разделе реализации. Если модуль В использует модуль А, в модуле В мо-
гут определяться потомки любых объектов, описанных в интерфейсном разделе
модуля А.

ВНИМАНИЕ
В отличие от остальных, объектный тип можно определять только в разделе описания
типов самого внешнего блока программы или модуля. Иными словами, нельзя описы-
вать объектный тип внутри подпрограммы и в разделе описания переменных.

Описание объекта monster в модуле приведено в листинге 7.1.

Листинг 7.1. Описание объекта monster


unit monsters;
interface
uses Graph;
type monster = object
procedure init(x_, y_, health_, ammo_ : word);
procedure attack;
procedure draw;
procedure erase;
procedure hit;
procedure move(x_, y_ : word);
private
x, y : word;
health, ammo : word;
color : word;
end;
implementation
{ ----------------------- реализация методов объекта monster ------------------- }
procedure monster.init(x_, y_, health_, ammo_ : word);
begin
x := x_; y := y_;
health := health_;
ammo := ammo_;
color := yellow;
end;
procedure monster.attack; { ------------------------------- monster.attack ------ }
begin
if ammo = 0 then exit;
dec(ammo); setcolor(color); outtextXY(x + 15, y, 'ба-бах!');
end;
продолжение 

145
146 Часть I. Основы программирования

Листинг 7.1 (продолжение)


procedure monster.draw; { ------------------------------- monster.draw -------- }
begin
setcolor(color); outtextXY(x, y, '@');
end;
procedure monster.erase; { ------------------------------- monster.erase ------- }
begin
setcolor(black); outtextXY(x, y, '@');
end;
procedure monster.hit; { ------------------------------- monster.hit --------- }
begin
if health = 0 then exit;
dec(health);
if health = 0 then begin color := red; draw; exit; end;
attack;
end;
procedure monster.move(x_, y_ : word); { ------------------ monster.move -------- }
begin
if health = 0 then exit;
erase; x := x_; y := y_; draw;
end;end.
Для стирания изображения объекта с экрана он выводится поверх старого цветом
фона (метод erase). Перемещение объекта (метод move) выполняется путем стира-
ния на старом месте и отрисовки на новом.
Атака (метод attack) реализована схематично: если имеется ненулевой боезапас, он
уменьшается на единицу, после чего выводится диагностическое сообщение. Если
объект атакован (метод hit), он теряет единицу здоровья, но атакует в ответ, а при
потере последней единицы здоровья выводится на экран красным цветом. Объект,
потерявший в сражении все здоровье, двигаться и атаковать не может.

Экземпляры объектов
Переменная объектного типа называется экземпляром объекта. Часто экземпляры
называют просто объектами. Время жизни и видимость объектов зависят от вида
и места их описания и подчиняются общим правилам Паскаля. Экземпляры объ-
ектов, так же как и переменные других типов, можно создавать в статической или
динамической памяти, например:
var Vasia : monster; { описывается статический объект }
pm : ^monster; { описывается указатель на объект }
...
new(pm); { создается динамический объект }
Можно определять массивы объектов или указателей на объекты и создавать из
них динамические структуры данных. Если объектный тип описан в модуле, для
создания в программе переменных этого типа следует подключить модуль в раз-
деле uses:
uses graph, monsters;

146
Глава 7. Введение в объектно-ориентированное программирование 147

Доступ к элементам объекта осуществляется так же, как к полям записи: либо с ис-
пользованием составного имени, либо с помощью оператора with.
Vasia.erase;
with pm^ do begin init(100, 100, 30); draw; end;
При обращении указывается имя экземпляра объекта, а не имя типа. Если поля
объекта являются открытыми или объектный тип описан в той же программной
единице, где используется, к полям можно обращаться так же, как к методам:
pm^.x := 300; { если бы x было public }
Если объект описан в модуле, получить или изменить значения элементов со спец-
ификатором private в программе можно только через обращение к соответствую-
щим методам. При создании каждого объекта выделяется память, достаточная для
хранения всех его полей. Методы объекта хранятся в одном экземпляре. Для того
чтобы методу было известно, с данными какого экземпляра объекта он работает,
при вызове ему в неявном виде передается параметр self, определяющий место рас-
положения данных этого объекта. Фактически внутри метода обращение к полю x
объекта имеет вид self.x. При необходимости имя self можно использовать внутри
метода явным образом, например, @self представляет собой адрес начала области,
в которой хранятся поля объекта.
Объекты одного типа можно присваивать друг другу, при этом выполняется по-
элементное копирование всех полей. Кроме того, в Паскале определены правила
расширенной совместимости типов объектов (см. с. 153). Все остальные действия
выполняются над отдельными полями объектов.
В листинге 7.2 приведен пример программы, использующей модуль monsters.

Листинг 7.2. Пример использования модуля monsters


program test_monster;
uses graph, crt, monsters;
var Vasia : monster; { 1 }
x, y : word;
gd, gm : integer;
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;
Vasia.init(100, 100, 10, 10); { 2 }
Vasia.draw; { 3 }
Vasia.attack; { 4 }
readln;
x := 110;
while x < 200 do begin
Vasia.move(x, x); inc(x, 7); { 5 }
Vasia.hit; { 6 }
delay(200);
end;
readln;
end.

147
148 Часть I. Основы программирования

Для тестирования объекта monster в программе определена переменная Vasia типа


monster (оператор 1). Для нее вызываются процедуры инициализации (2), отрисов-
ки (3) и атаки (4), а затем перемещения по экрану (5) с имитацией попадания в объ-
ект при каждом перемещении (6). Таким образом проверяется работоспособность
всех методов объекта.
Пример программы, в которой используется массив объектов типа monster, приве-
ден в листинге 7.3. Все объекты инициализируются случайными значениями, а за-
тем начинают хаотически перемещаться по экрану. Если два монстра оказываются
на достаточно близком расстоянии друг от друга, они считаются атакованными
(для обоих экземпляров вызывается метод hit)1. Выход из этой в высшей степени
медитативной программы, которую можно назвать «монстры от испуга скушали
друг друга», выполняется по нажатию любой клавиши.

Листинг 7.3. Пример использования массива объектов типа monster


program dinner;
uses graph, crt, monsters;
const n = 30;
var stado : array [1 .. n] of monster;
x, y : array [1 .. n] of integer;
gd, gm : integer;
i, j : word;
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;
randomize;
for i := 1 to n do begin
stado[i].init(random(600), random(440), random(10), random(8));
stado[i].draw;
end;
repeat
for i := 1 to n do begin
x[i] := random(600); y[i] := random(440);
stado[i].move(x[i], y[i]);
end;
for i := 1 to n – 1 do
for j := i + 1 to n do
if (abs(x[i] – x[j]) < 15) and (abs(y[i] – y[j]) < 15)
then begin
stado[i].hit; stado[j].hit;
end;
delay(200);
until keypressed;
end.

1
Перед запуском программы рекомендуется из эстетических соображений закомментиро-
вать вывод диагностического сообщения «ба-бах!» в методе attack.

148
Глава 8. Иерархии объектов
Управлять большим количеством разрозненных объектов достаточно сложно. С этой
проблемой можно справиться путем упорядочивания и ранжирования объектов, то
есть объединяя общие для нескольких объектов свойства в одном объекте и используя
этот объект в качестве базового.
Эту возможность предоставляет механизм наследования. Он позволяет строить ие-
рархии, в которых объекты-потомки получают свойства объектов-предков и могут
дополнять их или изменять. Таким образом, наследование обеспечивает важную
возможность повторного использования кода. Написав и отладив код базового объ-
екта, его можно приспособить для работы в различных ситуациях с помощью на-
следования. Это экономит время разработки и повышает надежность программ.

ПРИМЕЧАНИЕ
Для обозначения объектов-предков также используется термин «родительские объ-
екты», а потомков называют производными объектами.

Объекты, расположенные ближе к началу иерархии, объединяют в себе наиболее


общие черты для всех нижележащих объектов. По мере продвижения вниз по ие-
рархии объекты приобретают все больше конкретных особенностей.

Наследование
Объект в Паскале может иметь произвольное количество потомков и только одного
предка. При описании объекта имя его предка записывается в круглых скобках по-
сле ключевого слова object.
Допустим, нам требуется ввести в игру еще один тип персонажей, который должен
обладать свойствами объекта monster, но по-другому выглядеть и атаковать. Будет
логично сделать новый объект потомком объекта monster. Проанализировав код ме-
тодов этого объекта, переопределим только те, которые реализуются по-другому
(листинг 8.1).

Листинг 8.1. Переопределение методов после добавления нового типа персонажей


type daemon = object (monster)
procedure init(x_, y_, health_, ammo_, magic_ : word);
procedure attack;
procedure draw;
продолжение 

149
150 Часть I. Основы программирования

Листинг 8.1 (продолжение)


procedure erase;
procedure wizardry;
private
magic : word;
end;{ ------------------------- реализация методов объекта daemon ----------------- }
procedure daemon.init(x_, y_, health_, ammo_, magic_ : word);
begin
inherited init(x_, y_, health_, ammo_);
color := green;
magic := magic_;
end;
procedure daemon.attack; { --------------------------------- daemon.attack ---- }
begin
if ammo = 0 then exit;
dec(ammo);
if magic > 0 then begin
outtextXY(x + 15, y, 'БУ-БУХ!'); dec(magic); end
else
outtextXY(x + 15, y, 'бу-бух!');
end;
procedure daemon.draw; { ----------------------------------- daemon.draw ---- }
begin
setcolor(color); outtextXY(x, y, '%)');
end;
procedure daemon.erase; { ----------------------------------- daemon.erase ---- }
begin
setcolor(black); outtextXY(x, y, '%)');
end;
procedure daemon.wizardry; { -------------------------------- daemon.wizardry - }
begin
if magic = 0 then exit;
outtextXY(x + 15, y, 'крибле-крабле-бумс!'); dec(magic);
end;
Наследование полей. Унаследованные поля доступны в объекте точно так же, как
и его собственные. Изменить или удалить поле при наследовании нельзя. Таким об-
разом, потомок всегда содержит количество полей, большее или равное количеству
полей своего предка.
Объект daemon содержит все поля своего предка и одно собственное поле magic, в ко-
тором хранится «магическая сила» объекта.
Наследование методов. В потомке объекта можно не только описывать новые ме-
тоды, но и переопределять существующие, в отличие от полей. Метод можно пере-
определить либо полностью, либо дополнив метод предка.
В объекте daemon описан новый метод wizardry, с помощью которого объект приме-
няет свою магическую силу, а метод инициализации init переопределен, потому что
количество полей объекта изменилось. Однако необходимость задавать значения

150
Глава 8. Иерархии объектов 151

унаследованным полям осталась, и соответствующий метод есть в объекте monster,


поэтому из нового метода инициализации сначала вызывается старый, а затем вы-
полняются дополнительные действия (присваивание значения полю ammo).
Вызов метода предка из метода потомка выполняется с помощью ключевого слова
inherited (унаследованный). Можно вызвать метод предка и явным образом с по-
мощью конструкции monster.init.

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

Методы отрисовки draw и erase также переопределены, потому что изображение де-
мона отличается от изображения монстра и, следовательно, формируется другой по-
следовательностью подпрограмм (для простоты представим демона в виде «смайли-
ка» — схематичной улыбки, часто используемой в электронной переписке).
Переопределен и метод attack: теперь атака выполняется по-разному в зависимо-
сти от наличия магической силы. Чтобы подчеркнуть, что демон атакует не так, как
монстр, текст диагностического сообщения изменен.
Чтобы перемещать демона, требуется выполнить те же действия, что записаны
в методе move для перемещения монстра: необходимо стереть его изображение
на старом месте, обновить координаты и нарисовать на новом месте. На первый
взгляд можно без проблем унаследовать этот метод, а также метод hit1. Так мы
и поступим.
Добавим описание объекта daemon в интерфейсную часть модуля monsters (c. 145),
а тексты его методов — в раздел реализации. Проверим работу новых методов с по-
мощью программы:
program test_inheritance;
uses graph, crt, monsters;
var Vasia : daemon;
gd, gm : integer;
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;
Vasia.init(100, 100, 20, 10, 6);
Vasia.draw; Vasia.attack;
readln;
Vasia.erase;
readln;
end.

1
В следующем разделе разъясняется, почему первый взгляд не всегда верен. Проблемы все-
таки будут, но решаемые.

151
152 Часть I. Основы программирования

И в предке и в потомке есть одноименные методы. Вызывается всегда тот метод,


который соответствует типу объекта, потому что при вызове указывается имя эк-
земпляра заданного типа (рис. 8.1). Это можно рассматривать как простейший вид
полиморфизма.

Рис. 8.1. Раннее связывание

Раннее связывание
Продолжим тестирование объекта daemon, вставив в приведенную выше програм-
му перед первой из процедур readln вызовы методов, унаследованных из объекта
monster.
Vasia.move(200, 100); Vasia.move(200, 200); Vasia.hit;
Результаты запуска программы разочаровывают: на экране появляется изображе-
ние не демона, а монстра — символ @! Значит, из метода move вызываются методы
рисования и стирания объекта-предка. Да и метод атаки, вызываемый из hit, судя
по диагностическому сообщению, также принадлежит объекту monster. Чтобы разо-
браться, отчего это происходит, рассмотрим механизм работы компилятора.
Исполняемые операторы программы в виде инструкций процессору находятся
в сегменте кода. Каждая подпрограмма имеет точку входа. Вызов подпрограммы
при компиляции заменяется на последовательность команд, которая передает
управление в эту точку, а также выполняет передачу параметров и сохранение реги-
стров процессора. Этот процесс называется разрешением ссылок и в других языках

152
Глава 8. Иерархии объектов 153

чаще всего выполняется не компилятором, а специальной программой — редакто-


ром связей, или компоновщиком.
Таким образом, при компиляции метода move объекта monster на место вызова ме-
тодов erase и draw вставляются переходы на первые исполняемые операторы этих
методов из объекта monster. Вызвав метод move из любого потомка monster, мы в лю-
бом случае попадем в методы erase и draw объекта monster, потому что они жестко
связаны друг с другом еще до выполнения программы (см. рис. 8.1).
Аналогичная ситуация и с методом attack. Если он вызывается непосредственно для
экземпляра объекта daemon, то все в порядке, но вызвать его из метода hit, описан-
ного в объекте-предке, невозможно, потому что при компиляции метода hit в него
была вставлена передача управления на метод attack объекта monster (см. рис. 8.1).
Этот механизм называется ранним связыванием, так как все ссылки на подпро-
граммы компилятор разрешает до выполнения программы. Ясно, что с помощью
раннего связывания не удастся обеспечить возможность вызова из одной и той же
подпрограммы метода то одного объекта, то другого. Это можно сделать только
в случае, если ссылки будут разрешаться на этапе выполнения программы в момент
вызова метода. Такой механизм в Паскале есть: он называется поздним связыванием
и реализуется с помощью так называемых виртуальных методов. Но перед тем, как
заняться их изучением, надо рассмотреть вопрос о совместимости типов объектов.

Совместимость типов объектов


Паскаль — язык со строгой типизацией. Операнды, участвующие в выражениях,
параметры подпрограмм и их аргументы, левая и правая части оператора присваи-
вания должны подчиняться правилам соответствия типов. Для объектов понятие
совместимости расширено: производный тип совместим со своим родительским ти-
пом. Эта расширенная совместимость типов имеет три формы:
 между экземплярами объектов;
 между указателями на экземпляры объектов;
 между параметрами и аргументами подпрограмм.
Во всех трех случаях совместимость односторонняя: родительскому объекту мо-
жет быть присвоен экземпляр любого из его потомков, но не наоборот. Это связано
с тем, что при присваивании должны быть заполнены все поля, а потомок имеет
либо такой же размер, как предок, либо больший.
Например, если определены переменные
type pmonster = ^monster;
pdaemon = ^daemon;
var m : monster;
d : daemon;
pm : pmonster;
pd : pdaemon;
то приведенные ниже операторы присваивания допустимы:
m := d; pm := pd;

153
154 Часть I. Основы программирования

Поля и методы, введенные в потомке, после таких присваиваний недоступны, по-


тому что объекты базового класса не имеют информации о существовании эле-
ментов, определенных в производном. Например, обращение pm^.wizardry оши-
бочно несмотря на то, что на самом деле указатель pm ссылается на объект типа
daemon.
Даже если метод переопределен в потомке, через указатель на предка вызывается
метод, описанный в предке. Так, в результате выполнения оператора pm^.draw на
экране появится изображение объекта-предка — символ @, потому что тип вызывае-
мого метода соответствует типу указателя, а не типу того объекта, на который он
ссылается.
Если известно, что указатель на предка на самом деле хранит ссылку на потомка,
можно обратиться к элементам, определенным в потомке, с помощью явного преоб-
разования типа, например pdaemon(pm)^.wizardry.

ПРИМЕЧАНИЕ
Здесь у вас может возникнуть вопрос, зачем вообще присваивать объекты разных
типов. Забегая вперед, скажу, что эта возможность используется в основном вместе
с виртуальными методами, которые дают возможность единообразного обращения
к разным объектам одной иерархии.

Если объект является параметром подпрограммы, ему может соответствовать ар-


гумент того же типа или типа любого из его потомков, но есть разница между пере-
дачей объектов по значению и по адресу.
Параметр, передаваемый по значению, представляет собой копию объекта-аргумента,
содержащую только те поля данных и методы, которые имеются в объекте-параметре.
Это значит, что при передаче по значению тип аргумента приводится к типу пара-
метра.
При передаче объекта по адресу подпрограмме передается указатель на фактиче-
ский объект, то есть приведение типов не выполняется.
В Паскале определена функция typeof, которая определяет фактический тип объ-
екта. Ее параметром может быть объектный тип, объект или указатель на объект.
typeof( тип | объект | указатель_на_объект )
Для пояснения разницы в передаче параметров по адресу и по значению рассмо-
трим подпрограмму, использующую эту функцию:
procedure check(m1 : monster; var m2 : monster);
begin
if typeof(m1) = typeof(monster)
then writeln(’это монстр!’)
else writeln(’это не монстр!’);
if typeof(m2) = typeof(monster)
then writeln(’это монстр!’)
else writeln(’это не монстр!’);
end;

154
Глава 8. Иерархии объектов 155

Если в вызывающей программе определены переменные


var m : monster;
d : daemon;
то при вызове check(m, d); получим
это монстр!
это не монстр!
а обращение check(d, m); даст в результате
это монстр!
это монстр!
Как видно из этого примера, в подпрограмме тип объекта, передаваемого по адресу,
может изменяться в зависимости от аргумента.
Если параметр подпрограммы является указателем на объект, передаваемый по
значению, то аргумент может быть указателем как на этот же объект, так и на любо-
го из его потомков. Например, если заголовок процедуры имеет вид
procedure checkp(p1 : pmonster; var p2 : pmonster);
первым параметром в нее можно передавать указатели как на объекты типа monster,
так и на любые производные объекты. На месте второго параметра может быть
только указатель типа pmonster.
Объекты, фактический тип которых может изменяться во время выполнения про-
граммы, называются полиморфными. Полиморфным может быть объект, опреде-
ленный через указатель или переданный в подпрограмму по адресу.
Полиморфные объекты широко применяются в программах, потому что они обе-
спечивают гибкость: например, описав в программе массив указателей на объекты
базового типа и инициализировав его объектами различных производных типов,
можно обрабатывать их в одном цикле, а список, содержащий указатели на объ-
ект базового класса, может на самом деле хранить ссылки на любые объекты ие-
рархии. Подпрограммы, параметрами которых являются полиморфные объекты,
могут без изменений и даже без перекомпиляции использоваться для объектов,
о существовании которых при написании подпрограммы еще ничего не было из-
вестно.
Полиморфные объекты обычно применяются вместе с виртуальными методами,
которые мы рассмотрим в следующем разделе.

Позднее связывание. Виртуальные методы


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

155
156 Часть I. Основы программирования

компилятору, что эти методы будут обрабатываться по-другому. Для этого в Паска-
ле существует директива virtual. Она записывается в заголовке метода, например:
procedure attack; virtual;
Слово virtual в переводе с английского значит «фактический». Объявление метода
виртуальным означает, что все ссылки на этот метод будут разрешаться по факту
его вызова, то есть не на стадии компиляции, а во время выполнения программы.
Этот механизм называется поздним связыванием. Для его реализации необходимо,
чтобы адреса виртуальных методов хранились там, где ими можно будет в любой
момент воспользоваться, поэтому компилятор формирует для этих методов табли-
цу виртуальных методов (VMT — virtual method table). В первое поле этой таблицы
записывается размер объекта, а затем идут адреса виртуальных методов (в том чис-
ле и унаследованных) в порядке описания в объекте. Для каждого объектного типа
создается одна VMT.
Каждый объект во время выполнения программы должен иметь доступ к VMT.
Обеспечение этой связи нельзя поручить компилятору, так как она должна уста-
навливаться позже — при создании объекта во время выполнения программы. По-
этому связь экземпляра объекта с VMT устанавливается с помощью специального
метода, называемого конструктором.
Класс, имеющий хотя бы один виртуальный метод, должен содержать конструк-
тор:
type monster = object
constructor init(x_, y_, health_, ammo_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure hit;
procedure move(x_, y_ : word);
private
x, y : word;
health, ammo : word;
color : word;
end;
daemon = object (monster)
constructor init(x_, y_, health_, ammo_, magic_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure wizardry;
private
magic: word;
end;
По ключевому слову constructor компилятор вставляет в начало метода фрагмент,
который записывает ссылку на VMT в специальное поле объекта (память под это

156
Глава 8. Иерархии объектов 157

поле выделяется компилятором). Следовательно, прежде чем использовать вирту-


альные методы, необходимо вызвать конструктор объекта.
Конструктор обычно используется для инициализации объекта. В нем выполняет-
ся выделение памяти под динамические переменные или структуры, если они есть
в объекте, и присваиваются начальные значения. Если в объекте есть поля, которые
также являются объектами, в конструкторе вызываются конструкторы этих объек-
тов.
Объект может содержать несколько конструкторов. Повторный вызов конструк-
тора вреда программе не наносит, а вот если конструктор вообще не вызвать и по-
пытаться использовать виртуальный метод, поведение программы не определено1.
Если включить режим контроля границ (с помощью директивы {$R+}), в этой ситу-
ации произойдет ошибка времени выполнения. Конструктор должен быть вызван
для каждого создаваемого объекта. Присваивание одного объекта другому возмож-
но только после конструирования обоих.
Вызов виртуального метода выполняется так: из объекта берется адрес его VMT,
из VMT выбирается адрес метода, а затем управление передается этому методу
(рис. 8.2). Таким образом, при использовании виртуальных методов из всех однои-
менных методов иерархии всегда выбирается тот, который соответствует фактиче-
скому типу вызвавшего его объекта.

Рис. 8.2. Позднее связывание

1
Когда в документации встречается термин «поведение программы не определено», это
чаще всего означает очень скверное поведение.

157
158 Часть I. Основы программирования

Поскольку связь с VMT устанавливается в самом начале конструктора, в его теле


также можно пользоваться виртуальными методами.
Правила описания виртуальных методов:
 Если в объекте метод определен как виртуальный, во всех потомках он также
должен быть виртуальным. Отсутствие ключевого слова virtual в заголовке
метода потомка приведет к ошибке.
 Заголовки всех одноименных виртуальных методов должны совпадать (параме-
тры должны иметь одинаковый тип, количество и порядок следования, функции
должны возвращать значение одного и того же типа)1.
 Переопределять виртуальный метод в каждом из потомков не обязательно: если
он выполняет устраивающие потомка действия, он будет унаследован.
 Объект, имеющий хотя бы один виртуальный метод, должен содержать кон-
структор.
Для иллюстрации работы виртуальных методов используем программу со с. 148,
сменив в ней тип элементов массива с monster на daemon (листинг 8.2). Предвари-
тельно в модуле monsters методы attack, draw и erase объявим как виртуальные,
а в процедурах init заменим ключевое слово procedure на слово constructor.

Листинг 8.2. Пример использования виртуальных методов


program game_2;
uses graph, crt, monsters;
const n = 30;
var stado : array [1 .. n] of daemon;
x, y : array [1 .. n] of integer;
gd, gm : integer;
i, j : word;
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;
randomize;
for i := 1 to n do begin
stado[i].init(random(600), random(440), random(10), random(8), random(6));
stado[i].draw;
end;
repeat
for i := 1 to n do begin
x[i] := random(600); y[i] := random(440);
stado[i].move(x[i], y[i]);
end;
for i := 1 to n – 1 do
for j := i + 1 to n do
if (abs(x[i] – x[j]) < 15) and (abs(y[i] – y[j]) < 15)
then begin

1
При переопределении обычных методов никаких ограничений на соответствие типов не
накладывается.

158
Глава 8. Иерархии объектов 159

stado[i].hit; stado[j].hit;
end;
delay(200);
until keypressed;
end.
Единственное изменение, которое пришлось сделать в части исполняемых операто-
ров программы, — добавление еще одного параметра в метод инициализации init.
Запустив программу1, можно наблюдать процесс самоуничтожения демонов, что
свидетельствует о том, что теперь из методов move и hit, унаследованных из базо-
вого класса, вызываются методы attack, draw и erase, определенные в производном
классе.
Виртуальные методы незаменимы и при передаче объектов в подпрограммы. В за-
головке подпрограммы описывается либо объект базового типа, передаваемый по
адресу, либо указатель на этот объект, а при вызове в нее передается объект или ука-
затель производного класса. В этом случае виртуальные методы, вызываемые для
объекта из подпрограммы, будут соответствовать типу аргумента, а не параметра.
Рассмотрим пример: функцию, которая передвигает объект по экрану по нажатию
клавиш управления курсором (листинг 8.3).

Листинг 8.3. Функция перетаскивания объекта по экрану


function drag(var m : monster) : boolean;
var key : char;
dx, dy : integer;
step : integer;
begin
step := 5;
key := readkey;
if key = chr(0) then key := readkey;
dx := 0; dy := 0;
case ord(key) of
72 : dy := –step;
75 : dx := –step;
77 : dx := step;
80 : dy := step;
27 : begin drag := false; exit end;
end;
m.move(m.x + dx, m.y + dy);
drag := true
end;
На место параметра этой функции можно передавать не только объекты типа
monster, но и любых его потомков, потому что из метода перемещения будут вы-
званы методы отрисовки и стирания того объекта, экземпляр которого был передан
в функцию drag в качестве аргумента:

1
Предварительно рекомендуется отключить вывод сообщения в методе attack.

159
160 Часть I. Основы программирования

program dragging;
uses graph, crt, monsters;
var Vasia : monster;
Misha : daemon;
x, y : integer;
gd, gm : integer;
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;
Vasia.init(200, 200, 10, 8); Vasia.draw;
while drag(Vasia) do;
Misha.init(400, 400, 10, 8 ,2); Misha.draw;
while drag(Misha) do;
end.

ПРИМЕЧАНИЕ
Здесь имеется в виду, что drag помещена в модуль monsters как отдельная функция.
Если же рассматривать передвижение объекта по нажатию клавиш управления кур-
сором как свойство объекта, можно описать эту функцию как метод объекта. В этом
случае параметры ему не потребуются.

При описании классов рекомендуется определять как виртуальные те методы,


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

160
Глава 9. Объекты в динамической памяти
Для хранения объектов в программах чаще всего используется динамическая па-
мять, поскольку это обеспечивает гибкость программы и эффективное использо-
вание памяти. Благодаря расширенной совместимости типов можно описать ука-
затель на базовый класс и хранить в нем ссылку на любой его объект-потомок, что
в сочетании с виртуальными методами позволяет единообразно работать с различ-
ными классами иерархии. Из объектов или указателей на объекты создают различ-
ные динамические структуры.

Динамические объекты. Деструкторы


Для выделения памяти под объекты используются процедура и функция new. На-
пример, если определены указатели
type pmonster = ^monster;
pdaemon = ^daemon;
var pm : pmonster;
pd : pdaemon;
можно создать объекты с помощью вызовов
new(pm); { или pm := new(pmonster); }
new(pd); { или pd := new(pdaemon); }
При использовании new в форме процедуры параметром является указатель, а в функ-
цию передается его тип. Так как после выделения памяти объект обычно инициа-
лизируют, для удобства определены расширенные формы new с двумя параметрами.
На месте второго параметра задается вызов конструктора объекта:
new(pm, init(1, 1, 1, 1); { или pm := new(pmonster, init(1, 1, 1, 1)); }
new(pd, init(1, 1, 1, 1, 1); { или pd := new(pdaemon, init(1, 1, 1, 1, 1)); }
Обращение к методам динамического объекта выполняется по обычным правилам
Паскаля, например:
pm^.draw; pm^.attack;
С объектами в динамической памяти часто работают через указатели на базовый
класс, то есть описывают указатель базового класса, а инициализируют его, создав
объект производного класса, например:
pm := new(pdaemon, init(1, 1, 1, 1, 1));

161
162 Часть I. Основы программирования

Как уже говорилось, такие объекты называют полиморфными. Они используются


для того, чтобы можно было единообразно работать в программе с объектами раз-
ных классов. Например, оператор pm^.draw будет автоматически вызывать разные
методы в зависимости от того, на объект какого типа в данный момент ссылается
указатель pm1. Определить, на объект какого типа в данный момент ссылается ука-
затель, можно с помощью функции typeof (см. с. 154).
Для освобождения памяти, занятой объектом, применяется процедура Dispose:
Dispose(pm);
При выполнении этой процедуры освобождается количество байтов, равное разме-
ру объекта, соответствующего типу указателя. Следовательно, если на самом деле
в указателе хранится ссылка на объект производного класса, который, как извест-
но, может быть больше своего предка, часть памяти не будет помечена как свобод-
ная, но доступ к ней будет невозможен, то есть появится мусор (рис. 9.1). Второй
случай появления мусора возникает при применении процедуры Dispose к объекту,
поля которого являются указателями (рис. 9.2). Объект, содержащий динамиче-
ские поля, мы рассмотрим на с. 165.

Рис. 9.1. Неверное удаление полиморфного объекта

Рис. 9.2. Неверное удаление объекта с динамическими полями

1
Это верно только для виртуальных методов.

162
Глава 9. Объекты в динамической памяти 163

Для корректного освобождения памяти из-под полиморфных объектов следует ис-


пользовать вместе с процедурой Dispose специальный метод — деструктор. В доку-
ментации по Borland Pascal ему рекомендуется давать имя done, например:
destructor monster.done;
begin
end;
Для правильного освобождения памяти деструктор записывается вторым параме-
тром процедуры Dispose.
Dispose(pm, done);
Для простых объектов деструктор может быть пустым, а для объектов, содержа-
щих динамические поля, в нем записываются операторы освобождения памяти для
этих полей. В деструкторе можно описывать любые действия, необходимые для
конкретного объекта, например закрытие файлов. Исполняемый код деструктора
никогда не бывает пустым, потому что компилятор по служебному слову destructor
вставляет в конец тела метода операторы получения размера объекта из VMT. Де-
структор передает этот размер процедуре Dispose, и она освобождает количество
памяти, соответствующее фактическому типу объекта.

ВНИМАНИЕ
Вызов деструктора вне процедуры Dispose память из-под объекта не освобождает.

Деструкторы рекомендуется делать виртуальными, для того чтобы при вызове


всегда выполнялся деструктор, соответствующий типу объекта. Деструкторы обя-
зательно использовать только для динамических полиморфных объектов, однако
можно их применять и для статических объектов. В объекте можно определить не-
сколько деструкторов (естественно, в этом случае они должны иметь разные имена).
Вариант кода модуля monsters, содержащий конструкторы и деструкторы (он ис-
пользуется в следующем разделе), приведен в листинге 9.1.

Листинг 9.1. Модуль monsters, использующий конструкторы и деструкторы


unit monsters;
interface
uses Graph;
type pmonster = ^monster;
monster = object
constructor init(x_, y_, health_, ammo_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure hit;
procedure move(x_, y_ : word);
destructor done;
private
x, y : word;
продолжение 

163
164 Часть I. Основы программирования

Листинг 9.1 (продолжение)


health, ammo : word;
color : word;
end;
pdaemon = ^daemon;
daemon = object (monster)
constructor init(x_, y_, health_, ammo_, magic_ : word);
procedure attack; virtual;
procedure draw; virtual;
procedure erase; virtual;
procedure wizardry;
private
magic : word;
end;
implementation
{ ------------------- реализация методов объекта monster ---------------------- }
constructor monster.init(x_, y_, health_, ammo_ : word);
begin
x := x_; y := y_;
health := health_;
ammo := ammo_;
color := yellow;
end;
procedure monster.attack; { -------------------------------- monster.attack --- }
begin
if ammo = 0 then exit;
dec(ammo); setcolor(color); outtextXY(x + 15, y, 'ба-бах!');
end;
procedure monster.draw; { -------------------------------- monster.draw ----- }
begin
setcolor(color); outtextXY(x, y, '@');
end;
procedure monster.erase; { ---------------------------------- monster.erase --- }
begin
setcolor(black); outtextXY(x, y, '@');
end;
procedure monster.hit; { ---------------------------------- monster.hit ----- }
begin
if health = 0 then exit;
dec(health);
if health = 0 then begin color := red; draw; exit; end;
attack;
end;
procedure monster.move(x_, y_ : word); { --------------------- monster.move --- }
begin
if health = 0 then exit;
erase;
x := x_; y := y_;
draw;
end;

164
Глава 9. Объекты в динамической памяти 165

destructor monster.done; { ----------------------------------- monster.done --- }


begin
end;
{ ----------------------- реализация методов объекта daemon ------------------- }
constructor daemon.init(x_, y_, health_, ammo_, magic_ : word);
begin
inherited init(x_, y_, health_, ammo_);
color := green;
magic := magic_;
end;
procedure daemon.draw; { ----------------------------------- daemon.draw ---- }
begin
setcolor(color); outtextXY(x, y, '%)');
end;
procedure daemon.erase; { ----------------------------------- daemon.erase --- }
begin
setcolor(black); outtextXY(x, y, '%)');
end;
procedure daemon.attack; { ---------------------------------- daemon.attack --- }
begin
if ammo = 0 then exit;
dec(ammo);
if magic > 0 then begin
outtextXY(x + 15, y, 'БУ-БУХ!'); dec(magic); end
else outtextXY(x + 15, y, 'бу-бух!');
end;
procedure daemon.wizardry; { -------------------------------- daemon.wizardry - }
begin
if magic = 0 then exit;
outtextXY(x + 15, y, 'крибле-крабле-бумс!'); dec(magic);
end;
end.
Для использования этого модуля не обязательно иметь в распоряжении его полный
код — достаточно интерфейсного раздела (и, конечно, файла .tpu). В программе, ис-
пользующей этот модуль, можно описывать производные классы, в которых добав-
лены новые поля и методы и переопределены имеющиеся. При этом новые объек-
ты будут перемещаться с помощью метода, который был написан до их появления!

Организация объектов во время проектирования


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

165
166 Часть I. Основы программирования

разному, объявляется в абстрактном классе как виртуальный и имеет пустое тело.


Таким образом, абстрактный класс определяет интерфейс для всей иерархии.
Абстрактные классы и указатели на них используются в качестве параметров под-
программ и при работе со структурами данных, предназначенных для хранения
объектов одной иерархии.
Альтернативой наследованию при проектировании объектов служит вложение, ког-
да один объект включает в себя поля, являющиеся объектами или указателями на
объекты. Например, если есть объект «двигатель», а требуется описать объект «са-
молет», логично сделать двигатель полем этого объекта, а не его предком.
Вид вложения, когда в классе описано поле объектного типа, называют композици-
ей. Если в классе описан указатель на поле объектного типа, это обычно называют
агрегацией. При композиции время жизни всех объектов — и объемлющего, и его
полей — одинаково. Агрегация представляет собой более слабую связь между объ-
ектами, потому что объекты, на которые ссылаются поля-указатели, могут появ-
ляться и исчезать в течение жизни содержащего их объекта, кроме того, один и тот
же указатель может ссылаться на объекты разных классов в пределах одной иерар-
хии.
Поле-указатель может также ссылаться не на один объект, а на неопределенное ко-
личество объектов, например, быть указателем на начало линейного списка. Если
объект предназначается для хранения других объектов, он называется контейне-
ром. Объекты в контейнере могут храниться в виде массива, списка, стека или дру-
гой динамической структуры. Методы контейнера обычно включают его создание,
дополнение, просмотр, а также поиск и удаление элементов.
В качестве примера контейнера рассмотрим объект list, предназначенный для ра-
боты со связным списком объектов класса monster и его потомков:
type list = object
constructor init;
procedure add(pm : pmonster);
procedure draw;
destructor done;
private
beg : pnode;
end;
В объекте одно поле beg — указатель на начало списка элементов типа node:
type pnode = ^node;
node = record
pm : pmonster; { указатель на объект pmonster }
next : pnode; { указатель на следующий элемент списка }
end;
Структура объекта list поясняется на рис. 9.3. Конструктор объекта инициализи-
рует нулем указатель на начало списка, метод add добавляет элемент в начало спи-
ска, процедура draw предназначена для отображения на экране всех объектов, на-
ходящихся в списке, деструктор done — для освобождения памяти из-под объекта.

166
Глава 9. Объекты в динамической памяти 167

Рис. 9.3. Список полиморфных объектов

В программе (листинг 9.2) создается список из n объектов, тип которых выбирается


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

Листинг 9.2. Программа, работающая со списком полиморфных объектов


program demo_list;
uses graph, crt, monsters;
const n = 30;
type pnode = ^node;
node = record
pm : pmonster;
next : pnode;
end;
list = object
constructor init;
procedure add(pm : pmonster);
procedure draw;
destructor done;
продолжение 

167
168 Часть I. Основы программирования

Листинг 9.2 (продолжение)


private
beg : pnode; end;
constructor list.init; { --------------------------------------- list.init ---- }
begin beg := nil end;
procedure list.add(pm : pmonster); { --------------------------- list.add ----- }
var p : pnode;
begin
new(p);
p^.pm := pm;
p^.next := beg;
beg := p;
end;
procedure list.draw; { --------------------------------------- list.draw ---- }
var p : pnode;
begin
p := beg;
while p <> nil do begin
p^.pm^.draw;
p := p^.next;
end;
end;
destructor list.done; { --------------------------------------- list.done ---- }
var p : pnode;
begin
while beg <> nil do begin
p := beg;
dispose(p^.pm, done); { 1 }
beg := p^.next; { 2 }
dispose(p); { 3 }
end
end;
procedure report(message: string); { --------------------------- report ------- }
var s : string;
begin
str(MemAvail, s);
outtext(message + s);
moveto(0, GetY + 12);
end;
var stado : list;
x, y : integer;
gd, gm : integer;
p : pmonster;
i : word;
{ ---------------------------------- главная программа ------------------------ }
begin
gd := detect; initgraph(gd, gm, '...');
if graphresult <> grOk then begin
writeln('ошибка инициализации графики'); exit end;

168
Глава 9. Объекты в динамической памяти 169

randomize;
report(' доступно в начале программы: ');
stado.init;
for i := 1 to n do begin
case random(2) of
0 : p := new(pmonster, init(random(600), random(440), 10, 8));
1 : p := new(pdaemon, init(random(600), random(440), 10, 8, 6));
end;
stado.add(p); { добавление объекта в список }
end;
report(' доступно после выделения памяти: ');
stado.draw; { отрисовка объектов }
stado.done; { уничтожение объектов }
report(' доступно после освобождения памяти: ');
readln;
end.
Рассмотрим подробнее деструктор объекта list. Уничтожение объекта состоит
в освобождении памяти из-под двух типов структур: полиморфных графических
объектов в списке и записей типа node, которые служат для связи элементов списка.
Список уничтожается поэлементно, начиная с первого элемента. Для этого исполь-
зуются два указателя: указатель beg на начало списка из объекта list и вспомога-
тельный указатель p.
Вызов dispose в операторе 1 освобождает память из-под первого объекта типа
monster или daemon; затем указатель beg продвигается к следующей записи node с по-
мощью оператора 2. После этого освобождается память из-под самой записи node
(оператор 3), и процесс повторяется, пока весь список не будет уничтожен. Важно
отметить способ, которым освобождаются объекты monster:
dispose(p^.pm, done);
Здесь p^.pm является указателем на объект monster, метод done — деструктором этого
объекта. Фактически p^.pm не обязательно указывает на объект типа monster — это
может быть любой его потомок. Уничтожаемый объект является полиморфным,
и во время компиляции его фактический размер и тип неизвестны. Деструктор done
ищет размер экземпляра в VMT объекта и передает этот размер процедуре dispose,
которая освобождает именно такое количество байтов в динамической памяти, ка-
кое фактически занимал объект.

169
.

170
Часть II. Практикум

В этой части книги на примерах подробно разбираются ос-


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

171
Семинар 1. Линейные программы
Теоретический материал: глава 1.

На этом семинаре вы освоите работу в среде Turbo Pascal 7.0 или аналогичных средах
и научитесь писать линейные программы. Чтобы осмысленно написать даже самую
простую программу, надо представлять себе, из каких элементов она состоит, по ка-
ким правилам описываются константы, переменные и выражения, как выполняется
ввод с клавиатуры и вывод на экран — в общем, все, о чем написано в первой главе.
Чтобы заставить программу работать, необходимо освоить основные приемы ис-
пользования оболочки Turbo Pascal 7.0 (приложение 7) или аналогичных. Для на-
чала рассмотрим программу, выполняющую расчет по простейшей формуле.

Задача С1.1. Валютные операции


Написать программу перевода суммы в рублях в американские доллары и евро.

Начинать создание любой программы надо с определения ее исходных данных и ре-


зультатов. При этом задумываются не только о смысле величин, но и о том, какой
диапазон значений они могут принимать. В данном случае сомнений нет: вводить
надо величину в рублях, которая может быть только положительной и верхний
предел которой не ограничен1; в результате мы должны получить два положитель-
ных вещественных числа с точностью два знака после запятой.
Следующим шагом является описание алгоритма решения задачи. Для этого суще-
ствуют разные способы. Часто для описания алгоритмов используются так называ-
емые схемы алгоритмов (блок-схемы, структурные схемы), в которых для каждого
вида действия применяется своя геометрическая фигура, а порядок вычислений за-
дается стрелками между этими фигурами. Мы будем применять схемы алгоритмов
на следующих семинарах.
Алгоритм решения данной задачи очевиден, и его проще всего описать в словесной
форме: ввести величину, разделить ее на курсы доллара и евро и вывести результа-
ты. Теперь можно перейти к написанию программы на Паскале (листинг С1.1).

Листинг С1.1. Перевод суммы в рублях в доллары и евро


program valuta; { 1 }
const kurs_dollar = 29.8;

1
По оптимистической оценке.

172
Семинар 1. Линейные программы 173

kurs_euro = 43.3; { 2 }
var rouble, dollar, euro : real; { 3 }
begin
writeln('Введите сумму в рублях: '); { 4 }
readln(rouble); { 5 }
dollar := rouble / kurs_dollar; { 6 }
euro := rouble / kurs_euro; { 7 }
writeln('Рублей: ', rouble:8:2, ' Долларов: ', dollar:7:2,
' Евро: ', euro:7:2); { 8 }
readln;
end.
Рассмотрите каждую строку программы, найдите в первой главе сведения о поняти-
ях, выделенных далее курсивом. Вместо комментариев в этой программе записаны
просто цифры для того, чтобы ссылаться на соответствующие строки программы.
В первой строке обычно пишут заголовок программы.
Разделы описаний представлены строками 2 и 3. Здесь два раздела: описания кон-
стант и описания переменных. Все переменные, используемые в программе, долж-
ны быть предварительно описаны, чтобы компилятор знал, сколько под них выде-
лить места в памяти и что с ними можно делать. Внутреннее представление одного
и того же целого и вещественного числа различно: для вещественных чисел (тип
real) в памяти хранится мантисса и порядок, а целые (тип integer) представляются
просто в двоичной форме. Более того, для действий с целыми и вещественными
величинами формируются различные наборы машинных команд. Поэтому-то опи-
сание типа каждой переменной является обязательным.
Тип переменных выбирается исходя из возможного диапазона значений и требуе-
мой точности представления данных. Имена переменным дают исходя из их назна-
чения. От того, насколько удачно подобраны имена, зависит читабельность про-
граммы — одна из ее важнейших характеристик.
Далее между ключевыми словами begin и end располагается раздел операторов.
В строке 4 записано так называемое приглашение. Оно применяется для того, что-
бы пользователь программы (сейчас это вы) знал, в какой момент следует вводить
данные с клавиатуры. Стандартная процедура writeln выводит на экран записан-
ную в ней строку символов, и курсор переходит на следующую строку.
ПРИМЕЧАНИЕ
Процедура — это последовательность операторов, к которой можно обратиться по име-
ни. Термин «стандартная процедура» означает, что процедура с таким именем включена
в состав библиотек, поставляемых вместе с компилятором Паскаля. К процедуре можно
обратиться, задав ее имя, а затем в скобках — так называемые параметры. Некоторые
процедуры параметров не имеют.

Ввод суммы выполняется в строке 5 с помощью стандартной процедуры readln.


Наконец, в строках 6 и 7 находится то, ради чего написана эта программа, — вы-
числения. Справа от знака операции присваивания (:=) записано выражение, сле-
ва — имя переменной. Вся эта конструкция называется оператором присваивания.
Выражение записывается и вычисляется по правилам Паскаля.

173
174 Часть II. Практикум

В строке 8 выполняется вывод результатов вычислений с помощью процедуры


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

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

Аналогично выводятся и результаты вычислений. При выполнении программы


строки символов, заключенные в апострофы, выводятся на экран без изменений,
а вместо имен переменных подставляются их значения, отформатированные в со-
ответствии с нашими указаниями:
Введите сумму в рублях:
567.2
Рублей: 567.20 Долларов: 19.03 Евро: 15.50
Символы, вводимые с клавиатуры, выделены курсивом. Обратите внимание, что
пробелы, расположенные внутри апострофов, выводятся на экран наряду с осталь-
ными символами, а те пробелы и символы перевода строки, которыми разделяются
параметры процедуры writeln, на формат вывода влияния не оказывают. Они пред-
назначены исключительно для удобства восприятия.

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

Последним оператором записан вызов процедуры ввода readln, но не указано, что


вводить. Этот вызов не обязателен и используется чтобы было удобнее наблюдать
результаты работы программы. Процедура readln без параметров просто ожида-
ет нажатия клавиши Enter. Налюбовавшись на результаты, надо нажать клавишу
Enter, и выполнение программы завершится.
Давайте теперь разберемся, что вам потребуется сделать для того, чтобы получить
такие же результаты на своем компьютере, и каким образом они получаются.

174
Семинар 1. Линейные программы 175

Работа в интегрированной среде


Компьютер не знает ни русского языка, ни языка Паскаль. Он понимает только
инструкции процессора, или машинные коды, которые задают весьма примитив-
ные действия, например, сложение двух величин или переход к заданной ячейке
памяти. Переводом с Паскаля в машинные коды занимается компилятор. Каждый
оператор языка переводится в последовательность машинных команд. Ясно, что
успешно перевести в машинные команды можно только программу, которая напи-
сана без ошибок в строгом соответствии с правилами языка (его синтаксисом).
Языки программирования имеют гораздо более строгие, но и более простые пра-
вила, чем естественные языки. Эти правила не допускают двояких толкований.
Например, если последовательность символов начинается с латинской буквы, это
имя, а если с цифры — число. Когда компилятор не может опознать какую-либо
конструкцию как допустимую, он выдает сообщение об ошибке.
Практически любая программа содержит обращения к стандартным процедурам.
Даже в самой простой программе (листинг С1.1) использовались процедуры ввода
с клавиатуры и вывода на экран. Стандартные процедуры хранятся в специальных
файлах в виде машинных кодов. Компилятор извлекает их оттуда и подключает
к программе, после чего загружает готовую программу в оперативную память ком-
пьютера, и она начинает выполняться. В этот-то момент мы и видим приглашение
к вводу. Весь процесс превращения текста на Паскале в исполняемую программу
представлен на рис. С1.1.

Рис. С1.1. Процесс компиляции программы

Если мы хотим написать на компьютере поэму, шпаргалку или жалобу, мы пользу-


емся текстовым редактором (например, MS Word); для создания письма лучше по-
дойдет почтовый редактор, а для ввода текста программы удобнее всего применять
редактор программ. После написания программу приходится отлаживать1, то есть
искать и устранять ошибки в алгоритме. Для этого используется специальная про-
грамма, называемая отладчиком.
Итак, для создания программы требуются как минимум редактор, компилятор, от-
ладчик и библиотека стандартных процедур. Все это, а также многое другое, со-
держится в так называемой интегрированной среде разработки (по-английски
Integrated Development Environment, IDE), иначе называемой оболочкой.
1
Иными словами, избавлять от «лажи».

175
176 Часть II. Практикум

Паскаль повсеместно используется для обучения не в последнюю очередь потому,


что обладает простой и удобной средой. Ее описание можно найти в приложении 7,
а здесь приведены минимальные сведения, необходимые для начала работы.
Среда запускается с помощью файла turbo.exe из каталога BIN, расположенного
внутри каталога, в котором установлена среда Turbo Pascal 7.0. Управление оболоч-
кой осуществляется с помощью меню. Войти в меню можно с помощью мыши или
функциональной клавиши F10.
Команда FileNew открывает новое окно для ввода текста программы. При вводе
и редактировании текста используются следующие клавиши:
 перемещение по тексту — ↑, ↓, ←, →, Home, End, PageUp, PageDown;
 переход на новую строку — Enter;
 стирание предыдущего символа — BackSpace;
 выделение фрагмента текста — клавиши ↑, ↓, ←, → при нажатой клавише Shift;
 копирование выделенного фрагмента в буфер — Ctrl+Insert;
 вставка фрагмента из буфера в то место, где находится курсор, — Shift+Insert.
Для сохранения программы на диске проще всего пользоваться клавишей F2. Если
программа сохраняется впервые, надо выбрать каталог и задать имя файла. Среда
присвоит ему расширение .pas.

СОВЕТ
Перед каждым запуском программы сохраняйте ее на диске — ведь если она «повиснет»,
все ваши усилия по набору текста окажутся напрасными.

Компиляция и запуск программы выполняются нажатием клавиш Ctrl+F9, просмотр


результатов работы программы — нажатием клавиш Alt+F5, выход из режима про-
смотра — нажатием клавиши Enter, выход из среды — нажатием клавиш Alt+X.
Если файл с программой существует, удобно войти в среду двойным щелчком мы-
шью на имени файла в оболочке типа FAR (если они правильно настроены).
Важной частью среды является ее справочная система. У нее есть маленький не-
достаток — она англоязычная. Зато она всегда под рукой: установив курсор на слу-
жебное слово или имя стандартной подпрограммы и нажав клавиши Ctrl+F1, вы
незамедлительно получите о них исчерпывающую справку. Для получения инфор-
мации о языковых средствах нужно приучать себя пользоваться не книгами (в том
числе и этой), а справочной системой оболочки, потому что только она обладает
наиболее полной и достоверной информацией, соответствующей используемой
вами версии программного продукта. Кроме того, освоение одной системы значи-
тельно облегчит изучение других, более новых систем, книг по которым может не
быть вообще.
Вот и все, что требуется знать для начала работы. Запустите среду, откройте новое
окно, введите в него текст программы, сохраните ее и запустите на выполнение.
Прописные и строчные буквы в Паскале не различаются, однако принято ключе-
вые слова записывать строчными буквами.

176
Семинар 1. Линейные программы 177

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

ВНИМАНИЕ
В соответствии с правилами Паскаля в вещественных числах дробная часть отделяется
не запятой, а точкой!

Если же после нажатия клавиш Ctrl+F9 вы наблюдаете в верхней строке окна ан-
глийский текст на красном фоне — это сообщение о синтаксической ошибке. Из-
учите следующий раздел, а затем сличите текст программы на экране с текстом
в книге и исправьте найденные неточности.

Ошибки компиляции
Начинающие, да и не только они, часто делают ошибки при вводе программы. Ком-
пилятор устанавливает курсор на предположительное место ошибки в тексте про-
граммы, выдает свою версию о причине ошибки и прекращает работу. Полный
список сообщений об ошибках приведен в приложении 4, а здесь рассмотрим не-
сколько примеров.
Отсутствие точки с запятой между операторами. Компилятор устанавливает
курсор на оператор, перед которым пропущена точка с запятой, и выдает сообще-
ние о том, что он ожидал увидеть на этом месте:
Error 85: ";" expected.
Отсутствие закрывающего апострофа в строковой константе. Поскольку компи-
лятор не может найти, где заканчивается строковая константа, выдается сообщение
о том, что она превышает допустимую длину:
Error 8: String constant exceeds line.
Отсутствие открывающего апострофа в строковой константе приводит либо
к выдаче предыдущего сообщения, либо к сообщению о синтаксической ошибке:
Error 5: Syntax error.
Не описана переменная. Эта ошибка выдается и в том случае, если вы ошиблись
в написании имени или ключевого слова, например пропустили букву:
Error 3: Unknown identifier.
Несоответствие количества открывающих и закрывающих скобок. Если отсутству-
ет закрывающая скобка, выдается такое сообщение:
Error 89: ")" expected.
Неверное написание ключевых слов и имен стандартных процедур. В зависимости от
места ошибки могут быть выданы различные сообщения, например:
Error 36: BEGIN expected.
Error 3: Unknown identifier.

177
178 Часть II. Практикум

Компилятор не всегда верно предполагает причину ошибки. Например, если между


параметрами процедуры пропущена запятая, он выдаст сообщение о том, что в этом
месте ожидалась закрывающая скобка (ошибка 89). Чтобы в этом убедиться, убе-
рем запятую после первого параметра в операторе 8 листинга С1.1:
writeln('Рублей: ' rouble:8:2, ' Долларов: ', dollar:7:2, Евро: ', euro:7:2);
После исправления каждой ошибки программу пытаются скомпилировать повтор-
но до тех пор, пока компилятор не будет удовлетворен.
Наша программа настолько проста, что после исправления синтаксических ошибок
ее отладку можно считать завершенной, но в реальных программах в этот момент
отладка только начинается. Методы отладки мы будем неоднократно обсуждать на
следующих семинарах, а пока рассмотрим еще одну простую задачу.

Задача С1.2. Временной интервал


Заданы моменты начала и конца некоторого промежутка времени в часах, минутах
и секундах (в пределах одних суток). Найти продолжительность этого промежут-
ка в тех же единицах.

Исходными данными для этой задачи являются шесть целых величин, задающих
моменты начала и конца интервала, результатами — три целых величины.
Вы уже знаете, что тип переменной выбирается исходя из диапазона и требуемой
точности представления данных, а имя дается в соответствии с ее содержимым.
Нам потребуется хранить исходные данные, представляющие собой положитель-
ные числа, не превышающие 60, поэтому можно ограничиться типом byte.
Назовем переменные для хранения начала интервала hour1, min1 и sec1, для хране-
ния конца интервала — hour2, min2 и sec2, а результирующие величины — hour, min
и sec.
Для решения задачи необходимо преобразовать оба момента времени в секунды,
вычесть первый из второго, а затем преобразовать результат обратно в часы, ми-
нуты и секунды. Следовательно, нам потребуется промежуточная переменная для
хранения интервала в секундах. Она может иметь весьма большие значения, ведь
в сутках 86 400 секунд. В величинах типа byte могут храниться значения, не превы-
шающие 255, поэтому здесь использовать его нельзя. Для этой переменной следует
выбрать длинный целый тип (longint), поскольку «обычный» целый тип integer
может хранить только значения из диапазона –32 768...32 767.
Текст программы приведен в листинге С1.2.

Листинг С1.2. Временной интервал


program Interval;
var hour1, min1, sec1, hour2, min2, sec2, hour, min, sec : byte;
sum_sec : longint;
begin
writeln(' Введите время начала интервала (час мин сек)');
readln(hour1, min1, sec1);
writeln(' Введите время конца интервала (час мин сек)');

178
Семинар 1. Линейные программы 179

readln(hour2, min2, sec2);


sum_sec := (hour2 - hour1) * 3600 + (min2 - min1) * 60 + sec2 - sec1;
hour := sum_sec div 3600;
min := (sum_sec - hour * 3600) div 60;
sec := sum_sec - hour * 3600 - min * 60;
writeln(' Продолжительность промежутка от ',
hour1:2, ':', min1:2, ':', sec1:2, ' до ',
hour2:2, ':', min2:2, ':', sec2:2, ' равна ',
hour:2, ':', min:2, ':', sec:2);
readln;
end.
Обратите внимание на операцию целочисленного деления div. Ее операнды долж-
ны быть целыми, и результат получается тоже целый (дробная часть просто отбра-
сывается).
Проверьте (протестируйте) программу на различных наборах исходных данных.
Тест — это не только данные, которые вы вводите в программу, но и ее заранее про-
считанная реакция. В корне неверно, увидев долгожданное приглашение к вводу,
беспорядочно стучать по клавишам, вводя первые попавшиеся цифры! Вводите
такие числа, которые позволили бы легко проверить в уме правильность работы
программы. Можно воспользоваться и калькулятором.
Главное — всегда, даже в самых простых случаях, проверять все, что выдает ваша
программа. Ведь из того, что она выполняется, совсем не следует то, что она рабо-
тает правильно!

ВНИМАНИЕ
Данные при вводе разделяются пробелами, символами перевода строки или табуляции
(но не запятыми!).

Попробуйте при вводе поменять моменты начала и конца отсчета интервала1. Про-
грамма выдаст странные трехзначные числа. Это происходит потому, что отрица-
тельные величины, получившиеся в результате вычисления выражения, были при-
своены переменным, которые могут быть только положительными.
Для внутреннего представления отрицательных целых чисел используется так на-
зываемый дополнительный код. Любое отрицательное число имеет двоичную еди-
ницу в старшем разряде. Например, внутреннее представление числа –1 состоит
из двоичных единиц во всех разрядах. В типах данных, состоящих только из поло-
жительных значений, старший (в данном случае восьмой) разряд воспринимается
точно так же, как и все остальные. Вот почему при интерпретации отрицательного
числа как положительного получаются числа, бˆольшие или равные 128 (27).
Выходов из этой ситуации, как всегда, два: либо проверять допустимость вво-
димых значений, что является темой следующего семинара, либо использовать
для представления величин тип данных, в множество значений которого входят

1
Только не вводите вместо чисел буквы или другие символы — компилятор будет вас ругать,
часто употребляя при этом слово «invalid».

179
180 Часть II. Практикум

и отрицательные числа. Мы пойдем по второму пути. Замените тип byte типом


shortint и протестируйте программу еще раз.

Задача С1.3. Расчет по формуле


Написать программу расчета по заданной формуле:

π ⋅ x − e0,2 α
+ 2 tg 2α + 1, 6 ⋅ 103 ⋅ log10 x 2 .
y=
2 tg 2α ⋅ sec x

Из формулы видно, что исходными данными для этой программы являются две
величины — x и γ. Поскольку их тип и точность представления в условии не ого-
ворены, выберем для них вещественный тип (real). Все, что от нас требуется для
решения этой задачи, — правильно записать формулу на языке Паскаль.
Для работы с вещественными величинами в Паскале (см. табл. 1.10) существуют
стандартные функции получения числа π (Pi), вычисления квадрата (sqr), квадрат-
ного корня (sqrt) и экспоненты (exp), однако нет ни тангенса, ни секанса, ни деся-
тичного логарифма. Ничего не поделаешь — придется выразить эти функции через
имеющиеся в библиотеке:

sin α 1 ln α .
tg α = ; sec α = ; log10 α =
cos α cos α ln10
Второе, на что стоит обратить внимание, — повторяющееся два раза выражение 2tg2α.
Для упрощения записи формулы и оптимизации вычислений полезно вычислить
его заранее и запомнить результат в промежуточной переменной (листинг С1.3).

Листинг С1.3. Расчет по формуле


program formula;
var a, x, y : real; { исходные данные и результат }
temp : real; { промежуточная переменная }
begin
writeln('Введите а и х:');
readln(a, x);
writeln('Исходные данные:'); { контрольный вывод исходных данных }
writeln('a = ', a:6:2, ' x = ', x:6:2);
temp := 2 * sin(2 * a) / cos(2 * a);
y := (sqrt(Pi * x) - exp(0.2 * sqrt(a)) + temp +
1.6e3 * ln(sqr(x)) / ln(10)) / (temp * 1 / cos(x));
writeln('Результат: y = ', y:6:2);
readln;
end.
Обратите внимание на следующие моменты.
 Аргумент функции всегда заключается в круглые скобки. Если аргументом слу-
жит результат вычисления другой функции, они вкладываются друг в друга, как
матрешки. Функция Pi не имеет аргументов, поэтому для ее вызова достаточно
указать ее имя.

180
Семинар 1. Линейные программы 181

 Запись 1.6e3 представляет собой вещественную константу с порядком. Она за-


писывается без пробелов, и никаких действий по умножению мантиссы на 103 во
время выполнения программы не производится. Константы с порядком обычно
используются для представления очень больших и очень малых величин. В дан-
ном случае можно было использовать обычную целую константу 1600.
 В отличие от математической записи, в выражениях нельзя опускать знак умно-
жения.
Основная проблема начинающих — непонимание порядка вычисления выражения.
Все операции выполняются в соответствии с приоритетами (освежите свои знания
о приоритетах операций по главе 1), а если операции имеют одинаковый приори-
тет, они выполняются слева направо.
Самый высокий приоритет имеют функции, затем идут умножение и деление, еще
ниже — сложение и вычитание. Изменить порядок вычислений можно с помощью
круглых скобок. Количество открывающих скобок в выражении должно быть рав-
но количеству закрывающих.
Для примера рассмотрим выражение 2 / x × y. Деление и умножение имеют один
и тот же приоритет, поэтому сначала 2 делится на x, а затем результат этих вычисле-
ний умножается на y. Иными словами, это выражение эквивалентно формуле

2
× y.
x
Если же мы хотим, чтобы выражение x × y было в знаменателе, следует заключить
его в круглые скобки или сначала поделить числитель на х, а потом на y, то есть за-
писать как 2 / (x × y) или 2 / x / y.
В приведенной ранее программе и числитель и знаменатель должны заключаться
в скобки. Можно записать эту формулу и другим способом — более лаконичным,
но менее очевидным:
y := (sqrt(Pi * x) - exp(0.2 * sqrt(a)) + temp +
1600 * ln(sqr(x)) / ln(10)) / temp * cos(x);
Круглые скобки в выражениях можно использовать и без необходимости, просто
для визуальной группировки частей сложного выражения.

Ошибки времени выполнения


Программа, которую мы написали, весьма ненадежна. Например, при вводе зна-
чения α = 0 выдается ошибка времени выполнения (run-time error) «деление на
ноль»:
Error 200: Division by zero.
При вводе отрицательных значений x или γ программа также завершается аварийно
в результате попытки извлечь квадратный корень из отрицательной величины. При
этом выдается сообщение:
Error 207: Invalid floating point operation.

181
182 Часть II. Практикум

В отличие от синтаксических ошибок, которые выявляет компилятор в процессе


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

Итоги
1. Приступая к написанию программы, четко определите, что является ее исходны-
ми данными и что требуется получить в результате.
2. Выбирайте тип переменных с учетом диапазона и требуемой точности представ-
ления данных.
3. Давайте переменным имена, отражающие их назначение.
4. Ввод с клавиатуры предваряйте приглашением, а выводимые значения — по-
яснениями. Для контроля сразу же после ввода выводите исходные данные на
дисплей (по крайней мере, в процессе отладки).
5. До запуска программы подготовьте тестовые примеры, содержащие исходные
данные и ожидаемые результаты. Отдельно проверьте реакцию программы на
неверные исходные данные.
6. При записи выражений обращайте внимание на приоритет операций.
7. Разделяйте данные при вводе пробелами, символами перевода строки или табу-
ляции.
8. Тщательно форматируйте текст программы и снабжайте его содержательными
комментариями.

Задания
Напишите программу расчета по двум формулам. Предварительно подготовьте те-
стовые примеры для второй формулы с помощью калькулятора (результаты вы-
числений по обеим формулам должны совпадать). Отсутствующие функции вы-
разите через имеющиеся.
1 1 5
1. z1 = 2 sin2 (3π − 2α) cos2 (5π + 2α); z2 = − sin( π − 8α).
4 4 2
π
2. z1 = cos α + sin α + cos 3α + sin 3α ; z2 = 2 2 cos α ⋅ sin( + 2α).
4
sin 2α + sin 5α − sin 3α
3. z1 = ; z2 = 2 sin α .
cos α + 1 − 2 sin2 2α

182
Семинар 1. Линейные программы 183

sin 2α + sin 5α − sin 3α


4. z1 = ; z2 = tg3α .
cos α − cos 3α + cos 5α
1 2
5. z1 = 1 − sin 2α + cos 2α ; z2 = cos2 α + cos4 α.
4

6. z1 = cos α + cos 2α + cos 6α + cos 7α ; α 5


z2 = 4 cos ⋅ cos α ⋅ cos 4α .
2 2

7. z1 = cos2 ( 3 π − α ) − cos2 (11 π + α ); z2 =


2 α
sin .
8 4 8 4 2 2
1 2
8. z1 = cos4 x + sin2 y + sin 2 x − 1; z2 = sin( y + x) ⋅ sin( y − x).
4
α−β
9. z1 = (cos α − cos β)2 − (sin α − sin β)2 ; z2 = −4 sin2 ⋅ cos(α + β).
2
π
sin( + 3α)
2 5 3
10. z1 = ; z2 = ctg( π + α) .
1 − sin(3α − π) 4 2
1 − 2 sin2 α 1 − tgα .
11. z1 = ; z2 =
1 + sin 2α 1 + tgα
sin 4α cos 2α 3
12. z1 = ⋅ ; z2 = ctg( π − α) .
1 + cos 4α 1 + cos 2α 2
sin α + cos(2β − α) 1 + sin 2β
13. z1 = ; z2 = .
cos α − sin(2β − α) cos 2β
cos α + sin α
14. z1 = ; z2 = tg 2α + sec 2α .
cos α − sin α

2b + 2 b2 − 4 1
15. z1 = ; z2 = .
2 b+2
b −4 +b+2

x 2 + 2 x − 3 + ( x + 1) x 2 − 9 x+3
16. z1 = ; z2 = .
2
x − 2 x − 3 + ( x − 1) x − 9 2 x−3

(3m + 2)2 − 24m


17. z1 = 2 ; z2 = − m .
3 m−
m
a+2 a 2 a− 2 1
18. z1 = ( − + )⋅ ; z2 = .
2a 2a + 2 a − 2a a+2 a+ 2

183
184 Часть II. Практикум

1 + a + a2 1 − a + a2 −1 4 − a2 .
19. z1 = ( + 2 − ) (5 − 2a2 ); z2 =
2a + a2 2a − a2 2
(m − 1) m − (n − 1) n m − n.
20. z1 = ; z2 =
3 2
m n + nm + m − m m

184
Семинар 2. Разветвляющиеся вычислительные
процессы
Теоретический материал: глава 2, с. 34–39.

В линейной программе все операторы выполняются последовательно, один за дру-


гим. Таким способом можно записывать только очень простые алгоритмы. Чтобы
в зависимости от конкретных значений величин обеспечить выполнение разных
последовательностей операторов, применяют операторы ветвления if и case.

Задача С2.1. Выстрел по мишени


Дана заштрихованная область (рис. С2.1) и точка с координатами (x, y). Написать
программу, определяющую, попадает ли точка в область. Результат вывести в виде
текстового сообщения.

Рис. С2.1. Мишень для задачи 2.1

Начинать решение даже простейшей задачи необходимо с четкого описания ее ис-


ходных данных и результатов. В данном случае это очевидно: исходными данными
являются два вещественных значения: координаты точки («выстрела»), обозначим
их x и y. Для представления этих величин в программе выберем тип real.

СОВЕТ
Если для решения какой-либо задачи требуется точность более 11–12 десятичных раз-
рядов или диапазон, не входящий в 10–39..1038, следует описать переменные как double.
Для сугубо специальных случаев (а также параноиков) предназначен тип extended.

185
186 Часть II. Практикум

Результат — одно из текстовых сообщений: «Точка попадает в область» или «Точ-


ка не попадает в область». Запишем условия попадания точки в область в виде фор-
мул. Область можно описать как круг1, пересекающийся с треугольником. Точка
может попадать либо в круг, либо в треугольник, либо в их общую часть:

⎧ x≤0 ⎫
⎪ ⎪
{ 2 2
}
круг: x + y ≤ 1 ; треугольник: ⎨ y ≤ 0 ⎬ .
⎪ y ≥ − x − 2⎪
⎩ ⎭
Программа для решения задачи приведена в листинге С2.1.

Листинг С2.1. Выстрел по мишени


program shot;
var x, y: real;
begin
writeln(' Введите значения х и у:');
readln(x, y);
if (sqr(x) + sqr(y) <= 1) or (x <= 0) and (y <= 0) and (y >= – x – 2)
then writeln(' Точка попадает в область')
else writeln(' Точка не попадает в область');
end.
Приоритет операций отношения в Паскале самый низкий, поэтому они заключа-
ются в скобки. Три условия попадания точки в треугольник должны выполняться
одновременно, поэтому они объединяются с помощью логической операции and. Ее
приоритет выше, чем у операции or, поэтому дополнительных скобок не требуется.

ПРИМЕЧАНИЕ
Для удобочитаемости программы можно ставить скобки даже в тех местах, где они не
обязательны, например, для визуальной группировки условий.

Выполните программу несколько раз, задавая различные положения точки вне


и внутри каждой из областей.
Рассмотрим пример другой заштрихованной области (рис. С2.2).
Условный оператор для определения попадания точки в эту область имеет вид
if (y <= 0) and
((sqr(x – 1) + sqr(y) <= 1) or (sqr(x + 1) + sqr(y) <= 1))
then writeln(' Точка попадает в область')
else writeln(' Точка не попадает в область');
Точка может попасть либо в правый полукруг, либо в левый, в обоих случаях зна-
чение y должно быть меньше или равно нулю. Для того чтобы операция or была
выполнена раньше, чем операция and, необходимы круглые скобки.

1
Справка: уравнение окружности радиуса R с центром в точке (x0, y0) выглядит так: (x – x0)2 +
+ (y – y0)2 = R2; уравнение прямой — y = kx + b.

186
Семинар 2. Разветвляющиеся вычислительные процессы 187

Рис. С2.2. Другой вариант мишени

Задача С2.2. Определение времени года


Написать программу, которая по номеру месяца выводит время года.

Эта программа не нуждается в дополнительных комментариях (листинг С2.2):

Листинг С2.2. Определение времени года


program season;
var month: word;
begin
writeln( 'Введите номер месяца: ');
readln(month);
case month of
1,2,12 : writeln(' Зима ');
3 .. 5 : writeln(' Весна');
6 .. 8 : writeln(' Лето ');
9 .. 11: writeln(' Осень');
else writeln(' Такие месяцы встречаются редко! ');
end;
end.

Задача С2.3. Простейший калькулятор


Написать программу, реализующую калькулятор на четыре арифметических дей-
ствия.

Исходными данными для этой программы являются два вещественных операн-


да и знак операции, представляющий собой символ. Алгоритм работы программы
очевиден: в зависимости от значения введенного знака операции выполнить соот-
ветствующее действие над операндами (листинг С2.3).

Листинг С2.3. Простейший калькулятор


program calculator;
var a, b, res: real;
op : char;
продолжение 

187
188 Часть II. Практикум

Листинг С2.3 (продолжение)


begin
writeln( ' Введите первый операнд: '); readln(a);
writeln( ' Введите операцию: '); readln(op);
writeln( ' Введите второй операнд: '); readln(b);
case op of
'+' : res := a + b;
'–' : res := a – b;
'*' : res := a * b;
'/' : res := a / b;
else begin
writeln( ' Недопустимая операция '); exit
end;
end;
writeln( ' res', res:6:2);
end.
Обратите внимание, что, если по какой-либо ветви требуется записать не один, а не-
сколько операторов, они заключаются в блок с помощью ключевых слов begin и end.
Стандартная процедура exit обеспечивает выход из программной единицы, в кото-
рой она записана.

СОВЕТ
Хотя наличие слова else не обязательно, рекомендуется всегда обрабатывать случай,
когда значение выражения не совпадает ни с одной из констант. Это облегчает поиск
ошибок при отладке программы.

Итоги
1. Если в одном условном операторе требуется проверить выполнение нескольких
условий, они записываются после ключевого слова if и объединяются с помощью
логических операций and, or, xor и not. Получившееся выражение вычисляется
в соответствии с приоритетами операций.
2. Если в какой-либо ветви вычислений условного оператора if требуется выпол-
нить более одного оператора, то они объединяются в блок с помощью ключевых
слов begin и end.
3. Проверка вещественных величин на равенство опасна.
4. В операторе варианта выражение, стоящее после ключевого слова case, и констан-
ты, помечающие ветви, должны быть одного и того же порядкового типа.
5. Рекомендуется всегда описывать в операторе case ветвь else.
6. Оператор case предпочтительнее оператора if в тех случаях, когда количество
направлений вычисления в программе больше двух, а выражение, по значению
которого производится переход на ту или иную ветвь, имеет порядковый тип.
Часто это справедливо даже для двух ветвей, поскольку повышает наглядность
программы.

188
Семинар 2. Разветвляющиеся вычислительные процессы 189

Задания
Задание С2.1
Написать программу, которая по введенному значению аргумента вычисляет зна-
чение функции, заданной в виде графика. Параметр R задается константой.

№ Графики

продолжение 

189
190 Часть II. Практикум

№ Графики

190
Семинар 2. Разветвляющиеся вычислительные процессы 191

№ Графики

10

11

12

13

продолжение 

191
192 Часть II. Практикум

№ Графики

14

15

16

17

192
Семинар 2. Разветвляющиеся вычислительные процессы 193

№ Графики

18

19

20

193
194 Часть II. Практикум

Задание С2.2
Написать программу, которая определяет, попадает ли точка с заданными коорди-
натами в область, закрашенную на рисунке серым цветом. Результат работы про-
граммы вывести в виде текстового сообщения.

№ Область № Область

1 2

3 4

5 6

7 8

194
Семинар 2. Разветвляющиеся вычислительные процессы 195

№ Область № Область

9 10

11 12

13 14

15 16

продолжение 

195
196 Часть II. Практикум

№ Область № Область

17 18

19 20

196
Семинар 3. Организация циклов
Теоретический материал: глава 2, с. 39–48.

Цикл — это фрагмент программы, повторяемый многократно. В Паскале три опе-


ратора цикла — while, repeat и for. В принципе, без них можно обойтись, посколь-
ку любой цикл можно реализовать с помощью условного оператора if и оператора
перехода goto, но операторы цикла гораздо удобнее и нагляднее. У каждого из них
есть предпочтительная область применения.

Задача С3.1. Вычисление суммы ряда


Написать программу вычисления значения функции sin с помощью степенного ряда
с точностью ε по формуле

x3 x5 x7
y=x− + − + ...
3! 5! 7!

Этот ряд сходится на всей числовой оси. Для достижения заданной точности
требуется суммировать члены ряда до тех пор, пока абсолютная величина оче-
редного члена не станет меньше или равна ε. Зависимость абсолютной величины
члена ряда от его номера для x = 5 (аргумент задается в радианах) представлена
на рис. С3.1.

Рис. C3.1. Зависимость абсолютной величины члена ряда от его номера

197
198 Часть II. Практикум

Запишем в общем виде формулу для вычисления n-го члена ряда:

n x 2n −1
Cn = (−1) .
(2n − 1)!
На первый взгляд может показаться, что придется организовать циклы для рас-
чета факториала и степеней. При этом можно получить очень большие числа, при
делении которых друг на друга произойдет потеря точности, поскольку количество
значащих цифр, хранимых в ячейке памяти, ограничено. Кроме того, большие чис-
ла могут переполнить разрядную сетку.
Мы поступим по-другому. Легко заметить, что (n+1)-й член ряда вычисляется после
n-го, поэтому программа получится более простой и эффективной, если находить
член ряда не «с нуля», а умножением предыдущего члена на некоторую величину.
Найдем эту величину. Для этого сначала запишем формулу для (n+1)-го члена
ряда, подставив в предыдущее выражение (n+1) вместо n:

(n +1) x 2n +1 .
Cn +1 = (−1)
(2n + 1)!
Теперь найдем выражение α, на которое надо будет умножить Сn, чтобы получить
Cn+1:
n +1
C (−1) x 2n +1 (2n − 1)! x2
Cn +1 = Cn α => α = n +1 = = − ;
Cn (2n + 1)! (−1)n x 2n −1 2n (2n + 1)

x2 .
Cn +1 = −Cn
2n (2n + 1)

Запишем алгоритм вычисления суммы ряда в словесной форме.


1. Ввести исходные данные (аргумент и точность).
2. Задать начальные значения номеру члена ряда, первому члену ряда и сумме
ряда.
3. Организовать цикл, в котором:
1) вычислить очередной член ряда;
2) добавить его к сумме ряда;
3) перейти к следующему члену ряда;
4) проверить, достигнута ли точность вычислений.
4. Вывести значение функции.
Определить заранее, сколько членов ряда потребуется просуммировать для дости-
жения точности, невозможно, поскольку при анализе условия выхода использу-
ется переменная, вычисляемая на каждой итерации цикла. Такие циклы потенци-
ально опасны, поэтому для предотвращения зацикливания полезно предусмотреть

198
Семинар 3. Организация циклов 199

аварийный выход из цикла по достижении некоторого максимально допустимого


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

Листинг С3.1. Вычисление суммы бесконечного ряда


program sinus;
const MaxIter = 500; { максимально допустимое количество итераций }
var x, eps : double; { аргумент и точность }
c, y : double; { член ряда и его сумма }
n : integer; { номер члена ряда }
done : boolean; { признак достижения точности }
begin
writeln('Введите аргумент и точность:');
readln(x, eps);
done := true;
c := x; { первый член ряда }
y := c; { начальное значение суммы }
n := 1;
while abs(c) > eps do begin
c := -c * sqr(x) / 2 / n / (2 * n + 1) ; { очередной член ряда }
y := y + c; { добавление члена ряда к сумме }
inc(n); { переход к следующему члену ряда }
if n <= MaxIter then continue;
writeln('Ряд расходится!');
done := false; break { аварийный выход из цикла }
end;
if done then
writeln('Аргумент: ', x:10:6, #13#10,
'Значение функции: ', y:10:6, #13#10,
'Вычислено с точностью ', eps:8:6, ' за ', n, ' итераций');
readln;
end.
Первый член ряда равен x, поэтому, чтобы при первом проходе цикла значение вто-
рого члена вычислялось правильно, начальное значение n должно быть равно 1.
Максимально допустимое количество итераций удобно задать с помощью имено-
ванной константы. Для аварийного выхода из цикла применяется процедура break,
которая обеспечивает переход к первому после цикла оператору. Символы #13#10
при выводе вызывают переход на следующую строку.
Пример работы программы (курсивом показаны данные, вводимые пользователем):
Введите аргумент и точность:
7.5 0.00001
Аргумент: 7.500000
Значение функции: 0.938000
вычислено с точностью 0.000010 за 15 итераций

199
200 Часть II. Практикум

Поскольку выход в случае как аварийного, так и нормального завершения цикла


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

СОВЕТ
Называйте переменную-флаг так, чтобы по имени можно было понять, о чем свиде-
тельствует ее истинное значение.

Измените программу так, чтобы она печатала наряду со значениями аргумента


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

Задача С3.2. Нахождение корня нелинейного уравнения


Найти корень уравнения cos(x)=x методом деления пополам с точностью 0,0001.

Исходные данные для этой задачи — точность, результат — число, представляющее


собой корень уравнения1. Оба значения имеют вещественный тип.
Суть метода деления пополам очень проста. Задается интервал, в котором есть
ровно один корень (следовательно, на концах этого интервала функция имеет зна-
чения разных знаков, как показано на рис. С3.2). Вычисляется значение функции
в середине этого интервала. Если оно того же знака, что и значение на левом конце
интервала, значит, корень находится в правой половине интервала, иначе — в ле-
вой. Процесс повторяется для найденной половины интервала до тех пор, пока его
длина не станет меньше заданной точности.
В приведенной далее программе исходный интервал задан с помощью констант,
значения которых взяты из графика функции. Для уравнений, имеющих несколь-
ко корней, можно написать дополнительную программу, определяющую интерва-
лы, содержащие ровно один корень, путем вычисления и анализа таблицы значений
функции (программа печати таблицы значений функции приведена в листинге 2.4).

Листинг С3.2. Нахождение корня нелинейного уравнения


program root;
var x, L, R : real;
begin
L := 0; R := 1; { левая и правая границы интервала }
repeat

1
Корнем уравнения называется значение, при подстановке которого в уравнение оно пре-
вращается в тождество.

200
Семинар 3. Организация циклов 201

x := (L + R) / 2; { середина интервала }
if (cos(x) - x) * (cos(L) - L) < 0 { если значения разных знаков, то }
then R := x { корень в правой половине интервала }
else L := x { иначе корень в левой половине интервала }
until abs(R - L) < 1e-4;
writeln('Корень равен ', x:9:4);
end.

Рис. C3.2. График функции y = cos(x) – x

Добавьте в программу подсчет количества итераций, необходимых для достижения


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

Задача С3.3. Количество квадратов в прямоугольнике


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

Исходными данными являются два натуральных числа — длины сторон прямоу-


гольника. Алгоритм решения задачи состоит в выборе большей стороны и определе-
нии, сколько раз в ней целиком поместится меньшая сторона. Для остатка большей

201
202 Часть II. Практикум

стороны повторяется та же процедура. Цикл завершается, когда остаток становится


равным нулю. Графическая иллюстрация алгоритма приведена на рис. С3.3. Текст
программы с пояснениями приведен в листинге С3.3.

Рис. C3.3. Графическая иллюстрация алгоритма решения задачи C3.3

Листинг С3.3. Количество квадратов в прямоугольнике


program rectangle;
var a, b, { длины сторон прямоугольника }
k, { число одинаковых квадратов каждого размера }
n, { общее число квадратов }
buf : integer; { буферная переменная для обмена длин сторон }
begin
writeln(' Введите стороны прямоугольника');
readln(a, b);
n := 0; { пока ни одного квадрата не найдено }
repeat
if a < b then begin { занесение в а наибольшей стороны }
buf := a; a := b; b := buf;
end;
k := a div b; { в большей стороне k раз поместится меньшая }
a := a mod b; { остаток большей стороны }
writeln (k, ' квадратов ', b, ' x ', b);
inc(n, k); { увеличение общего числа найденных квадратов }
until a = 0;
writeln ( ' Всего квадратов:', n);
readln;
end.

Задача С3.4. Пифагоровы числа


Написать программу, которая выводит на печать все пифагоровы числа, не превы-
шающие 15.
Пифагоровы числа — это тройки натуральных чисел таких, что треугольник, дли-
ны сторон которого пропорциональны (или равны) этим числам, является прямоу-
гольным.
Будем решать эту задачу методом перебора вариантов. Возведя в квадрат каждую
пару натуральных чисел в заданных пределах, проверим, не является ли их сумма
квадратом натурального числа (листинг С3.4).

202
Семинар 3. Организация циклов 203

Листинг С3.4. Пифагоровы числа


program Pythagor;
const n = 15;
var a, b, c, cx : integer;
begin
for a := 1 to n do
for b := a to n do begin { перебор всех пар чисел от 1 до 15 }
cx := sqr(a) + sqr(b); { сумма квадратов пары чисел }
c := trunc(sqrt(cx)); { кандидат на третье число }
if (sqr(c) = cx) and (c <= n) then writeln (a:3, b:3, c:3);
end;
end.

Итоги
1. При написании любого цикла надо иметь в виду, что в нем всегда явно или неявно
присутствуют четыре элемента, реализующие: начальные установки, тело цикла,
модификацию параметра цикла и проверку условия продолжения цикла.
2. Области применения операторов цикла:
 оператор for применяется, если требуется выполнить тело цикла заданное
число раз;
 оператор repeat используют, когда цикл требуется обязательно выполнить
хотя бы один раз, например, при анализе корректности ввода данных;
 оператор while удобнее во всех остальных случаях.
3. Выражение, определяющее условие продолжения циклов while и repeat, вычис-
ляется в соответствии с приоритетами операций и должно иметь тип boolean.
4. Для принудительного перехода к следующей итерации цикла используется про-
цедура continue, для преждевременного выхода из цикла — процедура break.
5. Чтобы избежать ошибок при программировании циклов, рекомендуется:
 заключать в блок тело циклов while и for, если в них требуется выполнить
более одного оператора;
 проверять, всем ли переменным, встречающимся в правой части операторов
присваивания в теле цикла, присвоены до этого начальные значения, а также
возможно ли выполнение других операторов;
 проверять, изменяется ли в цикле хотя бы одна переменная, входящая в усло-
вие выхода из цикла;
 если количество повторений цикла заранее не известно, предусматривать
аварийный выход из цикла по достижении некоторого достаточно большого
количества итераций.

Задания
Задание С3.1
Вычислить и вывести на экран в виде таблицы значения функции, заданной гра-
фически (см. задание С.2.1), на интервале от xнач до xкон с шагом dx. Интервал и шаг

203
204 Часть II. Практикум

задать таким образом, чтобы проверить все ветви программы. Таблицу снабдить за-
головком и шапкой.

Задание С3.2
Для десяти выстрелов, координаты которых задаются с клавиатуры, вывести тек-
стовые сообщения о попадании в мишень из задания С2.2.

Задание С3.3
Вычислить и вывести на экран в виде таблицы значения функции, заданной с по-
мощью ряда Тейлора, на интервале от xнач до xкон с шагом dx с точностью ε. Та-
блицу снабдить заголовком и шапкой. Каждая строка таблицы должна содержать
значение аргумента, значение функции и количество просуммированных членов
ряда.

x +1 1 ⎛1 1 1 ⎞
1. ln
x −1
=2
n =0 (

2n + 1) x 2 n +1
= 2 ⎜ + 3 + 5 + ... ⎟ ,
⎝ x 3x 5x ⎠
x > 1.


(−1)n x n x2 x3 x4
2. e −x
= ∑
n =0
n!
=1− x + − +
2! 3! 4!
− ..., x < ∞.


xn x2 x3 x4
3. e x = ∑ n ! = 1 + x + 2! + 3! + 4! − ...,
n =0
x < ∞.


(−1)n x n +1 x2 x3 x4
4. ln (x + 1) = ∑n =0
n +1
=x−
2
+
3

4
− ..., − 1 < x ≤ 1.


1+ x x 2n +1 ⎛ x3 x5 ⎞
5. ln
1− x
=2
n =0

2n + 1
= 2⎜ x +

⎝ 3
+
5
+ ... ⎟ ,


x < 1.


xn ⎛ x2 x3 ⎞
6. ln(1 − x) = − ∑n =1
n
= −⎜x +
⎝ 2
+
3
+ ...⎟ −1 ≤ x < 1 .

π

(−1)n +1 x 2n +1 π x3 x5
7. arcctg x =
2
+ ∑n =0
2n + 1
=
2
−x+
3

5
− ... | x |≤ 1.

π

(−1)n +1 π 1 1 1
8. arctg x =
2
+ ∑
n =0 (
2n + 1) x 2n +1
= − + 3 − 5 ... x > 1.
2 x 3x 5x

(−1)n x 2n +1

x3 x5 x7
9. arctg x = ∑ =x− + − + ... | x |≤ 1 .
n =0
(2n + 1) 3 5 7

204
Семинар 3. Организация циклов 205


x 2n +1 x3 x5 x7
10. Arth x = ∑ 2n + 1 = x +
n =0
3
+
5
+
7
+ ..., | x |< 1.


1 1 1 1
11. Arth x = ∑ (2n + 1)x
n =0
2 n +1
= + 3 + 5 + ..., | x |> 1 .
x 3x 5x

π

(−1)n +1 π 1 1 1
12. ar ctg x = − +
2 ∑
n =0
(2n + 1) x 2n+1
=− − + 3 − 5 + ..., x < −1 .
2 x 3x 5x

(−1)n x 2n x4 x6 x8

2
−x
13. e = = 1 − x2 + − + − ..., | x |< ∞.
n! 2! 3! 4!
n =0


(−1)n x 2n x2 x4 x6
14. cos x = ∑
n =0
(2n)!
=1− + −
2! 4! 6!
+ ..., | x |< ∞ .


sin x (−1)n x 2n x2 x4 x6
15.
x
= ∑n =0
(2n + 1)!
=1− + −
3! 5! 7!
− ..., | x |< ∞ .


( x − 1)2n +1 ⎛ x − 1 (x − 1)3 (x − 1)
5 ⎞
16. ln x = 2 ∑ (2n + 1)(x + 1)
n =0
2 n +1
= 2⎜ + +
⎜⎝ x + 1 3 (x + 1)3 5 (x + 1)5
+ ...⎟ , x > 0.
⎟⎠


(−1)n ( x − 1)n +1 ( x − 1)2 ( x − 1)3
17. ln x = ∑
n =0
(n + 1)
= ( x − 1) −
2
+
3
+ ..., 0 < x ≤ 2 .


( x − 1)n +1 x − 1 ( x − 1)2 ( x − 1)3 1
18. ln x = ∑ (n + 1)(x + 1)
n =0
n +1
=
x
+
2x 2
+
3x 3
+ ..., x > .
2

1 ⋅ 3 ⋅ ... ⋅ (2n − 1) ⋅ x 2n +1
19. arcsin x = x + ∑n =1
2 ⋅ 4 ⋅ ... ⋅ 2n ⋅ (2n + 1)
=

x3 1 ⋅ 3 ⋅ x5 1 ⋅ 3 ⋅ 5 ⋅ x7 1 ⋅ 3 ⋅ 5 ⋅ 7 ⋅ x9
=x+ + + + ..., x < 1.
2⋅3 2⋅4⋅5 2⋅4⋅6⋅7 2⋅4⋅6⋅8⋅9

π ⎛

1 ⋅ 3 ⋅ ... ⋅ (2n − 1) ⋅ x 2n +1 ⎞
20. arccos x = −⎜x +
2 ⎜

∑ n =1
2 ⋅ 4 ⋅ ... ⋅ 2n ⋅ (2n + 1) ⎟
⎟ =

π ⎛ x3 1 ⋅ 3 ⋅ x5 1 ⋅ 3 ⋅ 5 ⋅ x7 1 ⋅ 3 ⋅ 5 ⋅ 7 ⋅ x9 ⎞
= −⎜x + + + + ... ⎟ , x < 1.
2 ⎜⎝ 2⋅3 2⋅4⋅5 2⋅4⋅6⋅7 2 ⋅ 4 ⋅ 6 ⋅ 8 ⋅ 9 ⎟⎠

205
Семинар 4. Одномерные массивы
Теоретический материал: глава 3, с. 49–54.

Массив — самая простая и самая распространенная структура данных. Чтобы опи-


сать массив в Паскале, надо сообщить компилятору, сколько в нем элементов, ка-
кого типа эти элементы и как они нумеруются. Массив не является стандартным
типом данных, поэтому он задается в разделе описания типов:
type имя_типа = array [тип_индекса] of тип_элемента
При описании типа индексов можно использовать только константы или констант-
ные выражения. Переменные не допускаются, потому что место под массив резер-
вируется до выполнения программы. Например:
const n = 10;
type mas = array [1 .. n] of real;
С элементом массива можно делать все, что допустимо для переменных того же
типа.

Задача С4.1. Количество элементов между минимумом и максимумом


Написать программу, которая для 10 целочисленных элементов определяет, сколь-
ко положительных элементов располагается между максимальным и минимальным
элементами.
Запишем алгоритм в самом общем виде.
1. Считать исходные данные в массив.
2. Определить, где расположены его максимальный и минимальный элементы, то
есть найти их индексы.
3. Просмотреть все элементы, расположенные между ними. Если элемент массива
больше нуля, увеличить счетчик элементов на единицу.
Перед написанием программы полезно составить тестовые примеры, чтобы более
наглядно представить себе алгоритм. Ниже представлен массив из 10 чисел и обо-
значены искомые величины:

6 –8 15 9 –1 3 5 –10 12 2
макс + + + мин

Для этого примера программа должна вывести число 3.

206
Семинар 4. Одномерные массивы 207

Порядок расположения элементов в массиве заранее не известен — сначала может


следовать как максимальный, так и минимальный элемент, более того, они могут
совпадать. Поэтому, прежде чем искать количество положительных элементов, тре-
буется определить, какой из этих индексов больше, чтобы просматривать массив от
меньшего номера к большему.
Принцип поиска максимального элемента в массиве был подробно рассмотрен
в главе 3 (листинг 3.1). Для решения поставленной задачи нам требуется знать не
значение максимума, а его положение в массиве, то есть индекс. Программа поиска
индекса (номера) максимума приведена в листинге С4.1.

Листинг С4.1. Номер максимального элемента


program index_max_elem;
const n = 10;
var a : array [1 .. n] of integer; { массив }
i : integer; { номер текущего элемента }
imax : integer; { номер максимального элемента }
begin
writeln('Введите ', n, ' элементов массива');
for i := 1 to n do read(a[i]);
imax := 1;
for i := 2 to n do
if a[i] > a[imax] then imax := i;
writeln('Номер максимального элемента: ', imax)
writeln('Максимальный элемент: ', a[imax])
end.
Как видите, в этой программе в переменной imax запоминается номер максимально-
го из просмотренных элементов. По этому номеру выполняется выборка элемента
из массива.
Запишем уточненный алгоритм решения нашей задачи.
1. Определить, где в массиве расположены его максимальный и минимальный
элементы:
 задать начальные значения индексов искомых максимального и минималь-
ного элементов (например, равные номеру его первого элемента, но можно
использовать любые другие значения индекса, не выходящие за границу мас-
сива);
 просмотреть массив, поочередно сравнивая каждый его элемент с ранее най-
денными максимумом и минимумом. Если очередной элемент больше ранее
найденного максимума, принять этот элемент за максимум (то есть запом-
нить его индекс). Если очередной элемент меньше ранее найденного мини-
мума, принять этот элемент за минимум.
2. Определить границы просмотра массива для поиска положительных элементов,
находящихся между его максимальным и минимальным элементами:
 если максимум расположен в массиве раньше, чем минимум, принять левую
границу просмотра равной индексу максимума, иначе — индексу минимума;

207
208 Часть II. Практикум

 если максимум расположен в массиве раньше, чем минимум, принять пра-


вую границу просмотра равной индексу минимума, иначе — индексу макси-
мума.
3. Определить количество положительных элементов в найденном диапазоне:
 обнулить счетчик положительных элементов;
 просмотреть массив в указанном диапазоне. Если очередной элемент больше
нуля, увеличить счетчик на единицу.
Для экономии времени значения элементов массива при отладке задаются путем
инициализации (листинг С4.2).

Листинг С4.2. Количество элементов между минимумом и максимумом


program num_positive_1;
const n = 10;
a : array [1 .. n] of integer = (1, 3, -5, 1, -2, 1, -1, 3, 8, 4);
var i : integer; { индекс текущего элемента }
imax : integer; { индекс максимального элемента }
imin : integer; { индекс минимального элемента }
ibeg : integer; { начало интервала }
iend : integer; { конец интервала }
count : integer; { количество положительных элементов }
begin
for i := 1 to n do write(a[i]:3); writeln; { отладочная печать }
imax := 1; imin := 1; { начальные значения номеров макс. и мин. эл-тов }
for i := 1 to n do begin
if a[i] > a[imax] then imax := i; { новый номер максимума }
if a[i] < a[imin] then imin := i; { новый номер мимимума }
end;
writeln(' max= ', a[imax], ' min= ', a[imin]); { отладочная печать }
if imax < imin then ibeg := imax else ibeg := imin; { левая граница }
if imax < imin then iend := imin else iend := imax; { правая граница }
writeln('ibeg = ', ibeg, ' iend= ', iend); { отладочная печать }
count := 0;
for i := ibeg + 1 to iend – 1 do { подсчет количества положительных }
if a[i] > 0 then inc(count);
writeln(' Количество положительных: ', count);
end.

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

Массив просматривается, начиная с элемента, следующего за максимальным


(минимальным), до элемента, предшествующего минимальному (максимально-
му).

208
Семинар 4. Одномерные массивы 209

Тестовых примеров для этой задачи должно быть по крайней мере три — для слу-
чаев, когда элемент a[imin] расположен левее элемента a[imax]; элемент a[imin] рас-
положен правее элемента a[imax]; элементы a[imin] и a[imax] совпадают. Последняя
ситуация имеет место, когда в массиве все элементы имеют одно и то же значение.
Желательно также проверить, как работает программа, если элементы a[imin]
и a[imax] расположены рядом, а также в начале и в конце массива (граничные слу-
чаи). В массиве должны присутствовать как положительные, так и отрицательные
элементы.
При отладке программ, использующих массивы, удобно заранее подготовить ис-
ходные данные в текстовом файле и считывать их в программе. Помимо всего про-
чего это дает возможность, не торопясь, продумать, какие данные требуется ввести
для полной проверки программы, и заранее рассчитать, что должно получиться
в результате.
Результат работы программы тоже бывает полезно выводить не на экран, а в тек-
стовый файл для последующего неспешного анализа. Работа с файлами подробно
рассматривается в главе 3 на с. 67, а в первой главе давались самые необходимые
сведения. В листинге С4.3 приведена версия предыдущей программы, использую-
щая файлы.

Листинг С4.3. Количество элементов между минимумом и максимумом (файл)


program num_positive_2;
const n = 10;
var f_in, f_out : text; { 1 }
a : array [1 .. n] of integer;
i, imax, imin, ibeg, iend, count : integer;
begin
assign(f_in, 'D:\pascal\input.txt'); { 2 }
reset(f_in); { 3 }
assign(f_out, 'D:\pascal\output.txt'); { 4 }
rewrite(f_out); { 5 }
for i := 1 to n do read(f_in, a[i]); { 6 }
imax := 1; imin := 1;
...
writeln(f_out, ' Количество положительных: ', count); { 7 }
close(f_out); { 8 }
end.
В операторе 1 объявлены файловые переменные f_in и f_out стандартного типа
«текстовый файл», которые связываются с файлами на диске в операторах 2 и 4
с помощью процедуры assign. Если полный путь не указан, предполагается, что
файл находится в текущем каталоге. В операторе 3 файл открывается для чтения
(процедура reset), в операторе 5 — для записи (rewrite). Если файл, который требу-
ется открыть для записи, существует, он стирается и создается заново.
Операции ввода-вывода для текстовых файлов аналогичны консольным (опера-
торы 6 и 7). Файл, в который выполняется запись, после окончания работы нуж-

209
210 Часть II. Практикум

но обязательно закрывать с помощью процедуры close, иначе информация может


быть потеряна (оператор 8).
Входной файл input.txt можно создать в любом текстовом редакторе. Он, естествен-
но, должен существовать до первого запуска программы. Если файл, открытый для
чтения, не существует, выдается сообщение об ошибке. Можно взять обработку
этой ошибки в свои руки (см. функцию IOResult, приложение 4).

СОВЕТ
При отладке программы можно выводить одну и ту же информацию и на экран,
и в текстовый файл. Для этого каждую процедуру вывода дублируют.

Задача С4.2. Сумма элементов правее последнего отрицательного


Написать программу, которая для n вещественных элементов определяет сумму
элементов, расположенных правее последнего отрицательного элемента.

В этой задаче количество элементов задано переменной величиной. Предполага-


ется, что она будет известна на этапе выполнения программы до того, как мы бу-
дем вводить сами элементы. Допустим также, что известно максимально возмож-
ное количество элементов. В этом случае память под массив можно выделить «по
максимуму», а затем заполнить только часть этой памяти. Фактическое количество
введенных элементов запоминается в переменной, которая затем участвует в орга-
низации циклов по массиву, задавая его верхнюю границу.
В листинге С4.4 приведена программа, иллюстрирующая этот подход. В ней вы-
полняется только считывание элементов с клавиатуры и их вывод на экран.

Листинг С4.4. Выделение памяти «с запасом»


program example;
const n = 10000;
var a : array [1 .. n] of integer;
i : integer; { номер текущего элемента }
nf : integer; { фактическое количество элементов в массиве }
begin
writeln('Введите количество элементов');
readln(nf);
if nf > n then begin
writeln('Превышение размеров массива'); exit
end;
writeln('Введите элементы');
for i := 1 to nf do read(a[i]);
for i := 1 to nf do write(a[i]:4);
end.
Несмотря на то что значение константы n определяется «с запасом», надо обязатель-
но проверять, не запрашивается ли большее количество элементов, чем возможно.
Привычка к проверке подобных, казалось бы, маловероятных случаев позволит вам

210
Семинар 4. Одномерные массивы 211

создавать более надежные программы, а нет ничего более важного для программы,
чем надежность.
Если же стоит задача вводить неизвестное количество чисел до тех пор, пока не
будет введен какой-либо признак окончания ввода, то заранее выделить доста-
точное количество памяти не удастся и придется воспользоваться так называе-
мыми динамическими структурами данных, например списком. Мы рассмотрим
эти структуры на семинаре 8, а пока остановимся на первом предположении — что
количество элементов массива вводится с клавиатуры до начала ввода самих эле-
ментов.
Перейдем к созданию алгоритма решения задачи. По аналогии с предыдущей за-
дачей первым приходит в голову такое решение: просматривая массив с начала
до конца, найти номер последнего отрицательного элемента, а затем организовать
цикл суммирования всех элементов, расположенных правее него. Вот как выглядит
построенная по этому алгоритму программа (сразу же признаюсь, что она далеко не
так хороша, как может показаться с первого взгляда):
program sum_elem_1;
const n = 1000;
var a : array [1 .. n] of real;
i : integer; { номер текущего элемента }
ineg : integer; { номер последнего отрицательного элемента }
nf : integer; { фактическое количество элементов в массиве }
sum : real; { сумма элементов }
begin
writeln('Введите количество элементов');
readln(nf);
if nf > n then begin
writeln('Превышение размеров массива'); exit
end;
writeln('Введите элементы');
for i := 1 to nf do read(a[i]);
writeln('Исходный массив:'); { 1 }
for i := 1 to nf do write(a[i]:4); writeln; { 2 }
for i := 1 to nf do
if a[i] < 0 then ineg = i; { 3 }
sum := 0;
for i := ineg + 1 to nf do sum = sum + a[i];
writeln('Сумма: ', sum:7:2);
end.
Номер последнего отрицательного элемента массива формируется в переменной
ineg. При просмотре массива в эту переменную последовательно записываются но-
мера всех отрицательных элементов массива (оператор 3), таким образом, после
выхода из цикла в ней остается номер самого последнего элемента.
С целью оптимизации программы может возникнуть мысль объединить цикл на-
хождения номера последнего отрицательного элемента с циклами ввода и кон-
трольного вывода элементов массива, но я так делать не советую, потому что ввод

211
212 Часть II. Практикум

данных, их вывод и анализ — разные по смыслу действия и смешивание их в одном


цикле не прибавит программе ясности.
После отладки программы операторы контрольного вывода (операторы 1 и 2) мож-
но удалить или закомментировать. В последующих примерах для экономии места я
их приводить не буду, но это не значит, что вы должны поступать так же!
Теперь перейдем к критическому анализу нашей первой попытки решения зада-
чи. Для массивов, содержащих отрицательные элементы, эта программа работает
верно, но при их отсутствии выдает сумму всех элементов массива. Это связано
с тем, что, если в массиве нет ни одного отрицательного элемента, переменной ineg
значение в цикле не присваивается. Оно остается равным значению, заданному по
умолчанию.
Для глобальных переменных это 0, но, если мы поместим этот фрагмент в подпро-
грамму, как обычно и делается, в этой переменной окажется случайное число —
очень вероятно, что весьма большое или отрицательное. Как вы понимаете, в таком
случае наша программа, если не «вылетит», будет вычислять нечто загадочное и не-
предсказуемое.
Следовательно, в программу необходимо внести проверку, есть ли в массиве хотя
бы один отрицательный элемент. Для этого переменной ineg присваивается началь-
ное значение, не входящее в множество допустимых индексов массива (например,
–1). После цикла поиска номера отрицательного элемента выполняется проверка,
сохранилось ли начальное значение ineg неизменным. Если да, то это означает, что
условие a[i] < 0 в операторе 3 не выполнилось ни разу и отрицательных элементов
в массиве нет (листинг С4.5).

Листинг С4.5. Сумма элементов правее последнего отрицательного


program sum_elem_2;
const n = 1000;
var a : array [1 .. n] of real;
i : integer; { номер текущего элемента }
ineg : integer; { номер последнего отрицательного элемента }
nf : integer; { фактическое количество элементов в массиве }
sum : real; { сумма элементов }
begin
writeln('Введите количество элементов');
readln(nf);
if nf > n then begin
writeln('Превышение размеров массива'); exit
end;
writeln('Введите элементы');
for i := 1 to nf do read(a[i]);
ineg = -1;
for i := 1 to nf do
if a[i] < 0 then ineg = i;
if ineg = -1 then writeln('Отрицательных элементов нет')

212
Семинар 4. Одномерные массивы 213

else begin
sum := 0;
for i := ineg + 1 to nf do sum = sum + a[i];
writeln('Сумма: ', sum:7:2);
end
end.
Если не останавливаться на достигнутом и подумать, можно предложить более ра-
циональное решение: просматривать массив в обратном порядке, суммируя его эле-
менты, и завершить цикл, как только встретится отрицательный элемент:
program sum_elem_3;
const n = 1000;
var a : array [1 .. n] of real;
i : integer; { номер текущего элемента }
nf : integer; { фактическое количество элементов в массиве }
sum : real; { сумма элементов }
is_neg : boolean; { признак наличия отрицательного элемента }
begin
writeln('Введите количество элементов');
readln(nf);
if nf > n then begin
writeln('Превышение размеров массива'); exit
end;
writeln('Введите элементы');
for i := 1 to nf do read(a[i]);
is_neg := false;
sum := 0;
for i := nf downto 1 do begin
if a[i] < 0 then begin is_neg := true; break end;
sum = sum + a[i];
end;
if is_neg then writeln('Сумма: ', sum:7:2);
else writeln('Отрицательных элементов нет');
end.
В этой программе каждый элемент массива анализируется не более одного раза,
а ненужные элементы не просматриваются вообще, поэтому для больших масси-
вов этот вариант предпочтительнее. Впрочем, если в процессоре поддерживается
опережающее считывание данных, он будет работать медленнее.
Для исчерпывающего тестирования этой программы необходимо ввести по край-
ней мере три варианта исходных данных для случаев, когда массив содержит один
элемент, более одного и ни одного отрицательного элемента.

ПРИМЕЧАНИЕ
Строго говоря, для обеспечения надежности этой программы следовало бы объявить
переменную, в которой хранится сумма в виде значения типа double, потому что сумма
величин типа real может выйти за границы диапазона его представления.

213
214 Часть II. Практикум

Задача С4.3. Сжатие массива


Написать программу, которая «сжимает» целочисленный массив из 10 элементов,
удаляя из него элементы, меньшие заданной величины. Освободившиеся в конце мас-
сива элементы заполнить нулями.

Составим тестовый пример, чтобы более наглядно представить себе алгоритм. Ис-
ходный массив:
6 –8 15 9 –1 3 5 –10 12 2

Допустим, требуется удалить из него все элементы, величина которых меньше 5.


Результат должен иметь вид
6 15 9 5 12 0 0 0 0 0

Исходными данными являются массив и заданное число, результатом — преобра-


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

Рис. C4.1. Сжатие с использованием дополнительного массива

Листинг С4.6. Сжатие массива (дополнительный массив)


program compress_1;
const n = 10;
type mas = array [1 .. n] of integer;
var a, b : mas;
i : integer; { номер текущего элемента в массиве a }
j : integer; { номер текущего элемента в массиве b }
x : integer; { заданное число }
begin
writeln('Введите число'); readln(x);
writeln('Введите элементы массива');
for i := 1 to n do read(a[i]);
j := 0;
for i := 1 to n do
if a[i] >= x then begin
inc(j); b[j] := a[i];

214
Семинар 4. Одномерные массивы 215

end;
a := b;
writeln('Преобразованный массив:');
for i := 1 to n do write(a[i]:4);
end.
Обнуление «хвоста» массива происходит естественным образом, поскольку в Па-
скале глобальные переменные обнуляются.
Однако для массивов большой размерности выделение двойного объема памяти
может оказаться слишком расточительным. Поэтому далее приводится вариант
программы, в которой преобразование массива выполняется «in situ», что по латы-
ни означает «на месте».
Алгоритм работы этой программы выглядит следующим образом.
1. Просмотрев массив, определить номер самого первого из удаляемых элемен-
тов.
2. Если таковой есть, сдвигать каждый последующий элемент массива на первое
«свободное» место, обнуляя оставшуюся часть массива.
Иллюстрация алгоритма приведена на рис. С4.2.

Рис. C4.2. Сжатие массива «на месте»

В программе, приведенной в листинге С4.7, переменная j так же, как и в пред-


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

Листинг С4.7. Сжатие массива «на месте»


program compress_2;
const n = 10;
type mas = array [1 .. n] of integer;
var a : mas;
i : integer; { номер текущего элемента }
j : integer; { номер элемента, в который помещается текущий }
x : integer; { заданное число }
begin
writeln('Введите число'); readln(x);
writeln('Введите элементы массива');
for i := 1 to n do read(a[i]);
j := 0;
for i := 1 to n do { поиск номера первого удаляемого элемента }
продолжение 

215
216 Часть II. Практикум

Листинг С4.7 (продолжение)


if a[i] < x then begin j := i; break end;

if j <> 0 then begin { если есть, что удалять, }


for i := j + 1 to n do { просмотреть оставшуюся часть массива }
if a[i] >=x then begin { такие элементы надо оставить в массиве }
a[j] := a[i]; inc(j); end;
for i := j to n do a[i] := 0; { обнуление "хвоста" массива }
end;
writeln('Преобразованный массив:');
for i := 1 to n do write(a[i]:4);
end.
Для тестирования этой программы используйте несколько значений переменной
х — таких, чтобы из массива: не был удален ни один элемент; были удалены все
элементы; была удалена часть элементов.

Задача С4.4. Быстрая сортировка массива


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

Под сортировкой понимается упорядочивание элементов в соответствии с каким-


либо критерием — чаще всего по возрастанию или убыванию. Существует множе-
ство методов сортировки, различающихся по поведению, быстродействию, требуе-
мому объему памяти, а также ограничениям, накладываемым на исходные данные.
В листинге 3.3 рассмотрен один из наиболее простых методов — сортировка вы-
бором. Он характеризуется квадратичной зависимостью времени сортировки t от
количества элементов N:
t = a N 2 + b N lg N.
Здесь a и b — константы, зависящие от программной реализации алгоритма. Ины-
ми словами, для сортировки массив требуется просмотреть порядка N раз. Суще-
ствуют алгоритмы и с лучшими характеристиками1, самый известный из которых
предложен Ч. Э. Р. Хоаром и называется алгоритмом быстрой сортировки. Для него
зависимость имеет вид
t = a N lg N + b N.
Давайте рассмотрим этот интересный алгоритм. Его идея состоит в следующем.
К массиву применяется так называемая процедура разделения относительно средне-
го элемента. Вообще-то в качестве «среднего» можно взять любой элемент массива,
но для наглядности будет выбираться элемент примерно из середины интервала.
Процедура разделения делит массив на две части. В левую помещаются элемен-
ты, меньшие элемента, выбранного в качестве среднего, а в правую — большие.
Это достигается путем просмотра массива попеременно с обоих концов, причем
каждый элемент сравнивается с выбранным средним, и элементы, находящиеся

1
Наихудшим по характеристикам является любимый студентами метод пузырька.

216
Семинар 4. Одномерные массивы 217

в «неподходящей» части, меняются местами. После завершения процедуры раз-


деления средний элемент оказывается на своем окончательном месте, то есть его
«судьба» определена, и мы можем про него забыть. Далее процедуру разделения
необходимо повторить отдельно для левой и правой частей: в каждой части выби-
рается среднее, относительно которого она делится на две, и т. д.
Понятно, что одновременно процедура не может заниматься и левой и правой
частями, поэтому необходимо каким-то образом запомнить запрос на обработку
одной из двух частей (например, правой) и заняться оставшейся частью (напри-
мер, левой). Так продолжается до тех пор, пока не окажется, что очередная обраба-
тываемая часть содержит ровно один элемент. Тогда нужно вернуться к последнему
из необработанных запросов, применить к нему все ту же процедуру разделения,
и т. д. В конце концов массив будет полностью упорядочен.
Для хранения границ еще не упорядоченных частей массива более всего подходит
структура данных, называемая стеком. Мы будем рассматривать «настоящие» сте-
ки на семинаре 8, а пока просто уловите идею. Представьте себе туннель, в который
въезжает вереница машин. Ширина туннеля позволяет ехать только в один ряд.
Если окажется, что выезд из туннеля закрыт и придется ехать обратно задним хо-
дом, машина, ехавшая первой, сможет покинуть туннель в самую последнюю оче-
редь. Это и есть стек, принцип организации которого — «первым пришел, послед-
ним ушел».
В программе, приведенной в листинге С4.8, стек реализуется в виде двух массивов,
stackl и stackr, а также переменной sp, используемой как «указатель» на вершину
стека. В этой переменной хранится номер последнего заполненного элемента мас-
сива (рис. С4.3).

Рис. С4.3. Стек для хранения границ несортированных фрагментов массива

Для этого алгоритма количество элементов в стеке не может превышать n, поэтому


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

ПРИМЕЧАНИЕ
Существует более простая реализация метода быстрой сортировки, основанная на
рекурсии.

217
218 Часть II. Практикум

Листинг С4.8. Быстрая сортировка массива


program quick_sort;
const n = 500;
var arr : array [1 .. n] of real;
middle : real; { средний элемент }
temp : real; { буферная переменная для обмена двух значений в массиве }
sp : integer; { указатель на вершину стека }
i, j : integer;
f : text;
stackl, stackr : array [1 .. n] of integer; { стеки границ фрагментов }
left, right : integer; { границы сортируемого фрагмента }
begin
assign(f, 'D:\pascal\input.txt'); reset(f);
for i := 1 to n do read(f, arr[i]);
sp := 1; stackl[1] := 1; stackr[1] := n; { 1 }
while sp > 0 do begin { 2 }
{ выборка границ фрагмента из стека: }
left := stackl[sp]; { 3 }
right := stackr[sp]; { 4 }
dec(sp); { 5 }
while left < right do begin { 6 }
{ разделение фрагмента arr[left] .. arr[right]: }
i := left; j := right; { 7 }
middle := arr[(left + right) div 2]; { 8 }
while i < j do begin { 9 }
while arr[i] < middle do inc(i); { 10 }
while middle < arr[j] do dec(j); { 11 }
if i <= j then begin
temp := arr[i]; arr[i] := arr[j]; arr[j] := temp;
inc(i); dec(j);
end;
end;
if i < right then begin { 12 }
{ запись в стек границ правой части фрагмента: }
inc(sp);
stackl[sp] := i;
stackr[sp] := right;
end;
right := j; { 13 }
{ теперь left и right ограничивают левую часть }
end
end;
writeln(' Упорядоченный массив:');
for i := 1 to n do write(arr[i]:8:2);
writeln;
end.
На каждом шаге сортируется один фрагмент массива. Левая граница фрагмента
хранится в переменной left, правая — в переменной right. Сначала фрагмент уста-
навливается размером с массив целиком (оператор 1). В операторе 8 выбирается
«средний» элемент фрагмента.

218
Семинар 4. Одномерные массивы 219

Для продвижения по массиву слева направо в цикле 10 используется переменная i,


справа налево — переменная j (в цикле 11). Их начальные значения устанавлива-
ются в операторе 7. После того как оба счетчика «сойдутся» где-то в средней части
массива, происходит выход из цикла 9 на оператор 12, в котором заносятся в стек
границы правой части фрагмента. В операторе 13 устанавливаются новые границы
левой части для сортировки на следующем шаге.
Если сортируемый фрагмент уже настолько мал, что сортировать его не требуется,
происходит выход из цикла 6, после чего выполняется выборка из стека границ еще
не отсортированного фрагмента (операторы 3 и 4). Если стек пуст, происходит вы-
ход из главного цикла 2. Массив отсортирован.
Быстрая сортировка является одним из лучших методов упорядочивания, однако
существует целый ряд алгоритмов, которые предпочтительнее применять для дан-
ных, отвечающих определенным критериям. Советую вам на досуге ознакомиться
с этими алгоритмами по книге [7]. Оптимальный выбор наиболее подходящего для
каждого случая метода сортировки данных — показатель класса программиста.

Итоги
1. Массив не является стандартным типом данных, он задается в разделе описания
типов. Если тип массива используется только в одном месте программы, можно
задать тип прямо при описании переменных.
2. Размерность массива может быть только константой или константным выраже-
нием. Рекомендуется задавать ее с помощью именованной константы.
3. Тип элементов массива может быть любым, кроме файлового, тип индексов —
интервальным, перечисляемым или byte.
4. При описании массива можно задать начальные значения его элементов. При
этом он описывается в разделе описания констант.
5. С массивами в целом можно выполнять только одну операцию — присваивание.
При этом массивы должны быть одного типа. Все остальные действия выполня-
ются с отдельными элементами массива.
6. Автоматический контроль выхода индекса за границы массива по умолчанию не
выполняется. Можно включить его с помощью директивы {$R+}.
7. При работе с массивами удобно готовить исходные данные в текстовом файле
и считывать их в программе.
8. Существует много алгоритмов сортировки. Они различаются по быстродей-
ствию, занимаемой памяти и области применения.

Задания
Вариант 1
1. Найти сумму отрицательных элементов массива.
2. Найти произведение элементов массива, расположенных между максимальным
и минимальным элементами.
3. Упорядочить элементы массива по возрастанию.

219
220 Часть II. Практикум

Вариант 2
1. Найти сумму положительных элементов массива.
2. Найти произведение элементов массива, расположенных между максимальным
по модулю и минимальным по модулю элементами.
3. Упорядочить элементы массива по убыванию.
Вариант 3
1. Найти произведение элементов массива с четными номерами.
2. Найти сумму элементов массива, расположенных между первым и последним
нулевыми элементами.
3. Преобразовать массив таким образом, чтобы сначала располагались все положи-
тельные элементы, а потом — все отрицательные (элементы, равные 0, считать
положительными).
Вариант 4
1. Найти сумму элементов массива с нечетными номерами.
2. Найти сумму элементов массива, расположенных между первым и последним
отрицательными элементами.
3. Сжать массив, удалив из него все элементы, модуль которых не превышает 1.
Освободившиеся в конце массива элементы заполнить нулями.
Вариант 5
1. Найти максимальный элемент массива.
2. Найти сумму элементов массива, расположенных до последнего положительного
элемента.
3. Сжать массив, удалив из него все элементы, модуль которых находится в интер-
вале [a, b]. Освободившиеся в конце массива элементы заполнить нулями.
Вариант 6
1. Найти минимальный элемент массива.
2. Найти сумму элементов массива, расположенных между первым и последним
положительными элементами.
3. Преобразовать массив таким образом, чтобы сначала располагались все элемен-
ты, равные нулю, а потом — все остальные.
Вариант 7
1. Найти номер максимального элемента массива.
2. Найти произведение элементов ма