ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ РОССИЙСКОЙ ФЕДЕРАЦИИ НАЦИОНАЛЬНЫЙ ИССЛЕДОВАТЕЛЬСКИЙ ЯДЕРНЫЙ УНИВЕРСИТЕТ «МИФИ»
Д.В. Демидов ОСНОВЫ ПРОГРАММИРОВАНИЯ В ПРИМЕРАХ НА ЯЗЫКЕ PASCAL
Учебное пособие
Москва 2010
УДК 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
Оглавление ПРЕДИСЛОВИЕ ......................................................................................... 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
3
Предисловие Учебное пособие представляет собой переработанный и расширенный текст лекций по курсу «Информатика», который читается в НИЯУ МИФИ на кафедре кибернетики студентам групп К1-221, К1-222, К1-223, К1-224, К1-681. Целью самого курса является не столько обучение конкретному языку программирования, сколько формирование абстрактного мышления и освоение приемов программирования в целом. Пособие дополняет доступную в библиотеке литературу по программированию, освещая в сжатой и доступной форме важные аспекты программирования, сохраняя при этом структуру курса. Поскольку конспектирование лекций по тематике программирования весьма непросто, пособие, по мнению автора, окажется полезным при подготовке к семестровому контролю и зачету. В пособии дается минимальное описание синтаксиса языка Паскаль, а также описание типов данных и языковых конструкций, присущих таким распространённым диалектам языка, как Turbo Pascal (Free Pascal) и Delphi. Изложение сопровождается материалом по таким вопросам, как: представление данных в памяти ЭВМ; предпосылки появления тех или иных синтаксических конструкций в операторных языках программирования; семантика операторов императивных языков программирования; сложность вычислений и оптимизация, зависимость эффективности алгоритмов от структур данных; стиль программирования. Пособие содержит теоретический и практический материал, включая примеры построения алгоритмов и оценки их сложности, примеры практической реализации алгоритмов на языке Паскаль, задания для самостоятельного выполнения. Многолетняя практика показала, что аудиторные занятия не могут научить программировать. Преподаватель может лишь направить, указать «белые пятна», дать подсказки, проверить навыки. А изучать язык, развивать абстрактное мышление, воспитывать стиль программирования обучаемый должен сам. Искусством программирования можно овладеть только через практику. Итак, практика каждый день.
4
Глава 1. История и основные понятия программирования Информатика Информатика (русскоязычный аналог термина Computer Science) изучает ряд проблем взаимодействия человека с электронно-вычислительной машиной (ЭВМ). Сегодня выделяют несколько направлений информатики: алгоритмы и структуры данных; теория трансляторов и языки программирования; математические основы информатики (системы счисления, дискретная математика, криптография и др.); теория вычислений и сложность вычислений; базы данных, сети, искусственный интеллект. Данный курс посвящён в основном алгоритмам и языкам программирования. Остальные направления подробно освещаются на курсах по дискретной математике, базам данных и интеллектуальным системам и других. Прежде чем перейти непосредственно к языку Паскаль, рассмотрим основные понятия информатики, выясним, что предстоит программировать и как.
Алгоритм, машина Тьюринга, последовательная архитектура ЭВМ Эра информатики берет начало в 30-х гг. XX в., когда стало возможным создание вычислительных машин на основе электронных устройств. До этого существовали лишь механические устройства, впервые построенные в XVII в.: суммирующие часы Вильгельма Шикарда, 1623 г. (кстати, в этом же году родился Блез Паскаль); счётное устройство Блеза Паскаля, 1642 г.; ступенчатый вычислитель Готфрида Лейбница, 1673 г., впервые предложившего двоичную систему счисления. В XIX в. Чарльз Бэббидж изобретает “Аналитическую машину” (1834 г.), в основе которой заложен принцип разделения информации на команды и данные.
5
В 1843 г. 28-летняя графиня Августа Ада Лавлейс пишет научную работу об аналитической машине Бэббиджа, заложившую научные основы программирования на вычислительных машинах. В её работе была приведена программа, предназначенная для решения уравнения Бернулли1. В России в 70-80-х гг. XIX в. появляются механические арифмометры шведско-русского изобретателя Однера В.Т. и русского математика Чебышёва П.Л. В конце XIX в. Холлерит строит счётную машину на перфокартах, которая затем участвует в переписи населения США. В 1936 г. Аланом Тьюрингом была предложена абстрактная вычислительная машина для формализации понятия «алгоритм», которое, пожалуй, является важнейшим понятием императивного программирования. Гипотетическая машина Тьюринга положила начало теории алгоритмов, теории вычислений и программированию. Сейчас достаточно уяснить основную идею этой машины. Подробнее о ней можно узнать из курсов по дискретной математике. Машина Тьюринга имеет управляющее устройство, способное находиться в одном из множества состояний и бесконечную в обе стороны ленту, разделенную на ячейки. В ячейках ленты могут быть записаны символы некоторого конечного алфавита. Управляющее устройство может перемещаться влево и вправо по ленте, считывать и записывать символы в ячейки. Перемещение управляющего устройства основано на правилах перехода, которые представляют алгоритм, реализуемый машиной Тьюринга. Правила перехода применяются последовательно, путём циклического просмотра набора этих правил и применения подходящего на данном шаге правила до тех пор, пока в текущей ячейке ленты не будет встречен специальный символ останова. Именно идея последовательного исполнения правил (команд, инструкций) и легла в основу архитектуры первых ЭВМ 1940-х гг. Здесь следует отметить, что первые ЭВМ не были программируемыми, а набор команд и программ ЭВМ определялся их соста1
В связи с этим фактом графиню Лавлейс считают первым программистом. Некоторые программисты-романтики в день её рождения (10 декабря) отмечают день программиста. Впрочем, другие отсчитывают 28=256-й день в году – 13 сентября (или 12 сентября в високосный год).
6
вом и способом коммутации составляющих их блоков. Первую ЭВМ разработали в начале 1940-х гг. в Университете Пенсильвании Эккерт и Мочли, она называлась ENIAC (Electronic Numerical Integrator And Computer) – электронный численный интегратор и вычислитель. Перепрограммирование такой машины заключалось в перестройке её блоков и устройств и их перекоммутации. В 1945-м был опубликован отчет по архитектуре новой ЭВМ EDVAC, разработанной тем же коллективом, к которому присоединился Джон фон Нейман и др. В новой архитектуре процессорное устройство отделялось от памяти, а основным принципом являлось хранение и данных и программ в одном виде. Один и тот же подход к рассмотрению данных и команд стал настоящим прорывом в области вычислений. Строительство EDVAC, однако, затянулось, и первой ЭВМ, в которой эта архитектура реализована, стала ЭВМ «Марк I», разработанная в 1948 г. в Университете Манчестера (Великобритания). ЭВМ EDVAC разработана годом позже и введена в эксплуатацию ещё через 2 года. Лишь тогда термин «программирование» конкретизировался до термина «программирование ЭВМ». Первая в СССР ЭВМ с хранимой в памяти программой построена под руководством С.А. Лебедева и запущена в эксплуатацию в 1950-м. До сих пор в основе подавляющего большинства современных вычислительных машин, гораздо более сложных, чем EDVAC или «Марк I», всё также лежит последовательная архитектура, предложенная коллективом из Университета Пенсильвании, не совсем справедливо называемая архитектурой фон Неймана. Интересно, что первая в СССР ЭВМ с производительностью 1 млн операций в секунду (БЭСМ-6) построена в 1965-м. Следующим мощным толчком к развитию ЭВМ стало создание процессоров и наборов команд, но это был чисто технологический прорыв в уже проложенном архитектурном русле. Интересно, что любая функция, которая может быть вычислена физическим устройством (ЭВМ), вычисляется и машиной Тьюринга (тезис Чёрча–Тьюринга). Таким образом, все подобные вычислительные машины оказываются эквивалентными гипотетической машине Тьюринга. Все задачи, разрешимые на машине Тьюринга, можно решить на современной ЭВМ, и наоборот. Помимо применения последовательной архитектуры ведутся исследования и в области параллельных вычислений. Представители
7
параллельной архитектуры – нейропроцессоры, квантовые процессоры. Однако пока их создание весьма трудоёмко, а способы решения задач только разрабатываются. Примеры программируемых устройств – компьютеры, КПК, мобильные телефоны, калькуляторы, бытовая техника, цифровая аппаратура, бортовые системы управления. Сегодня можно программировать не только аппаратные устройства, но и программы. Примеры программируемых программ – игровые персонажи, программные агенты.
Алгоритм Итак, что же такое алгоритм? def. Алгоритм – точный набор инструкций, описывающий порядок действий исполнителя для достижения результата. В частности, алгоритм требует для работы некоторые исходные данные и выдает некоторый результат. Алгоритм имеет следующие признаки: 1) определенность (детерминированность) – следующий шаг однозначно определяется текущим состоянием, что гарантирует постоянство результата для одних и тех же входных данных; 2) понятность исполнителю – алгоритм состоит только из команд, входящих в систему команд исполнителя; 3) конечность – способность завершить работу за конечное число шагов при корректных входных данных; 4) массовость (общность) – применимость для разных входных данных. Основная задача начинающих программистов – развитие абстрактного мышления. Нужно учиться создавать алгоритмы, соответствующие перечисленным выше признакам. И если о первых двух признаках позаботится компилятор, то за последние два придётся отвечать самим. Алгоритм является важнейшим понятием императивного программирования. def. Императивное программирование описывает процесс вычисления в виде инструкций, изменяющих состояние программы. Такое название было получено из-за повелительного наклонения в естественном языке, с помощью которого выражаются приказы, команды исполнителю. Главный вопрос императивного программирования – «Как вычислять?»
8
По сути, императивное программирование – это процедура записи алгоритма на языке, понятном ЭВМ. Противоположное по духу направление программирования – декларативное программирование. def. Декларативное программирование описывает результат вычислений и его свойства без предписывания последовательности действий. Ведь на самом деле важен результат, а не способ его достижения. Главный вопрос декларативного программирования – «Что должно получиться?» Ветвями декларативного программирования являются функциональное и логическое программирование. Навыки декларативного программирования обычно приобретаются на старших курсах.
Первое поколение операторных языков В определении алгоритма фигурирует некий исполнитель алгоритма. Если исполнитель – ЭВМ, то допустимые команды – система команд процессора ЭВМ, и здесь уместно назвать такой алгоритм программой или программным кодом. def. Программа – алгоритм, описанный в терминах конкретной системы команд ЭВМ. Команды предназначены для проведения вычислений, работы с ячейками памяти и аппаратными устройствами ЭВМ, управления порядком выполнения команд и многого другого. Но что такое команда для ЭВМ, которая понимает только язык чисел в своей системе счисления? Это специальный числовой код, который, будучи записанным в определённую область памяти в какой-либо момент времени, предлагает ЭВМ выполнить соответствующее действие, а результат (при его наличии) записать в указанное другим числом место. Поскольку данные и программы в ЭВМ хранятся в одном виде, то данные, в том числе знакомые нам символы, также кодируются числами. Для этого систему команд дополняет таблица кодов, где каждому символу соответствует числовое представление. Таким образом, каждая команда программы и все необходимые данные оказываются закодированными в виде последовательности чисел. Получается программа, представляющая собой последовательность кодов. Будем называть ее машинным кодом.
9
def. Исполнение машинного кода – последовательная интерпретация команд применительно к конкретным данным в соответствии с системой команд ЭВМ. Важно понять, что для ЭВМ машинный код – текст на том единственном языке команд, который она понимает. Поскольку первые программы писались именно в кодах, то языки команд по праву считают первым поколением языков программирования.
Языки программирования второго поколения Первые программисты наизусть помнили коды всех команд и символов, как азбуку Морзе. Но с возрастанием числа доступных команд в процессорах и усложнением решаемых задач становилось всё более трудоёмким не только программирование, но и развитие существующих программ. Всё это послужило предпосылками появления языков второго поколения – ассемблеров. Отныне каждой команде приписывался свой символьный мнемонический код, свои имена получили также регистры процессора, стало возможным записывать данные не только в числовой, но и символьной форме. Программа теперь представляла собой удобочитаемый текст, форматированный так, что каждая команда располагалась на отдельной строке: MOV ADD CLI
AX, DX DX, BX
lbl: NOP JMP
lbl
В результате человеку стало гораздо удобнее, а вот ЭВМ текст на языке ассемблера уже не понимала. Для исполнения такой программы теперь требовалась трансляция (перевод) исходного текста программы с языка ассемблера в машинные коды. Трансляцию можно было осуществить двумя способами: с помощью интерпретатора ассемблера; с помощью компилятора ассемблера с последующей интерпретацией. В чём различие способов трансляции? Интерпретатор – исполняемая программа, входными данными для которой является другая программа, записанная на языке ас-
10
семблера. Задача такого интерпретатора состоит в последовательном преобразовании каждой строки программы на языке ассемблера в машинный код и его немедленном исполнении с дальнейшим переходом к следующей строке программы. Таким образом, для исполнения программы таким способом необходимо сначала запустить программу-интерпретатор. Другой подход состоит в однократном преобразовании текста на ассемблере в машинный код (компиляция) с помощью другой программы (компилятора). Этот машинный код далее можно исполнять произвольное число раз, уже не пользуясь посредником (программой-интерпретатором), так как машинный код способна интерпретировать сама ЭВМ. Сегодня применяются оба способа трансляции. С появлением ассемблеров немного изменилась и терминология: текст на ассемблере стал называться исходным кодом программы, а машинный код, полученный в результате компиляции, – исполняемым кодом. С той поры под программой понимают и то и другое.
Языки программирования третьего поколения С помощью простейших команд приходится долго объяснять, а чтобы заставить ЭВМ сделать что-либо более-менее полезное, требовалось написать много строк ассемблерного кода. Средства операционных систем, конечно, сильно облегчали задачу, автоматизируя работу с устройствами и реализуя некоторые типовые операции. Однако, глядя на программу, едва ли можно было сказать, что она делает, а для того чтобы разобраться в чужом коде или понять хотя бы структуру программы, требовалась масса времени. Так возникла необходимость в более развитых языковых средствах. И они появились. Третье поколение языков программирования характеризуется уходом от аппаратного уровня. Теперь программистам уже не нужно было думать о командах процессора, регистрах, ячейках памяти, прерываниях операционной системы – это стало уделом компиляторов. Произошел большой качественный скачок: место команд процессора заняли операторы, место ячеек памяти и регистров – переменные, место прерываний операционной системы – стандартная библиотека процедур и функций, данные стали типизированными, появились структуры данных.
11
Что это дало? Программа так и осталась последовательностью, только уже не команд процессора, а операторов, но добавилось важнейшее свойство – структурированность. Вообще, операторы (так же как и команды процессора) описывают некоторые алгоритмические действия, но только более сложные. Например, операторы ветвления и операторы циклов делают явной структуру программы, допуская вложенность операторов друг в друга, а составной оператор позволяет обособить последовательность операторов в теле программы в рамках процедуры или функции. Арифметические, логические и битовые операции и операции сравнения стало возможным записывать в привычной со школьной скамьи записи со скобками, в отличие от префиксной формы записи в ассемблерах, когда после команды перечислялись её аргументы. Если раньше для вычисления сложной формулы требовалось написать множество команд в нужном порядке, каждая из которых выполняла лишь одно действие, то теперь в одной строке можно было записывать сложные выражения и сразу же присваивать результат соответствующей переменной. Порядок вычислений определял компилятор на основе приоритетов операций и расставленных программистом скобок. Встроенные атомарные типы данных и конструкторы типов данных дали возможность создания сложных статических и динамических структур данных. Процедуры и функции позволили отделять часто используемые фрагменты кода от основной программы. Таким образом, программный код на языке третьего поколения стал в несколько раз компактней и наглядней, а алгоритмы и структуры данных вышли на первый план. Было положено начало структурному программированию. Пример программы на ассемблере, вычисляющей сумму чисел от 1 до 10: MOV MOV
AX, 0 CX, 10
ADD LOOP
AX, CX rep ; уменьшение CX на 1. Выход при CX=0
rep:
В этой программе данные хранятся в двух ячейках (регистрах процессора) – AX и CX. Регистр AX – аккумулятор, а регистр CX – одновременно и слагаемое и счётчик цикла, меняющийся от 10 до
12
0. Повторение сложения реализовано с помощью команды условного перехода LOOP на метку rep. Переход осуществляется до тех пор, пока СХ не станет равен 0, причём каждый переход уменьшает CX на единицу. К этому моменту в аккумуляторе AX будет накоплена вычисляемая сумма. Пример аналогичной программы на Паскале: var sum, i: integer; begin sum := 0; for i := 1 to 10 do sum := sum + i; end.
Как видно из примера, аналогичный алгоритм удаётся записать с помощью операторов более наглядно и практически всегда короче, чем с помощью команд процессора. Позднее в языки вводились новые понятия – объекты, классы, интерфейсы, компоненты, события и обработчики событий. Это дало возможность оперировать классами объектов и отношений между ними. Выделялись методы программирования, ориентированные на события, объекты, компоненты и др. За несколько десятилетий в мире возникло множество языков императивного программирования третьего поколения, сильно отличавшихся друг от друга как синтаксически, так и своими новшествами и особенностями: Фортран, Алгол, Кобол, ПЛ-1, Паскаль, Си, Ада. Однако все они принципиально схожи, так как основываются на концепции «Как вычислять?» Какие проблемы возникли при использовании языков третьего поколения? Во-первых, задача преобразования программы на языке третьего поколения в машинный код перестала быть тривиальной: если в ассемблере каждому мнемоническому коду соответствовал один машинный код команды, то здесь это однозначное соответствие исчезло и появилось множество способов преобразования структуры операторов. Современный компилятор стал гораздо более сложной программой, способной оптимизировать генерируемый машинный код по скорости или расходу памяти. Во-вторых, абстрагирование от аппаратного устройства ЭВМ привело к возникновению новых типов программных ошибок и неоптимальному расходу ресурсов ЭВМ. От ошибок можно избавиться с помощью средств компиляции, отладки и тестирования.
13
Ошибки можно разделить на два класса по времени их возникновения или обнаружения: ошибки проектирования (разработки), выявляемые при компиляции, и ошибки времени выполнения, выявляемые лишь во время работы программы. Как правило, чем позже обнаруживается ошибка, тем сложнее найти её источник и дороже её исправить, поэтому разработчики языков программирования и компиляторов стремятся минимизировать ошибки времени выполнения. Например, язык Си очень гибок, и для получения работоспособной программы, как шутят программисты, требуется 1 раз её скомпилировать и 100 раз запустить. Язык Ada предельно строг, и для получения работоспособной программы требуется 100 раз её скомпилировать и 1 раз запустить.
Зависимость от платформы, байт-код и псевдокод На протяжении эволюции языков третьего поколения их разработчики пытались так снизить зависимость от аппаратной платформы, чтобы исходный код не менялся при смене платформы, а менялся лишь компилятор. К сожалению, в полной мере эту проблему решить не удалось в силу большого различия программноаппаратных архитектур. Для переноса программы на языке третьего поколения, скажем, с IBM PC на Apple, приходится менять исходный код, чтобы учесть особенности архитектуры, и перекомпилировать программу с помощью соответствующего компилятора. Технология Java решает проблему переносимости с помощью виртуальной машины, имеющей свою систему команд. def. Байт-код – машинный код, состоящий из команд виртуальной машины java. Под каждую аппаратную платформу разрабатывается программа-интерпретатор байт-кода, которая называется виртуальной javaмашиной (jvm). Она учитывает особенности каждой конкретной программно-аппаратной архитектуры. Компилятору Java остаётся преобразовать исходный код на языке Java в этот байт-код. Таким образом, Java-программу достаточно один раз скомпилировать в байт-код, и она будет работать на любой платформе с javaмашиной. Чтобы не зависеть от конкретного языка при представлении алгоритмов широкой общественности (пусть даже такого независимого от платформы) и в то же время исключить различное толко-
14
вание инструкций (шагов алгоритма) в периодической литературе по информатике часто используется псевдокод. def. Псевдокод – полуформальное описание алгоритма в терминах ограниченного естественного языка с элементами математики и теории множеств. Например, Начало Установить sum = 0 В цикле от i = 1 до 10 выполнять Установить sum = sum + i Конец цикла Конец
Сейчас для исследования таких характеристик алгоритмов, как временная сложность и требовательность к ресурсам (памяти) используются системы команд или языки абстрактных машин. Например, Дональд Кнут в своей книге «Искусство программирования» применяет систему команд абстрактной машины MIX, близкой по своей архитектуре к настоящим процессорам (с точки зрения системы команд). Исследователи в области лямбдаисчисления, функционального программирования пользуются категориальной абстрактной машиной (КАМ). Также применяются абстрактная машина Тьюринга и нормальные алгорифмы Маркова. Известны их программные реализации на ЭВМ – своего рода виртуальные машины-интерпретаторы.
Языки четвёртого поколения С усложнением программных систем появилась потребность в специализации и разделении труда. Универсальные языки становились всё менее удобными для решения узких задач. Начало меняться мышление: программисты стали больше думать о самой задаче, а не о том, как приспособить задачу для её решения на каком-либо языке программирования. Языкам четвёртого поколения свойственен высокий уровень абстракции, они не универсальны, так как нацелены на определённую нишу. Например, задача проведения вычислений (то, ради чего создавались ЭВМ) уступила место задаче организации доступа к хранилищам данных. Появились структурированный язык запросов SQL, встроенные языки разработки приложений с базами данных
15
от компаний SAP, Oracle, Software AG, IBM, Lotus. В области автоматизированного проектирования (CAD) можно отметить встроенные языки визуальных инженерных сред, таких как AutoCAD, MathCAD и т.п. В области искусственного интеллекта – языки в инструментальных средствах разработки интеллектуальных систем. Термин «программа» снова изменил своё значение – чаще всего программа на языке четвёртого поколения уже не является самостоятельной, а исполняется под управлением своей программной среды (интерпретируется). Такая программа совсем не обязательно описывает какой-либо алгоритм, т.е. не диктует последовательность действий, это берёт на себя среда выполнения. Грань между императивным и декларативным подходами здесь истончается. В целях увеличения скорости выполнения программа на языке четвёртого поколения может компилироваться в некоторый промежуточный код наподобие байт-кода java и храниться как в текстовом, так и скомпилированном виде.
16
Глава 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 практически идентичен синтаксису Паскаля.
17
Пришло время взглянуть на внутреннее устройство программы, ввести основные понятия. Рассмотрим пример программы, которая определяет, являются ли два числа взаимно простыми, т.е. не имеют общих делителей, кроме единицы: 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) writeln(M, ' and else writeln(M, ' and readln; end.
выдача результата then ', N, ' are not coprime.') ', N, ' are coprime.');
Атомы и молекулы языков программирования Для операторного языка важнейшим строительным материалом являются, конечно же, операторы. Операторы – приказы для ЭВМ в стиле «делай то-то и то-то», выполняющие в языке программирования роль сказуемых, которые записываются с помощью так называемых ключевых слов. В приведенном примере это оператор цикла for, условный оператор if then else, составной оператор begin end. В Паскале все
18
операторы разделяются между собой символом «;» (точка с запятой). Над чем может производить действия ЭВМ? Над данными в ячейках памяти. Ячейка памяти или совокупность ячеек представляют своего рода контейнер для хранения данных, который с точки зрения программы называется переменной. Содержимое контейнера тогда будет называться значением переменной. Для удобства данные типизируют. Типом данных называют поименованное множество значений, задаваемое различными способами. При объявлении переменной указывается её тип, т.е. ограничивается область значений переменной. Как правило, в языке определено несколько встроенных типов данных, например целые и вещественные числа, строки, а также имеются механизмы для определения сложных типов данных на основе простых, например, массивов, структур, классов. Тип данных также определяет и множество допустимых действий над значениями этого типа. В связи с этим роль типов данных настолько велика, что далее она будет рассмотрена более подробно. Для обращения к переменной в теле программе ей назначается уникальное имя – идентификатор. Идентификаторы даются также типам данных, подпрограммам (процедурам и функциям), модулям программы, константам. В нашем примере идентификаторами переменных являются M, N, K, i, а также результат функции notcoprime. В примере использованы встроенные типы данных языка Паскаль – integer и boolean. Важную роль в языке играют такие конструкции, как выражения. Они строятся из констант и переменных, связанных различными функциями и операциями. Например, (Pi * sqr(radius) / 2). Операции (арифметические, логические, сравнения и др.) и функции предназначены для выполнения каких-либо действий над аргументами. Они, как и операторы, играют роль своего рода клея в программе. В примере для вычисления результата notcoprime используется булево выражение, составленное из операций mod, or, =. Совокупность связанных операторов можно выделить в отдельный фрагмент программы – подпрограмму (процедуру или функцию). Функция отличается от процедуры только тем, что возвращает значение, это даёт возможность вставлять вызовы функций в выражения. Связанные по какому-либо смысловому признаку про-
19
цедуры, функции, типы данных имеет смысл выносить в отдельные модули. Тогда основная программа будет применять эти модули. Сами модули также могут использовать другие модули, в том числе модули стандартной библиотеки. В примере реализована одна функция notcoprime, принимающая два аргумента и возвращающая истину, если аргументы не являются взаимно простыми.
Ввод и вывод Представьте программу, в которую все исходные данные уже заложены. Если их требуется изменить, то сначала следует исправить исходный код, а затем перекомпилировать программу. Представьте, что эта программа ни с кем не делится результатами своей работы. Такая программа неуправляема и бесполезна. Но цель программиста состоит в разработке полезных программ или, по крайней мере, таких программ, которыми можно пользоваться. Здесь уместно ввести понятие пользователя или оператора, т.е. человека, взаимодействующего с программой. Итак, программист должен позаботиться о пользователях программы, определить способы их взаимодействия с программой. Взаимодействие может заключаться в передаче данных и (или) управления. Простейший способ взаимодействия обеспечивается средствами ввода-вывода, встроенными практически в любой операторный язык программирования. Подсистема ввода данных обеспечивает работу с клавиатурой, подсистема вывода выдает информацию на дисплей. Вообще говоря, в основе системы ввода-вывода лежит механизм работы с файлами, который будет рассмотрен в следующих главах. Взаимодействие с другими программами и устройствами, такими как мышь, сканер, принтер, осуществляется посредством специальных программных интерфейсов. Что происходит при обращении к подсистеме ввода с клавиатуры? Работа программы приостанавливается до момента возврата управления пользователем. Подсистема ввода начинает сканировать сигналы от клавиатуры и запоминать коды нажатых клавиш. По окончании ввода полученная последовательность передаётся в программу. Если осуществлялся ввод значения переменной, то происходит приведение этой последовательности к типу перемен-
20
ной, например к числу с плавающей запятой. Если преобразование невозможно, возникает ошибка времени выполнения. Система вывода передаёт на экран дисплея последовательность символов. При выводе значения числовой переменной происходит обратное преобразование числа в строку. В примере для ввода данных используется процедура readln, а для вывода текста и значений переменных – процедуры write и writeln стандартной библиотеки. В чём же заключается работа программиста с точки зрения пользователя? Операторы и операции позволяют выполнять некоторые действия над данными. Обращение к данным происходит, так или иначе, через переменные. Таким образом, задача программиста состоит в том, чтобы обеспечить сбор данных от пользователя, преобразовать их и выдать пользователю результат на основе конечных значений переменных.
Понятие блока и замечания по стилю Для удобства восприятия текста программы человеком принято структурировать программу – разделять на независимые блоки и подчёркивать вложенность блоков друг в друга с помощью отступов от начала строки. Блок представляет собой последовательность инструкций, логически связанных между собой и выполняющих в совокупности относительно независимый участок работы. Принято выделять блоки с помощью составного оператора begin end. В приведённом примере код структурирован и снабжён комментариями. Таким образом, подчёркнута структура программы, что также облегчает её понимание. Обратите внимание на то, как названы типы и переменные, как подобраны ключевые слова. Из названий можно сделать некоторые выводы о назначении обозначаемой сущности, а ведь это сильно облегчает понимание программы. Возьмём за правило выбирать осмысленные идентификаторы для вводимых типов, переменных, функций. В противном случае разобраться в программе с такими идентификаторами, как asdf, vv12, type2 и fff4, будет невозможно. В сообществах программистов существуют негласные стилистические правила именования. Сравните: program mypr; var M,N: integer;
21
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. В примере использованы следующие знаки пунктуации: ; отделяет операторы, объявления; : отделяет переменную от ее типа; , отделяет элементы списка друг от друга; = логическая операция равенства;
22
()
круглые скобки, обрамляющие списки параметров или выражения; := оператор присваивания; > операция сравнения «больше»; // однострочный комментарий; ' апостроф (обрамляет строку); . конец программы, а также отделение целой и дробной части числа, отделение записи и поля записи. Разделителями являются пробел, управляющие символы, комментарии.
Общая структура программы на языке Паскаль Программа coprimes включала следующие разделы: заголовок программы (program); раздел объявления переменных (var); раздел процедур и функций (procedure/function); тело программы (begin … end.). Также в программе могут присутствовать: раздел объявления констант (const); раздел объявления типов данных (type); раздел объявления используемых модулей (uses); раздел объявления меток (label). Кроме заголовка и тела программы разделы могут повторяться. Тело программы является обязательным разделом. Таким образом, минимальная программа на Паскале выглядит следующим образом: begin end.
А программа, печатающая на дисплее приветствие, выглядит так: begin writeln("Hello world!"); end.
В языке Паскаль принят следующий подход: сначала необходимо сделать объявления переменных, типов и только затем исполь-
23
зовать. В языке 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.
Роль типизации в операторных языках Далее рассмотрим способы представления данных, их типы и переменные. Как уже говорилось, тип данных – поименованное множество значений, допускающее определённые действия над значениями. Помимо множества значений тип определяет: множество допустимых операций; множество допустимых преобразований в другие типы; внутреннюю структуру хранения данных.
24
Какие проблемы решают с помощью типизации? Типы данных имеют огромное значение при проектировании программ, играют большую роль при компиляции и во время исполнения программы. Проектирование. Как показал опыт программирования, разные типы данных по-разному эффективны для разных задач: одни типы данных хороши для хранения (компактны), другие – для поиска (организованы), третьи – для проведения вычислений и т.п. Таким образом, типы данных во многом определяют способы обработки данных и общее быстродействие программы, а значит, конструировать типы данных в отрыве от задачи недальновидно. Поэтому при проектировании типы данных разрабатываются с учётом алгоритмов их последующей обработки. Компиляция. Человеку свойственно ошибаться. Типы данных помогают компилятору минимизировать ошибки в программе до её исполнения. При компиляции осуществляется сверка типов переменных и присваиваемых им значений. Если присваивается выражение, то тип выражения определяется на основе типов его аргументов и использованных операций. Кроме того, осуществляется проверка допустимости аргументов операций, например, строки нельзя умножать, а массивы сравнивать. Исполнение. В объектно-ориентированном программировании во время выполнения программы используется механизм динамического определения типа объекта для вызова полиморфных методов класса. Здесь следует отметить, что введение типов в язык программирования оборачивается и увеличением сложности построения программ, а также снижением гибкости. Языки сценариев, как правило, являются нетипизированными, что делает их лёгкими для освоения и использования, но всё же менее производительными.
Введение в типы данных Какие типы данных используются в программировании? Типы данных можно условно разделить на несколько групп: простые, структурированные, процедурный тип и указатели. К простым относятся числовые типы (целые, вещественные), логический, перечисляемый, строковый (в зависимости от языка программирования строка может являться простым типом, как в языке Паскаль, а может быть разновидностью массивов, как, например, в Си).
25
К структурированным типам данных относятся массивы, записи, классы, файлы, множества. Процедурный тип определяет число и тип аргументов процедуры (функции); значениями данного типа являются соответственно процедуры или функции. Этот тип играет большую роль в событийно-ориентированном программировании. Механизм указателей позволяет оперировать адресами участков памяти, в которых размещаются значения переменных, как значениями особого типа. Это важнейший механизм для построения динамических структур данных в частности в объектноориентированном программировании. Как задаются множества значений типа? С точки зрения математики множество значений типа можно задать двумя способами: перечислив все возможные значения или указав способ получения всех значений. С точки зрения операторного языка программирования множество значений типа данных определяется в основном внутренним представлением значений в памяти ЭВМ и механизмами контроля типов. Множество значений простого типа определяется способом внутреннего представления. Так, для целых чисел с шагом 1 максимальное число определяется как 28*N, где N – число байт, отводимое для представления числа в ЭВМ с двоичной системой счисления. Если требуется хранить знак числа, то для этого отводится один бит, что в два раза уменьшает диапазон допустимых чисел. Если требуется хранить число с плавающей запятой, то часть памяти отводится под мантиссу, а часть – под порядок числа. Таким образом, числа с плавающей запятой имеют разную точность представления. Множество значений строкового типа определяется алфавитом языка и максимальной длиной строки, зависящей от внутреннего представления. Множество значений структурированного типа есть декартово произведение множеств значений типов элементов, составляющих структурированный тип. Как определяются новые типы данных? Они определяются с помощью встроенных механизмов определения типов и встроенных (стандартных) типов данных. Встроенные механизмы определения типов позволяют определять новые структурированные типы данных. Для каждой группы структурированных типов имеется свой механизм – своего рода метатип, который нужно конкретизи-
26
ровать для построения нового типа. Например, для определения массива необходимо сначала определить тип элементов массива, а для определения записи необходимо определить структуру записи – набор типизированных полей записи. Целые и вещественные встроенные типы, как правило, зависят от длины машинного слова в битах (разрядности ЭВМ), например 8, 16, 32, 64. Машинное слово целиком помещается в регистры процессора, а значит, обрабатывается быстро и просто. Полезным свойством типизированных переменных является то, что с помощью оператора присваивания на основе информации о типе можно создавать копии значений переменной, поскольку известен размер памяти, занимаемый значениями. Как следствие, значения переменных структурированных типов (массивы или записи) можно копировать без поэлементного присваивания. При работе с указателями следует иметь в виду, что в случае присваивания создаётся лишь копия адреса области памяти, а не собственно области памяти. В Java механизм указателей отсутствует, однако возможность создавать динамические структуры данных имеется: в момент присваивания создаётся копия области памяти, в которой размещается динамическая структура. В Object Pascal работа с динамическими массивами возможна без применения указателей.
27
Глава 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 значащих разрядов, целые Переменные вещественных типов могут принимать как положительные, так и отрицательные значения. Значения задаются либо явно с помощью констант, либо неявно через выражения. Примеры объявления и задания переменных:
28
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 = false then …
if a then … if not a then …
if i < j then a := true else a := false;
a := i < j;
29
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() или поставив знак # перед кодом.
30
Рис. 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.
31
Следует отметить, что в языке Си используется один и тот же тип 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), так как при этом программа не будет зависеть от внутреннего представления строк в Паскале. В частности, в языке Си, строки представляются по-иному, а символы в строке индексируются с нуля.
32
Структурированные типы данных Для чего вводятся структурированные типы данных? Любая программа оперирует данными, но данные вовсе не обязаны быть такими же простыми, как числа или символы. В реальности это самые разнообразные структуры, которые программисты пытаются разложить на простейшие элементы и построить программу для их обработки. Представьте, что нужно представить и обработать результаты сотен измерений температуры. Каждое измерение представимо в виде вещественного числа, но не объявлять же для этого сотни разных переменных? Для этого есть массивы. Массивы Массив представляет собой последовательность элементов одного типа. Для объявления массива необходимо указать число и тип элементов массива. Для обращения к элементу массива необходимо указать имя переменной-массива и в квадратных скобках индекс элемента. Например, 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;
33
При таком подходе переменные можно будет присваивать, передавать в виде параметров в функции и процедуры. Массивы могут иметь и несколько размерностей. В этом случае необходимо через запятую указать диапазоны индексов для каждой размерности. В частности, для представления матриц 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.
34
Записи Представьте, что в программе требуется описать некоторый объект реального мира, например запись в телефонной книжке, и далее работать с ним как с единым целым. Это возможно только с помощью совокупности переменных. В Паскале для этого введён тип-запись, в Си – тип-структура. Для объявления типа-записи следует использовать ключевое слово 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.
35
Файлы Понятно, что хранить телефонную книгу в памяти программы неудобно, так как при завершении работы программы все данные будут потеряны. Чтобы избежать утраты данных, необходимо записать их во внешнюю память (жесткий диск). Для этого используются файлы. В языке Паскаль предусмотрены средства для работы с файлами разного типа. Телефонная книга в нашем примере представляет собой последовательность однотипных записей, поэтому удобно объявить типизированный файл. В языке Паскаль существуют и другие типы данных – последовательности, множества, классы. Первые два используются сравнительно редко, поэтому оставлены на самостоятельное изучение. Рассмотрение классов и объектно-ориентированного подхода в программировании выходит за рамки данного пособия.
36
Глава 4. Присваивание и ветвление Оператор присваивания Оператор присваивания – один из базовых в императивном программировании. Происходит он из команды процессора по копированию содержимого одной ячейки памяти в другую (mov в ассемблере). Вот несколько применений оператора присваивания: инициализация значений переменных; запоминание (в некоторой области памяти) промежуточных результатов вычислений для последующего использования; создание копии значения (копии содержимого в области памяти). Оператор действует над двумя аргументами: первый аргумент – некоторая переменная, второй – вычислимое выражение. В языке Паскаль оператор обозначается «:=» (двоеточие равно). Обычно используется инфиксная запись оператора, т.е. сначала записывается переменная, затем оператор, а потом – присваиваемое выражение. Например, beta := 180/n; height := (0.5 + sin(beta)) * tg(alpha);
При компиляции проверяется соответствие типов выражения и переменной. Если типы не совместимы, то возникает ошибка преобразования типов. Например, нельзя число с плавающей запятой присвоить целочисленной переменной. Операционная семантика2 оператора присваивания: 1) во время выполнения программы вычисляется присваиваемое выражение по правилам вычисления выражений. Если при вычислении выражения возникает ошибка, то выполнение прекращается; 2) если требуется приведение типов, то оно выполняется, т.е. полученное значение преобразуется в формат типа переменной;
2
Операционная семантика описывает то как следует интерпретировать фрагмент программы в виде последовательности вычислительных шагов.
37
3) если объём области памяти, отведённой для значения переменной, не равен объему, необходимому для хранения нового значения, то память, выделенная под предыдущее значение переменной, возвращается программному окружению, и выделяется область памяти для нового значения переменной; 4) полученное значение помещается в область памяти, выделенную для переменной. Пояснения: На первом шаге ошибки могут возникать по различным причинам: при вычислении арифметических операций (деление на 0, переполнение типов), преобразовании аргументов функций к нужным типам (ошибка преобразования типов), обращении к несуществующим элементам массивов и т.п. На втором шаге может потребоваться преобразовать, например, число 500 к типу byte, с помощью которого можно представить числа от 0 до 256, тогда возникает ошибка переполнения типа. Если же переменная имеет тип real, то число 500 будет успешно преобразовано в формат числа с плавающей запятой 5.0e+2. Третий шаг выполняется при присваивании строк или динамических массивов, так как выделяемая область памяти зависит от их текущей длины. Для остальных типов объём памяти, необходимый для хранения значения данного типа, вычисляется еще на этапе компиляции. Применение оператора связано с побочным эффектом, возникающем на третьем и четвёртом шаге: предыдущее значение переменной безвозвратно теряется. Этот эффект негативно проявляется в случаях, когда память под значение переменной выделяется динамически в самой программе. Если выделенную память не вернуть программному окружению до присваивания, то после присваивания она так и будет считаться занятой и будет потеряна для дальнейшего использования. Это распространённая ошибка программистов приводит к так называемой утечке памяти. В Java реализован механизм автоматической сборки «мусора», который возвращает неиспользуемые более области памяти программному окружению (менеджеру памяти). Неявное присваивание Присваивание может происходить неявно, т.е. без применения оператора присваивания.
38
Рассмотрим пример: 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. В программе это не-
39
допустимо, поскольку приведёт к ошибочному завершению работы программы. Уместно использовать ветвление. На псевдокоде алгоритм для вычисления корней уравнения может выглядеть так: установить 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
40
writeln('no solution'); end.
d = b2-4ac
Да
Нет D >= 0 ?
x1 = (-b + √d)/2a
x2 = (-b - √d)/2a
Вывести x1 и x2
Вывести «Корней нет»
Рис. 2. Блок-схема алгоритма вычисления корней квадратного уравнения
Оператор ветвления case Ещё один оператор ветвления case … of используется в тех случаях, когда существует несколько альтернативных ветвей. Например, стоит задача получить название месяца по его порядковому номеру в году. Функция, которая это делает, может выглядеть следующим образом: function GetMonthName(MonthNumber: integer): string; begin case MonthNumber of 1: GetMonthName := 'Gennaio'; 2: GetMonthName := 'Febbraio'; 3: GetMonthName := 'Marzo';
41
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';
42
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.
… Аргумент-переменная Подмножество значений 1
Ветвь 1
…
Подмножество значений N
Иначе
Ветвь N
Ветвь N+1
…
… Рис. 3. Блок-схема исполнения оператора case
43
Замечания: значения в альтернативах не должны повторяться или пересекаться между собой – это гарантирует единственность выбора альтернативы; при описании альтернативы можно использовать только константные значения и их комбинации; если нужно указать несколько операторов в одной ветви, то необходимо использовать составной оператор begin … end после двоеточия: case of : begin … end; : … end;
Главное – научиться выбирать подходящий оператор. В силу особенности реализации case-оператор характеризуется логарифмическим временем поиска, а цепочка равносильных if-операторов – более медленным линейным. Возьмём за правило использовать оператор if так, чтобы в основную ветвь помещался наиболее часто исполняемый код, поскольку в случае истинности условного выражения он сразу выполнится, а в случае ложности условного выражения необходим дополнительный переход на альтернативную ветвь.
Выражения и операции Выражение определяет способ вычисления какого-либо значения и записывается согласно синтаксическим правилам языка программирования. В нотации РБНФ3 выражение можно определить следующим образом: 3
Нотация РБНФ (расширенная Бэкус-Наурова форма) была предложена Н. Виртом для описания синтаксиса формальных языков, к которым относится и Паскаль. Описание состоит из набора правил, в каждом из которых определяется некая языковая конструкция (в левой части правила) через комбинацию других конструкций (в правой части).
44
выражение = переменная | константа | вызов_функции | (выражение) | унарная_операция выражение | выражение бинарная_операция выражение вызов_функции = имя_функции "(" [ выражение {"," выражение}] ")"
Здесь знак «|» означает выбор, квадратными скобками обрамляют необязательные конструкции, фигурными – повторение конструкции 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)
Выражения вычисляются в определённом порядке: прежде остальных вычисляются выражения, находящиеся внутри скобок; перед вычислением значения операции или перед вызовом функции вычисляются её аргументы; порядок вычисления операций определяется их относительным приоритетом. Операции с одним приори-
45
тетом вычисляются слева направо, однако в рамках оптимизации компилятор может изменить этот порядок. Аргументы операций должны быть либо одинакового типа, либо совместимых типов. Операции делят на несколько групп: арифметические (+, -, *, /, div, mod); логические (and, or, not, xor); битовые (shl, shr, and, or, not, xor); сравнения (=, , , >=, side3) and (side2 + side3 > side1) and (side1 + side3 > side2); if exist then writeln('Triangle exists') else writeln('Triangle does not exist'); end.
2. Написать программу, производящую над двумя вводимыми операндами указанную операцию ( +, -, *, /). Обратите внимание на потенциальное деление на ноль:
46
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:
1 x2 y2 U 1 x2 1 y2
if x, y D; if x, y D,
где 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 1)and ((y>-x)and(x