Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие...

172
ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ РОССИЙСКОЙ ФЕДЕРАЦИИ НАЦИОНАЛЬНЫЙ ИССЛЕДОВАТЕЛЬСКИЙ ЯДЕРНЫЙ УНИВЕРСИТЕТ «МИФИ» Д.В. Демидов ОСНОВЫ ПРОГРАММИРОВАНИЯ В ПРИМЕРАХ НА ЯЗЫКЕ PASCAL Учебное пособие Москва 2010

Transcript of Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие...

Page 1: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ РОССИЙСКОЙ ФЕДЕРАЦИИ

НАЦИОНАЛЬНЫЙ ИССЛЕДОВАТЕЛЬСКИЙ ЯДЕРНЫЙ УНИВЕРСИТЕТ «МИФИ»

Д.В. Демидов

ОСНОВЫ ПРОГРАММИРОВАНИЯ

В ПРИМЕРАХ НА ЯЗЫКЕ PASCAL

Учебное пособие

Москва 2010

Page 2: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

УДК 004.43 (075) ББК 32.973-018.1я7 Д30

Демидов Д.В. Основы программирования в примерах на языке Паскаль: Учебное пособие. М.: НИЯУ МИФИ, 2010. – 172 с.

Пособие представляет собой переработанный и расширенный текст лекций по

курсу «Информатика», который читается автором в НИЯУ МИФИ на кафедре № 22 (специальность «Прикладная математика и информатика»).

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

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

Рецензент проф. М.А. Иванов

Рекомендовано редсоветом МИФИ в качестве учебного пособия

ISBN 978-5-7262-1303-3 © НИЯУ МИФИ, 2010

Редактор Е.Г. Станкевич

Подписано в печать 22.06.2010 Формат 60x84 1/16 Печ. л. 10,75 Уч. изд. л. 10,75 Тираж 100 экз.

Изд. № 047-1 Заказ № 207

Национальный исследовательский ядерный университет «МИФИ».

Типография НИЯУ МИФИ. 115409, Москва, Каширское ш., 31

Page 3: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

3

Оглавление

ПРЕДИСЛОВИЕ ......................................................................................... 4

ГЛАВА 1. ИСТОРИЯ И ОСНОВНЫЕ ПОНЯТИЯ ПРОГРАММИРОВАНИЯ .......................................................................... 5

ГЛАВА 2. «СТРОИТЕЛЬНЫЙ МАТЕРИАЛ» ОПЕРАТОРНОГО ЯЗЫКА ПРОГРАММИРОВАНИЯ ......................................................... 17

ГЛАВА 3. ТИПЫ ДАННЫХ ЯЗЫКА ПАСКАЛЬ ................................. 28

ГЛАВА 4. ПРИСВАИВАНИЕ И ВЕТВЛЕНИЕ ..................................... 37

ГЛАВА 5. СОРТИРОВКА И ПОИСК ..................................................... 48

ГЛАВА 6. ЦИКЛЫ И РЕКУРРЕНТНЫЕ СООТНОШЕНИЯ ............. 61

ГЛАВА 7. ОПЕРАЦИИ НАД МАССИВАМИ И МАТРИЦАМИ ......... 73

ГЛАВА 8. СТРУКТУРИРОВАНИЕ ПРОГРАММ ................................ 81

ГЛАВА 9. РЕКУРСИВНЫЕ ПРОЦЕДУРЫ И ФУНКЦИИ ................. 92

ГЛАВА 10. СТРОКИ И МНОЖЕСТВА................................................ 102

ГЛАВА 11. ТЕКСТОВЫЕ ФАЙЛЫ ...................................................... 113 ГЛАВА 12. ЗАПИСИ И ТИПИЗИРОВАННЫЕ ФАЙЛЫ .................. 124

ГЛАВА 13. НЕТИПИЗИРОВАННЫЕ ФАЙЛЫ .................................. 134

ГЛАВА 14. ДИНАМИЧЕСКИЕ ПЕРЕМЕННЫЕ, ЛИНЕЙНЫЕ СПИСКИ .................................................................................................. 140

ГЛАВА 15. ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ ................. 154

ГЛАВА 16. КОМАНДНАЯ СТРОКА, СТИЛЬ, ТЕСТИРОВАНИЕ И ОТЛАДКА ............................................................................................ 163

СПИСОК ЛИТЕРАТУРЫ...................................................................... 172

Page 4: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

4

Предисловие Учебное пособие представляет собой переработанный и расши-

ренный текст лекций по курсу «Информатика», который читается в НИЯУ МИФИ на кафедре кибернетики студентам групп К1-221, К1-222, К1-223, К1-224, К1-681.

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

В пособии дается минимальное описание синтаксиса языка Пас-каль, а также описание типов данных и языковых конструкций, присущих таким распространённым диалектам языка, как Turbo Pascal (Free Pascal) и Delphi. Изложение сопровождается материа-лом по таким вопросам, как:

представление данных в памяти ЭВМ; предпосылки появления тех или иных синтаксических

конструкций в операторных языках программирования; семантика операторов императивных языков програм-

мирования; сложность вычислений и оптимизация, зависимость эф-

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

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

Многолетняя практика показала, что аудиторные занятия не мо-гут научить программировать. Преподаватель может лишь напра-вить, указать «белые пятна», дать подсказки, проверить навыки. А изучать язык, развивать абстрактное мышление, воспитывать стиль программирования обучаемый должен сам. Искусством програм-мирования можно овладеть только через практику. Итак, практика каждый день.

Page 5: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

5

Глава 1. История и основные понятия программирования

Информатика Информатика (русскоязычный аналог термина Computer

Science) изучает ряд проблем взаимодействия человека с электрон-но-вычислительной машиной (ЭВМ). Сегодня выделяют несколько направлений информатики:

алгоритмы и структуры данных; теория трансляторов и языки программирования; математические основы информатики (системы счисле-

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

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

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

Алгоритм, машина Тьюринга, последовательная архитектура ЭВМ

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

До этого существовали лишь механические устройства, впервые построенные в XVII в.:

суммирующие часы Вильгельма Шикарда, 1623 г. (кста-ти, в этом же году родился Блез Паскаль);

счётное устройство Блеза Паскаля, 1642 г.; ступенчатый вычислитель Готфрида Лейбница, 1673 г.,

впервые предложившего двоичную систему счисления. В XIX в. Чарльз Бэббидж изобретает “Аналитическую машину”

(1834 г.), в основе которой заложен принцип разделения информа-ции на команды и данные.

Page 6: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

6

В 1843 г. 28-летняя графиня Августа Ада Лавлейс пишет науч-ную работу об аналитической машине Бэббиджа, заложившую на-учные основы программирования на вычислительных машинах. В её работе была приведена программа, предназначенная для реше-ния уравнения Бернулли1.

В России в 70-80-х гг. XIX в. появляются механические ариф-мометры шведско-русского изобретателя Однера В.Т. и русского математика Чебышёва П.Л. В конце XIX в. Холлерит строит счёт-ную машину на перфокартах, которая затем участвует в переписи населения США.

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

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

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

Именно идея последовательного исполнения правил (команд, инструкций) и легла в основу архитектуры первых ЭВМ 1940-х гг.

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

1 В связи с этим фактом графиню Лавлейс считают первым программистом. Неко-торые программисты-романтики в день её рождения (10 декабря) отмечают день программиста. Впрочем, другие отсчитывают 28=256-й день в году – 13 сентября (или 12 сентября в високосный год).

Page 7: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

7

вом и способом коммутации составляющих их блоков. Первую ЭВМ разработали в начале 1940-х гг. в Университете Пенсильва-нии Эккерт и Мочли, она называлась ENIAC (Electronic Numerical Integrator And Computer) – электронный численный интегратор и вычислитель. Перепрограммирование такой машины заключалось в перестройке её блоков и устройств и их перекоммутации.

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

Строительство EDVAC, однако, затянулось, и первой ЭВМ, в которой эта архитектура реализована, стала ЭВМ «Марк I», разра-ботанная в 1948 г. в Университете Манчестера (Великобритания). ЭВМ EDVAC разработана годом позже и введена в эксплуатацию ещё через 2 года. Лишь тогда термин «программирование» конкре-тизировался до термина «программирование ЭВМ». Первая в СССР ЭВМ с хранимой в памяти программой построена под руко-водством С.А. Лебедева и запущена в эксплуатацию в 1950-м.

До сих пор в основе подавляющего большинства современных вычислительных машин, гораздо более сложных, чем EDVAC или «Марк I», всё также лежит последовательная архитектура, предло-женная коллективом из Университета Пенсильвании, не совсем справедливо называемая архитектурой фон Неймана. Интересно, что первая в СССР ЭВМ с производительностью 1 млн операций в секунду (БЭСМ-6) построена в 1965-м.

Следующим мощным толчком к развитию ЭВМ стало создание процессоров и наборов команд, но это был чисто технологический прорыв в уже проложенном архитектурном русле.

Интересно, что любая функция, которая может быть вычислена физическим устройством (ЭВМ), вычисляется и машиной Тьюрин-га (тезис Чёрча–Тьюринга). Таким образом, все подобные вычис-лительные машины оказываются эквивалентными гипотетической машине Тьюринга. Все задачи, разрешимые на машине Тьюринга, можно решить на современной ЭВМ, и наоборот.

Помимо применения последовательной архитектуры ведутся ис-следования и в области параллельных вычислений. Представители

Page 8: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

8

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

Примеры программируемых устройств – компьютеры, КПК, мо-бильные телефоны, калькуляторы, бытовая техника, цифровая ап-паратура, бортовые системы управления. Сегодня можно програм-мировать не только аппаратные устройства, но и программы. При-меры программируемых программ – игровые персонажи, про-граммные агенты.

Алгоритм Итак, что же такое алгоритм? def. Алгоритм – точный набор инструкций, описывающий по-

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

Алгоритм имеет следующие признаки: 1) определенность (детерминированность) – следующий

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

2) понятность исполнителю – алгоритм состоит только из команд, входящих в систему команд исполнителя;

3) конечность – способность завершить работу за конеч-ное число шагов при корректных входных данных;

4) массовость (общность) – применимость для разных входных данных.

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

def. Императивное программирование описывает процесс вы-числения в виде инструкций, изменяющих состояние программы. Такое название было получено из-за повелительного наклонения в естественном языке, с помощью которого выражаются приказы, команды исполнителю. Главный вопрос императивного програм-мирования – «Как вычислять?»

Page 9: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

9

По сути, императивное программирование – это процедура за-писи алгоритма на языке, понятном ЭВМ. Противоположное по духу направление программирования – декларативное программи-рование.

def. Декларативное программирование описывает результат вычислений и его свойства без предписывания последовательности действий. Ведь на самом деле важен результат, а не способ его дос-тижения. Главный вопрос декларативного программирования – «Что должно получиться?»

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

Первое поколение операторных языков В определении алгоритма фигурирует некий исполнитель алго-

ритма. Если исполнитель – ЭВМ, то допустимые команды – систе-ма команд процессора ЭВМ, и здесь уместно назвать такой алго-ритм программой или программным кодом.

def. Программа – алгоритм, описанный в терминах конкретной системы команд ЭВМ.

Команды предназначены для проведения вычислений, работы с ячейками памяти и аппаратными устройствами ЭВМ, управления порядком выполнения команд и многого другого. Но что такое ко-манда для ЭВМ, которая понимает только язык чисел в своей сис-теме счисления? Это специальный числовой код, который, будучи записанным в определённую область памяти в какой-либо момент времени, предлагает ЭВМ выполнить соответствующее действие, а результат (при его наличии) записать в указанное другим числом место.

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

Page 10: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

10

def. Исполнение машинного кода – последовательная интер-претация команд применительно к конкретным данным в соответ-ствии с системой команд ЭВМ.

Важно понять, что для ЭВМ машинный код – текст на том един-ственном языке команд, который она понимает. Поскольку первые программы писались именно в кодах, то языки команд по праву считают первым поколением языков программирования.

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

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

MOV AX, DX ADD DX, BX CLI

lbl: NOP

JMP lbl

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

с помощью интерпретатора ассемблера; с помощью компилятора ассемблера с последующей ин-

терпретацией. В чём различие способов трансляции? Интерпретатор – исполняемая программа, входными данными

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

Page 11: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

11

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

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

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

Языки программирования третьего поколения С помощью простейших команд приходится долго объяснять, а

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

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

Page 12: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

12

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

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

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

Пример программы на ассемблере, вычисляющей сумму чисел от 1 до 10:

MOV AX, 0 MOV CX, 10

rep: ADD AX, CX LOOP rep ; уменьшение CX на 1. Выход при CX=0

В этой программе данные хранятся в двух ячейках (регистрах

процессора) – AX и CX. Регистр AX – аккумулятор, а регистр CX – одновременно и слагаемое и счётчик цикла, меняющийся от 10 до

Page 13: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

13

0. Повторение сложения реализовано с помощью команды услов-ного перехода LOOP на метку rep. Переход осуществляется до тех пор, пока СХ не станет равен 0, причём каждый переход уменьшает CX на единицу. К этому моменту в аккумуляторе AX будет накоп-лена вычисляемая сумма.

Пример аналогичной программы на Паскале: var sum, i: integer; begin sum := 0; for i := 1 to 10 do sum := sum + i; end.

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

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

За несколько десятилетий в мире возникло множество языков императивного программирования третьего поколения, сильно от-личавшихся друг от друга как синтаксически, так и своими новше-ствами и особенностями: Фортран, Алгол, Кобол, ПЛ-1, Паскаль, Си, Ада. Однако все они принципиально схожи, так как основыва-ются на концепции «Как вычислять?»

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

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

Page 14: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

14

Ошибки можно разделить на два класса по времени их возникнове-ния или обнаружения: ошибки проектирования (разработки), выяв-ляемые при компиляции, и ошибки времени выполнения, выявляе-мые лишь во время работы программы. Как правило, чем позже обнаруживается ошибка, тем сложнее найти её источник и дороже её исправить, поэтому разработчики языков программирования и компиляторов стремятся минимизировать ошибки времени выпол-нения. Например, язык Си очень гибок, и для получения работо-способной программы, как шутят программисты, требуется 1 раз её скомпилировать и 100 раз запустить. Язык Ada предельно строг, и для получения работоспособной программы требуется 100 раз её скомпилировать и 1 раз запустить.

Зависимость от платформы, байт-код и псевдокод На протяжении эволюции языков третьего поколения их разра-

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

Технология Java решает проблему переносимости с помощью виртуальной машины, имеющей свою систему команд.

def. Байт-код – машинный код, состоящий из команд виртуаль-ной машины java.

Под каждую аппаратную платформу разрабатывается програм-ма-интерпретатор байт-кода, которая называется виртуальной java-машиной (jvm). Она учитывает особенности каждой конкретной программно-аппаратной архитектуры. Компилятору Java остаётся преобразовать исходный код на языке Java в этот байт-код. Таким образом, Java-программу достаточно один раз скомпилировать в байт-код, и она будет работать на любой платформе с java-машиной.

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

Page 15: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

15

вание инструкций (шагов алгоритма) в периодической литературе по информатике часто используется псевдокод.

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

Например,

Начало Установить sum = 0 В цикле от i = 1 до 10 выполнять Установить sum = sum + i Конец цикла Конец

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

Языки четвёртого поколения С усложнением программных систем появилась потребность в

специализации и разделении труда. Универсальные языки станови-лись всё менее удобными для решения узких задач. Начало менять-ся мышление: программисты стали больше думать о самой задаче, а не о том, как приспособить задачу для её решения на каком-либо языке программирования.

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

Page 16: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

16

от компаний SAP, Oracle, Software AG, IBM, Lotus. В области ав-томатизированного проектирования (CAD) можно отметить встро-енные языки визуальных инженерных сред, таких как AutoCAD, MathCAD и т.п. В области искусственного интеллекта – языки в инструментальных средствах разработки интеллектуальных сис-тем.

Термин «программа» снова изменил своё значение – чаще всего программа на языке четвёртого поколения уже не является само-стоятельной, а исполняется под управлением своей программной среды (интерпретируется). Такая программа совсем не обязательно описывает какой-либо алгоритм, т.е. не диктует последователь-ность действий, это берёт на себя среда выполнения. Грань между императивным и декларативным подходами здесь истончается.

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

Page 17: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

17

Глава 2. «Строительный материал» операторного языка программирования В предыдущей главе были рассмотрены основные вехи развития

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

Задача читателя состоит в том, чтобы научиться терпеливо и точно закладывать в компьютер такую последовательность дейст-вий, которая приведёт к желаемому результату. Можно сказать, что компьютер – это идеальный исполнитель, буквально понимающий команды, быстро их выполняющий, не задающий дополнительных вопросов и не испытывающий эмоций.

Язык Паскаль В 1971 г. доктор Никлаус Вирт из Института информатики

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

С тех пор прошло несколько десятилетий, а язык применяется до сих пор. За прошедшее время язык развивался не только самим Виртом, но и коммерческими компаниями. Наибольший вклад в распространение языка внесла компании Borland, которая долгое время развивала свой компилятор Паскаля, среду программирова-ния Turbo Pascal, работавшую под DOS, среду Borland Pascal под Windows. В 1990-х наступила эра компонентного программирова-ния, появились интегрированные среды разработки на Object Pascal – Delphi и Kylix, c недавнего времени язык именуется Delphi.

Эволюция языка привела к появлению множества его диалектов, новых языков, компиляторов, сред программирования. Так, при непосредственном участии Н. Вирта из Паскаля вышли языки Мо-дула, Модула-2, Оберон, среда программирования бортовых систем XDS. Сегодня развивается и Open Source версия языка – Free Pascal, существуют интерпретаторы языка Паскаль. Интересно, что в ERP-системе Microsoft Business Solutions Navision синтаксис встроенного языка четвертого поколения С/AL практически иден-тичен синтаксису Паскаля.

Page 18: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

18

Пришло время взглянуть на внутреннее устройство программы, ввести основные понятия. Рассмотрим пример программы, которая определяет, являются ли два числа взаимно простыми, т.е. не име-ют общих делителей, кроме единицы: program coprimes; var M,N: integer; // Определить, являются ли числа M и N взаимно простыми function notcoprime(M,N: integer): boolean; var K, i: integer; Res: Boolean; begin Res := false; if N > M then K := M else K := N; for i := 2 to K do Res := Res or (N mod i = 0) and (M mod i = 0); notcoprime := Res; end; begin // ввод данных write('Please enter a natural number: N='); readln(N); write('Please enter a natural number: M='); readln(M); // вызов функции и выдача результата if notcoprime(M,N) then writeln(M, ' and ', N, ' are not coprime.') else writeln(M, ' and ', N, ' are coprime.'); readln; end.

Атомы и молекулы языков программирования Для операторного языка важнейшим строительным материалом

являются, конечно же, операторы. Операторы – приказы для ЭВМ в стиле «делай то-то и то-то», выполняющие в языке программиро-вания роль сказуемых, которые записываются с помощью так на-зываемых ключевых слов.

В приведенном примере это оператор цикла for, условный опе-ратор if then else, составной оператор begin end. В Паскале все

Page 19: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

19

операторы разделяются между собой символом «;» (точка с запя-той).

Над чем может производить действия ЭВМ? Над данными в ячейках памяти. Ячейка памяти или совокупность ячеек представ-ляют своего рода контейнер для хранения данных, который с точки зрения программы называется переменной. Содержимое контей-нера тогда будет называться значением переменной.

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

Для обращения к переменной в теле программе ей назначается уникальное имя – идентификатор. Идентификаторы даются также типам данных, подпрограммам (процедурам и функциям), модулям программы, константам. В нашем примере идентификаторами пе-ременных являются M, N, K, i, а также результат функции notco-prime. В примере использованы встроенные типы данных языка Паскаль – integer и boolean.

Важную роль в языке играют такие конструкции, как выраже-ния. Они строятся из констант и переменных, связанных различ-ными функциями и операциями. Например, (Pi * sqr(radius) / 2). Операции (арифметические, логические, сравнения и др.) и функ-ции предназначены для выполнения каких-либо действий над ар-гументами. Они, как и операторы, играют роль своего рода клея в программе.

В примере для вычисления результата notcoprime используется булево выражение, составленное из операций mod, or, =.

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

Page 20: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

20

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

В примере реализована одна функция notcoprime, принимающая два аргумента и возвращающая истину, если аргументы не являют-ся взаимно простыми.

Ввод и вывод Представьте программу, в которую все исходные данные уже

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

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

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

Что происходит при обращении к подсистеме ввода с клавиату-ры?

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

Page 21: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

21

ной, например к числу с плавающей запятой. Если преобразование невозможно, возникает ошибка времени выполнения.

Система вывода передаёт на экран дисплея последовательность символов. При выводе значения числовой переменной происходит обратное преобразование числа в строку.

В примере для ввода данных используется процедура readln, а для вывода текста и значений переменных – процедуры write и writeln стандартной библиотеки.

В чём же заключается работа программиста с точки зрения пользователя?

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

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

структурировать программу – разделять на независимые блоки и подчёркивать вложенность блоков друг в друга с помощью отсту-пов от начала строки. Блок представляет собой последовательность инструкций, логически связанных между собой и выполняющих в совокупности относительно независимый участок работы. Принято выделять блоки с помощью составного оператора begin end.

В приведённом примере код структурирован и снабжён коммен-тариями. Таким образом, подчёркнута структура программы, что также облегчает её понимание. Обратите внимание на то, как на-званы типы и переменные, как подобраны ключевые слова. Из на-званий можно сделать некоторые выводы о назначении обозначае-мой сущности, а ведь это сильно облегчает понимание программы. Возьмём за правило выбирать осмысленные идентификаторы для вводимых типов, переменных, функций. В противном случае разо-браться в программе с такими идентификаторами, как asdf, vv12, type2 и fff4, будет невозможно. В сообществах программистов су-ществуют негласные стилистические правила именования.

Сравните: program mypr; var M,N: integer;

Page 22: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

22

function f(M,N: integer): boolean; var K, bb: integer; a: Boolean; begin a := false; if N > M then K := M else K := N; for bb := 2 to K do a := a or (N mod bb = 0) and (M mod bb = 0); f := a; end; begin readln(N); readln(M); if f(M,N) then writeln(M, ' and ', N, ' are not coprime.') else writeln(M, ' and ', N, ' are coprime.'); end.

Возьмём за правило структурировать программы.

Синтаксис Основные строительные элементы программы для типового

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

def. Алфавит – множество допустимых символов языка про-граммирования.

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

В примере использованы следующие ключевые слова: для операторов – begin, end, if, then, else, for, to, do; встроенные логические и арифметические операции – or,

mod, and; другие – program, var, function.

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

Page 23: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

23

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

:= оператор присваивания; > операция сравнения «больше»; // однострочный комментарий; ' апостроф (обрамляет строку); . конец программы, а также отделение целой и дробной час-

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

ментарии.

Общая структура программы на языке Паскаль Программа coprimes включала следующие разделы:

заголовок программы (program); раздел объявления переменных (var); раздел процедур и функций (procedure/function); тело программы (begin … end.).

Также в программе могут присутствовать: раздел объявления констант (const); раздел объявления типов данных (type); раздел объявления используемых модулей (uses); раздел объявления меток (label).

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

Таким образом, минимальная программа на Паскале выглядит следующим образом: begin end.

А программа, печатающая на дисплее приветствие, выглядит так: begin writeln("Hello world!"); end.

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

Page 24: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

24

зовать. В языке C++ переменные можно объявлять по ходу про-граммы, но также до их использования.

Встроенные, определяемые и типизированные константы Константы именуются для удобства. Например, чтобы не писать

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

Удобно объявлять в виде констант предельные значения, на-пример границы массивов. Этот приём минимизирует число правок в исходном тексте программы при необходимости изменить какое-либо предельное значение.

Константы определяются с помощью ключевого слова const. Ес-ли при определении константы указывается её тип, то она стано-вится типизированной и может играть роль переменной, отличие от обычных переменных только в том, что она инициализирована, т.е. ей присвоено начальное значение: const // константы pi и e – определены в модуле Math pi = 3.14159; max = 16; // объявление типизированной константы (переменной) k: integer = 89; var a,b: array[1..max] of real; i: integer; begin for i:=1 to max do begin a[i] := pi*i; b[i] := a[i]*pi; end; end.

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

переменные. Как уже говорилось, тип данных – поименованное множество значений, допускающее определённые действия над значениями. Помимо множества значений тип определяет:

множество допустимых операций; множество допустимых преобразований в другие типы; внутреннюю структуру хранения данных.

Page 25: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

25

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

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

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

Исполнение. В объектно-ориентированном программировании во время выполнения программы используется механизм динами-ческого определения типа объекта для вызова полиморфных мето-дов класса.

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

Введение в типы данных Какие типы данных используются в программировании? Типы

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

Page 26: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

26

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

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

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

Как задаются множества значений типа? С точки зрения мате-матики множество значений типа можно задать двумя способами: перечислив все возможные значения или указав способ получения всех значений. С точки зрения операторного языка программиро-вания множество значений типа данных определяется в основном внутренним представлением значений в памяти ЭВМ и механиз-мами контроля типов.

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

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

Как определяются новые типы данных? Они определяются с помощью встроенных механизмов определения типов и встроен-ных (стандартных) типов данных. Встроенные механизмы опреде-ления типов позволяют определять новые структурированные типы данных. Для каждой группы структурированных типов имеется свой механизм – своего рода метатип, который нужно конкретизи-

Page 27: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

27

ровать для построения нового типа. Например, для определения массива необходимо сначала определить тип элементов массива, а для определения записи необходимо определить структуру записи – набор типизированных полей записи.

Целые и вещественные встроенные типы, как правило, зависят от длины машинного слова в битах (разрядности ЭВМ), например 8, 16, 32, 64. Машинное слово целиком помещается в регистры процессора, а значит, обрабатывается быстро и просто.

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

В Java механизм указателей отсутствует, однако возможность создавать динамические структуры данных имеется: в момент присваивания создаётся копия области памяти, в которой размеща-ется динамическая структура. В Object Pascal работа с динамиче-скими массивами возможна без применения указателей.

Page 28: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

28

Глава 3. Типы данных языка Паскаль Рассмотрим типы данных языка:

1) простые типы данных; 2) указатели; 3) структурированные типы данных; 4) процедурный тип.

Некоторые типы уже встроены в язык и не требуют объявления, их называют стандартными или встроенными типам данных. Все остальные типы необходимо объявлять с помощью специальных конструкций.

К встроенным типам относятся: простые целые и вещественные типы; логический тип Boolean; символьный тип Char и тип-строка String; текстовый файл Text; нетипизированный указатель pointer; тип-строка с завершающим нулевым символом PChar.

Числовые типы Как уже говорилось, целые и вещественные типы отличаются

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

Byte 1 байт 0..(28–1), беззнаковые целые Word 2 байта 0…(216–1), беззнаковые целые Shortint 1 байт -27..(27–1), знаковые целые Integer 2 байта -215..(215–1), знаковые целые Longint 4 байта -231..(231–1), знаковые целые Real 6 байт 11–12 значащих разрядов Single 4 байта 7–8 значащих разрядов Double 8 байт 15–16 значащих разрядов Extended 10 байт 19–20 значащих разрядов Comp 8 байт 19–20 значащих разрядов, целые Переменные вещественных типов могут принимать как положи-

тельные, так и отрицательные значения. Значения задаются либо явно с помощью констант, либо неявно через выражения. Примеры объявления и задания переменных:

Page 29: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

29

type Numeric = integer; var i: integer; m,n: real; number: Numeric; begin i := 3; I := $F3; // шестнадцатиричный формат m := -5.78; m := 8e-5; // формат со степенью: 8*10-5 n := m + 3.13*2 - i; number := i; end.

Все числа представляются в двоичной системе счисления. Для представления знаковых чисел старший бит используется для ука-зания знака (0 – у положительных, 1 – у отрицательных).

Логический тип Переменные логического типа Boolean могут принимать только

два значения False (ложь) и True (истина). Выполняется постулат False < True. Во внутреннем представлении ЭВМ ложь представля-ется нулём, а истина – единицей. Есть и другие логические типы, в которых истинным считается любое значение, отличное от нуля: var flag: boolean; a, b: integer; begin a := 3; b := 4; flag := false; flag := a < b; flag := flag and (a=b); end.

Рассмотрим несколько примеров, в которых участвуют пере-менные логического типа: if a = true then … if a then … if a = false then … if not a then … if i < j then a := true else a := false; a := i < j;

Page 30: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

30

if i < j then a := false else a := true; a := not (i < j); a := i >= j;

Эквивалентные формы записи в правом столбце более предпоч-тительны.

Рассмотрим записи if i < j then a := true; и a := i < j; Можно ли считать их эквивалентными? Нет, так как присваивание в первом случае выполняется не всегда, а только когда i < j.

Символы и строки Почему в ASCII-таблице (рис. 1) всего 256 символов? Встроенный символьный тип Char объединяет символы коди-

ровки ASCII. По своей мощности символьный тип равен типу Byte, т.е. имеет всего 256 различных значений. Все символы имеют свой собственный код в таблице ASCII от 0 до 255. Таким образом, для однобайтовой кодировки, примером которой является кодировка ASCII, возможно 256 различных символов.

Как видно из таблицы, символы английского алфавита идут в алфавитном порядке. При сравнении символов реально сравнива-ются их коды в таблице, поэтому символ 'a' будет меньше всех ос-тальных символов алфавита. Заглавные буквы представляются дру-гими символами и соответственно имеют другие коды. Помимо букв в ASCII-таблице закодированы цифры, знаки пунктуации, управляющие символы (перевод строки, табуляция и др.), символы псевдографики. Символьные константы записываются в одинарных кавычках. Узнать код символа можно с помощью функции ord(), а получить символ по коду можно с помощью функции chr() или по-ставив знак # перед кодом.

Page 31: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

31

Рис. 1. ASCII-таблица

Примеры использования типа char и функций ord и chr: var c: char; a, b: byte; begin c := 'Z'; a := ord(c); // a = 122 b := 32; c := chr(b); // с = ' ' – пробел с := #32; // тоже самое write(chr(10)+chr(13)); end.

Page 32: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

32

Следует отметить, что в языке Си используется один и тот же тип char для представления чисел от 0 до 255 и символов.

Значениями встроенного строкового типа string, по сути, явля-ются последовательностями значений символьного типа, т.е. строка в Паскале – 0 и более символов, идущих один за другим. Внутрен-нее представление строк – последовательность байт, причём пер-вый байт отводится для хранения реальной длины строки. Уже упоминалось, что максимальное значение, представимое в ячейке памяти размером в 1 байт, равно 255. Поэтому максимальная длина строки ограничивается 255 символами. Программист может ещё больше ограничить длину строки, указав максимальное число сим-волов при объявлении переменной строкового типа. Строковые константы записываются в одинарных кавычках, как и символьные константы. Для помещения в строку одинарной кавычки (апостро-фа) его следует записать дважды.

Примеры объявления и задания переменных строкового типа: var s: string; c: char; fio: string[60]; leng: byte; begin s := 'Don''t do it'; // строка с апострофом fio := 'Niklaus Wirth'; writeln(s + ' ' + fio); c := s[2]; // 'o' leng := ord(s[0]); // определение длины строки leng := length(s); // определение длины строки // Строка с непечатаемыми символами перевода строки // и возврата каретки s := 'string 1'#10#13'string 2'; end.

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

Page 33: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

33

Структурированные типы данных Для чего вводятся структурированные типы данных? Любая

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

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

Массивы Массив представляет собой последовательность элементов од-

ного типа. Для объявления массива необходимо указать число и тип элементов массива. Для обращения к элементу массива необ-ходимо указать имя переменной-массива и в квадратных скобках индекс элемента. Например, var m, n: array[1..100] of real; x: array[1..100] of real; begin for i:=1 to 100 do m[i] := random; n := m; // допустимо x := m; // недопустимо, при компиляции будет ошибка end.

Здесь число элементов массива указывается с помощью диапа-зона, причём минимальное число будет соответствовать минималь-ному индексу элемента, а максимальное – максимальному индексу элемента.

Следует отметить, что при таком объявлении массивов не рабо-тает механизм контроля типов, так как типы переменных считают-ся различными, несмотря на то, что их структура идентична. По-этому хорошим тоном считается объявление типа-массива: type arr = array[1..100] of real; var m, n, x: arr;

Page 34: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

34

При таком подходе переменные можно будет присваивать, пе-

редавать в виде параметров в функции и процедуры. Массивы могут иметь и несколько размерностей. В этом случае

необходимо через запятую указать диапазоны индексов для каждой размерности. В частности, для представления матриц 100х200 можно использовать следующее объявление: type Matrix = array[1..100, 1..200] of real; var m: Matrix; begin // инициализируем элемент 23 строки в 48 столбце m[23,48] := -4.89; end.

С точки зрения структур данных здесь объявляется двумерная матрица вещественных чисел, у которой 100 строк и 200 столбцов. Для обращения к элементу этой матрицы следует записать что-то вроде m[i, j].

Рассмотрим далее следующий пример: type Arr = array [1..200] of real; Matrix = array[1..100] of Arr; var m: Matrix; a: Arr; i,j: integer; begin // в а копируется 23-я строка матрицы a := m[23]; // 48-й элемент в массиве, но не в матрице! a[48] := -4.89; // 48-й элемент 23-его массива матрицы m[23][48] := -4.89; end.

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

Page 35: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

35

Записи Представьте, что в программе требуется описать некоторый

объект реального мира, например запись в телефонной книжке, и далее работать с ним как с единым целым. Это возможно только с помощью совокупности переменных. В Паскале для этого введён тип-запись, в Си – тип-структура. Для объявления типа-записи сле-дует использовать ключевое слово record и далее описать состав-ляющие запись переменные, которые называются полями записи. Например, type phone_rec = record of fio: string[60]; phone: string[20]; e_mail: string[50]; end; var pr1,pr2: phone_rec; begin pr1.fio := 'Ivanon Ivan'; pr1.phone := '89169110203'; pr1.e_mail := '[email protected]'; pr2 := pr1; writeln('fio: ' + pr1.fio); writeln('phone: ' + pr1.phone); writeln('e-mail: ' + pr1.e_mail); end.

Доступ к полям записи осуществляется через символ '.' после имени переменной типа-записи. Значением переменной типа-записи является совокупность значений полей записи.

Телефонная книга на 200 номеров может быть определена с по-мощью массива записей: type phone_book = array [1..200] of phone_rec; var pb: phone_book; begin … pb[i].fio := … … end.

Page 36: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

36

Файлы Понятно, что хранить телефонную книгу в памяти программы

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

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

Page 37: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

37

Глава 4. Присваивание и ветвление Оператор присваивания

Оператор присваивания – один из базовых в императивном про-граммировании. Происходит он из команды процессора по копиро-ванию содержимого одной ячейки памяти в другую (mov в ассемб-лере). Вот несколько применений оператора присваивания:

инициализация значений переменных; запоминание (в некоторой области памяти) промежуточ-

ных результатов вычислений для последующего исполь-зования;

создание копии значения (копии содержимого в области памяти).

Оператор действует над двумя аргументами: первый аргумент – некоторая переменная, второй – вычислимое выражение. В языке Паскаль оператор обозначается «:=» (двоеточие равно). Обычно используется инфиксная запись оператора, т.е. сначала записывает-ся переменная, затем оператор, а потом – присваиваемое выраже-ние. Например, beta := 180/n; height := (0.5 + sin(beta)) * tg(alpha);

При компиляции проверяется соответствие типов выражения и переменной. Если типы не совместимы, то возникает ошибка пре-образования типов. Например, нельзя число с плавающей запятой присвоить целочисленной переменной.

Операционная семантика2 оператора присваивания: 1) во время выполнения программы вычисляется присваи-

ваемое выражение по правилам вычисления выражений. Если при вычислении выражения возникает ошибка, то выполнение прекращается;

2) если требуется приведение типов, то оно выполняется, т.е. полученное значение преобразуется в формат типа переменной;

2 Операционная семантика описывает то как следует интерпретировать фрагмент программы в виде последовательности вычислительных шагов.

Page 38: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

38

3) если объём области памяти, отведённой для значения переменной, не равен объему, необходимому для хране-ния нового значения, то память, выделенная под преды-дущее значение переменной, возвращается программно-му окружению, и выделяется область памяти для нового значения переменной;

4) полученное значение помещается в область памяти, вы-деленную для переменной.

Пояснения: На первом шаге ошибки могут возникать по различным причи-

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

На втором шаге может потребоваться преобразовать, например, число 500 к типу byte, с помощью которого можно представить числа от 0 до 256, тогда возникает ошибка переполнения типа. Ес-ли же переменная имеет тип real, то число 500 будет успешно пре-образовано в формат числа с плавающей запятой 5.0e+2.

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

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

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

Неявное присваивание Присваивание может происходить неявно, т.е. без применения

оператора присваивания.

Page 39: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

39

Рассмотрим пример: var a,b: integer; function Sum(arg1,arg2: integer): integer; begin Sum := arg1 + arg2; end; begin a := 5; b := 3; writeln(sum(a,b)); end;

В этой программе определена функция суммирования двух чи-сел, в которой выражение arg1 + arg2 присваивается возвращаемо-му значению функции. В начале работы программы происходит инициализация значений переменных a и b. Далее на экран печата-ется значение, возвращаемое функцией sum. При этом функции передаются два аргумента: a и b. В момент вызова функции sum вычисляются аргументы и осуществляется их неявное присваива-ние локальным переменным функции, т.е. значение переменной a присваивается переменной arg1, а значение переменной b присваи-вается переменной arg2.

Оператор ветвления if Ветвление в операторных языках программирования – точка

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

Для чего используется ветвление? Рассмотрим пример вычисления корней квадратного уравнения

ax2+bx+c=0. Как известно, решение существует только в том слу-чае, если дискриминант уравнения неотрицателен, в противном случае под корнем возникнет число меньше 0. В программе это не-

Page 40: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

40

допустимо, поскольку приведёт к ошибочному завершению работы программы. Уместно использовать ветвление. На псевдокоде алго-ритм для вычисления корней уравнения может выглядеть так: установить d = b2-4ac если d >= 0 то установить x1 = (-b + √d)/2a установить x2 = (-b - √d)/2a вывести значения x1 и x2 на экран иначе сообщить, что корней нет

Конструкция «если – то – иначе» используется для того, чтобы разделить два возможных случая и выполнить различный набор действий. В языке Паскаль имеется аналогичный оператор ветвле-ния, записываемый с помощью ключевых слов if then else. Данный оператор позволяет записать две альтернативных ветви, причём ветвь else является необязательной. В условии выбора должно сто-ять выражение, имеющее логический тип (Boolean), т.е. значением выражения должна быть либо истина, либо ложь.

Операционная семантика оператора ветвления if: 1) вычислить условное выражение оператора; 2) если получена истина, то выполнить оператор в основ-

ной ветви (после then). Если получена ложь и преду-смотрена альтернативная ветвь (после else), то выпол-нить оператор в альтернативной ветви.

Блок-схема алгоритма приведена на рис. 2. В нашем случае ос-новная ветвь содержит в себе несколько инструкций, поэтому её следует поместить в операторные скобки begin … end (так назы-ваемый составной оператор): var a,b,c,d,x1,x2: real; begin readln(a,b,c); d := b*b-4*a*c; if d >= 0 then begin x1 := (-b + sqrt(d))/(2*a); x2 := (-b – sqrt(d))/(2*a); writeln(x1, x2); end else

Page 41: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

41

writeln('no solution'); end.

Рис. 2. Блок-схема алгоритма вычисления корней квадратного уравнения

Оператор ветвления case Ещё один оператор ветвления case … of используется в тех слу-

чаях, когда существует несколько альтернативных ветвей. Напри-мер, стоит задача получить название месяца по его порядковому номеру в году. Функция, которая это делает, может выглядеть сле-дующим образом: function GetMonthName(MonthNumber: integer): string; begin case MonthNumber of 1: GetMonthName := 'Gennaio'; 2: GetMonthName := 'Febbraio'; 3: GetMonthName := 'Marzo';

d = b2-4ac

D >= 0 ?

x1 = (-b + √d)/2a

x2 = (-b - √d)/2a

Вывести «Корней нет»

Вывести x1 и x2

Да Нет

Page 42: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

42

4: GetMonthName := 'Aprile'; 5: GetMonthName := 'Maggio'; 6: GetMonthName := 'Giugno'; 7: GetMonthName := 'Luglo'; 8: GetMonthName := 'Agosto'; 9: GetMonthName := 'Settembre'; 10: GetMonthName := 'Ottobre'; 11: GetMonthName := 'Novembre'; 12: GetMonthName := 'Dicembre'; else GetMonthName := ''; end; end;

Оператор case принимает один аргумент, который может при-нимать значения порядковых типов (целых типов, типов-перечислений, логического или символьного типа). Для каждого варианта значения может быть указана своя ветвь. Также есть воз-можность описать ветвь else.

Конечно, эту задачу можно решить и с помощью оператора if, но тогда понадобится 12 вложенных операторов if вместо одного case. Кроме того, скорость работы оператора case будет гораздо выше: function GetMonthName(MonthNumber: integer): string; begin if MonthNumber = 1 then GetMonthName := 'Gennaio'; else if MonthNumber = 2 then GetMonthName := 'Febbraio'; else if MonthNumber = 3 then … else GetMonthName := ''; end; end;

Значения в операторе case можно комбинировать. Например, функция определения сезона по номеру месяца может выглядеть следующим образом: function GetSeason(MonthNumber: integer): string; begin case MonthNumber of 1,2,12: GetSeason:= 'L''inverno';

Page 43: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

43

3..5: GetSeason:= 'La primavera'; 6..8: GetSeason:= 'L''estate'; 9..11: GetSeason:= 'L''autunno'; else GetMonthName := ''; end; end;

Здесь указываются несколько вариантов либо с помощью пере-числения через запятую, либо с помощью диапазона значений (ин-тервала). Допускается указывать несколько интервалов через запя-тую.

NB! Одинарная кавычка внутри строки удваивается. Операционная семантика оператора ветвления case:

1) найти значение аргумента оператора среди вариантов; 2) если найден вариант, включающий искомое значение, то

выполнить оператор в соответствующей ветви. Если ва-риант не найден и предусмотрена альтернативная ветвь (после else), то выполнить оператор в альтернативной ветви.

Ветвление с помощью оператора case графически изображено на рис. 3.

Рис. 3. Блок-схема исполнения оператора case

Аргумент-переменная

Ветвь 1 …

Подмножество значений 1

Подмножество значений N

… Иначе

Ветвь N Ветвь N+1

Page 44: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

44

Замечания: значения в альтернативах не должны повторяться или

пересекаться между собой – это гарантирует единствен-ность выбора альтернативы;

при описании альтернативы можно использовать только константные значения и их комбинации;

если нужно указать несколько операторов в одной ветви, то необходимо использовать составной оператор begin … end после двоеточия:

case <var> of <value1>: begin … end; <value2>: … end;

Главное – научиться выбирать подходящий оператор. В силу

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

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

Выражения и операции Выражение определяет способ вычисления какого-либо значе-

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

В нотации РБНФ3 выражение можно определить следующим образом:

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

Page 45: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

45

выражение = переменная | константа | вызов_функции | (выражение) | унарная_операция выражение | выражение бинарная_операция выражение вызов_функции = имя_функции "(" [ выражение {"," выражение}] ")"

Здесь знак «|» означает выбор, квадратными скобками обрамля-ют необязательные конструкции, фигурными – повторение конст-рукции 0 и более раз, в кавычки помещают символы языка.

Из определения следует, что простейшими выражениями явля-ются переменные и константы. В Паскале к множеству переменных относятся любые переменные, объявленные в секциях var, типизи-рованные инициализированные константы, переменные, объявлен-ные в секции объявления параметров процедур и функций, а также части переменных структурированного типа, в частности, элемен-ты массивов, поля записей и объектов. Примеры простейших вы-ражений: 5 8.92 pi arr[i] a_phone_rec.fio

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

Любое выражение, поставленное в скобки, также является вы-ражением.

Перед выражением может стоять унарная операция (например, «-» или not).

Бинарные операции записываются в инфиксной форме, т.е. знак операции стоит между операндами-выражениями. Примеры: (a or b) and (not a or not b) and true (arr[i] + arr[j])*sin(alpha)

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

Page 46: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

46

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

Операции делят на несколько групп: арифметические (+, -, *, /, div, mod); логические (and, or, not, xor); битовые (shl, shr, and, or, not, xor); сравнения (=, <>, <, >, >=, <=); строковая операция конкатенации (+); операция взятия адреса значения переменной @.

Тип всего выражения можно определить по используемым опе-рациям и типу аргументов операций. Компилятор следит за тем, чтобы операция была допустима для типов аргументов. Например, аргументы операций div и mod могут быть только целочисленных типов, причём результат неизбежно имеет целочисленный тип. Операции сравнения допустимы для многих типов аргументов, но компилятор отследит, чтобы типы были одинаковы или совмести-мы. Результат операций сравнения неизбежно имеет логический тип, независимо от типов аргументов.

Задания для самостоятельного выполнения 1. Применение сложных выражений внутри if, применение case. Написать программу, определяющую существование треуголь-

ника по трём сторонам: var side1, side2, side3: integer; exist: Boolean; begin writeln('Enter 3 sides:'); readln(side1); readln(side2); readln(side3); exist := (side1 + side2 > side3) and (side2 + side3 > side1) and (side1 + side3 > side2); if exist then writeln('Triangle exists') else writeln('Triangle does not exist'); end.

2. Написать программу, производящую над двумя вводимыми операндами указанную операцию ( +, -, *, /). Обратите внимание на потенциальное деление на ноль:

Page 47: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

47

var num1, num2: integer; op: char; begin writeln('Enter 2 numbers:'); readln(num1); readln(num2); writeln('Еnter operation: '); read(op); case op of ‘+’: writeln(num1 + num2); ‘-’: writeln(num1 - num2); ‘*’: writeln(num1 * num2); ‘/’: if num2 = 0 then writeln ('Division by zero!') else writeln(num1/num2); else writeln('Unknown operation!'); end; end.

3. Для произвольной точки (x, y) вычислить выражение U в за-висимости от принадлежности области D:

,,

;,

111

22

22

DyxifDyxif

yxyxU

где D – заштрихованная область.

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

Один из вариантов программы выглядит следующим образом: var x,y,u: real; begin writeln('Enter 2 numbers:'); readln(x); readln(y); if (x*x + y*y < 1)and ((y> x)and(x>0) or (y< x)and(x<0)) or (x*x + y*y > 1)and ((y>-x)and(x<0) or (y<-x)and(x>0)) then u := sqrt(1+x*x*y*y); else u := sqr(1+x)*sqr(1+y); end.

Page 48: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

48

Глава 5. Сортировка и поиск Сортировка и поиск – важные виды деятельности человека, ко-

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

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

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

критерий сравнения существует и известен (доступ-ность);

объекты допускают сравнение по этому критерию (при-менимость);

результат сравнения двух объектов однозначен (разли-чимость объектов).

Применение критерия можно реализовать с помощью операций сравнения над двумя объектами, а принятие решений после срав-нения – с помощью оператора ветвления.

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

Рассмотрим возможную схему сортировки по возрастанию множества кубиков с числами, записанную на псевдокоде: 1. Найти кубик с минимальным числом 2. Поместить найденный кубик в начало ряда 3. Если ещё остались кубики,

Page 49: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

49

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

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

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

Если бы программа писалась на языке ассемблера, то переход к началу цикла представлял бы собой инструкцию безусловного или условного перехода jmp, je, jg и др. – от англ. jump (прыжок). Од-нако в языках третьего поколения имеются операторы цикла, кото-рые объединяют в себе проверку условия повторения, тело цикла (что повторять) и переход на новый виток цикла. При этом струк-тура программы становится прозрачной и простой.

Таким образом, операторы циклы вполне применимы для реше-ния задач сортировки и поиска.

Операторы цикла Вводятся три оператора для разных видов циклов:

с предусловием; с постусловием; с параметром.

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

Page 50: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

50

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

Оператор цикла while Оператор предназначен для описания циклов с предусловием,

записывается с помощью ключевых слов while и do и читается так: «Пока истинно <условие> выполнять <инструкции>».

Если требуется указать несколько инструкций в теле цикла, то следует использовать составной оператор begin…end: while loop_condition do begin … end;

Операционная семантика оператора while: 1) вычисление значения предусловия; 2) если полученное значение истинно, то выполнить опера-

тор в теле цикла, иначе завершить выполнение цикла (перейти к оператору, следующему за оператором цик-ла);

3) перейти на шаг 1. Оператор цикла repeat Оператор предназначен для описания циклов с постусловием,

записывается с помощью ключевых слов repeat и until и читается так: «Выполнять <инструкции> до тех пор, пока <условие> не ста-нет истинным». repeat … until exit_condition;

Операционная семантика оператора цикла repeat: 1) выполнить операторы в теле цикла; 2) вычислить значение постусловия; 3) если полученное значение ложно (!), то перейти на

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

Нужно отметить, что условие в цикле repeat имеет обратный смысл, по сравнению с условием цикла while: для цикла repeat сле-

Page 51: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

51

дует указывать условие завершения, а для цикла while – условие выполнения.

Оператор цикла for Оператор предназначен для описания циклов с параметром, за-

писывается с помощью ключевых слов for, to/downto, do и читает-ся так: «Изменяя <параметр-переменную> от <начально-го значения> до <конечного значения>, выполнять <инструкции>».

Если требуется указать несколько инструкций в теле цикла, то следует использовать составной оператор begin…end.

Параметр цикла всегда меняется на единицу на каждом сле-дующем витке цикла, а с помощью ключевых слов to и downto можно указать направление изменения. Если требуется, чтобы па-раметр цикла возрастал с каждым витком, то следует использовать ключевое слово to, при этом начальное значение должно быть не больше конечного значения параметра: for i:=1 to 7 do begin … end;

Чтобы параметр цикла убывал с каждым витком, следует ис-пользовать ключевое слово downto, при этом начальное значение должно быть не меньше конечного значения параметра: for i:=10 downto 1 do begin … end;

Операционная семантика оператора цикла for: 1) присвоить параметру цикла начальное значение; 2) если текущее значение параметра цикла не превышает

конечное значение (для цикла to), то выполнить опера-тор в теле цикла, иначе завершить выполнение цикла;

3) вычислить новое значение параметра цикла; 4) перейти на шаг 2.

Замечания: если начальное значение параметра цикла равно конеч-

ному, то выполнится один виток цикла, а если начальное значение больше конечного (для to), то ни одного;

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

Page 52: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

52

значение параметра цикла после завершения цикла счи-тается неопределённым.

Операторы управления циклами: continue, break, exit Оператор continue досрочно завершает очередной виток цикла и

переходит к новому витку. Оператор break досрочно прерывает выполнение цикла и пере-

даёт управление оператору, следующему за оператором цикла. Оператор exit досрочно прерывает выполнение процедуры или

программы. Все три оператора не имеют параметров. Правила применения операторов цикла Если требуется проверять выполнимость некоторого условия до

витка цикла, то имеет смысл использовать оператор цикла с преду-словием.

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

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

Если известно максимальное количество витков цикла и в то же время цикл следует прервать, когда условие повторения перестанет выполняться, то можно использовать как циклы с условием, так и цикл с параметром в сочетании с оператором break.

Проиллюстрируем последнее правило на двух примерах: // первый вариант i := 1; while Condition and (i <= Max) do begin … i := i+1; end; // второй вариант for i:=1 to Max do begin … if not Condition then break; … end;

Page 53: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

53

Алгоритмы сортировки и поиска Вернёмся к задаче сортировки и поиска. Чтобы построенные

схемы можно было реализовать на ЭВМ, необходимо выбрать ти-пы данных и переписать алгоритм в терминах операторного языка.

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

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

Блок-схема алгоритма поиска приведена на рис. 4. По заверше-нии алгоритма индекс элемента с минимальным значением будет храниться в переменной minIndex. Программу, реализующую этот алгоритм, можно написать как с помощью цикла while, так и с по-мощью цикла for: const Max = 200; type TCubes = array [1..Max] of integer; var cubes: TCubes; i, minIndex: integer; begin // инициализация массива … // поиск минимального элемента minIndex := 1; for i:=2 to Max do begin if cubes[i] < cubes[minIndex] then minIndex := i; end; // вывод на экран минимального значения writeln(cubes[minIndex]); end.

Page 54: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

54

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

Рис. 4. Блок-схема алгоритма поиска

Установить minIndex = индекс первого элемента массива

Установить i = индекс вто-рого элемента массива

Индекс i < индекса последнего элемента массива

Элемент с индексом i < элемента с индексом minIndex?

Установить minIndex = i

Да

Увеличить i на 1

Нет

Да Нет

Page 55: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

55

Сортировка Будем считать, что исходные данные также хранятся в массиве.

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

Построим блок-схему алгоритма сортировки (рис. 5). По завершении алгоритма индекс CountIndex равен индексу по-

следнего элемента исходного массива. Программа может выглядеть следующим образом: const Max = 200; type TCubes = array [1..Max] of integer; var cubes, sorted: TCubes; i, minIndex, CountIndex: integer; begin // инициализация массива … // поиск первого минимального элемента minIndex := 1; for i:=2 to Max do begin if cubes[i] < cubes[minIndex] then minIndex := i; end; // вставка первого элемента sorted[1] := cubes[minIndex]; for CountIndex := 2 to Max do begin // поиск минимального элемента большего, // чем sorted[CountIndex-1] minIndex := 1; for i:=2 to Max do begin if (cubes[i] > sorted[CountIndex-1]) and (cubes[i] < cubes[minIndex]) then minIndex := i; end; // вставка sorted[CountIndex] := cubes[minIndex];

Page 56: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

56

end; // вывод результата на экран … end.

Рис. 5. Блок-схема алгоритма сортировки

Найти индекс минимального элемента исходного массива

Поместить элемент в начало резуль-тирующего массива

СountIndex < индекса по-следнего элемента массива

Да Нет

Увеличить CountIndex на 1

Найти индекс минимального элемента исходного массива, который больше элемента, помещённого

результирующий массив последним

Поместить элемент в результирующий массив по индексу CountIndex

Установить CountIndex = 2

Page 57: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

57

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

Оценка сложности алгоритмов и оптимизация При использовании дополнительного массива для сортировки

расход памяти был увеличен в два раза. Обычно к этому прибегают для получения выигрыша в скорости. Так, в методах сортировки без дополнительного массива (в том же объёме памяти), требуется постоянно менять местами элементы массива.

Два ресурса – память и время – являются основными при оценке алгоритмов. При сегодняшнем уровне развития вычислительной техники стоимость памяти упала настолько, что ресурс памяти пе-рестал быть дефицитным и оптимизация по объему памяти отошла на второй план. Если раньше нехватка памяти приводила к боль-шим изменениям архитектуры программ, то сейчас это влияние минимизировано. Главный невосполнимый ресурс – время – вышел на первый план. Несмотря на то, что быстродействие техники вы-росло на порядки, это не повод писать медленные программы и транжирить память. Следует помнить, что выросли размер и слож-ность программ, а компьютеры теперь решают гораздо более слож-ные задачи.

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

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

Page 58: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

58

большинство современных ЭВМ имеет общие архитектурные кор-ни, что снижает значимость этого различия.

Оценим сложность алгоритма поиска минимального элемента в массиве: minIndex := 1; for i:=2 to Max do begin if cubes[i] < cubes[minIndex] then minIndex := i; end;

Емкостная сложность вычисляется просто: требуется n = Max ячеек для хранения элементов массива, а также ячейки для пере-менных minIndex, I и константы Max. Если все они типа Integer, под который отводятся два байта, то всего получается (n + 3)*2 = 2*n+6 байт.

Оценим временную сложность алгоритма. Очевидно, в любом случае выполняются следующие инструкции: одно начальное при-сваивание, n–1 присваиваний значения счетчику цикла i и столько же сравнений в условии оператора if. Если копать глубже, то для витка цикла требуется также проверка условия выполнения цикла i<=n, переход к следующему витку и т.п, однако, как будет показа-но далее, по большому счёту всё это не имеет большого значения для общей сложности.

Присваивание внутри ветвления выполняется в зависимости от истинности условия. Можно предположить, что в среднем при ве-роятности 50/50 будет выполнено (n–1)/2 присваиваний. Обраще-ние к элементу массива по индексу, вообще говоря, требует неко-торого времени для вычисления адреса ячейки памяти, но для про-стоты будем считать доступ по индексу мгновенным, тем более, что счетчик цикла for неявно как раз и соответствует адресу ячейки памяти с элементом массива.

Тогда всего получается 1+(n–1)+(n–1)+(n–1)/2 = 2.5*n–1.5 ко-манды. Если предположить, что присваивание и сравнение выпол-няются за один квант времени, то в среднем потребуется ave = 2.5*n–1.5 кванта времени.

Рассмотрим некоторые экстремальные случаи. Если массив от-сортирован по возрастанию, то минимальный элемент является первым элементом массива. В этом случае присваивание внутри ветвления вообще не выполняется, т.е. суммарное количество ко-

Page 59: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

59

манд равно min = 1+(n–1)+(n–1) = 2*n–1. Это нижняя оценка слож-ности алгоритма – минимально возможное время выполнения. Если массив отсортирован по убыванию, то минимальный элемент будет последним в массиве, а присваивание внутри ветвления будет вы-полняться на каждом витке цикла. Т.е. суммарное количество ко-манд равно max = 1+(n–1)+(n–1)+(n–1)=3*n–2. Это верхняя оценка сложности алгоритма.

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

В построенном алгоритме каждый элемент массива просматри-вается один раз, что является ценным свойством. Впрочем, для ал-горитма поиска это неудивительно.

Оценим сложность алгоритма сортировки. Емкостная сложность складывается из двух массивов длины n, трёх переменных и кон-станты, что даёт (2*n + 4)*2 = 4*n + 8 байт.

Для оценки временной сложности этого алгоритма удобно ис-пользовать предыдущие оценки для алгоритма поиска, поскольку он часто здесь используется. В любом случае выполняется алго-ритм поиска, далее присваивание, далее n–1 раз в цикле выполня-ется алгоритм поиска и присваивание. Таким образом, всего полу-чаем n раз для алгоритма поиска + n присваиваний:

ave = n * (2.5*n–1.5) + n = 2.5 * n2 – 0.5 * n min = n * (2*n–1) + n = 2 * n2 max = n * (3*n–2) + n = 3 * n2 – n Замечание: часто за среднюю оценку можно взять среднее

арифметическое минимальной и максимальной оценки. Методы оценки сложности алгоритмов описаны в замечательной книге До-нальда Кнута «Искусство программирования».

Page 60: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

60

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

Почему так получилось? Из-за вложенности циклов каждый элемент исходного массива теперь просматривается не один, а n раз (по числу витков цикла верхнего уровня), т.е. каждый уровень вложенности циклов даёт увеличение сложности на порядок. По-лученный алгоритм сортировки является одним из наиболее мед-ленных. Существуют алгоритмы сортировки меньшей сложности, например O(n*log(n)). Если сравнить графики этих функций, то будет видно, что парабола растет гораздо быстрее.

Page 61: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

61

Глава 6. Циклы и рекуррентные соотношения

Сортировка в постоянном объеме памяти4 Задача: реализовать алгоритм сортировки в том же объеме па-

мяти. Ограничение на объем памяти означает, что использовать дополнительный массив запрещается.

Алгоритм строится следующим образом: 1) найти минимальный элемент массива. Первый и мини-

мальный элемент поменять местами. Запомнить количе-ство элементов в отсортированной последовательности (на первом шаге это один элемент);

2) начиная с конца растущей отсортированной последова-тельности, выполнить действие 1. Повторять до тех пор, пока отсортированная последовательность не достигнет длины n–1. Оставшийся элемент будет максимальным.

Прежде чем приступать к реализации алгоритма рассмотрим за-дачу обмена значениями двух переменных a и b. Очевидно, фраг-мент кода a := b; b := a; не даст должного результата, так как после первого присваивания значение a будет потеряно. Можно воспользоваться дополнитель-ной переменной buffer для сохранения a: buffer := a; a := b; b := buffer;

Однако гораздо интереснее решить эту задачу без дополнитель-ной переменной: a := a + b; b := a - b; a := a - b;

4 Данный подраздел – продолжение предыдущей главы. Перенесён в гла-ву 6 из соображений сбалансированности глав по времени изложения.

Page 62: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

62

Такое решение чревато переполнением типа при вычислении суммы или разности, т.е. не является универсальным. Здесь можно вспомнить, что числа представляются в двоичной форме, над кото-рой определены безопасные битовые операции. Применяя опера-цию «исключающее или»: a := a xor b; b := a xor b; a := a xor b;

Проверка. Пусть a = 510 = 01012, b = 310 = 00112. Тогда a = 0101 xor 0011 = 0110 b = 0110 xor 0011 = 0101 = 5 a = 0110 xor 0101 = 0011 = 3

Вернемся к алгоритму сортировки. Потребуются два вложенных цикла, причем число витков второго цикла будет постоянно сокра-щаться с ростом отсортированной последовательности. Поскольку последовательность растет слева, имеет смысл двигать нижнюю границу счетчика цикла. Тогда const Max = 200; type TCubes = array [1..Max] of integer; var cubes: TCubes; i, min, sorted: integer; begin // инициализация массива … for sorted:=1 to Max-1 do begin // поиск минимального элемента, начиная с sorted min := sorted; for i := sorted+1 to Max do begin if cubes[i] < cubes[min] then min := i; end; // обмен cubes[sorted] := cubes[sorted] xor cubes[min]; cubes[min] := cubes[sorted] xor cubes[min]; cubes[sorted] := cubes[sorted] xor cubes[min]; end; // вывод результата на экран … end.

Page 63: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

63

По сравнению с алгоритмом сортировки выборкой, построен-

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

Примеры инициализации массива Когда алгоритмическая часть завершена, пришло время вспом-

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

Заполнить исходный массив можно несколькими способами: посредством ввода с клавиатуры (если массив не очень большой) или же случайными числами. Рассмотрим оба варианта инициали-зации массива и вывод массива на экран с помощью оператора цикла: begin // инициализация массива с клавиатуры for i:=1 to Max do begin write('element ', i, ' = '); readln(cubes[i]); end; // инициализация массива случайными числами randomize; // инициализация датчика случайных чисел for i:=1 to Max do begin cubes[i] := random(100); end; // вывод на экран элементов массива через пробел for i:=1 to Max do begin write(cubes[i], ' '); end; end.

Page 64: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

64

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

граммы в оператор цикла repeat until. Таким образом, программа выполнится как минимум один раз, а далее всё будет зависеть от желания пользователя. Например: var s: string; begin repeat … // тело программы // запрос на повтор writeln('run once again (yes/no)? '); readln(s); until s ='nо'; end.

Пример вложенных циклов Вывести на экран: 1 1 3 1 3 5 1 3 5 7 1 3 5 7 9 Для решения этой задачи посмотрим на внешний вид результата

– треугольная матрица со стороной n=5. Матрица – квадрат, а зна-чит, предполагаемая сложность алгоритма – квадратичная, а это в свою очередь может говорить о наличии двух вложенных циклов в алгоритме. Действительно, один цикл может отвечать за перебор строк, а второй – за перебор столбцов, причем элементы выше главной диагонали печататься не должны. Далее можно заметить, что значение элемента можно однозначно вычислить по его поло-жению в матрице. Пусть i[1..5] – номер строки, а j[1..5] – номер столбца, тогда aij = 2* j – 1. В принципе алгоритм готов. var i,j,n: integer; begin n:=5; for i:=1 to n do begin for j:=1 to n do begin if j<=i then write(2*j–1); end; writeln;

Page 65: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

65

end; end.

После того как наивный алгоритм завершен, можно взяться за его оптимизацию. Условие j<=i, по сути, ограничивает число вит-ков второго цикла, так как никаких действий вне if не выполняется. Получаем var i,j,n: integer; begin n:=5; for i:=1 to n do begin for j:=1 to i do write(2*j–1); writeln; end; end.

Общее число витков второго цикла теперь уменьшилось c n2 до 1+2+…+n = (n+1)*n/2. Сложность, тем не менее, осталась квадра-тичной, но по сравнению со сложностью прежней версии заметно улучшилась.

Понимание программ Что напечатает следующая программа?

const n=30; m=10; var i: integer; begin for i:=2 to m do if (n mod i =0) and (m mod i =0) then writeln(i,` `); end.

Для чисел от 2 до m=10 программа будет проверять, является ли это число делителем n и m (нулевой остаток). Т.е. программа напе-чатает общие делители n и m. Поскольку используется writeln, чис-ла будут выведены в столбик: 2 5 10

Page 66: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

66

Рекуррентные соотношения и их реализация в виде циклических программ

Рассмотрим задачу вычисления экспоненты ex. На заре развития ЭВМ тригонометрические функции приходи-

лось реализовывать программно. Когда появились языки третьего поколения, то вместе с компиляторами начали поставляться стан-дартные библиотеки, включающие набор математических функций. С появлением математических сопроцессоров большинство функ-ций были реализованы на аппаратном уровне. Сейчас любой про-цессор умеет вычислять эти и другие функции, но сначала было разложение в ряд Тейлора. Такие функции, как exp, ln, sin, tg и др., аппроксимируются многочленом, который можно вычислить лишь с некоторой точностью в силу его бесконечности и ограниченности машинного представления чисел. Однако этой точности оказывает-ся достаточно для прикладных вычислений.

Итак,

1

2

!...

!...

!2!11

n

nnx

nx

nxxxe .

Можно вычислить сумму последовательности, ограничив число n. Причем чем больше предел суммирования, тем точнее результат. Возведение в степень и вычисление факториала можно реализовать с помощью цикла; сложение членов последовательности также удобно реализовать в виде цикла. Рассмотрим один из способов реализации: var i,j, n, fact, pow: integer; sum, x: real; begin readln(x); sum := 1; n := 100; for i := 1 to n do begin // вычисление степени x и факториала pow := 1; fact := 1; for j := 1 to i do begin pow := pow * x; fact := fact * j; end; // добавление к сумме очередного члена ряда

Page 67: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

67

sum := sum + pow/fact; end; writeln('exp(x)=',sum); end.

Оценим сложность этого алгоритма. Основной характеристикой задачи здесь будет число суммируемых членов последовательности n, так как именно от него зависит количество вычислений. Видно, что число витков внутреннего цикла зависит от значения счётчика внешнего цикла: на первом витке внешнего цикла внутренний цикл выполнится один раз, на втором – два раза и т.д. По методу Гаусса в общей сложности будет выполнено (n+1)*n/2 витков внутреннего цикла. Таким образом, суммарное число выполняемых команд равно 2+n*6+(n+1)*n/2*5 = 2.5n2+8.5n+2.

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

nxaa nn 1 .

Это соотношение называется рекуррентным (от лат. recurro – возвращаться). Оно позволяет вычислить очередной член последо-вательности гораздо быстрее. Например, рекуррентное соотноше-ние для арифметических прогрессий выглядит так: an = an-1+b, для геометрических прогрессий: an = an-1*q, для факториала: an = an-1*n, для степени: an = an-1*x. При этом всегда необходимо задавать пер-вый член последовательности (её базис).

Таким образом, в программе на каждом витке основного цикла очередной член последовательности будет вычисляться на основе предыдущего: var i,j, n: integer; sum, x, member: real; begin readln(x); member := 1; sum := member; n := 100; for i := 1 to n do begin member := member * x / i; sum := sum + member;

Page 68: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

68

end; writeln('exp(x)=',sum); end.

Сложность этого алгоритма линейная, так как от вложенного цикла удалось избавиться: 3+n*(3+2) = 5*n+3 = O(n). Это яркий пример упрощения вложенных циклов.

Оптимизация Зачем оптимизировать программу, когда современные компью-

теры обладают огромными аппаратными мощностями? На самом деле разница в сложности огромна: в оптимизированном варианте для расчета суммы тысячи членов ряда требуется выполнить по-рядка тысячи инструкций, а в наивном алгоритме – уже порядка миллиона (коэффициентами и меньшими степенями можно пре-небречь). А теперь представьте, что разработанная функция вызы-вается миллионы раз в каком-либо цикле внутри приложения, на-пример компьютерной игры. Вспомните работу в Word: пока чело-век печатает, программа автоматически пересчитывает число стра-ниц, применяет нужные параметры форматирования, выравнивает текущую строку по ширине страницы, периодически выполняет автосохранение и проверку орфографии, подчеркивая незнакомые слова. Одним словом, всегда стремитесь оптимизировать свой ал-горитм. Как сказал А.Д. Мишин, «делайте хорошо, плохо само по-лучится». Возьмём это за правило.

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

С точностью до epsilon Хотя алгоритм и стал эффективней, всё же число n было задано

совершенно произвольным образом. Для задач, где точность вы-числений играет решающую роль, такой подход неприемлем. Раз-ложение sin(x) представляет собой бесконечную сумму убывающе-го ряда, где каждый последующий член ряда вносит всё меньший вклад в общую сумму. В теории пределов говорят о вычислении

Page 69: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

69

сумм с точностью до некоторого малого , при этом суммирование прекращается при выполнении условия |an-an-1| < либо, как вари-ант, более простого условия |an| < . В нашей программе имеет смысл заменить цикл for на цикл с постусловием repeat until: var i,j: integer; sum, x, member, epsilon: real; begin readln(x); member := 1; sum := member; epsilon := 0.0001; i := 1; repeat member := member * x / i; sum := sum + member; i := i+1; until member < epsilon; writeln('exp(x)=',sum); end.

Эта программа посчитает сумму с точностью до 4-го знака по-сле запятой.

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

В качестве самостоятельного упражнения напишите программу, рассчитывающую sin(x) по формуле

...)!12(

)1(...!5!3

)sin(12

153

mxxxxx

mm

Идея для решения: из формулы для an найти рекуррентное соот-ношение для вычисления an через an-1, т.е. зависимость an = f(an-1). В программе для вычисления экспоненты достаточно будет изменить всего пару строк.

Page 70: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

70

Более сложные рекуррентные соотношения В рассмотренных соотношениях последующий член зависел

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

Задача о кроликах и числа Фибоначчи Задача формулируется следующим образом. Как-то хозяин ку-

пил пару кроликов. В первый месяц они росли, а во второй месяц принесли еще пару кроликов. Через месяц начала плодоносить вы-росшая пара кроликов, и первая пара также принесла потомство. Каждый месяц каждая взрослая пара кроликов приносила еще одну пару кроликов. Сколько пар кроликов будет у хозяина через n ме-сяцев?

Леонардо из Пизы, сын Боначчи, обнаружил, что численность пар кроликов в любом месяце может быть рассчитана с помощью соотношений

fib(1) = 1 fib(2) = 1 fib(n) = fib(n–1) + fib(n–2) Так был открыт ряд чисел (ряд Фибоначчи), каждое из которых

представляет собой численность пар кроликов в i-м месяце: 1 1 2 3 5 8 13 21… При построении программы простейшим решением было бы за-

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

Тогда программу можно построить следующим образом: var i,n, f1,f2,f3: integer; begin readln(n); f1 := 1; f2 := 1; f3 := 1; // расчёт

Page 71: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

71

for i:=3 to n do begin f3 := f1 + f2; f1 := f2; f2 := f3; end; writeln('rabbits = ', f3); end.

Этому алгоритму, очевидно, свойственна линейная временная сложность и константная емкостная сложность, так как удалось обойтись без массива!

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

Многочлен Чебышёва Рекуррентное соотношение для многочленов Чебышёва выгля-

дит следующим образом: T0(x) = 1; T1(x) = x; Tn(x) = 2xTn-1(x) – Tn-2(x). Согласно указанным соотношениям T2(x) = –1 + 2x2; T3(x) = 2x*(–1 + 2x2) – x = –3x + 4x3; T4(x) = 2x*(–3x + 4x3) – (–1 + 2x2) = 1 – 8x2 +8x4; … Задача состоит в нахождении коэффициентов k-го многочлена

Чебышёва Tk(x). Идеи для решения. Коэффициенты многочлена можно сохранять

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

для k = 0 – [1, 0, …] для k = 1 – [0, 1, 0, …] для k = 2 – [–1, 0, 2, 0, …] для k = 3 – [0, –3, 0, 4, 0, …] для k = 4 – [1, 0, –8, 0, 8, 0,…] … Задача сводится к вычислению элементов массива для очеред-

ного многочлена Tn на основе двух массивов Tn-1 и Tn-2, хранящих

Page 72: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

72

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

1) умножение многочлена на константу равносильно ум-ножению всех элементов массива на эту константу;

2) умножение многочлена на переменную х равносильно сдвигу каждого элемента вправо на 1 разряд;

3) разность многочленов равносильна разности соответст-вующих элементов в массивах коэффициентов.

Таким образом, Tn[i] = Tn-1[i-1]*2 – Tn-2[i] для всех i, кроме 0, а Tn[0] = –Tn-2[0]. Потребуются два цикла: в главном будет нарастать счетчик k, а во вложенном – рассчитываться по выведенной фор-муле коэффициенты многочлена Tk.

Page 73: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

73

Глава 7. Операции над массивами и матрицами

Сдвиг элементов массива Дан массив чисел. Необходимо сдвинуть все элементы массива

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

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

Для случая сдвига вправо разумно организовать проход по мас-сиву с конца. Алгоритм тривиален: var a: array [1..n] of integer; i: integer; begin … for i:=n downto 2 do a[i] := a[i-1]; a[1] := 0; … end.

Для циклического сдвига необходимо сначала запомнить по-следний элемент: var a: array [1..n] of integer; buf,i: integer; begin … buf := a[n]; for i:=n downto 2 do a[i] := a[i-1]; a[1] := buf; … end.

Для сдвига влево меняется направление прохода по массиву и граничные условия.

Page 74: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

74

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

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

Чтобы не усложнять алгоритм лучше воспользоваться дополни-тельным массивом, если это не запрещается условием задачи. Не-обходимо построить взаимно-однозначное соответствие между элементами массивов, т.е. получить формулу для расчета индекса newi элемента в новом массиве по его индексу i в исходном масси-ве. Для обычного сдвига newi = i + k, но для циклического сдвига необходимо предусмотреть случай, когда newi становится больше N. Модифицируем формулу следующим образом: newi = (i + k) mod N, где i, newi [1..N]. Действительно, если i + k = N + 3, то (i + k) mod N + 1 = 4. Но если i + k = N, то должно получиться N, а у нас получается 0. Тогда модифицируем формулу следующим образом: newi = (i + k – 1) mod N + 1. var a,b: array [1..n] of integer; i,k: integer; begin … for i:=1 to n do b[(i+k-1) mod n + 1] := a[i]; … end.

Вообще, если бы индекс массива i [lo..hi], то формула выгля-дела бы так:

newi = (i + k – lo) mod (hi – lo + 1) + lo.

Реверсирование массива Рассмотрим произвольный массив. Необходимо построить дру-

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

Если можно использовать дополнительный массив, то формула для расчёта индекса такова: newi = n – i + 1. В противном случае можно построить следующую процедуру: сначала обменять места-

Page 75: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

75

ми первый и последний элементы массива, далее второй и предпо-следний и т.д.

Однако если построить цикл от 1 до n, то элементы поменяются местами дважды и получится исходный массив, поэтому обмен следует вести только до середины массива, которая определяется как n div 2. Для нечетных n – это индекс элемента, предшествую-щего центральному элементу, который останется на своём месте: var a: array [1..n] of integer; i,buf: integer; begin … for i:=1 to n div 2 do begin buf := a[i]; a[i] := a[n-i+1]; a[n-i+1] := buf; end; … end.

Слияние массивов Даны два массива чисел длины N, упорядоченных по возраста-

нию. Следует получить массив длины 2*N, также упорядоченный по возрастанию и состоящий из элементов исходных массивов.

Для решения этой задачи явно требуются операторы цикла. Введем три счетчика i, j, k, где i, j [1..N] – текущие позиции в ис-ходных массивах, а k [1..2*N] – текущая позиция в новом масси-ве.

Для определения того, из какого массива копировать очередной элемент, на каждом витке цикла необходимо сравнивать текущие элементы исходных массивов и копировать наименьший. После этого необходимо увеличивать счетчик того массива, из которого скопирован элемент. Так как должны быть скопированы все эле-менты исходных массивов, то по окончании работы программы счетчики должны принять максимальные значения. Тогда конечное состояние работы программы можно записать как (i=n) and (j=n). Ясно, что в этом случае k = 2*n. Отрицание этого выражения мож-но считать условием выполнения цикла:

not ((i=n) and (j=n)) not (i=n) or not (j=n) (i<=n) or (j<=n)

Page 76: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

76

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

Здесь есть подводный камень: массивы не могут закончиться одновременно. Один из массивов всегда закончится раньше друго-го и тогда программа все же обратится к несуществующему эле-менту. Тогда необходимо модифицировать условие выполнения цикла на (i<=n) and (j<=n). В этом случае цикл прекратится, как только один из счетчиков достигнет n. Тогда останется просто ско-пировать оставшиеся элементы другого массива: var a,b: array [1..n] of integer; c: array [1..2*n] of integer; i,j,k: integer; begin … i:=1; j:=1; k:=1; while (i<=n) and (j<=n) do begin if a[i] < b[j] then begin c[k] := a[i]; i := i+1; end else if a[i] > b[j] then begin c[k] := b[j]; j := j+1; end else begin c[k] := a[i]; k := k+1; c[k] := a[i]; // второй раз тоже самое i := i+1; j := j+1; end; k := k+1; end; // дописываем остаток, если есть if i>n then while (j<=n) do begin c[k] := b[j]; j := j+1;

Page 77: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

77

k := k+1; end else while (i<=n) do begin c[k] := a[i]; i := i+1; k := k+1; end; … end.

В общем случае программа копирует один или более элементов из одного массива, затем из другого, после опять из первого и т.д., сохраняя порядок чисел, пока оба массива не закончатся.

Работа с матрицами Матрица – массив, обладающий не менее чем двумя измерения-

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

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

ли равны 0. Если хотя бы один элемент ненулевой, то проверку ос-тальных элементов можно не делать: var matrix: array[1..n,1..n] of integer; i,j: integer; triangle: boolean; begin … triangle := true; // начинаем сразу со второй строки for i:=2 to n do begin // с первого столбца до диагонали for j:=1 to i-1 do begin if matrix[i,j] <> 0 then begin triangle := false; break; end;

Page 78: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

78

end; // break прерывает только тот цикл, в котором находится if triangle then break; end; … end.

Генерация матрицы заданного вида 1. Дано целое число n>1 и действительное число х. Написать

процедуру, заполняющую матрицу вида

1 x x2 . . . xn-1 xn x 0 0 . . . 0 xn-1 x2 0 0 . . . 0 xn-2 xn-1 0 0 . . . 0 x xn xn-1 xn-2 . . . x 1

Элементы, расположенные по периметру образуют степенной ряд, при этом налицо симметрия данных элементов. Это даёт воз-можность заполнить их в одном цикле, используя рекуррентное соотношение: var matrix: array[0..n,0..n] of integer; i,j: integer; x,p: real; begin readln(x); p := 1; // заполнение элементов по периметру матрицы for i:=0 to n do begin matrix[i,0] := p; // первый столбец matrix[0,i] := p; // первая строка matrix[n-i,n] := p; // последний столбец matrix[n,n-i] := p; // последняя строка p := p*x; end; // остальное заполняется нулями for i:=1 to n-1 do for j:=1 to n-1 do matrix[i,j]:=0; … end.

Page 79: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

79

2. Используя целочисленную квадратную матрицу А, построить матрицу В того же типа, где bij определяется следующим образом. Через аij проведем в А диагонали, параллельные главной и побоч-ной диагоналям матрицы; bij равен максимальному элементу в об-ласти матрицы А, ограниченной снизу проведенными диагоналями.

Пусть матрицы объявлены следующим образом: var A,B: array [0..9, 0..9] of integer;

Если провести аналогию между матрицей и системой коорди-нат, то элемент с индексом [0, 0] будет центром координат, ось абсцисс соответствует столбцам, ось ординат – строкам (направле-на вниз). Тогда диагонали в матрице для каждого элемента brs мож-но описать уравнениями прямых на плоскости:

i – r = j – s; i – r = – j + s. Здесь r, s – координаты очередного элемента матрицы B. Каж-

дый элемент brs выбирается из соответствующей области матрицы А как

brs = max{aij}, где i[0..n–1], j[0..n–1], i – r <= j – s, i – r <= –j+s. При поиске максимального элемента для очередного brs имеет

смысл инициализировать переменную max элементом, стоящим на пересечении соответствующих диагоналей матрицы A, т.е. ars. По-нятно, что в область входят только элементы, находящиеся на строках от 0 до r, причем в строке r это единственный элемент ars. Тогда границами цикла по строкам будут 0 и r–1, так как элемент ars учтён при инициализации. Границами цикла по столбцам в об-щем случае будут 0 и n, а ограничения будут проверяться явно: var A,B: array[0..n,0..n] of integer; i,j,r,s,max: integer; begin … // циклы для всех элементов матрицы B, счетчики r и s for r:=0 to n do for s:=0 to n do begin max := A[r,s]; // выбор максимального из соответствующей области в А for i:=0 to r-1 do for j:=0 to n do if (i <= j-s+r) and (i <= -j+s+r)

Page 80: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

80

and (A[i,j] > max) then max := A[i,j]; B[r,s] := max; end; … end.

Этому алгоритму свойственна сложность O(n4), так как выпол-няются четыре вложенных цикла. Однако для матриц алгоритм не так уж плох – он не хуже, чем алгоритмы квадратичной сложности для одномерных массивов.

3. Умножение матриц. Пусть имеется матрица A[M, K] и мат-рица B[K, N]. Получить матрицу C[M, N], где

]..1[],..1[,1

NjMibacK

kkjikij

.

Идея для решения: для заполнения матрицы C потребуется два вложенных цикла по i для строк и по j для столбцов, а чтобы вы-числить очередной элемент, нужен ещё один вложенный цикл по k.

Не страшно, если программа не сразу заработает. Помните: «Программа – зеркало глупости программиста».

Page 81: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

81

Глава 8. Структурирование программ Программы сложной структуры

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

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

Простое выделение блока программы и синтаксическое его оформление в виде процедуры поможет избежать чистого дублиро-вания кода в программе. Например: var a: array[1..10] of integer; i: integer; // определение процедуры с именем Print procedure Print;

Page 82: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

82

begin for i:=1 to 10 do write(a[i], ' '); writeln; end; begin … // заполнение массива a i := 4; Print; // вызов процедуры по имени // здесь i уже не равно 4 … // например, сортировка массива a Print; end.

Однако сильное сцепление остаётся, поскольку процедура и ос-новная программа работают с одними и теми же переменными. В данном случае процедура изменяет значение переменной i, что мо-жет сильно повлиять на работу программы после вызова Print. По-лучается, что качество программы не улучшилось. Побочного эф-фекта можно избежать, если объявить необходимые переменные внутри процедуры. Это можно сделать следующим образом: procedure Print; var i: integer; begin for i:=1 to 10 do write(a[i], ' '); writeln; end;

Теперь в процедуре объявлена своя переменная i. Хотя она име-ет то же имя, что и переменная в основной программе, конфликта имён не возникает, и переменная i перекрывает переменную в ос-новной программе.

Глобальные и локальные переменные. Область видимости переменных

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

Локальные переменные – переменные, объявленные внутри процедуры или функции. Память под значения локальных пере-

Page 83: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

83

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

Формальные и фактические параметры Построенная процедура Print до сих пор не слишком полезна,

так как умеет выводить на печать лишь массив а, да еще и только длины 10. Чтобы обобщить процедуру на все подобные массивы, необходимо объявить новый тип данных и параметризовать проце-дуру: const n = 10; type tarray = array[1..n] of integer; var a,b: tarray; procedure Print(mas: tarray); var i: integer; begin for i:=1 to n do write(mas[i], ' '); writeln; end; begin … // заполнение массива a Print(a); … // заполнение массива b Print(b); end.

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

Page 84: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

84

Теперь процедура Print умеет выводить на экран любой массив типа tarray и использует глобальную константу n, что вполне до-пустимо.

Что же происходит при вызове процедуры? Параметр mas пре-вращается в дополнительную локальную переменную, инициали-зируемую значением той переменной, которая передается в качест-ве параметра, т.е. происходит неявное присваивание или копирова-ние значения. Передаваемые переменные называют фактическими параметрами, а параметры в определении процедуры – формаль-ными.

Способы передачи параметров Аналогично процедуре Print для вывода массива можно напи-

сать процедуру Init для ввода массива: const n = 10; type tarray = array[1..n] of integer; var a,b: tarray; procedure Init(mas: tarray); var i: integer; begin randomize; for i:=1 to n do mas[i] := random(100)-50; end; begin Init(a); Init(b); end.

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

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

Page 85: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

85

procedure Init(var mas: tarray); var i: integer; begin randomize; for i:=1 to n do mas[i] := random(100)-50; end;

Тогда при вызове процедуры не будет создаваться дополнитель-ной переменной, а параметр mas станет синонимом переменной, передаваемой в качестве параметра.

Существует второй способ передачи параметра по ссылке, при котором значение передаваемой переменной менять нельзя. Для этого перед объявлением параметра следует указать модификатор const: procedure Print(const mas: tarray); var i: integer; begin for i:=1 to n do write(mas[i], ' '); writeln; end;

Здесь параметр mas также становится синонимом передаваемой по ссылке переменной, но менять её значение уже нельзя. Данный способ позволяет снизить накладные расходы на вызов процедуры: новая переменная не создаётся, и копирование не происходит.

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

В общем случае в заголовке процедуры может находиться спи-сок определений параметров, перечисляемых через точку с запя-той. При этом для каждого из них задаются имя, тип и способ пе-редачи. Хорошим стилем принято считать помещение объявлений параметров, передаваемых по значению, до объявлений парамет-ров, передаваемых по ссылке. Например: var res: integer; procedure Sum(a: integer; b: integer; var c: integer); begin c:=a+b;

Page 86: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

86

end; begin Sum(3,5,res); res := res * 8; end.

Для краткости параметры одного типа можно перечислить через запятую в одном объявлении: procedure Sum(a,b: integer; var c: integer);

Однако параметры, передаваемые по ссылке, должны быть объ-явлены по отдельности.

Если процедура имеет один параметр, передаваемый по ссылке, то имеет смысл оформить её в виде функции, возвращающей зна-чение данного параметра. При этом параметр исчезает, а для функ-ции в конце заголовка указывается тип возвращаемого значения через двоеточие. Чтобы вернуть значение из функции, нужно име-ни функции присвоить вычислимое выражение: function Sum(a, b: integer): integer; begin Sum := a+b; end;

Определение подпрограммы в виде функции позволяет сокра-тить текст основной программы, так как функция может непосред-ственно входить в выражение в том месте, где требуется результат её вычислений: begin res := Sum(3,5) * 8; end.

Следует предостеречь от использования имени функции в каче-стве локальной переменной. Хорошим тоном считается однократ-ное использование имени функции при возврате результата.

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

Page 87: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

87

function Calculate: integer; var i: integer; begin Calculate := 67; // далее вместо подстановки 67 функция вызовет себя i := Calculate * 7; Calculate := i; end;

Гораздо лучше ввести дополнительную локальную переменную и в конце вычислений присвоить функции её значение: function Calculate: integer; var i,j: integer; begin j := 67; i := j * 7; Calculate := i; end;

Вложенные подпрограммы Процедура (функция) может быть определена внутри другой

процедуры (функции). В этом случае вложенная функция должна размещаться между заголовком и телом другой процедуры (функ-ции): procedure proc1(a: integer); procedure proc2(a,b: integer); var … begin // здесь видны глобальные переменные, // параметры родительской функции, // собственные параметры и локальные переменные … end; var … begin // здесь видны глобальные переменные, // собственные параметры и локальные переменные … end;

Page 88: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

88

Следует отметить, что параметры и локальные переменные вложенной процедуры будут перекрывать одноименные параметры родительской функции и глобальные переменные.

Модульная структура программ Модули – более крупные структурные единицы программы.

Они имеют имя и могут содержать совокупность процедур и функ-ций, а также ряд дополнительных типов данных, переменных, кон-стант. В языке Паскаль модули определяются в отдельных файлах и начинаются с объявления module <имя>. К основной программе модули подключаются с помощью ключевого слова uses <список имён модулей>. В модули имеет смысл объединять логически свя-занные группы процедур и функций. Программист может создавать свои модули (например, для работы с мышкой, работы со специ-альными структурами данных и т.п.), а также использовать модули стандартной библиотеки языка. Модули могут использовать другие модули.

Некоторые стандартные модули Любая программа на языке Паскаль, созданная в среде Turbo

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

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

Модуль Graph предназначен для работы с экраном в графиче-ском режиме.

Структура модуля 1) объявление модуля (unit); 2) объявление используемых модулей (uses); 3) интерфейсная секция модуля (interface); 4) секция реализации модуля (implementation); 5) секция инициализации (begin … end).

Пример модуля, предоставляющего математические функции: unit MathUtils; interface

Page 89: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

89

function Sum(a,b: real):real; function Mult(a,b: real):real; implementation function Sum(a,b: real):real; begin Sum := a+b; end; function Mult(a,b: real):real; begin Mult:= a*b; end;

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

лированы все используемые модули. Скомпилированный модуль имеет расширение *.tpu.

Пример использования модуля: program math; uses MathUtils; // подключение модуля MathUtils begin writeln(Mult(5,Sum(3,8)); // вызов функций модуля end.

При запуске основной программы система осуществляет поиск и подключение скомпилированных модулей к программе. Поиск модулей и динамических библиотек выполняется в заранее опреде-ленных местах дискового пространства: системный каталог среды, системный каталог операционной системы, текущий каталог, ката-логи для поиска, указанные в среде (для Turbo Pascal – с помощью меню Options/Directories), каталоги, указанные в переменной $PATH операционной системы.

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

Page 90: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

90

Задания по процедурам и функциям 1. Написать процедуру, возвращающую разность средних ариф-

метических значений двух вещественных массивов из десяти эле-ментов. Вариант решения: const n = 10; type tarray = array[1..n] of real; function diff(const a, b: tarray); var i: integer; sum, rez: real; begin sum := 0; for i:=1 to n do sum := sum + a[i]; rez := sum/n; sum := 0; for i:=1 to n do sum := sum + b[i]; rez := rez – sum/n; diff := rez; end;

После оптимизации получаем решение, в два раза более эффек-тивное: function diff(const a, b: tarray); var i: integer; sum: real; begin sum := 0; for i:=1 to n do sum := sum + a[i] – b[i]; diff := sum/n; end;

2. Что напечатает следующая программа? var a, b, c, d, e: integer; procedure X(a, b, c: integer; var d: integer);

Page 91: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

91

var e: integer; 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

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

Здесь следует отметить, что глобальные переменные автомати-чески инициализируются 0, однако это поведение может отличать-ся в зависимости от компилятора языка. Поэтому не стоит рассчи-тывать на автоматическую инициализацию, а выполнять её само-стоятельно.

Page 92: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

92

Глава 9. Рекурсивные процедуры и функции

Рекурсивные подпрограммы, условие выхода из рекурсии Как было рассмотрено в предыдущих главах, процедура (функ-

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

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

Рассмотрим пример другой рекурсивной функции: function r(n: integer): integer; begin if n <= 1 then r := 1 else r := n * r(n - 1); end;

Данная программа вычисляет значение факториала, при этом используется рекуррентное соотношение n! = n*(n–1)!, где 1! = 1. Здесь вычисления завершатся, поскольку после серии рекурсивных вызовов при n = 1 выполнится основная ветвь условного оператора. Такое условие называют условием выхода из рекурсии, а ветвь, не содержащая рекурсивных вызовов, именуется терминальной.

Если одна подпрограмма вызывает другую, а та в свою очередь первую, то имеет место косвенная рекурсия: procedure FА(…); begin ... FB(…); ... end; procedure FВ(…);

Page 93: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

93

begin ... FA(…); ... end;

Однако по правилам языка Pascal нельзя использовать объект раньше, чем он был описан. Таким образом, определение подпро-граммы FA некорректно, поскольку имеет ссылку на неопределен-ный объект FB(). В этих случаях необходимо объявить (но не оп-ределять!) процедуру FB до процедуры FA. В языке Паскаль это выполняется с помощью директивы forward после заголовка под-программы. Например: procedure FВ(a: integer); forward; procedure FА(a: integer); begin ... FB(…); ... end; procedure FВ(a: integer); begin ... FA(…); ... end;

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

Правила построения рекурсивных подпрограмм: 1) рекурсивная подпрограмма должна иметь хотя бы одну

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

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

Page 94: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

94

Итерация и рекурсия. Сравнение подходов Рассмотрим пример рекурсивной функции, вычисляющей зна-

чение n-го члена ряда Фибоначчи: function fib(n: integer): integer; begin if n <= 2 then fib := 1 else fib := fib(n-1) + fib(n-2); end;

Функция fib содержит два рекурсивных вызова, а в общем слу-чае рекурсивных вызовов в теле подпрограммы может быть сколь-ко угодно. Как видно из программного кода, эта рекурсивная функция, как и функция вычисления факториала, практически ко-пирует рекуррентные соотношения, определяющие n-й член ряда. Таким образом, рекурсия оказывается более компактным способом реализации рекуррентных соотношений, нежели итерация. Суще-ствуют задачи, в частности обработка динамических структур дан-ных, для которых рекурсия – наиболее естественный способ реше-ния.

Но насколько эффективен этот способ? Итеративный алгоритм вычисления чисел Фибоначчи имел линейную временную слож-ность и константную емкостную сложность. Чтобы оценить вре-менную сложность рекурсивного алгоритма необходимо рассчи-тать общее число операций, выполняемых в результате всех рекур-сивных вызовов. Вызов подпрограммы будем считать одной опера-цией. Для n=1 или n=2 выполняются одна проверка условия выхода и возврат значения (две операции), а для остальных случаев после проверки условия выполняются два вычитания, два рекурсивных вызова, сложение и присваивание (семь операций). К этому числу ещё необходимо добавить число операций, выполняемых в резуль-тате двух рекурсивных вызовов. Тогда:

для n = 3 имеем ∑(3) = 7 + ∑(2) + ∑(1) = 7+2+2 = 11 операций; для n = 4 имеем ∑(4) = 7 + ∑(3) + ∑(2) = 7+11+2 = 20 операций; для n = 5 имеем ∑(5) = 7 + ∑(4) + ∑(3) = 7+20+10 = 37 операции; … Обобщая, получаем ∑(n) = 7 + ∑(n–1) + ∑(n–2), где ∑(2) = 2,

∑(1) = 2. Итак, получено рекуррентное соотношение, напоминающее со-

отношение для n-го члена ряда Фибоначчи, что для выбранного

Page 95: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

95

способа реализации вполне ожидаемо. Таким образом, сложность рекурсивного алгоритма пропорциональна fib(n). Для получения аналитического вида функций fib(n) и ∑(n) необходимо вспомнить удивительное свойство чисел Фибоначчи:

...618,12

51)1(

)(

nfibnfib

Тогда для больших n n

n

nfib 618,1618,1)(

Таким образом, функция fib(n) оказывается прямо пропорцио-нальна 1,618n. То же самое справедливо и для ∑(n). Рассмотрим графики функций fib(n), ∑(n) и 1,618n, представленные на рис. 6:

0

50

100

150

200

250

300

350

400

450

500

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

nfib(n)∑(n)n 2̂1,618^n

Рис. 6. График функции fib(n) в сравнении с другими

Видно, что графики fib(n) и sigma(n) повторяют форму графика 1,618n, а сами функции связаны соотношениями

fib(n) = 0,4475*1,618n; ∑(n) = 4,028*1,618n.

Page 96: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

96

Таким образом, сложность рекурсивного алгоритма оценивается как O(1,618n). Можно заметить, что функция fib(n) возрастает бы-стрее n2 и в точке n = 12 перегоняет её. Столь низкая производи-тельность рекурсивной функции fib(n) объясняется тем, что вычис-ления многократно повторяются. Визуально рекурсивные вызовы можно представить в виде дерева, где число повторных вычисле-ний растёт при углублении рекурсии (рис. 7).

Рис. 7. Дерево рекурсивных вызовов fib(n)

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

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

При вызове подпрограммы системное окружение программы выполняет ряд действий:

fib(n)

fib(n-1) fib(n-2)

fib(n-3) fib(n-2) fib(n-3) fib(n-4)

fib(n-3) fib(n-4)

Page 97: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

97

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

2) вычисляются значения фактических параметров, выпол-няется контроль типов;

3) для всех локальных переменных подпрограммы и пара-метров, передаваемых по значению, выделяется память. Кроме того, могут быть выделены дополнительные сис-темные ресурсы. Вычисленные значения параметров пе-редаются подпрограмме;

4) управление передается первому оператору подпрограм-мы.

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

Для хранения адресов возврата и состояний окружения (значе-ний параметров и переменных) используется стек. Стеком называ-ется упорядоченный набор некоторого переменного числа объек-тов, работающий по правилу: «Последним пришел, первым вышел» (от англ. LIFO – Last In, First Out). Схему работы стека можно про-иллюстрировать на следующем примере: представим, что машина заехала в узкий тупик, а за ней еще несколько машин. Выезжать они будут в обратном порядке, и последней выедет первая машина.

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

Рассмотрим работу стека и порядок вычислений fib(n) для n = 4: fib(4) в стек проверка n<=2 выделение памяти для промежуточных переменных @1,@2,@3 fib(3) в стек

Page 98: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

98

проверка n<=2 выделение памяти для промежуточных переменных @4,@5,@6 fib(2) в стек проверка n<=2 возврат 1 @41 (извлечение из стека) fib(1) в стек проверка n<=2 возврат 1 @51 из стека @6 = @4 + @5 = 2 возврат @6 @12 из стека fib(2) в стек проверка n<=2 возврат 1 @21 из стека @3 = @1+@2 = 3 возврат @3 3 из стека

Видно, что расчёта числа Фибоначчи требуется сначала рекур-сивно погрузиться на максимальную глубину, а лишь затем вычис-лить сумму: fib(4) fib(4-1) + fib(4-2) fib(3) + fib(2) (fib(3-1) + fib(3-2)) + fib(2) (fib(2) + fib(1)) + fib(2) (1 + fib(1)) + fib(2) (1 + 1) + fib(2) 2 + fib(2) 2 + 1 3

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

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

Page 99: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

99

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

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

Глядя на дерево рекурсивных вызовов для функции fib(n), воз-никает желание избежать повторных вычислений. Рассмотрим сле-дующий вариант реализации функции: function fib(n: integer): integer; function f(n, prev, pprev: integer): integer; begin if n <= 2 then f := prev else f := f(n-1, prev+pprev, prev); end; begin f(n, 1,1); end;

Дерево рекурсивных вызовов для fib(4) выглядит так: fib(4) f(4, 1, 1) f(4-1, 1+1, 1) f(3, 2, 1) f(3-1, 2+1, 2) f(2, 3, 2) 3

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

Сложность вычислений fib(n) при хвостовой рекурсии линейна, а сам алгоритм может быть легко преобразован в итеративный с помощью циклов.

Page 100: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

100

Правила построения рекурсивных подпрограмм с аккумулято-ром:

1) вводится вспомогательная функция с дополнительным параметром-аккумулятором, в котором будет накапли-ваться результат вычислений. В общем случае аккумуля-торов может быть несколько;

2) главная функция вызывает вспомогательную функцию с начальными значениями аккумуляторов;

3) при выполнении условия выхода из рекурсии вспомога-тельная функция возвращает значение аккумулятора;

4) рекурсивный вызов вспомогательной функции выполня-ется с таким значением аккумулятора, которое возвра-щалось бы главной функцией.

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

Задание для самостоятельного выполнения: написать функцию вычисления факториала f(n)=n*f(n-1) с помощью хвостовой рекур-сии.

Возможный вариант решения: function fact(n: integer): integer; function f(n, acc: integer): integer; begin if n <= 1 then f := acc else f := f(n-1, n*acc); end; begin f(n,1); end;

Проверка работы хвостовой рекурсии для 4!: fact(4) f(4, 1) f(4-1, 4*1) f(3, 4) f(3-1, 3*4) f(2, 12) f(2-1, 2*12) f(1, 24) 24

Page 101: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

101

Аналогично функции fib(n) факториал вычисляется в постоян-ном объеме памяти, и в последней рекурсивной ветви результат уже вычислен.

Иллюстрация работы обычной рекурсии для 4!: fact(4) 4*fact(4-1) 4*fact(3) 4*(3*fact(3-1)) 4*(3*fact(2)) 4*(3*(2*fact(2-1))) 4*(3*(2*fact(1))) 4*(3*(2*1)) 4*(3*2) 4*6 24

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

Page 102: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

102

Глава 10. Строки и множества Символы и строки в языке Паскаль

Строка в языке Паскаль представляет собой цепочку символов длиной от 0 до 255, причем программисту предоставляется воз-можность самому определять максимальную длину строки, но не более 255. Известно, что любые символы кодируются числами в соответствии с ASCII-таблицей и для работы со строками можно было бы обойтись массивами. Однако в силу того, что область применения строк сильно отличается от области применения мас-сивов, в Паскаль были введены два новых типа: char для символов и string для цепочек символов.

Для решения простых задач 255 символов в строке оказывается достаточно, но в сложных задачах такое ограничение на длину только снижает общность алгоритма. В Паскале имеется другой тип данных PChar, представляющий собой цепочку символов, ко-торая замыкается нулевым символом (с ASCII-кодом 0). Длина та-кой строки ограничивается максимальным объемом памяти, кото-рый система может адресовать. В 32-разрядных системах адрес за-нимает 4 байта (32 бита), т.е. мощность пространства адресов равна 2^32 = 4 Гб. В реальности операционная система не может отвести программе всё адресное пространство, так как ей самой требуется определенный ресурс. В языке Си такие строки носят название ASCII-z, т.е. строки символов ASCII, завершающиеся 0 (zero). Для определения длины такой строки достаточно вычислить порядко-вый номер нулевого символа относительно первого символа стро-ки.

В современных версиях языка Паскаль поддерживается коди-ровка символов Unicode, в которой под каждый символ отводится по два байта. Данное представление позволяет закодировать уже 2^16 = 65536 различных символов, что оказывается достаточным для кодировки символов всех языков планеты. Для удобства рабо-ты с различными представлениями строк программисту предостав-ляются процедуры преобразования строк одного типа в другой.

Примеры объявления строковых переменных: var s: string; const n = 15;

Page 103: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

103

var s1: string[n]; type string4 = string[4]; // строка, занимающая 5 байт var s2: string4;

Если строковой переменной присваивается строка, длина кото-рой превышает допустимую длину строковой переменной, то стро-ка усекается справа до нужной длины.

Синтаксически строки и символы в Паскале записываются в апострофах. Если требуется поместить в строку сам символ апост-рофа, то его необходимо записать дважды. Строковые переменные перед использованием также необходимо инициализировать. Если начальное значение неизвестно, то хорошим вариантом является инициализация пустой строкой. Для задания пустой строки следует поставить два апострофа: s := 'Hello world'; // обычная строка s := ''; // пустая строка s := 'Don''t do it'; // строка с одним апострофом внутри

К символам строки можно обращаться как к элементам массива по индексу, причём символы в строке нумеруются с 1. Взятый по индексу символ можно трактовать и как значение типа char, и как значение типа string. Изменение символа в строке по его индексу допустимо, однако не приветствуется: ch := s[3]; s2 := s[4]; s[3] := 'm';

Поскольку символы, по сути, являются кодами в ASCII-таблице, то в Паскале имеется возможность задания символа по его коду, а также определен ряд функций для работы с кодами символов: ch := #100; // 100 – код буквы 'd' ch := chr(100); // получение символа по коду code := ord(ch); // получение кода по символу

Таким образом, chr(ord(ch)) = ch и ord(chr(code)) = code. Строки можно выводить на экран и вводить с клавиатуры с по-

мощью стандартных процедур writeln() и readln(), символы – с по-мощью write() и read() соответственно.

Page 104: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

104

Строковые операции Важной операцией для работы со строками является конкатена-

ция (склеивание) строк. Её результатом является строка, образо-ванная в результате стыковки конца первой строки с началом вто-рой строки. Обозначается операция знаком + и записывается в ин-фиксной форме, т.е. между аргументами. Например: message := 'Length = ' + IntToStr(Length(s)) + ' symbols';

Для символов и строк определены операции отношения. При сравнении символов реально сравниваются их ASCII-коды, поэто-му, например, 'W' < 'w', '$' < '?', 'я' <'Ё'. Символы цифр от '0' до '9' упорядочены и имеют коды от 48 до 57 соответственно.

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

1) пустая строка меньше любой другой; 2) две строки равны тогда и только тогда, когда они имеют

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

3) если строки не равны, то сравниваются первые различ-ные символы от начала строк. Меньшей считается та строка, у которой символ имеет меньший ASCII-код.

Примеры: 'abc' = 'abc', правило 2 'abc' > 'ab', правило 1 ('с' > '') 'abc' < 'abc ', правило 1 ('' < ' ') 'abc' < 'xyz', правило 3 ('a' < 'x') 'a' < 'abc', правило 3 ('' < 'bc') '1200' < '45', правило 3 ('1' < '4') 'Anny' < 'anny', правило 3 ('A' < 'a')

Подпрограммы для работы со строками В стандартной библиотеке реализован ряд полезных подпро-

грамм для работы со строками. Рассмотрим некоторые из них: procedure Insert(Source: string; var S: string; Index: Integer) –

вставка строки внутрь другой строки. Процедура имеет три аргу-

Page 105: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

105

мента: Source – вставляемая строка, S – исходная строка, Index – позиция в исходной строке, в которую следует вставить другую строку. Меняется строка S: S := 'Hello world'; Insert(' my ', S, 6) S = 'Hello my world'

procedure Delete(var S: string; Index, Count: Integer) – удаление части строки. Процедура имеет три аргумента: S – исходная строка, Index – позиция в исходной строке, начиная с ко-торой следует удалять символы, Count – максимальное число уда-ляемых символов. Меняется строка S: S := 'Hello world'; Delete(S, 4, 5) S = 'Helrld' Delete(S, 10, 5) S = 'Hello wor'

function Copy(S: string; Index, Count: Integer): string – копи-рование подстроки. Функция принимает три аргумента: S – строка, Index – индекс начального символа, с которого начинается копиро-вание, Count – количество копируемых символов. Возвращается участок строки S от индекса Index длиной не более Count: Copy('Hello world', 4, 5) = 'lo wo' Copy('Hello', 4, 5) = 'lo'

function Pos(Substr: string; S: string): Integer – поиск подстро-ки в строке. Функция принимает два аргумента: Substr – подстрока для поиска, S – строка, в которой следует искать подстроку. Воз-вращается первая слева позиция найденной подстроки. Если под-строка не найдена, то возвращается 0: Pos('llo', 'Hello world') = 3 Pos('low', 'Hello world') = 0

procedure Str(X [: Width [: Decimals ]]; var S) – преобразование числа в строковое представление. Число X может быть как целого, так и вещественного типа. Форматирующие модификаторы Width и Decimals указывают общую длину строки и число цифр после запя-той, аналогично writeln. В S возвращается полученная строка: Str(36.6:7:2, S) S = ' 36.60'

Page 106: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

106

procedure Val(S; var V; var Code: Integer) – преобразование

строки в числовое представление. Процедура имеет три параметра: S – символьное представление числа, V – числовая переменная, в которую записывается результат преобразования, Code – числовая переменная для возврата ошибки. В случае успешного преобразо-вания Code равна нулю, в случае ошибки в Code помещается ин-декс символа, который привел к возникновению ошибки: Val('36.6', n, code) n = 36.6, code = 0 Val('780*2', n, code) code = 4

Процедуру Val удобно использовать для безопасного чтения числа с экрана. Например: begin … // запрашиваем пользователя до тех пор, // пока не будет введено число repeat writeln('Enter a number: '); readln(str); // безопасное чтение в строку Val(str, number, code); // безопасное преобразование if code > 0 then writeln('Wrong input'); until code = 0; … end.

Можно оформить этот фрагмент кода в виде функции.

Примеры подпрограмм редактирования строк 1. Реализовать процедуру insert:

procedure Insert(sub: string; var s: string; ind: integer); var i: integer; res: string; begin res := ''; for i:=1 to ind -1 do res := res + s[i]; res := res + sub; for i := ind to length(s) do res := res + s[i]; s := res;

Page 107: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

107

end;

2. Удалить лишние пробелы между словами в строке. Рассмот-рим следующее решение: procedure Filter(var S: string); var i: integer; begin for i:=1 to length(s)-1 do begin if (s[i] = ' ') and (s[i+1] = ' ') then Delete(s,i,1); end; end;

В ходе работы со строкой уменьшается её длина, а граничные значения счётчика цикла for вычисляются один раз ещё до первого витка. Значит, функция length(s) выполнится один раз, и цикл for не узнает об изменении длины строки внутри цикла. В данном случае произойдет ошибочное обращение к несуществующему элементу строки. В общем случае, если строка укорачивается, то цикл будет выполняться лишнее число раз, а если строка удлиняется, то цикл завершится преждевременно. Вместо цикла for следует использо-вать while, так как условие цикла while вычисляется перед каждым витком: procedure Filter(var S: string); var i: integer; begin i:=1; while i<length(s)-1 do begin if (s[i] = ' ') and (s[i+1] = ' ') then Delete(s,i,1) else i := i+1; // нюанс end; end;

Если убрать else, то из трех пробелов будет удаляться лишь вто-рой.

Множества и типы множеств В Паскале множество – совокупность элементов. Тип-

множество – тип данных, определяющий множества на конечном наборе элементов. Элементы множества считаются неупорядочен-

Page 108: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

108

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

Множества-переменные описываются следующим образом: var <имя множества>: set of <тип элементов множества>;

Например: var // множество символов (256 элементов) CharSet: set of char; // множество букв латинского алфавита LettersSet: set of 'a'..'z','A'..'Z'; // множество цифр (максимум 10 элементов) DigitSet: set of 0..9; // множество значений истинности (максимум 2 элемента) BoolSet: set of boolean;

Тип-множество определяется по общим правилам. Например, type TCharSet = set of char;

Для задания множества необходимо перечислить элементы, входящие в это множество. Элементы записываются в квадратных скобках:

[] – пустое множество; [e1, e2, e5, en] – непустое множество. Кроме прямого перечисления элементов множества можно кон-

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

Теоретико-множественные операции в языке Паскаль S1 * S2 Пересечение множеств S1 и S2. S1 + S2 Объединение множеств S1 и S2. S1 – S2 Разность множеств S1 и S2. E in S Проверка принадлежности элемента E множеству S.

Результат – значение истинности.

Page 109: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

109

S1=S2 Проверка на равенство множеств S1 и S2. Результат – значение истинности.

S1<S2 Проверка на включение множества S1 в S2. Результат – значение истинности.

S1>S2 Проверка на включение множества S2 в S1. Результат – значение истинности.

Задания на множества и строки 1. Выразить множества D, F на диаграмме (рис. 8) через множе-

ства А, B, C:

Рис. 8. Диаграмма пересечения множеств

Ответ: D := A*B-C; F := A*B*C;

Чему равно значение выражений C > D, F < C*B, D+F = A*B? Ответ: False, True, True. 2. Вывести на экран содержимое множества. для вывода на эк-

ран содержимого множества необходимо вывести по отдельности каждый элемент множества: var s: set of char; k: byte; begin … for k := 0 to 255 do if chr(k) in s then write(chr(k), ' '); end.

A D F

B

C

Page 110: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

110

3. Оставить в строке только первое вхождение каждого символа, взаимный порядок оставленных символов сохранить. Использовать множества: var s: set of char; input, result: string; i: byte; begin s := []; result := ''; for i := 1 to length(input) do if not (input[i] in s) then begin result := result + input[i]; s := s+[input[i]]; // добавление элемента к множеству end; end.

4. Дана строка символов, содержащая путь к файлу. Выделить имя диска, расширение файла и имя файла, если они есть. При от-сутствии соответствующих данных возвращаются пустые строки: procedure SplitPath(Path: string; var disk, name, ext: string); var i: integer; begin disk := copy(Path, 1, pos(':\',Path)); i:=length(Path); while (i>0) and (Path[i] <> '.') do i := i-1; ext := copy(Path, i+1, length(Path)-i); while (i>0) and (Path[i] <> '\') do i := i-1; name := copy(Path, i+1, length(Path)-i-length(ext)-1); end;

5. Реализовать функцию преобразования строки в число с пла-вающей запятой: function Evaluate(s: string): real; var i: integer; int, frac: real; comma: boolean; begin int := 0; frac := 0;

Page 111: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

111

comma := false; // признак дробной части i:=1; while i<length(s) and not comma do begin case s[i] of '0'..'9': int := int*10 + ord(s[i])-ord('0'); '.': comma := true; end; i := i+1; end; if comma then begin for i:=length(s) downto comma+1 do frac := frac/10 + ord(s[i])-ord('0'); frac := frac/10; end; r := int + frac; if s[1] = '-' then r := -r; Evaluate := r; end;

6. Реализовать алгоритм поиска подстроки в строке:

function Pos(sub, S: string): integer; var i,j: integer; ok, found: boolean; begin Pos := 0; i:=1; while (i<=length(s)) do begin while s[i] <> sub[1] do i:=i+1; // ищем первый символ // сравниваем остаток ok := true; j:=1; while (i+j-1<=length(s)) and (j<=length(sub)) do begin ok := ok and (s[i+j-1] = sub[j]); j := j+1; end; // если все символы совпали, то подстрока найдена found := ok and (j>length(sub)); if found then break; i := i+1; end; if found then Pos := i; end;

Данный алгоритм далёк от оптимального – информация о сим-волах в подстроке и результаты предыдущих сравнений никак не

Page 112: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

112

используются после увеличения i. Подобные алгоритмы называют алгоритмами «грубой силы и невежества» (ГСН, brute force algo-rithm). Существуют гораздо более эффективные алгоритмы, на-пример алгоритм Кнута–Морриса–Пратта или алгоритм Бойера–Мура.

Page 113: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

113

Глава 11. Текстовые файлы Понятие файла

Файл, записанный на какой-либо электронный носитель, пред-ставляет собой обособленный поименованный набор данных про-извольной длины. В таком представлении файл статичен. Основное назначение таких файлов состоит в хранении информации любого рода от неструктурированных данных до исполняемых программ и библиотек. Для этого, как правило, применяются энергонезависи-мые носители, такие как гибкие и жесткие магнитные диски, ленты, оптические диски, flash-накопители и т.п. Для указания местопо-ложения файла на носителе используют либо абсолютный путь, либо путь относительно текущего каталога. Форма записи пути к файлу различается в зависимости от операционной системы. Для MS Windows путь имеет вид [<диск>:\]{<каталог>\}<имя фай-ла>[.<расширение>]. Здесь <>{}[] – метасимволы, [] – необязатель-ная конструкция, {} – конструкция, повторяющаяся 0 и более раз. Например:

coprime.pas – файл в текущем каталоге bin\coprime.exe – относительный путь к файлу D:\winnt\paint.exe – абсолютный путь к файлу Но прежде чем файл окажется на каком-либо носителе, необхо-

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

Вообще говоря, понятие файла является абстракцией, лежащей в основе системы ввода-вывода. Ввод – процесс передачи данных в оперативную память извне (клавиатура, носители данных, порты ввода-вывода), а вывод – процесс передачи данных из оперативной памяти во внешнюю среду (дисплей, носители данных, порты вво-да-вывода). Каждый канал передачи данных между устройством и оперативной памятью имеет свое уникальное имя, а для осуществ-ления операций ввода-вывода используется тот же механизм рабо-ты с файлами. Например, устройствам соответствуют такие имена файлов, как COM1, COM2, PRN, CON, NUL.

В языке Паскаль для работы с файлами существуют различные средства, учитывающие специализацию файлов. Читаемость чело-

Page 114: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

114

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

текстовые файлы; бинарные файлы.

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

Примеры текстовых файлов: txt-файлы, файлы с исходными текстами программ (*.pas, *.c, *.hpp и др.), пакетные исполняемые файлы (*.bat), интернет-страницы, скрипты, размеченные файлы (*.asp, *.vb, *.txt, *.htm, *.xml, *.xslt, *.xsd) и любые другие файлы, читаемые человеком в простейших текстовых редакторах.

Бинарный файл представляет собой произвольную последова-тельность байт. При открытии бинарного файла в текстовом редак-торе содержимое каждого байта будет отображаться в виде соот-ветствующего ему символа в ASCII-таблице.

Бинарные файлы в свою очередь делятся на два подкласса: типизированные файлы; нетипизированные файлы.

Типизированный файл представляет собой последовательность однотипных структур данных, известных программисту. Эти структуры данных еще называют компонентами. Типизированный файл может содержать 0 и более компонент.

Нетипизированный файл – произвольная последовательность байт. Таким образом, нетипизированным файлом можно считать любой файл, который нельзя рассматривать как текстовый или ти-пизированный. Если не представляется возможным выяснить, можно ли файл рассматривать как текстовый или типизированный или же это вообще не важно, то файл также следует считать нети-пизированным.

Эти классы определяют лишь способ интерпретации внутрен-ней структуры файлов программистом, поэтому один и тот же файл

Page 115: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

115

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

Буферизация операций ввода-вывода Жесткие диски, внешние устройства, подключенные через пор-

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

В Паскале по умолчанию буфер для работы с файлами имеет размер 128 байт. Однако существует возможность изменить размер буфера до начала работы с файлом.

Механизм работы с файлами в языке Паскаль Работа с файлами в языке Паскаль производится посредством

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

Общую схему работы с файлом можно представить следующим образом:

1) объявление файловой переменной; 2) связывание файловой переменной с физическим файлом;

Page 116: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

116

3) открытие файла; 4) выполнение операций ввода-вывода; 5) закрытие файла.

Для работы с текстовыми файлами следует объявлять файловые переменные типа TextFile. Например: var f,g: Text; // в Delphi тип TextFile

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

Общие процедуры и функции работы с файлами Рассмотрим программу, распечатывающую на экране содержи-

мое текстового файла: var f: Text; path, s: string; begin write('Enter file path: '); readln(path); assign(f, path); // связывание файла с переменной reset(f); // открытие файла на чтение // пока не достигнут конец файла while not EOF(f) do begin readln(f, s); // чтение из файла writeln(s); end; close(f); // закрытие файла end.

В этой программе используется ряд стандартных подпрограмм

работы с файлами: procedure Assign(var f; filename: string) – связывает файловую

переменную f с физическим файлом, путь к которому указан в строке filename. При этом существование файла не требуется.

procedure Reset(var f [: File; RecSize: Word ]) – открывает су-ществующий физический файл, на чтение-запись. Если файл не существует, то возникает ошибка ввода-вывода. Если файл был открыт ранее, то он закрывается и открывается заново. Текстовые файлы открываются только на чтение. Параметр RecSize передает-

Page 117: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

117

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

procedure Rewrite(var f [: File; RecSize: Word ]) – открывает на запись новый файл, связанный с переменной f. Если файл суще-ствовал ранее, то он стирается, и создается новый файл. Если f бы-ла связана с пустым путем Assign(F,''), то после вызова Rewrite f будет связана со стандартным файлом вывода. Текстовые файлы открываются только на запись. Параметр RecSize передается толь-ко для нетипизированных файлов и указывает размер блока для операций ввода-вывода.

function EOF(var f): boolean – возвращает истину, если при чтении достигнут конец файла, и ложь – в противном случае. Со-кращение EOF происходит от англ. End Of File (конец файла).

procedure Сlose(var f) – закрывает открытый файл, связанный с переменной f. Если файл был открыт для записи, то перед закрыти-ем в файл дописывается содержимое буфера записи.

procedure Erase(var f) – уничтожает физический файл, связан-ный с переменной f. До вызова Erase файл должен быть закрыт.

procedure Rename(var f; newname : string) – переименовывает физический файл, связанный с переменной f.

Организация доступа к данным в файле. Курсор В программе при работе с файлами удобно считывать информа-

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

Существуют два режима доступа к содержимому файла: последовательный; прямой.

Схема последовательного доступа такова: 1) при открытии файла курсор позиционируется на первом

символе файла; 2) в ходе операций чтения последовательно считываются

участки файла, пока не будет достигнут конец файла.

Page 118: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

118

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

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

Схема прямого доступа: 1) при открытии файла курсор позиционируется на первом

символе файла; 2) курсор перемещается на нужный участок файла; 3) считывается нужный участок.

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

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

Чтение и запись текстовых файлов Для чтения из файла используются процедуры: procedure Read( [ var F: Text; ] V1 [, V2,...,Vn ] ) – считывает из

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

procedure Readln([ var F: Text; ] V1 [, V2, ...,Vn ]) – аналогично Read, но после чтения курсор позиционируется в начало следую-щей строки.

Для записи в файл используются процедуры: procedure Write( [var F: Text; ] P1 [ , P2,..., Pn] ) – записывает в

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

Page 119: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

119

procedure Writeln([ var F: Text; ] P1 [, P2, ...,Pn ]) – аналогично Write, но после записи в файл помещается маркер конца строки (#13#10).

Значения сложных типов можно вводить и выводить в тексто-вый файл только поэлементно. Например: var f: Text; a: array[1..200] of integer; i: integer; begin write('Enter file path: '); readln(path); assign(f, path); rewrite(f); // открытие файла на запись for i:=1 to 200 do writeln(f, a[i]); // запись элементов в столбик close(f); // закрытие файла end.

Видно, что для операций чтения-записи применяются одно-

именные процедуры Read и Write, используемые для ввода значе-ний с клавиатуры и вывода на экран. Как уже говорилось, для ра-боты с этими устройствами используется тот же файловый меха-низм. Существуют две системные переменные: INPUT – файловая переменная, связанная со стандартным вводом операционной сис-темы (клавиатурой), и OUTPUT – файловая переменная, связанная со стандартным выводом операционной системы (экраном). Для программы эти файлы всегда открыты (INPUT – на чтение, OUT-PUT – на запись), и в явном виде их нельзя открывать и закрывать. Так, Read(x) равносильно Read(INPUT, x), а Write(x) равносильно Write(OUTPUT, x).

Другие стандартные подпрограммы для работы только с тексто-выми файлами.

procedure Append(var f: Text) – открывает на запись физиче-ский файл, связанный с переменной f. Запись производится в конец файла, т.е. данные добавляются, не замещая ранее записанных дан-ных. Процедура работает только с текстовыми файлами.

procedure Flush(var f: Text) – сброс содержимого буфера запи-си в файл, открытый процедурой rewrite или append. Процедура

Page 120: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

120

используется для досрочной записи в файл независимо от уровня заполнения буфера.

procedure SetTextBuf(var F: Text; var Buf [ ; Size: Integer] ) – замещение буфера, выделяемого по умолчанию, буфером Buf раз-мером Size.

function SeekEOF(var f: Text): boolean – сдвигает курсор отно-сительно текущей позиции до следующего символа, отличного от пробела. Если при этом достигнут конец файла, то возвращается истина, иначе возвращается ложь. Функция используется для пере-движения по файлу и пропуска пробелов.

function SeekEOLN(var f: Text): boolean – сдвигает курсор от-носительно текущей позиции до следующего символа, отличного от пробела, в этой же строке. Если при этом достигнут конец строки или файла, то возвращается истина, иначе возвращается ложь. Сокращение EOLN происходит от англ. End Of LiNe (конец строки).

Обработка ошибок ввода-вывода Ошибки ввода-вывода вызывают аварийную остановку про-

граммы. Для того чтобы программа была более универсальной и продолжала работу в любых случаях, существует возможность об-работки ошибок ввода-вывода внутри программы. Этим поведени-ем управляет директива компилятора $I. Вообще, директивы ком-пилятора размещаются в тексте программы внутри комментария {}. Для включения режима автоматической обработки ошибок сле-дует записать {$I+}, для отключения – {$I-}. Область действия ди-рективы простирается с момента её появления в программе до кон-ца программы или до отменяющей её директивы. По умолчанию опция включена.

Для ручного анализа ошибок ввода-вывода необходимо отклю-чить опцию $I и использовать стандартную системную функцию IOResult:

function IOResult: integer – возвращает результат последней операции ввода-вывода. При успешном завершении операции вво-да-вывода возвращается 0, а в случае ошибки – ненулевой код ошибки.

Если при отключенной опции $I происходит ошибка, то все по-следующие операции ввода-вывода игнорируются до первого об-ращения к функции IOResult. После вызова функции IOResult

Page 121: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

121

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

Основным уязвимым местом в программе при работе с файлами является момент открытия. В частности, ошибка может возникнуть при открытии несуществующего файла. Рассмотрим программу: var f: Text; begin write('Enter file path: '); readln(path); assign(f, path); {$I-} reset(f); {$I+} if IOResult <> 0 then writeln('Error while opening file '+path); exit; end; // штатная работа с файлом … end.

В данной программе при ошибке открытия файла будет выдано сообщение об ошибке и программа завершится.

Задача слияния словарей В двух текстовых файлах записаны слова – по одному в строке.

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

Данная задача схожа с задачей слияния массивов. Однако име-ются некоторые отличия. Во-первых, количество слов в файлах за-ранее неизвестно, файлы вообще могут быть пусты. Во-вторых, чтение элементов массивов и их сравнение могут осуществляться в одном выражении, а при работе с файлами требуется сначала счи-тать строки, а лишь затем сравнивать. Поэтому удобнее организо-вать один сложный цикл, который будет выполняться до тех пор, пока хотя бы один файл не считан до конца: var f, g, h: Text; s1, s2: string;

Page 122: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

122

fstRead, sndRead: boolean; begin assign(f, '…'); assign(g, '…'); assign(h, '…'); reset(f); // первый входной открываем на чтение reset(g); // второй входной открываем на чтение rewrite(h); // выходной открываем на запись fstRead := false; // строка из первого файла не считана sndRead := false; // строка из второго файла не считана // основной цикл слияния двух файлов while not EOF(f) or not EOF(g) do begin // безопасное считывание if not fstRead and not EOF(f) then begin readln(f, s1); fstRead := true; end; if not sndRead and not EOF(g) then begin readln(g, s2); sndRead := true; end; // слияние if not fstRead then begin // первый файл кончился, а второй еще нет writeln(h, s2); sndRead := false; end else if not sndRead then begin // второй файл кончился, а первый еще нет writeln(h, s1); fstRead := false; end else begin // оба файла еще не кончились if s1 < s2 then begin writeln(h, s1); fstRead := false; end else if s1>s2 then begin writeln(h, s2); sndRead := false; end else begin // при s1=s2 строка записывается только один раз

Page 123: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

123

writeln(h, s1); fstRead := false; sndRead := false; end; end; end; close(f); close(g); close(h); end.

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

Page 124: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

124

Глава 12. Записи и типизированные файлы Записи и массивы записей

Представьте, что в программе требуется описать некоторый объект реального мира, например запись в телефонной книжке. Это возможно только с помощью совокупности переменных. Для рабо-ты с этой совокупностью, как с единым целым, в языке Паскаль вводится тип-запись, в Си – тип-структура. Для объявления типа-записи следует использовать ключевое слово record и далее опи-сать составляющие запись переменные, которые называются поля-ми записи. Далее можно объявлять переменные нового сложного типа. Например: type phone_rec = record fio: string[60]; phone: string[20]; e_mail: string[50]; end; var pr1, pr2: phone_rec;

Здесь в типе phone_rec объявлено три поля fio, phone, e_mail строковых типов.

Для обращения к конкретному полю записи необходимо после имени переменной-записи через '.' указать имя поля: begin … pr1.fio := 'Ivanov Ivan'; pr1.phone := '89169110203'; pr1.e_mail := '[email protected]'; … writeln('fio: ' + pr1.fio); writeln('phone: ' + pr1.phone); writeln('e-mail: ' + pr1.e_mail); … end;

Кроме того, для обращения к полю записи может использовать-ся оператор with <имя записи> do. Тогда при обращении к полю имя записи можно не указывать:

Page 125: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

125

begin … with pr1 do begin writeln('fio: ' + fio); writeln('phone: ' + phone); writeln('e-mail: ' + e_mail); end; … end;

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

Для записей определена операция присваивания, при этом копи-руются все значения полей записи. Однако для записей не опреде-лены операции сравнения. Для сравнения записей необходимо по-парно сравнить значения одноименных полей. function Equal(pr1, pr2: phone_rec): boolean; begin Equal := (pr2.fio = pr1.fio) and (pr2.phone = pr1.phone) and (pr2.e_mail = pr1.e_mail); end.

Пример: описать телефонную книжку на 200 номеров. Это мож-но сделать с помощью массива однотипных записей. В следующей программе на экран выводится содержимое телефонной книги, хранящейся в массиве: type phone_book = array [1..200] of phone_rec; var pb: phone_book; pr: phone_rec; i: integer; begin … for i:=1 to 200 do begin pr := pb[i]; writeln('№', i); writeln('Name: ' + pr.fio); writeln('Phone: ' + pr.phone); writeln('E-mail: ' + pr.e_mail); writeln;

Page 126: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

126

end; end.

Вообще, записи и массивы можно комбинировать для создания сколь угодно сложных структур данных, вложенных друг в друга. Например: type passport_rec = record series: string[4]; number: string[6]; when: DateTime; whom: string[100]; end; person = record fio: string; birthdate: DateTime; passport: passport_rec; end; var p: person; begin p.fio := 'Smith'; p.passport.series := '4700'; … with p.passport do writeln(series, ' ', number); end.

Записи с вариантной частью В примере с телефонной книгой одна запись содержала три по-

ля. Однако часто в разных случаях уместен разный набор полей. Так, формат библиографической записи различается в зависимости от типа издания:

для книги следует указывать автора, название, год изда-ния, издательство;

для журнала – название, год и месяц издания, номер, из-дательство;

для газеты – название, дату выхода (день, месяц, год), номер, издательство.

Конечно, можно определить универсальную структуру данных, содержащую все возможные поля, и заполнять поля по мере надоб-ности. Например:

Page 127: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

127

type biblio = record bibtype: char; author: string[40]; name: string[40]; year: integer; month: byte; day: byte; number: integer; publisher: string[40]; end;

Но такая структура неэкономна с точки зрения расхода памяти. На одну запись расходуется 1+40+40+4+1+1+4+40 = 131 байт. При этом общими для всех трех типов изданий являются название, год, издательство, а остальная информация может различаться или во-все отсутствовать. Для создания более компактных структур дан-ных в языке Паскаль реализована возможность определения запи-сей с вариантной частью.

В таких записях после постоянной части, которая может и от-сутствовать, указывается вариантная часть с помощью ключевого слова case. Общий вид записи с вариантной частью: type <имя типа> = record <поле>: <тип>; … <поле>: <тип>; case <тег>: <перечислимый тип> of <список констант>: ( <поле>: <тип>; … <поле>: <тип>; ); … <список констант>: ( <поле>: <тип>; … <поле>: <тип>; ); end;

Финальное слово end закрывает и запись, и вариантную часть.

Page 128: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

128

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

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

С помощью вариантной части запись biblio можно сделать более компактной: type biblio = record name: string[40]; year: integer; publisher: string[40]; case bibtype: char of 'b': (author: string[40]); 'j': ( jmonth: byte; jnumber: integer ); 'n': ( nmonth: byte; day: byte; nnumber: integer ); end;

В данном случае размер записи равен (40+4+40+1) + (40) = 125. Схема распределения памяти для структуры с вариантной частью показана на рис. 9.

Page 129: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

129

Рис. 9. Схема распределения памяти для структуры с вариантной частью

Обращение к полям вариантной записи ничем не отличается от обращения к полям обычной записи. Однако программист должен сам определять, с каким из вариантов вариантной части работать в данный момент. Например, с помощью тега вариантной части: procedure Print(bib: biblio); begin with bib do begin writeln(name); writeln(year); writeln(publisher) case bibtype of 'b': writeln(author); 'j': writeln(jmonth, ' ', jnumber); 'n': writeln(nmonth, ' ', day, ' ', nnumber); end; end; end;

Поскольку все варианты накладываются на один и тот же уча-сток в памяти, то можно, например, записать в этот участок фами-лию автора книги author, а затем обратиться к номеру журнала jnumber. При этом байты со 2 по 5, занимаемые author, будут рас-ценены как число типа integer.

Типизированные файлы Понятно, что для долгосрочного хранения совокупности запи-

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

name (40)

year (4)

publisher (40)

bibtype (1)

author (40)

jmonth (1) jnumber (4)

nmonth (1)

number (4)

day (1)

Page 130: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

130

Для работы с типизированным файлом следует объявить файло-вую переменную, указав тип компонентов файла с помощью клю-чевых слов file of: var pb: file of phone_rec;

Или же ввести новый файловый тип phone_book, а затем объя-вить файловую переменную: type phone_book = file of phone_rec; var pb: phone_book;

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

Для открытия типизированных файлов используют знакомые процедуры rewrite и reset. Процедура rewrite открывает типизиро-ванный файл в режиме перезаписи, а процедура reset – в режиме чтения и записи. Следует напомнить, что текстовые файлы откры-ваются процедурой reset только на чтение.

Для выполнения операций чтения и записи используются про-цедуры Read и Write. Однако, в отличие от текстовых файлов, про-цедуры работают с целыми компонентами файла: Read считывает компонент целиком, а Write записывает компонент целиком. В сле-дующей программе на экран выводится содержимое телефонной книги, хранящейся в файле 'c:\dev\phones.bin' (предполагается, что файл уже существует): var pb: phone_book; pr: phone_rec; i: integer; begin Assign(pb, 'c:\dev\phones.bin'); Reset(pb); while not EOF(pb) do begin Read(pb, pr); // чтение одного компонента writeln('Name: ' + pr.fio); writeln('Phone: ' + pr.phone); writeln('E-mail: ' + pr.e_mail);

Page 131: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

131

end; Close(pb); end.

Другие процедуры и функции для работы с типизированными файлами:

procedure Seek(var F; N: Longint) – позиционирует курсор на компоненте с номером N. Процедура служит для организации пря-мого доступа к компонентам файла.

function FilePos(var F): Longint – возвращает номер компонен-та, перед которым в данный момент стоит курсор. Если курсор ука-зывает на конец файла, содержащего N компонентов, то возвраща-ется число N, так как последний компонент имеет номер N–1.

function FileSize(var F): Integer – возвращает число компонен-тов файла.

procedure Truncate(F) – укорачивает файл с конца до текущего положения курсора, так что вызов EOF(F) после Truncate вернет истину.

Функции EOLN, SeekEOF, SeekEOLN не применимы к типизи-рованным файлам.

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

Для организации простейшей базы данных телефонных записей требуется реализация вспомогательных сервисов: функции поиска записи (select) по фамилии, добавления новой записи (insert), изме-нение записи (update) и удаление записи (delete).

Поиск записи в телефонной книге по фамилии Функцию поиска можно реализовать аналогично поиску в мас-

сиве. Найдя запись с нужной фамилией, функция возвращает номер записи в файле и саму запись. Номер записи может понадобиться в дальнейшей работе, например для её обновления или удаления из файла: function Select( f: phone_book; fio: string; var N: longint; var rec: phone_rec): boolean; var

Page 132: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

132

found: Boolean; r: phone_rec; begin found := false; reset(f); while not eof(f) and not found do begin read(f, r); if r.fio = fio then begin N := FilePos(f); rec := r; found := true; end; end; Select := found; end;

Добавление записи в телефонную книгу Для простоты новые записи будут добавляться в конец файла:

procedure Insert(f: phone_book; p: phone_rec); begin reset(f); seek(f, FileSize(f)); // ставим курсор на конец файла write(f,p); close(f); end;

Обновление записи в телефонной книге под номером N Обновление записи тривиально: курсор позиционируется на

нужный компонент и осуществляется запись поверх него: procedure Update(

f: phone_book; N: integer; rec: phone_rec);

begin reset(f); seek(f, N); write(f, rec); // перезапись компоненты под номером N close(f); end;

Удаление из телефонной книги записи под номером N К сожалению, в стандартной библиотеке отсутствуют процеду-

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

Page 133: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

133

1) создать новый типизированный файл g, задав ему неко-торое имя;

2) скопировать в него записи файла f, начиная с 0 до N–1; 3) скопировать в него записи файла f, начиная с N+1 до

FileSize(f); 4) удалить прежний файл f; 5) переименовать новый файл g, дав ему имя прежнего

файла; 6) связать f с g.

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

1) считать последнюю запись; 2) записать её на место N (она гарантированно имеет тот

же размер); 3) поместить курсор на последнюю запись; 4) укоротить файл.

procedure Delete(f: phone_book; N: integer); var rec: phone_rec; begin reset(f); seek(f, FileSize(f)-1); read(f, rec); // читаем последнюю запись seek(f, N); write(f, rec); // записываем на позицию N seek(f, FileSize(f)-1); truncate(f); // укорачивание файла close(f); end;

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

Page 134: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

134

Глава 13. Нетипизированные файлы Для работы с бинарными файлами общего вида в Паскале име-

ется встроенный тип данных – нетипизированный файл. Файловые переменные для работы с такими файлами описываются следую-щим образом: var f: file;

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

В операциях ввода-вывода также используется буферизация, причем размер буфера ввода-вывода равен по умолчанию 128 бай-там. Иной размер буфера можно задать при открытии файла с по-мощью второго параметра процедур reset и rewrite: reset(f[,size]); rewrite(f[,size]);

Размер буфера size должен находиться в пределах от 1 байта до 64 Кб.

Чтение и запись нетипизированных файлов Чтение и запись выполняются блоками, объем которых равен

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

procedure BlockRead(var F: File; var Buf; Count: Integer [; var AmtTransferred: Integer]) – считывает в переменную Buf количе-ство блоков Count 1. В необязательном параметре AmtTransferred возвращается количество реально прочитанных блоков.

procedure BlockWrite(var f: File; var Buf; Count: Integer [; var AmtTransferred: Integer]) – выполняет запись в файл Сount блоков из области памяти Buf. В параметре AmtTransferred возвращается число успешно записанных блоков.

Page 135: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

135

В процедурах BlockRead и BlockWrite параметр Buf должен яв-ляться переменной, однако тип её не указан. Это означает, что в качестве параметра можно передать переменную любого типа.

Процедуры и функции Seek, FilePos, FileSize за размер одной компоненты берут размер блока (буфера), указанный при открытии нетипизированного файла.

Задание 1. Написать программу чтения вещественных чисел из текстового файла и записи их в нетипизированный файл блоками по N чисел. Для определения размера в байтах вещественного чис-ла использовать функцию SizeOf:

function SizeOf(X): Integer – возвращает размер в байтах облас-ти памяти, занимаемый значением переменной X произвольного типа.

Решение может выглядеть так: const N = 8; var buf: array[1..N] of real; f: text; g: file; i, k: integer; begin assign(f, '…'); reset(f); // входной файл assign(g, '…'); rewrite(g, sizeof(real) * 4); // выходной файл i := 0; while not eof(f) do begin i := i+1; read(f, buf[i]); if i = N then begin blockwrite(g, buf, 1); i := 0; end; end; // дополним нулями последнюю N-ку, если нужно if i > 0 then begin for k := i + 1 to N do buf[k] := 0; blockwrite(g, buf, 1); end; close(f); close(g);

Page 136: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

136

end.

Задание 2. Написать программу шифрования файла путем на-ложения ключа с помощью операции xor. Программа должна за-прашивать имена файлов с экрана.

Пусть имеется файл размером N байт и ключ – строка длиной М. Задача шифрования состоит в разбиении файла на последова-тельные участки длиной M и наложения ключа на каждый участок. Наложение ключа осуществляется с помощью битовой операции исключающего или для каждой пары соответствующих символов из очередного участка файла и ключа. Расшифровка файла осуще-ствляется с помощью повторного наложения того же ключа: const Size = 8; var f, g: file; Path: string; Key: string[Size]; buf: array [1..Size] of char; Transferred: longint; begin write('Введите ключ длины ', Size, ': '); readln(Key); write('Введите имя входного файла: '); readln(path); Assign(f, Path); {$I-} reset(f, 1); {$I+} if IOResult <>0 then begin writeln('Файл ', path,' не найден'); exit; end; write('Введите имя выходного файла: '); readln(path); Assign(g, path); Rewrite(g, 1); // шифрование while not EOF(f) do begin BlockRead(f, buf, Size, Transferred);

Page 137: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

137

// наложение ключа на очередной участок for k := 1 to Transferred do buf[k] := buf[k] xor key[k]; BlockWrite(g, buf, Transferred); end; Close(f); Close(g); end.

Если длина файла кратна длине ключа, то переменная Trans-ferred всегда будет равна Size, иначе на последнем витке цикла пе-ременная Transferred будет меньше Size.

Задание 3. Написать программу расчета энтропии файла. Эн-тропия рассчитывается по формуле

i

ii ppH )log( ,

где

ii

ii N

Np – доля вхождений (вероятность появления) симво-

ла i в файле, а Ni – число вхождений символа i в файле. Поскольку в компьютерах используется двоичная система счис-

ления и общее количество ASCII-символов равно 256, то основание логарифма имеет смысл выбрать равным двум, а пределы сумми-рования – от 0 до 255. Основание логарифма одновременно даёт название единицам измерения энтропии (2 – биты, 10 – диты, e – наты).

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

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

2) расчет энтропии на основе полученного массива по формуле. В стандартной библиотеке языка Паскаль име-ется функция ln(x), возвращающая натуральный лога-рифм. Для получения двоичного логарифма необходимо воспользоваться соотношением loga(b) = ln(b)/ln(a).

Вариант реализации программы выглядит следующим образом:

Page 138: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

138

const size = 1024; type // статистика вхождений символов symbols = array[0..255] of longint; var f: file; Path: string; total: longint; stats: symbols; b: byte; buf: array [1..size] of char; Transferred: longint; k: integer; H: real; begin write('Enter file path: '); readln(Path); Assign(F, Path); {$I-} Reset(F, 1); // буфер в 1 байт, чтобы узнать размер файла в байтах {$I+} if IOResult <>0 then begin Writeln('File not found'); exit; end; Total := FileSize(F); for b:=0 to 255 do stats[b] := 0; // чтение файла и подсчет вхождений символов while not EOF(F) do begin BlockRead(F, buf, size, Transferred); for k := 1 to Transferred do stats[Ord(buf[k])] := stats[Ord(buf[k])] + 1; end; Close(F); // расчет энтропии H := 0; for b := 0 to 255 do begin if stats[b] > 0 then H := H - (stats[b] * ln(stats[b]/Total)); end; // разделим на общий знаменатель слагаемых

Page 139: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

139

H := H / Total / ln(2); // вывод результата и статистики вхождений Writeln('H = ', H: 10: 10, ' bit'); for b := 0 to 255 do writeln('N(', b:3, ')= ', stats[b]); end.

Условие stats[b] > 0 необходимо для того, чтобы исключить вы-полнение функции ln(0) = - , которое приведёт к переполнению вещественного типа.

Для сжатых файлов, к которым относятся архивные файлы (*.zip, *.rar), мультимедиа файлы (*.mp3, *.avi, *.jpg), энтропия близка к 8. Это не случайно. Число 8 – максимальная энтропия для 256 символов, кодируемых восемью битами. Эта величина дости-жима только в том случае, если символы, как бы хаотично, встре-чаются с одинаковой частотой:

8)21(log)

21(log

2561

2561

82

255

082

ii Hp .

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

Page 140: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

140

Глава 14. Динамические переменные, линейные списки

Динамические переменные и их особенности При запуске программы для нее выделяется определенная об-

ласть в оперативной памяти. Выделенный ресурс памяти делится на несколько областей (сегментов):

сегмент кода; сегмент данных и стека; куча (динамическая память).

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

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

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

Понятно, что при такой схеме работы с памятью статические переменные для работы с динамическими данными применять

Page 141: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

141

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

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

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

представления которого в 32-разрядных системах отводится четыре байта (32 бита), что в сумме даёт 232 = 4 Гб адресного пространства. Для удобства всю память можно логически поделить на равные сегменты, причем старшие два байта адреса будут представлять собой номер сегмента памяти, а младшие два байта – смещение ячейки памяти относительно начала сегмента. Таким образом, об-щее число сегментов равно 216 при объеме одного сегмента также 216 байт (64 Кб).

Адрес ячейки памяти, выделенной под значение статической переменной, можно узнать по её идентификатору с помощью функции addr(v) или аналогичной унарной операции взятия адреса, обозначаемой символом '@', который записывается слева от иден-тификатора переменной. Помня о том, что адрес представляется 4-х байтным числом, можно вывести его на экран, предварительно преобразовав его к какому-либо числовому типу. Например: var i: integer; begin // адрес статической переменной известен до инициализации writeln(integer(@i)); i := 9; writeln(integer(addr(i))); end.

Page 142: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

142

В языке Паскаль реализована возможность использования ука-зателей двух видов: типизированных и нетипизированных.

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

Тип указателя указывается при его объявлении. Для типизиро-ванного указателя следует записать символ '^', а за ним название типа данных, для работы с которым вводится указатель. Нетипизи-рованные указатели имеют тип pointer. Указатели объявляются в секции переменных. Например: var pbyte: ^byte; // указатель на значение типа byte prec: ^phone_rec; // указатель на запись типа phone_rec p: pointer; // нетипизированный указатель

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

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

Примеры ссылок на статические переменные: var i: integer; p: pointer; pint: ^integer; begin pint := @i; // ссылка на переменную i p := addr(i); // здесь равносильно p := pint; end.

Page 143: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

143

Для обращения к данным, на которые ссылается указатель, предназначена унарная операция разыменования указателей ^, обо-значаемая символом '^', который в этом случае записывается спра-ва от идентификатора указателя. Операция разыменования опреде-лена только для типизированных указателей.

Например: var i: integer; pint: ^integer; begin i := 9; pint := @i; // Присваиваем указателю адрес переменной i // В ячейку по адресу, хранимому pint, присваивается 5, // что в данном случае равносильно инструкции i := 5; pint^ := 5; // Печатается содержимое ячейки по адресу, // хранимому pint, что равносильно writeln(i); writeln(pint^); i := 2 + pint^; // Равносильно i := 2 + i; end.

Таким образом, после выполнения инструкции pint := @i; разы-менованный указатель pint^ становится синонимом переменной i до следующего изменения значения pint. По сути, разыменованный указатель представляет собой обычную переменную, только безы-мянную. В зависимости от адреса (местонахождения в памяти) это будет либо статическая, либо динамическая переменная.

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

Page 144: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

144

Совместимость указателей разных типов При работе с указателями осуществляется контроль типов. При

этом: 1) типизированному указателю можно присваивать значе-

ние указателя того же типа, а также значения нетипизи-рованного указателя, при этом происходит неявное пре-образование типов;

2) нетипизированному указателю можно присваивать лю-бые значения-адреса, в том числе значения типизиро-ванных указателей, при этом информация о типе теряет-ся;

3) любому указателю можно присваивать ссылки на пере-менные, возвращаемые операцией взятия адреса @ и функцией addr;

4) любому указателю можно присваивать константу NIL, означающую, что указатель не ссылается на какую-либо конкретную ячейку памяти. Константа используется для инициализации значений указателей.

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

Что напечатает следующая программа? type pword = ^word; var p: pword; w: word; pint: ^integer;

P1 P2

37 37 =

≠ P1 P2

37

=

Рис. 10. Разница в сравнении указателей и значений по указателям

Page 145: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

145

begin // печать адреса переменной pint writeln(integer(@pint)); // ссылка на себя (значение = свой адрес) pint := @pint; // снова печать собственного адреса writeln(pint^); // true writeln(p = @(p^)); // true, приведение типов + разыменование writeln((pword(@w))^ = w); end.

Если тип значения, хранимого в ячейке памяти, отличается от типа значений типизированного указателя, который на него указы-вает, то осуществляется автоматическое преобразование типов. При этом возможны аномалии преобразования: const a: array[1..10] of char ='any string'; var p: ^word; begin p := @a; // преобразование первых двух символов в 2-байтовое число writeln(p^); end.

В результате будет напечатано число 28257, равное в 16-ричном представлении числу $6E61, где $61 (младший байт) – ASCII-код буквы ‘a’, $6E (старший байт) – ASCII-код буквы ‘n’.

Работа с динамической памятью Указатель может ссылаться как на участок статической памяти,

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

Для выделения и возврата динамической памяти предназначены следующие процедуры стандартной библиотеки:

Page 146: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

146

procedure New(var P: Pointer) – выделяет участок памяти для новой динамической переменной, на которую будет ссылаться ука-затель P. Размер участка соответствует типу данных, указанному при объявлении P. В результате работы процедуры значением ука-зателя P станет адрес первого байта выделенного участка. Если вы-делить требуемый объем памяти невозможно, то программа ава-рийно завершается.

procedure Dispose(var P: Pointer) – удаляет динамическую пе-ременную, на которую указывает P (возвращает выделенную под неё память системе), созданную с помощью процедуры New.

procedure GetMem(var P: Pointer; Size: Integer) – выделяет участок памяти для новой динамической переменной, на которую будет ссылаться указатель P. Размер участка в байтах равен Size. В результате работы процедуры значением указателя P станет адрес первого байта этого участка. Если выделить требуемый объем па-мяти невозможно, то программа аварийно завершается. Указатель P может быть любого типа.

procedure FreeMem(var P: Pointer[; Size: Integer]) – удаляет динамическую переменную, на которую указывает P, созданную с помощью процедуры GetMem.

procedure Initialize(var V [ ; Count: Integer ] ) – инициализация участка памяти V длины Count нулевыми значениями. Вызов про-цедуры уместен в том случае, если память выделялась не с помо-щью процедуры New.

Пример работы с динамическими переменными: type pword = ^word; var p1: pword; pr: ^phone_rec; begin new(p1); p1^ := 2; inc(p1^); dispose(p1); // выделение памяти под запись и заполнение её полей new(pr); pr^.fio := 'Ivanov'; pr^.phone := '123-56-78'; pr^.email := '[email protected]';

Page 147: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

147

// доступ к полям записи по указателю with pr^ do writeln(fio, phone, email); dispose(pr); end.

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

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

В следующем фрагменте указатель ptr изменяет свое значение на NIL и адрес ячейки с числом 53 безвозвратно теряется. Так мо-гут быть потеряны целые структуры данных, расположенные в ди-намической памяти: var ptr: ^Integer; begin … New(ptr); ptr^ := 53; … ptr := NIL; // с этого момента выделенная память потеряна … end;

Кроме того, попытка вызова Dispose для указателя, который принял значение NIL, приведет к ошибке.

Применение динамической памяти для работы с телефонной книгой

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

Page 148: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

148

Однако постоянные обращения к файлу приводят к низкому бы-стродействию программы. Удобнее и быстрее считать телефонную книгу из файла в динамическую память, произвести необходимые действия и сохранить все изменения обратно в файл. Потенциально динамические переменные могли стать хранилищем записей теле-фонной книги, однако явное объявление указателей в секции var для всех используемых динамических переменных равносильно объявлению статических переменных. Но число записей всё равно заранее неизвестно. Выходит, что такая схема работы не дает ника-кой выгоды при работе с динамической структурой. Кроме того, работа с динамическими переменными сложнее, так как требует явного выделения и возврата динамической памяти, приводит к дополнительному расходу статической памяти для переменных-указателей, а также требует применения операции разыменования.

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

Введём необходимые структуры данных: type phone_rec = record fio: string[60]; phone: string[20]; e_mail: string[50]; end;

Для размещения этой записи в динамической памяти потребует-ся переменная-указатель типа ^phone_rec. Но эту структуру необ-ходимо расширить ещё одним полем для хранения указателя на следующую запись, если таковая понадобится. Это поле также должно иметь тип ^phone_rec. Тогда новый тип будет иметь вид type phone_rec = record fio: string[60];

Page 149: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

149

phone: string[20]; e_mail: string[50]; next: ^phone_rec; end;

Такой тип данных не является допустимым с точки зрения син-таксиса языка Паскаль, так как одно из полей записи ссылается на тип, еще не обработанный компилятором (рекурсивная ссылка). Для разрешения этого конфликта разрешается объявить новый тип-указатель на запись ещё до объявления самого типа-записи, а далее использовать этот тип-указатель внутри объявления типа-записи: type pphone_rec = ^phone_rec; phone_rec = record fio: string[60]; phone: string[20]; e_mail: string[50]; next: pphone_rec; end;

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

На рис. 11 показана схема организации списка записей в памяти.

Рис. 11. Схема организации динамического списка в памяти

Статическая переменная-указатель Head на первую запись типа phone_rec

fio

phone

e_mail

next

Азов Иван

911-02-03

[email protected]

$ff034517

NIL

Динамическая память

Статическая память

290-05-14

$df0da302

fio

phone

e_mail

next

[email protected]

Боев Антон

Page 150: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

150

Таким образом, заранее объявляется лишь одна переменная-указатель на головной элемент списка, а сами записи и связи между ними создаются уже в динамической памяти. При этом первая за-пись ссылается на вторую, вторая – на третью и так далее до NIL. Константа NIL (нулевой адрес) должна завершать любой список, чтобы можно было различить конец списка.

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

дующим образом: 1) выделить память для новой записи. При этом адрес вы-

деленного участка станет значением какой-либо пере-менной-указателя;

2) заполнить поля записи с помощью операции разымено-вания;

3) полю next новой записи присвоить адрес первой записи в существующем списке. Этот адрес хранится в статиче-ской переменной-указателе Head;

4) адрес новой записи поместить в значение переменной Head.

Наглядно этот процесс проиллюстрирован на рис. 12.

Рис. 12. Схема процесса добавления записи в начало списка

Head

p

Head

p

Page 151: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

151

Таким образом, новая запись станет первой в списке. Применяя эту операцию многократно, можно получить список любой длины. Очевидно, начальным значением Head должен быть NIL.

Пример подпрограммы для добавления записи выглядит сле-дующим образом: procedure Add2List(var Head: pphone_rec; fio, phone, email: string); var p: pphone_rec; begin New(p); p^.fio := fio; p^.phone := phone; p^.email := email; p^.next := Head; Head := p; end;

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

Очень важно, чтобы переменная Head всегда указывала на пер-вую запись в списке. Зная адрес первой записи, можно перейти к любой записи списка с помощью поля next. Например, процедура вывода содержимого телефонной книги на экран может выглядеть так: procedure PrintList(Head: pphone_rec); var p: pphone_rec; begin p := Head; while p <> NIL do begin writeln(p^.fio, p^.phone, p^.email); p := p^.next; // переход к следующей записи end; end;

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

мическую память необходимо вернуть системе. Здесь следует осте-регаться потери указателя на начало списка. Например, вызов Dis-

Page 152: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

152

pose(Head) приведет к возврату памяти, выделенной для первой записи, и, одновременно, к потере ссылки на следующую запись, а значит, и на всю оставшуюся часть списка. Следовательно, до уда-ления очередной записи необходимо запоминать ссылку на остаток списка. Процедура удаления списка может выглядеть следующим образом: procedure DisposeList(var Head: pphone_rec); var p: pphone_rec; begin p := Head; while p <> NIL do begin // запоминаем указатель на следующую запись Head := Head^.next; // возвращаем память для первой записи Dispose(p); // переходим к остатку списка p := Head; end; end;

Задание. Реализовать процедуру поиска записи в списке по фа-милии: function Search(Head: pphone_rec; fio: string): pphone_rec; var p: pphone_rec; begin Search := NIL; p := Head; while p <> NIL do begin if p^.fio = fio then begin Search := p; break; // прекращение поиска, когда запись найдена end; p := p^.next; end; end;

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

Page 153: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

153

type // полезная часть, записываемая в типизированный файл phone_rec = record fio: string[60]; phone: string[20]; e_mail: string[50]; end; // служебная часть для работы с динамическими списками pNode = ^Node; Node = record pr: phone_rec; next: pNode; end;

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

Если пойти еще дальше, то служебную часть для работы со спи-ском можно сделать полностью независимой от типа данных, хра-нящихся в этом списке. Универсальный тип данных для работы со списком будет выглядеть следующим образом: type pNode = ^Node; Node = record data: pointer; next: pNode; end;

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

Page 154: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

154

Глава 15. Динамические структуры данных Организация стеков и очередей на линейных списках Рассмотрим следующую структуру данных:

type pNode = ^Node; Node = record data: pointer; next: pNode; end;

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

procedure Push(var Stack: pNode; data: pointer) – добавление элемента в стек;

function Pop(var Stack: pNode): pointer – извлечение элемента из стека (поскольку стек работает по принципу LIFO «Последним пришел, первым вышел», то функция Pop должна извлекать по-следний добавленный элемент);

function Length(Stack: pNode): boolean – текущая длина стека; procedure Clear(var Stack: pNode) – инициализация стека (если

в стеке находились какие-либо данные, то они должны удаляться). Пусть процедура Push добавляет элемент в начало списка. Тогда

// добавление элемента в стек procedure Push(var Stack: pNode; data: pointer); var n: pNode; begin New(n); n^.data := data; n^.next := Stack; Stack := n; end; // извлечение последнего добавленного элемента function Pop(var Stack: pNode): pointer; var n: pNode; begin if Stack <> nil then begin

Page 155: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

155

n := Stack; Result := n.data; Stack := Stack.next; Dispose(n); end else Result := nil; end; // определение длины стека или очереди function Length(Stack: pNode): Boolean; var p: pNode; i: integer; begin i := 0; p := Stack; while p<>NIL do begin i := i+1; p := p^.next; end; Result := i; end; // удаление всех элементов из стека procedure Clear(var Stack: pNode); begin while Stack <> NIL do begin data := Pop(Stack); dispose(data); end; end;

Схожей со стеком структурой данных является очередь – упоря-доченный набор некоторого переменного числа объектов, обработ-ка которых ведется по правилу: «Первым пришел, первым вышел» (от англ. FIFO – First In, First Out), т.е. в порядке поступления дан-ных. Например, часто используются очереди событий, очереди со-общений.

Для организации очереди в динамической памяти требуется, чтобы процедура PushQ и функция PopQ работали с противопо-ложными элементами списка. Например, если процедура PushQ добавляет элемент в начало списка, то извлекать следует послед-ний элемент списка, и наоборот. Пусть процедура PushQ добавляет элемент в конец списка. Тогда функция PopQ для очереди будет

Page 156: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

156

идентична функции Pop для стека. Реализация функции PushQ мо-жет выглядеть так: procedure PushQ(var Queue: pNode; data: pointer); var n, p: pNode; begin New(n); n^.data := data; n^.next := NIL; if Queue = NIL then Queue := n else begin p := Queue; while p^.next <> nil do p := p.next; p^.next := n; end; end;

Для очереди имеет смысл хранить два указателя: на голову и хвост. Тогда для добавления элемента в очередь не нужно будет проходить по всем элементам до её конца, а сразу узнавать, «кто последний». Процедура PushQ, очевидно, станет на степень более эффективной.

Двунаправленные списки, бинарные деревья Рассмотрим следующую структуру данных:

type pNode = ^Node; Node = record data: pointer; left: pNode; right: pNode; end;

Два поля типа pNode дают возможность создания двусвязных динамических структур. Интерес представляет двунаправленный список, каждый элемент которого указывает как на следующий, так и предыдущий элемент списка. Двунаправленный список удобен для реализации очереди с двумя «хвостами» – дека (от англ. DEQ – double-ended queue). Наличие двусторонних связей обеспечивает высокую скорость доступа к элементам дека, а также свободное перемещение по деку в обе стороны. Для дека удобно использовать

Page 157: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

157

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

Пример дека приведен на рис. 13.

Помимо двунаправленных линейных списков эта же структура

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

Data 1

Data 2

Root

Data 3 #

Data 4 # # Data 5 # #

Data 6 # #

Data 1 #

Data 2

Head

Tail

Data 3

Data N #

Рис. 13. Схема организации дека в памяти

Рис. 14. Схема организации дерева в памяти

Page 158: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

158

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

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

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

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

На рис. 15 представлен ориентированный граф.

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

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

1

2

3 5

4

Рис. 15. Пример ориентированного графа

Page 159: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

159

Типы данных, необходимые для представления приведенной

структуры в динамической памяти, могут быть такими: type pArcElem = ^ArcElem; pNodeElem = ^NodeElem; ArcElem = record Arc: pNodeElem; Data: pointer; Next: pArcElem; end; NodeElem = record Data: pointer; Arcs: pArcElem; Next: pNodeElem; end;

Запись NodeElem способна хранить адрес полезной структуры данных, указатели на список исходящих связей типа ArcElem и следующий в списке узел типа NodeElem. В свою очередь запись

1

2

3

4

5 #

#

#

#

#

#

Рис. 16. Схема организации графа в памяти

Page 160: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

160

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

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

Сортировка с использованием линейных списков Сортировку с использованием линейного списка можно постро-

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

Скелет программы сортировки может выглядеть следующим образом: const n = 100; type tarray = array [1..n] of integer; pNode = ^Node; Node = record elem: integer; next: pNode; end; // процедура вставки с сохранение порядка procedure OrderedInsert(var List: pNode; elem: integer); begin … end; // основная процедура сортировки открытого массива procedure Sort(var a: array of integer); var

Page 161: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

161

i: integer; List,p: pNode; begin List := NIL; for i:=Low(a) to High(a) do begin OrderedInsert(List, a[i]); end; // переписываем упорядоченный список обратно в массив p := List; for i:=Low(a) to High(a) do begin a[i] := p^.elem; p := p^.next; end; DisposeList(List); end; begin … Sort(a); … end.

Для того чтобы процедура сортировки работала для массивов любой длины, её параметр должен быть открытым массивом. Для открытых массивов не указывается диапазон значений индекса массива, однако он может быть определен с помощью функций Low(TypeOrArray) и High(TypeOrArray). Функция Low возвращает минимальное значение индекса массива, а функция High – макси-мальное. Функции работают как для переменных-массивов, так и типов-массивов. Когда в процедуру сортировки передается массив а, то неявно в процедуру передаётся информация о его типе, в том числе о диапазоне значений индекса.

Задание. Реализовать процедуру OrderedInsert для вставки эле-мента массива в упорядоченный список.

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

Использование указателя на указатель позволяет реализовать процедуру OrderedInsert лаконично и изящно, хотя и сложнее для понимания:

Page 162: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

162

// вставка с помощью указателя на указатель procedure OrderedInsert2(var List: pNode; elem: integer); var n: pNode; pp: ^pNode; begin New(n); n^.elem := elem; n^.next := NIL; // вставляем первый элемент if List = NIL then List := n else begin pp := @List; while (pp^ <> NIL) and (elem > pp^^.elem) do pp := @(pp^^.next); // если не последний, то приставим хвост if (pp^ <> NIL) then n^.next := pp^; // переставляем найденный указатель на новый элемент pp^ := n; end; end;

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

Page 163: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

163

Глава 16. Командная строка, стиль, тестирование и отладка

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

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

Например, утилита format (исполняемый файл format.com) опе-рационной системы DOS требует указания диска в качестве перво-го параметра. Вызов утилиты может выглядеть следующим обра-зом: format c: /q

При этом программа анализирует все параметры и запускает быстрое форматирование нужного диска. Если в командной строке указать format /? то на экран будет выдана справка об использовании утилиты с опи-санием всех возможных параметров.

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

function ParamCount: word – возвращает количество аргумен-тов, переданных в программу при вызове из командной строки;

function ParamStr(k: word): string – возвращает k-й аргумент, переданный в программу из командной строки.

Программа, выводящая на экран список аргументов, передан-ных из командной строки, может выглядеть следующим образом: program PrintArgs; var i,k,n: word; begin

Page 164: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

164

n := ParamCount; writeln('Всего аргументов в командной строке: ', n); for i := 1 to n do writeln('№', i, ': ', ParamStr(i)); end.

Пусть исполняемый файл программы имеет имя PrintArgs.exe. Если вызвать эту программу из командной строки следующим об-разом: printargs arg1 /? -275.15 "c:\program files\office" то на экране будет распечатано Всего аргументов в командной строке: 4 №1: arg1 №2: /? №3: -275.15 №4: c:\program files\office

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

Чтобы новый пользователь смог самостоятельно разобраться с аргументами программы, принято включать в программу справоч-ный режим, вызываемый с помощью аргумента «/?». При этом про-грамма должна выводить на экран описание правил работы с ней.

Тестирование и отладка Цель тестирования – нахождение ошибок. Формально ошибкой

можно считать несоответствие поведения программы предъявляе-мым требованиям. Цель отладки – нахождение причин возникнове-ния ошибки.

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

Page 165: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

165

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

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

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

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

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

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

При простейшем сценарии тестирования следует проверить: 1) работу программы для случайных входных данных;

Page 166: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

166

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

3) работу программы в исключительных ситуациях (внеш-нее окружение программы также может послужить ис-точником сбоев);

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

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

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

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

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

1) локализация ошибки; 2) выработка предположения о причине ошибки; 3) исправление программы в соответствии с выдвинутым

предположением; 4) попытка воспроизведения ошибки на тех же входных

данных; 5) при повторном проявлении ошибки перейти к шагу 2.

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

Перечислим подходы к локализации ошибки и анализу причин её возникновения:

Page 167: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

167

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

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

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

Работа отладчика управляется командами меню (Run в среде Turbo Pascal) или горячими клавишами:

Run to cursor – выполнить операторы, расположенные до теку-щей позиции курсора в редакторе исходного кода, и приостановить выполнение программы;

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

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

Run – запуск программы. Если она находится в режиме трасси-ровки, то ее выполнение продолжится с текущего шага трассиров-ки. Выполнение программы после команды Run уже не может быть приостановлено;

Page 168: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

168

Program Reset – выход из режима трассировки и остановка про-граммы.

Трассировка может быть продолжена любой из команд Run to cursor, Trace into и Step over. Если на каком-либо шаге трассировки возникает ошибка, то выполнение программы прекращается и на экран выводится сообщение об ошибке. При этом становится ясно, в какой строке ошибка возникла.

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

1) отладочной печатью значений переменных на экран или в файл;

2) отслеживанием значений переменных при трассировке. Первый способ наиболее прост, так как сводится к добавлению

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

Второй способ не требует изменения исходного кода. Програм-ма запускается на трассировку и приостанавливается в нужных местах. Во время очередной остановки с помощью диалога De-bugEvaluate/modify можно выяснить значение любой перемен-ной, попадающей в текущую область видимости. Для этого в поле Expression необходимо ввести имя переменной (элемента массива или записи) или выражение и нажать кнопку Evaluate. Значение будет вычислено и помещено в поле Result. При необходимости, значение переменной можно изменить, чтобы программа продол-жила работу с другим значением. Для этого необходимо ввести но-вое значение в поле New value и нажать кнопку Modify.

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

Page 169: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

169

Стиль Соглашения по идентификаторам Рекомендуется подбирать идентификаторы из слов английского

языка с учетом читаемости и максимальной смысловой нагрузки. Не рекомендуется использовать транслитерацию с русского языка.

Примеры неудачных идентификаторов: var UU: Boolean; Kk: string; Konec: Boolean; Shirina, Vysota: Real; procedure Zvuk(Chast, Dlit: Word); function EstFile(Im: String): Boolean;

Примеры удачных идентификаторов: const Eps = 0.0001; var Sum : Integer; Message: String; Done: Boolean; Width, Height: Real; procedure Beep(Hertz, MSec: Word); function ExistFile(FName: String): Boolean;

Часто названия идентификаторов берут из математики. При

этом используют следующие ассоциативные связи: alpha, beta, gamma – углы; i, j, k – счетчики элементов множеств, перечисление; x, y, z, p, q, r, s, t – неизвестные; p – вероятность; f, g, h – функции; a, b, c, d – коэффициенты или массивы коэффициентов; n, m – верхние границы диапазонов, размерности массивов, чис-

ло элементов; eps – точность; delta – дельта, разность; dx, dy – приращение;

Page 170: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

170

pi, e – известные константы.

Написание идентификаторов Зарезервированные слова языка Turbo Pascal рекомендуется за-

писывать строчными буквами. В любых идентификаторах каждое слово, входящее в идентифи-

катор, рекомендуется записывать с прописной буквы, остальные – строчные. Например: var NextX, LastX: Real; BeepOnError: Boolean;

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

Следует писать с прописной буквы идентификаторы, состоящие из одной буквы, если они не счетчики циклов.

Типы-записи и типы-классы начинать с буквы T (сокращение от type). Типы указателей начинать с буквы P (от pointer). Например: type TPhoneRec pPhoneRec = ^tPhoneRec;

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

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

Рекомендуется комментарии к программе писать внутри симво-лов { и }, а (* и *) использовать при отладке программы как «за-глушки» участков программного кода.

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

Page 171: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

171

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

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

Соглашения по читаемости программ Длина строк программы не должна превышать ширины экрана в

текстовом режиме (80 символов). Структура программы должна отражаться благодаря отступам

для вложенных операторов в 2–3 пробела. Рекомендуется:

операнды бинарных операций (+, := и т.п.) отделять от знака операции пробелом;

при перечислении идентификаторов после запятой ста-вить один пробел (как в литературном языке);

после символа-спецификатора типа «:» (двоеточие) ста-вить один пробел;

буквы в 16-ричных числах записывать прописными (на-пример, $1АFD);

директивы компилятора записывать прописными буква-ми (например, {$I+}).

Как правило, все примеры в справочной системе среды про-граммирования оформлены в хорошем стиле. Следование опреде-лённому стилю – признак зрелости программиста.

Page 172: Курс по информатике kurs... · 2013. 9. 12. · Учебное пособие Москва 2010 . УДК 004.43 (075) ББК 32.973-018.1я7 Д30 ... математические

172

Список литературы Основная

1. Зуев Е.А. Программирование на языке TURBO PASCAL 6.0, 7.0. М.: Радио и связь, Веста, 1993.

2. Джонс Ж., Харроу К. Решение задач в системе Турбо-Паскаль: Пер. с англ. М.: Финансы и статистика, 1991.

3. Пильщиков В.Н. Сборник упражнений по языку Паскаль. М.: Наука, 1989.

4.* Фаронов В.В. Турбо-Паскаль. В 3-х кн. М.: Изд.-во МВТУ,1992, 1993.

5. Гусева А.И. Учимся информатике: задачи и методы реше-ния. М.: Диалог-МИФИ, 2004.

* Книга находится в читальном зале библиотеки НИЯУ МИФИ Дополнительная

1. Епанешников А.М., Епанешников В.А. Программирование в среде Turbo Pascal 7.0. М.: Диалог-МИФИ, 1998. – 288 с.

2. Вирт Н. Алгоритмы и структуры данных. М.: Мир, 1989. –360 с.

3. Кнут Д. Искусство программирования для ЭВМ. М.: Мир, 1976.