Программирование в системе Express Pascal
В этом цикле статей будет рассказано о языке программирования Паскаль и его использовании. Изложение будет вестись на примере системы программирования Express Pascal, реализованной на ПЭВМ "Корвет".
Введение
Чтобы заставить компьютер что-нибудь сделать, ему нужно дать программу выполнения этого действия. А программу нужно сформулировать на каком-то языке. Языки, на которых мы пишем программы - это и есть языки программирования.
Паскаль - один из них. Он появился в конце 60-х гг. и сейчас входит в число самых популярных. Изначально Паскаль задумывался как учебный язык. Из этой цели вытекает несколько требований: во-первых, учебный язык программирования должен быть простым; во-вторых, в нем должны быть представлены все основные концепции построения языков программирования; в-третьих, работа на этом языке должна приучать к правильному стилю программирования. Паскаль блестяще соответствует всем этим требованиям. Более того, оказалось, что Паскаль годится и для профессионального программирования, так что ныне многие широко используемые программы пишутся на Паскале.
Этот язык действительно прост: его полное формальное описание с комментариями умещается на 30 страницах (для сравнения: подобное описание языка Алгол-68 - книга объемом около 500 страниц). Но простота достигнута не за счет отказа от каких-то возможностей, а благодаря очень удачной компоновке языка и использованию простых и ясных конструкций. Дополнительные элементы (расширения) языка, реализованные в системе Express Pascal (в качестве образца для которой была взята система Turbo Pascal), делают его пригодным для написания практически любых программ, в том числе и так называемых системных, предъявляющих очень высокие требования к языку программирования (например, программа "Дисковый редактор" для "Корвета" написана на Express Pascal).
Важной отличительной чертой Паскаля является высокая надежность программирования. Что это такое, лучше всего показывает одна история из программистского фольклора. Рассказывают, что серьезные неприятности при запуске одного из американских космических кораблей возникли из-за маленькой ошибки в программе, написанной на Фортране. В одной из строк вместо DO 5 1=1,10 было написано DO 5 1=1.10
(как быстро вы заметили, чем отличаются эти строки?). Эта почти незаметная глазу описка - точка вместо запятой катастрофически исказила смысл программы. Знающие Фортран легко поймут, в чем дело: компилятор распознал строку не как заголовок оператора цикла (чем она должна была бы быть), а как оператор присваивания: переменной DO5I присваивается значение 1.10. Пробелы в Фортране игнорируются, поэтому запись DO 5 I полностью эквивалентна записи DO5I; переменные объявлять заранее не нужно, поэтому компилятор просто завел новенькую переменную. А в результате программа вычислила совсем не то, что было нужно.
Синтаксис Паскаля (правила записи программ) устроен так, что подобные ошибки практически исключаются. Да и многие иные ошибки, которые причинили бы немало забот при работе с другими языками программирования, вы легко обнаружите с помощью Паскаль-компилятора.
Скажем несколько слов о способах реализации языков программирования. Изначально компьютер понимает только язык машинных команд - последовательности нулей и единиц. Чтобы научить его более сложному (для компьютера, но более простому для человека!) языку, нужно снабдить его соответствующей программой.
Программы, обучающие компьютер новым языкам, бывают двух типов: интерпретаторы и компиляторы. Интерпретатор заставляет компьютер исполнить программу на языке программирования в том виде, в каком она была написана человеком: ЭВМ берет очередной оператор программы, анализирует его и выполняет предписанные оператором действия. Если приходится повторно выполнять какой-либо оператор, то анализ его выполняется заново. Всякий раз, когда вам нужно выполнить программу, вы должны запускать интерпретатор.
Компилятор переводит программу с языка программирования на язык машинных команд, и компьютер исполняет программу в "родных" для него машинных кодах. Само исполнение происходит, естественно, без всякого участия компилятора: он уже сделал свое дело, осуществив перевод.
Оценим достоинства и недостатки интерпретаторов и компиляторов.
Достоинства интерпретатора. Программу можно выполнить, как только она написана (не тратя времени на компиляцию). На ПЭВМ интерпретатор всегда объединен с редактором текстов, поэтому, если в программе обнаружится ошибка, ее можно быстро исправить и немедленно запустить программу снова.
Недостатки интерпретатора. При каждом запуске программы в памяти компьютера кроме ее текста должен находиться интерпретатор; это сильно ограничивает максимальный размер программы. При каждом исполнении каждого оператора интерпретатор выполняет его анализ заново; на это уходит много времени, в результате скорость выполнения программы уменьшается приблизительно на порядок по сравнению со скоростью выполнения скомпилированной программы. По этим причинам большую программу, которую нужно будет постоянно использовать, создавать с помощью интерпретатора неэффективно.
Достоинства компилятора. Он порождает компактную и быстро работающую программу.
Недостатки компилятора. Процесс создания программы занимает много времени.
При использовании традиционно построенного компилятора приходится выполнять такую последовательность действий: подготовить текст программы с помощью редактора текстов; скомпилировать программу; отредактировать связи (присоединить к вашей программе написанные другими программистами кусочки, выполняющие стандартные действия); запустить полученную программу в машинных кодах. Если в ней обнаруживается ошибка, приходится возвращаться к исходному тексту, исправлять ошибку и повторять весь цикл заново. На таком компьютере, как "Корвет", исправление одной ошибки в самой маленькой программе занимает около 5 мин (а в большой программе - еще дольше).
Если вы работали на "Корвете" с Бейсик-интерпретатором и компилятором с Паскаля МТ+, вы на своем опыте постигли все сказанное.
По-видимому, Борланду первому пришла в голову мысль о том, как соединить достоинства компилятора и интерпретатора; эту мысль он реализовал в своей системе Turbo Pascal. (После того как идея реализована, она кажется очевидной. Но не так просто до нее додуматься. Первой моей реакцией, когда я услышал о системе Turbo Pascal, было: "Это невозможно!")
Идея крайне проста: создать интегрированную систему, включающую редактор текстов и компилятор. Вы вводите текст программы, пользуясь редактором; после того как ввод закончен, запускаете компилятор (он уже находится в памяти, и времени на его загрузку не требуется!); компилятор размещает рабочую программу в памяти, и ее можно тут же запустить. Если компилятор обнаружит ошибку в программе, он запускает редактор и устанавливает курсор в том месте текста, где находится ошибка. При работе с маленькой программой время, затрачиваемое на цикл "исправление ошибки - компиляция - запуск", составляет всего несколько секунд - потери по сравнению с интерпретатором практически незаметные. Скомпилированную программу можно записать на диск и выполнять впоследствии без участия интегрированной системы.
Express Pascal является именно такой интегрированной системой. Входной язык системы является расширением языка Turbo Pascal V. 4.0 для компьютеров типа IBM PC. Если в программе с IBM PC не использовались машиннозависимые черты языка, то ее можно без всяких изменений скомпилировать и выполнить на "Корвете".
Если вы спросите, каким языком лучше пользоваться при работе на "Корвете" - Бейсиком или Паскалем - и какому языку лучше обучать школьников, наш ответ будет однозначным: конечно, лучше Паскаль. Работать с системой Express Pascal ничуть не сложнее, чем с Бейсик-интерпретатором, а возможностей она предоставляет больше; человек, научившийся писать программы на Бейсике, приобретает и навыки, которые будут мешать ему писать серьезные программы, в то же время навыки, приобретенные при работе с Паскалем, обеспечивают хороший стиль программирования и позволяют легко осваивать новые языки.
В этом цикле статей мы постараемся научить вас писать программы на Паскале и работать с системой Express Pascal. При описании работы с компьютером мы предполагаем, что у вас есть Express Pascal и документация к нему. В документации подробно описано, как работать с системой, и приведено подробное формальное описание языка. Поэтому в "ИНФО" мы будем обращать внимание в первую очередь не на то, как выполнить какое-либо действие (это можно узнать из документации), а на то, когда, зачем и почему это действие можно выполнить. По сути этот цикл статей является учебником, дополняющим формальную документацию.
Простейшие элементы языка Паскаль
Давайте сначала напишем и выполним очень простую программку:
var x : integer;
begin
Write('Введите число: '); ReadLn(x);
x := x * x;
WriteLn('Квадрат введенного числа = ', x);
end.
Она вводит целое число, вычисляет его квадрат и выводит его на экран.
Обратите внимание на то, как размещен текст программы. Чтобы программу было легко читать и изменять, текст следует располагать некоторым регулярным образом. Сейчас мы сформулируем несколько первых правил, которых рекомендуется придерживаться.
- В каждой строке должен стоять только один оператор. Несколько операторов можно размещать в строке только в том случае, если они образуют неразрывную последовательность действий, как, например, в третьей строке примера: находящиеся в ней операторы выполняют выдачу вопроса и получение ответа.
- Слова begin и end всегда появляются парами. Парные слова begin и end должны либо находиться в одной и той же строке, либо начинаться с одной и той же позиции; в последнем случае в строках, где стоят begin и end, не должно быть ничего другого.
- Если одна конструкция (begin - end, цикла и т. п.) вложена в другую, то строки вложенной конструкции должны начинаться правее строк объемлющей конструкции. В примере строки, расположенные между begin и end, сдвинуты на две позиции вправо.
Далее по мере описания новых конструкций мы будем формулировать правила их записи.
Попробуйте скомпилировать и выполнить эту программку. Для этого:
- выйдите в основное меню системы Express Pascal (если вы находитесь в редакторе, нажмите для этого Esc);
- нажмите клавишу С; программа будет скомпилирована, в нижней части экрана появится сообщение о завершении компиляции; нажмите Esc, система вернется в основное меню;
- нажмите клавишу Р; начнется выполнение программы;
- в верхней строке экрана появится "Введите число: "; введите в ответ какое-нибудь число (например, 25) и нажмите ВК;
- во второй строке экрана появится "Квадрат введенного числа = 625", а в левом нижнем углу - "Press Esc". Ваша программа закончила свою работу, и теперь система ожидает, пока вы ознакомитесь с результатами ее трудов. Нажав Esc, вы вернетесь в основное меню.
Теперь займемся программой. Почему эти строки делают то, что они делают, - вводят число и выводят его квадрат? Почему мы написали их так, а не иначе?
Наша программа работает с числом. В период работы это число должно где-то храниться. Место, где программа хранит число, называется переменной. Переменную удобно понимать как ящичек, снабженный этикеткой - именем переменной. По этому имени мы можем обращаться к переменной. Ящички-переменные могут иметь разные формы и размеры, определяющие, что можно положить в ящичек. Форма и размер ящичка - это тип переменной.
В Паскале каждая переменная, используемая в программе, должна быть описана. Для описания переменной и используется первая строка нашей программы:
var x : integer;
Слово var служит признаком того, что дальше будет идти описание переменных. Каждый раз, когда описывается переменная (или группа переменных), нужно употребить это слово. Слово х - имя переменной. Имена переменных мы придумываем сами. Они могут состоять из латинских букв, цифр и знаков подчеркивания, но начинаться должны с буквы. В системе Express Pascal имя переменной может быть почти любой длины (единственное ограничение - оно должно целиком помещаться в одной строке, а ее максимальная длина - 127 литер).
Имена нужно давать не только переменным, но и другим объектам, которые вы описываете в программе (константам, типам, процедурам, функциям и т. п.). Во всех случаях имена строятся по одним и тем же правилам (описанным в предыдущем абзаце). Имя часто называют идентификатором (это слово вы можете встретить в других статьях или книгах, например в документации по системе Express Pascal).
Третье слово - integer - это тип переменной х. Тот факт, что тип переменной х есть integer, означает, что значением переменной может быть целое число в диапазоне от -32768 до 4-32767.
Двоеточие используется для отделения имени переменной от ее типа; точка с запятой завершает описание переменной.
В других реализациях максимально размер имени может быть ограничен. Например, компилятор МТ+ требует, чтобы имя было не длиннее 8 литер; Turbo Pascal допускает имена длиной до 63 литер. Возможность использовать длинные имена очень полезна. Пока ваша программа занимает только 6 строк и в ней используется только одна переменная, нетрудно разобраться, для чего используется переменная х. Но когда программа перевалила за 1000 строк, а число переменных - за несколько десятков, им надо давать осмысленные имена. Например, если в переменной нужно хранить счетчик страниц, назовите ее page_counter - и любому (в том числе и много месяцев спустя, когда особенности программы будут подзабыты) легко будет понять, что в ней хранится. Конечно, не нужно увлекаться и писать сверхдлинные имена - от этого неоправданно увеличится длина текста, да и вам придется изрядно потрудиться. Например, если в переменной хранится общее число страниц, ее можно назвать number_of_pages, но лучше использовать сокращение, например, назвать ее nr_pages.
Сокращения старайтесь использовать систематически. Например, если вместо слова number где-то использовано nr, делайте так и впредь и никогда не ставьте nr вместо другого слова.
Обратите также внимание на то, как использованы в примерах знаки подчеркивания для разделения длинного имени на отдельные слова. Кроме того, очень удобны для разделения имени прописные буквы. Судите сами: PageCounter, NumberOfPages, NrPages. Но имейте в виду: практически во всех реализациях Паскаля (в частности, в системе Express Pascal) строчные и прописные буквы не различаются (за исключением случаев, когда они входят в текстовые константы), так что имена PageCounter и pagecounter совпадают! Я сам обычно использую прописные буквы в именах процедур и функций, а в именах переменных, констант и типов - знаки подчеркивания.
В первой строке нашей программы есть три слова, выглядящих как имена: var, х и integer. X и integer действительно являются именами: имя х придумали мы, имя integer предопределено в языке для обозначения целочисленного типа. А вот var - это зарезервированное слово. В чем различие между именами и зарезервированными словами? Зарезервированные слова могут употребляться только в одном смысле - в том, который предписывает им синтаксис языка. Слово var обозначает начало описания переменных и не может быть использовано иначе. Имена же можно использовать в любом смысле (там, где требуется имя). Мы назвали переменную х, но могли бы назвать так константу или процедуру. И имя integer мы могли бы использовать для обозначения переменной (обычно этого делать не стоит, чтобы не усложнять работу; есть, однако, случай, когда удобно переопределить предопределенное имя - о нем мы расскажем позже).
В нашей программе использовано три зарезервированных слова: var, begin и end. Всего в Паскале таких слов около 50. Мы будем выделять их в тексте полужирным шрифтом, чтобы легче было замечать их и привыкать к ним.
Вернемся к тексту программы. (Уже так много сказано, а разобрана только одна строка! Но дальше дело пойдет быстрее.) Он состоит из двух частей: раздела описаний и раздела операторов. Раздел описаний у нас - одна строка (которую мы разобрали); в ней описывается переменная х.
Раздел операторов всегда начинается зарезервированным словом begin (его появление является признаком конца раздела описаний) и заканчивается зарезервированным словом end, после которого должна стоять точка. Между begin и end располагаются операторы. Они говорят о том, что программа должна делать (а описания - о том, с чем программа должна что-то делать). Операторы отделяются друг от друга точкой с запятой.
В нашей программе четыре оператора: первые два вводят число с клавиатуры, следующий вычисляет квадрат числа, последний выводит результат на экран. Займемся сначала оператором, вычисляющим квадрат числа:
x := x * x;
Его называют оператором присваивания. Он состоит из двух частей, левой и правой, соединенных двоеточием и знаком равенства (знаком присваивания). В левой части оператора присваивания должна находиться ссылка на переменную, в правой - выражение.
Ссылка на переменную (в нашем примере это просто имя переменной; о других видах ссылок на переменную мы поговорим позднее) указывает на место в памяти ("ящичек"), в которое можно что-то записать.
Выражение строится из констант, ссылок на переменные и вызовов функций с помощью знаков операций и круглых скобок по обычным математическим правилам. В арифметических выражениях для целых чисел можно использовать знаки
+ | сложение | |
- | вычитание | |
* | умножение | |
div | деление нацело | |
mod | взятие остатка |
(Внимание! Некоторые знаки операций являются зарезервированными словами: здесь это div и mod). Порядок выполнения операций - обычный математический: сначала умножение и деление, потом сложение и вычитание. Если порядок нужно изменить, употребляются скобки.
Константы в целочисленных арифметических выражениях записываются как обычные десятичные числа: 12, 1, 5421 и т. д. Пробелы внутри констант недопустимы.
Примеры выражений:
a + b
23 + x * y
-(nr_lines-1) div 60 - (x1 + y1) mod z22
Выполняется оператор присваивания так: вычисляется выражение, стоящее в правой части, и полученное значение присваивается переменной, указанной в левой части (т. е. кладется в ящичек, на который указывает ссылка на переменную в левой части). Использование ссылок на переменные в левой и правой частях существенно различается. У переменной, находящейся в правой части, нас интересует только значение - содержимое ящичка; нам совершенно неважно, где этот ящичек находится. У переменной из левой части нас, наоборот, интересует только ее местонахождение - в этот ящичек должно быть помещено вычисленное значение, а исходное содержимое неважно - оно просто теряется в результате выполнения оператора присваивания.
В нашей программе при присваивании берется значение переменной х (а туда оператор ReadLn(х) поместил то, что вы ввели с клавиатуры) и умножается опять же на значение переменной х. После того как значение вычислено, оно помещается в переменную х. Запомните: сначала вычисляется выражение: если в левой и правой частях оператора присваивания указана одна и та же переменная, то при вычислении выражения будет использоваться исходное значение переменной и только потом ей будет присвоено новое значение.
Немного подробнее об операциях div и mod. Для положительных чисел их применение дает очевидный результат:
25 div 6 = 4 25 mod 6 = 1 17 div 3 = 5 17 mod 3 = 2 15 div 5 = 3 15 mod 5 = 0
Если же один или оба операнда отрицательны, то результат операции i div j есть математическое частное от деления i на j, округленное до ближайшего целого значения по направлению к 0, а результат операции mod определяется по формуле i mod j = i - (i div j). Таким образом, знак результата операции mod всегда совпадает со знаком левого операнда. Примеры:
25 div -6 = -4 25 mod -6 = 1 -25 div 6 = -4 -25 mod 6 = -1 -25 div -6 = 4 -25 mod -6 = -1 17 div -3 = -5 17 mod -3 = 2 -15 div 5 = -3 -15 mod 5 = 0
Теперь займемся операторами, расположенными в первой и последней строках нашей программы. Они выполняют вывод информации на экран и ввод информации с клавиатуры. По своей форме (синтаксически) эти операторы являются вызовами процедур. Оператор вызова процедуры состоит из имени процедуры и списка параметров, заключенного в скобки.
Процедура - это набор действий, выполнять которые нужно достаточно часто; чтобы в программе не появлялись постоянно одни и те же куски, эти действия описываются один раз и оформляются в виде процедуры. Теперь для их выполнения достаточно вызвать процедуру с помощью соответствующего оператора вызова процедуры.
Процедуры для своей программы обычно пишет сам программист. Есть, однако, наборы действий, встречающиеся почти во всех программах - например, ввод и вывод данных. Для выполнения таких общих действий предназначены стандартные процедуры. Они создаются разработчиками компиляторов Паскаля, и их не нужно описывать - вы можете просто пользоваться ими. В языке Express Pascal около 90 стандартных процедур.
Рассмотрим четыре стандартные процедуры: Read, ReadLn, Write, WriteLn. Они выполняют ввод с клавиатуры и вывод на экран. Эти процедуры имеют переменное число параметров (может быть, и ни одною).
Параметрами Write являются выражения. Процедура преобразует их в текстовый вид и выводит на экран. В первом вызове процедуры Write нашего примера один параметр - строка "Введите число: ". Строковый параметр выводится на экран без всякого преобразования, и вы получаете в результате на экране строку "Введите число: ". Вывод на экран начинается с текущей позиции (при старте программы она находится в левом верхнем углу экрана; в текущей позиции находится курсор); после окончания вывода текущей становится та позиция, где закончился вывод текста. Если в процессе вывода достигается конец строки, то вывод продолжается с начала следующей (например, если конец строки достигнут в процессе вывода числа, то оно окажется разорванным на две части).
В последней строке нашей программы вызывается процедура WriteLn с двумя параметрами. Первый из них - строка, второй - переменная х. Переменная х имеет тип integer, поэтому ее содержимое будет выводиться в формате целого числа. Вывод на экран текстового представления числа, записанного в х, начнется сразу же после окончания предыдущего текста, никаких пробелов вставлено не будет. Поэтому мы и поставили пробел в конце строковой константы "Квадрат введенного числа = ".
WriteLn отличается от Write только тем, что она после завершения вывода переводит текущую позицию в начало следующей строки.
Чтобы лучше понять, как работают процедуры Write и WriteLn, попробуйте выводить с их помощью различный текст на экран и посмотрите, что у вас будет получаться.
Процедура Read выполняет ввод с клавиатуры текстов и чисел. Ее параметры - ссылки на переменные. Входная информация должна быть оформлена по определенным правилам в соответствии с типом переменной. В нашем примере параметр - переменная х типа integer, поэтому должна быть введена последовательность цифр, представляющая целое число, возможно, начинающаяся со знака "+" или "-". Если же будет введено что-либо другое, возникнет ошибка.
Когда вы, работая с программой, выполняете ввод, инициированный процедурой Read, все вводимые символы немедленно отображаются на экране. Но в программу эта информация попадет только после того, как вы нажмете клавишу "возврат каретки". Поэтому можно исправлять допущенные при вводе ошибки - нажатие клавиши back space (она называется еще "возврат на шаг", находится в правом верхнем углу клавиатуры и обозначена двойной стрелкой влево) приводит к стиранию последнего введенного символа.
Числа вводятся по тем же правилам, по которым они записываются в программе. Целые числа записываются последовательностью десятичных цифр, возможно, со знаком "+" или "-" в начале.
С помощью одного вызова процедуры Read можно ввести несколько чисел. При вводе они могут разделяться пробелами или располагаться на разных строках. Если в строке вы ввели больше чисел, чем задано параметров, то оставшуюся часть строки программа запомнит и использует при следующем вызове процедуры Read. Например, если в программе написано
Read(x); Read(у); Read(z);
(переменные х, у, z типа integer), а вы ввели с клавиатуры
11 222 333
то при выполнении первого вызова процедуры Read будет прочитано первое число (11) и занесено в переменную х; остаток будет использован при выполнении следующего вызова процедуры Read, и в переменную у попадет значение 222; в переменную z попадет значение 3333.
ReadLn отличается от Read тем, что после завершения ввода объектов, заданных ее параметрами, она пропускает все оставшиеся в строке символы до конца строки. Поэтому, если в программе написано
ReadLn(x); ReadLn(у); ReadLn(z);
а вы ввели с клавиатуры
11 222 333
то при выполнении первого вызова процедуры ReadLn будет прочитано первое число (11) и занесено в переменную х, остаток же будет отброшен. При выполнении следующего вызова процедуры ReadLn программа снова будет ожидать ввода с клавиатуры. Таким образом, вам придется ввести три строки, и из каждой будет использовано только первое число.
В заключение нашего простого примера рассмотрим другие целочисленные типы. Переменная х имеет у нас тип integer, это значит, что значениями ее могут быть целые числа в диапазоне от -32768 до +32767. Имеется еще четыре целочисленных типа:
- shortint - его значениями могут быть целые числа в диапазоне от -128 до +127;
- longint - его значениями могут быть целые числа в диапазоне от -2147483648 до +2147483647;
- byte - его значениями могут быть целые числа в диапазоне от 0 до 255;
- word - его значениями могут быть целые числа в диапазоне от 0 до 65535.
Те, кто знаком с двоичной системой счисления и с байтами, сразу увидят, что это значения, которые можно разместить в одном, двух или четырех байтах. Зачем же столько целочисленных типов? Почему бы не пользоваться только типом longint? Из соображений экономии. Чем короче значение, гем меньше места будет занимать скомпилированная программа и тем быстрее будет она работать. Если вас не волнуют размер программы и ее быстродействие, можете спокойно забыть про все целочисленные типы, кроме longint; но если вам важна эффективность программы, выбирайте тип покороче.
Лучший способ научиться программировать - писать программы. Попробуйте, как сумеете, изменить наш простой пример: пусть вычисляется не квадрат введенного числа, а какое-нибудь другое значение; пусть вводится не одно, а два числа и вычисляются их сумма и произведение; пусть выводятся на экран разные строки и числа. Пробуйте!
Еще одна простая программа
Попробуем создать еще одну простую программу - решения квадратного уравнения. Она будет вводив три коэффициента (а, Ь и с) и выдавать два корня уравнения. Никаких проверок (является ли уравнение квадратным, имеет ли оно вещественные корни) выполняться не будет.
var a, b, c : real; {коэффициенты уравнения}
D, SD : real; {дискриминант и квадратный корень из него}
x1, x2 : real; {корни уравнения}
begin
Write('Введите коэффициенты уравнения a, b и c: ');
ReadLn ( a, b, c );
D := Sqr(b) - (4*a*c);
SD := Sqrt(D);
x1 := ( -b + SD ) / ( 2*a);
x2 := ( -b - SD ) / ( 2*a);
WriteLn;
WriteLn('Корни квадратного уравнения:');
WriteLn(' x1 = ', x1 );
WriteLn(' x2 = ', x2 );
end.
Введите программу в компьютер и попробуйте решить с ее помощью несколько уравнений (как это делать, рассказывалось ранее).
А теперь разберемся, как она работает. Первые четыре строки описывают используемые переменные. Новое здесь - тип real (real - это предопределенный идентификатор, обозначающий вещественный тип). Значения переменных этого типа - вещественные числа (их еще называют действительными), т. е. такие, которые могут иметь дробную часть. Диапазон возможных значений вещественных - от 2*10-39 до 2*1038 (приблизительно); количество значащих десятичных цифр - около 12.
Напомним, что такое значащие цифры. Это понятие появляется при выполнении приближенных вычислений. Любой прибор, измеряющий физическую величину, дает некоторую ошибку, величина которой зависит от погрешности прибора. Например, если мы измерили вольтметров, напряжение и получили величину 37,25В, причем известно, что вольтметр имеет погрешность 0,1%, то верить можно только первым трем цифрам результата - четвертая yжe содержит ошибку. Истинное значение напряжения может быть 37,26В, или 37,2373В, или каким-нибудь еще - точного значения мы все равно не получим. Те цифры, которым можно верить в приближенном числе, и называются значащими.
В компьютере вещественное число представляется с помощью конечного числа цифр, поэтому только немногие числа будут представлены точно, большинство же - только приближенно. Ведь память компьютера состоит из конечного (хотя и очень большого) числа элементов, а на любом отрезке числовой оси имеется бесконечно много вещественных чисел. Поэтому, какой бы способ представления вещественных чисел мы ни выбрали, всегда найдется число, которое в данном компьютере данным способом не может быть представлено точно.
Способ преставления вещественных чисел, используемый в системе Express Pascal, обеспечивает 12 верных цифр. Обратите внимание - 12 верных цифр, считая от первой значимой, а не после десятичной точки. Например если вы получили в результате вычислений число 123456789.01200000, нельзя утверждать, что последние пять его цифр именно нули; истинное значение их может быть и другим, но компьютеру не по силам вычислить эти цифры. Четвертый после десятичной точки знак здесь уже может содержать ошибку. Но если результат есть 123.456789012, то верить можно девяти цифрам после десятичной точки.
Нужно заметить что приближенные вычисления на компьютере - весьма тонкая и деликатная наука. Этой теме посвящено много серьезных математических исследований. Ведь в процессе вычислений ошибки могут накапливаться, и из-за этого ошибка в результате может намного превосходить ошибки в исходных данных. Хороший пример того, на какие подводные камни можно наскочить, дает наша программа решения квадратного уравнения.
Попробуйте решить уравнение с коэффициентами 1, 4, 3. Ответ будет точным: 1 и -3. Но попробуйте взять коэффициенты 1, 10000000, 1; корни будут - 10000000 и 0! Но ведь из теоремы Виета следует, что ни один корень квадратного уравнения с ненулевым свободным членом не может равняться нулю. Действительно, если мы вычислим оба корня с точностью до 20 знаков, то получим 9099999.99999990000000 и 0.000000100000000000001000000. Если эти значения округлить до 12 знаков, то получим 10000000 и 0.0000001 - и именно такой результат нам хотелось бы получить. Не подумайте, что 0.0000001 - слишком маленькое число для компьютера и поэтому он не может не вычислить точно. Попробуйте решить уравнение с коэффициентами 1, 4е-18, Зе-36 (что означает такая запись, рассказано ниже) - результат получится абсолютно точным. Попробуйте чуть-чуть изменить коэффициенты 1, 3.999999999е-18, Зе-36 - соответствующим образом изменится ответ. Так что компьютер успешно справляется с гораздо меньшими числами. Откуда же возникает такая грубая ошибка при коэффициентах 1, 10000000, 1? Она не в программе и даже не в реализации вещественной арифметики. Плох выбранный алгоритм решения квадратного уравнения. Он будет давать заметную ошибку всегда, когда корни уравнения сильно различаются (абсолютная величина корней не существенна, важна именно величина отношения между корнями). Кстати, разработка алгоритма, который при любых коэффициентах квадратного уравнения вычислял бы его корни с точностью, обеспечиваемой машинной арифметикой, - задача на уровне курсовой работы студента, специализирующегося в области вычислительной математики.
После зарезервированного слова begin идут уже знакомые операторы вывода запроса и ввода коэффициентов. Но здесь мы будем вводить уже не целые, а вещественные числа. Правила записи вещественных чисел такие.
Если вещественное число не имеет дробной части, то оно может быть записано по правилам записи целых чисел.
Дробное вещественное число может быть записано как десятичная дробь, которая может начинаться со знака и в которой целая часть отделяется от дробной точкой (обратите внимание - точкой, а не запятой!). Если в записи вещественного числа присутствуем точка, то перед ней и после нее должны быть цифры (нельзя написать, например, 2. или .01 - нужно писать 2.0 и 0.01 соответственно).
Любое вещественное число может быть записано в экспоненциальной нотации, т. е. в виде целого или дробного числа, за которым следует буква Е (латинская; строчная или прописная) и целое число (возможно, со знаком). Экспоненциальная нотация означает следующее: число, стоящее перед буквой Е, должно быть умножено на 10 в степени число, стоящее после буквы Е. Например, 1.23е3 обозначает число 1230; 0.9е-5 обозначает число 0.000009. Экспоненциальная нотация предназначена для записи очень больших и очень маленьких чисел. Попробуйте сосчитать нули в числе 0.0000000000000000000093 - и тогда вы оцените преимущества записи 0.93е-20. В экспоненциальной записи часть числа, стоящая перед буквой Е, называется мантиссой, а часть числа, стоящая после буквы Е, называется порядком.
Внутри записи вещественного числа не могут появляться пробелы и другие разделители: например, запись "2.1 е 3" недопустима.
Примеры записи вещественных чисел:
14299 16.9453 Зе11 +14299 -2517.0 +2.311Е-12 -123456789123456789 0.0123 0.234е+21
В большинстве европейских стран и в США при записи десятичных дробей принято отделять дробную часть от целой не запятой (как у нас), а точкой. Это соглашение используется и во всех языках программирования. По мере распространения компьютеров и в нашей стране все чаще и чаще используется точка вмести запятой.
Как и в случае целых чисел, вещественные константы в программе записываются по тем же правилам, что и вводятся с клавиатуры.
В строках, описывающих переменные, находятся тексты, заключенные в фигурные скобки. Это комментарии. Они никак не обрабатываются компилятором и нужны для того, чтобы облегчить чтение и понимание программы человеком. Комментарий должен начинаться открывающей фигурной скобкой "{" и заканчиваться закрывающей фигурной скобкой "}"; внутри комментария могут стоять любые знаки, кроме закрывающей фигурной скобки. Комментарий можно поместить в любую точку Паскаль-программы, в которой может находиться пробел. Можно продолжить его на несколько строк; например, нашу программу мы могли бы начать, с комментария
{ Эта программа
вычисляет
корни квадратного уравнения }
Но мы рекомендуем все-таки ставить открывающую и закрывающую комментарий фигурные скобки в каждой строке и писать так
{ Эта программа }
{ вычисляет }
{ корни квадратного уравнения }
Такая привычка позволит избежать ошибок наподобие следующей:
var a, b, c : real; {коэффициенты уравнения
D, SD : real; дискриминант и квадратный корень из него
x1, x2 : real; корни уравнения}
Здесь описания переменных D, SD, xl, х2 попали в комментарий и стали невидимыми для компилятора. Такая ошибка очень неприятна: на первый взгляд все в порядке, но компилятор говорит, что идентификатор D не определен.
В комментарии мы рекомендуем включать сведения, необходимые для понимания того, что и как делает программа, но только те, которые не очевидны из текста программы. В частности, при описании переменных стоит указывать в комментарии, что будет в них находиться. Но, например, комментарий
Write('привет'); {вывести "Привет!"}
только загромождает программу.
После операторов ввода коэффициентов идут четыре строки, содержащие операторы присваивания. Эти операторы и вычисляют корни уравнения. Отличие этих операторов присваивания от тех, которые мы обсуждали раньше, только в том, что выражение, стоящее в правой части, является вещественным арифметическим выражением. Как и целое арифметическое выражение, вещественное строится из констант, ссылок на переменные и вызовов функций с помощью знаков операторов и круглых скобок. В вещественном выражении некоторое подвыражение может быть целым. Например:
var k, m, n : integer;
x, y, z : real;
begin
z:= (2*k + m div n) / (x * y);
end.
Здесь целым является подвыражение (2*k + m div n). В Паскале подвыражения вычисляются как целые до тех пор, пока это возможно; затем целое значение преобразуется в вещественное, и далее операции выполняются над вещественными числами.
В вещественных выражениях знаки операций div и mod могут появляться только внутри целых подвыражений. Для обозначения операции деления вещественных чисел используется знак "/". Если же операция "/" применяется к целым операндам, то они перед выполнением операции преобразуются в вещественные, после чего выполняется операция деления вещественных чисел.
В операторе присваивания в левой части может быть указана вещественная переменная, а в правой находиться целое выражение. В таком случае выражение будет вычислено как целое, а затем его результат будет преобразован в вещественное представление, и оно будет присвоено переменной, указанной в левой части.
Вообще, если в каком-то месте Паскаль-программы требуется вещественное значение, а указано целое, то целое значение автоматически преобразуется в вещественное. Обратное преобразование (вещественного значения в целое) никогда не выполняется автоматически. Таким образом, оператор присваивания, в левой части которого указана целая переменная, а в правой части находится вещественное выражение, является ошибочным. Однако при необходимости вещественное значение может быть преобразовано в целое с помощью стандартных функций.
Теперь о функциях. В Паскале они очень похожи на обычные математические: получают аргументы и вырабатывают результат. Вызов функции на Паскале записывается в такой форме:
имя функции (аргумент, ..., аргумент)
В качестве аргументов (или параметров) функции должны быть указаны выражения. Количество аргументов и их тип зависят от функции.
В первом из операторов присваивания нашей программы
D := Sqr(b) - (4*a*c);
используется функция Sqr. Она должна иметь один аргумент целого или вещественного типа; ее результатом является квадрат ее аргумента; тип результата будет целым, если аргумент был целым, и вещественным, если аргумент был вещественным.
Во втором операторе присваивания
SD := Sqrt(D);
используется функция Sqrt. Она должна иметь один аргумент вещественного типа; ее результат - квадратный корень из ее аргумента, он тоже имеет вещественный тип.
Обе эти функции являются стандартными, т. е. они созданы разработчиками компилятора и вы можете пользоваться ими, не заботясь об их описании. Вы можете описать в программе и свои функции; о том, как делать это, мы расскажем позже.
Обратите внимание на то, что аргументом функции может быть не только переменная, но и выражение. Например, вместо первых двух операторов присваивания мы могли бы написать один:
SD := Sqrt( Sqr(b) - (4*a*c) );
Мы не сделали так лишь потому, что собираемся в будущем усовершенствовать нашу программу, добавив проверку знака дискриминанта.
Еще одно замечание об операторах, вычисляющих корни. Можно было бы не вычислять отдельно квадратный корень из дискриминанта, а вместо этого последние два оператора присваивания заменить на
x1 := ( -b + Sqrt(D) ) / ( 2*a);
x2 := ( -b - Sqrt(D) ) / ( 2*a);
Это было бы хуже исходного варианта тем, что квадратный корень из дискриминанта вычислялся бы дважды (а операция извлечения квадратного корня довольно длинная). Конечно, в такой короткой программе, как наша, этого замедления работы вы не заметили бы; однако если программа должна была решить подряд тысячу квадратных уравнений, замедление было бы существенным.
Можно было би еще ускорить нашу программу, вынеся в отдельный оператор вычисление произведения 2*а.
Последние три строки в разделе операторов нашей программы выполняют вывод результатов на экран. Сейчас вещественные числа будут выводиться в экспоненциальном формате и занимать 17 позиций: одна позиция - знак числа, 12 - мантисса, одна - буква "е", еще одна - знак порядка, две позиции - порядок.
Если вам не нравится такой формат вывода, можете изменить его. Для этого после любого параметра процедуры Write (или WriteLn) можно поставить двоеточие и число. Эта комбинация задает количество позиций, отводимых на экране для выводимого значения. Если такое число не задано, процедура Write отведет ровно столько позиций, сколько необходимо для записи значения; если число позиций задано, то значение будет дополнено слева необходимым числом пробелов; ни если указанное число позиций меньше необходимого, Write нарушит заданные рамки и напечатает значение целиком.
Такая возможность удобна для вывода большого количества чисел в виде аккуратной таблицы. Попробуйте, например, выполнить такую программку:
var x11, x12, x13, x21, x22, x23, x31, x32, x33 : integer;
begin
x11:=1; x12:=31; x13:=-111;
x21:=22; x22:=0; x23:=1;
x31:=12345; x32:=-321; x33:=76;
WriteLn( x11, x12, x13 );
WriteLn( x21, x22, x23 );
WriteLn( x31, x32, x33 );
end.
Числа, выводимые на экран, слипнутся, и вы не сможете ничего понять. Но замените операторы вывода
WriteLn( x11:7, x12:7, x13:7 );
WriteLn( x21:7, x22:7, x23:7 );
WriteLn( x31:7, x32:7, x33:7 );
и на экране появится аккуратная таблица.
Кроме того, для вещественных чисел (и только для них) можно задать число десятичных знаков, поставив после числа позиций еще одно двоеточие, а после него - число десятичных знаков. Такой способ вывода удобен, когда заранее можно оценить порядок выводимых чисел. Например, если вы собираетесь решить квадратные уравнения, корни которых лежат в интервале от 0.1 до 10, и вам нужно знать три знака после точки, операторы вывода можно заменить на следующие:
WriteLn(' x1 = ', x1:6:3 );
WriteLn(' x2 = ', x2:6:3 );
Попробуйте и посмотрите, что получится.
Количество позиций и количество десятичных знаков может задаваться не только константами, но и произвольными целыми арифметическими выражениями. Это позволяет писать программы, способные изменять формат вывода в зависимости от исходных данных.
При выводе вещественных чисел всегда производится округление до последнего выводимого знака.
Теперь давайте посмотрим, как система будет реагировать на ошибки. Они могут встретиться в любой программе, и процесс их исправления (отладка) - обычный этап в разработке программы. Ошибки можно разделить на следующие категории:
- Ошибки в записи программы (синтаксические). Их замечает компилятор и сообщает о них.
- Ошибки, которые обнаруживаются системой при выполнении программы (например, деление на 0 или извлечение квадратного корня из отрицательного числа). Они диагностируются системой, и компилятор поможет вам в их обнаружении.
- Ошибки в алгоритме (например, вычисление дискриминанта по формуле Sqr(b)+2*а*с, а не Sqr(b+4*a*c).
Последние можно обнаружить только путем внимательного анализа программы; компилятор здесь ничем помочь не сможет. (В самом деле, откуда компилятору знать, как должно решаться квадратное уравнение и, вообще, что данная программа решает квадратное уравнение?)
Здесь мы обсудим, как использовать компилятор для поиска ошибок первых двух категорий. Попробуем делать ошибки и смотреть, как на них реагирует компилятор. (Здесь будет описываться поведение системы Express Pascal; поведение системы Turbo Pascal очень похоже, поведение других систем может отличаться внешне, но по сути будет тем же).
Давайте сначала в первой строке нашей программы заменим двоеточие на точку с запятой
var a, b, c ; real; {коэффициенты уравнения}
и попробуем скомпилировать ее. Компилятор обнаружит ошибку и выдаст сообщение:
Error 62: ":" expected Press Esc.
("Ошибка 62: ожидается ":". Нажмите Esc"). После того как вы нажмете Esc, система автоматически перейдет в режим редактирования текста, и курсор в тексте будет установлен на знак ";". Все, что вам остается сделать, - исправить эту литеру и снова запустит) компиляцию.
Несколько более сложным может оказаться поиск ошибки, вызванной искажением имени. Замените в первой строке программы b на w
var a, w, c : real; {коэффициенты уравнения}
и попробуйте скомпилировать программу. Компилятор выдаст сообщение об ошибке:
Error 73: Unknown identifier Press Esc.
("неизвестный идентификатор"). После нажатия Esc система перейдет в режим редактирования текста и курсор будет установлен на имя b в операторе ReadLn(а, b, с). Действительно, идентификатор b не был определен; но чтобы найти истинную причину ошибки, придется вернуться к разделу описания переменных.
Особенно трудно находить ошибки, вызванные несбалансированностью зарезервированных слов begin и end. Обь чно в хорошо написанной программе расстояние между парными словами begin и end может достигать нескольких десятков строк (в плохо написанной программе они могут оказаться еще дальше). Обнаружить это компилятор может, как правило, очень нескоро, зачастую только в конце программы. А найти причину ошибки можно только просмотрев все begin и end. Чтобы избежать таких ошибок, мы рекомендуем при наборе программы сразу после слова begin вводить слово end, а уж потом вставлять между ними операторы.
Теперь об ошибках, которые могут возникнуть в период выполнения программы. Первый их вид - ошибки в формате данных. Попробуйте, например, на запрос "Введите коэффициенты уравнения а, Ь, с:" ввести число с запятой вместо точки: "1 1,2 0,1". Внизу экрана появится общение:
Run-Time Error 15. РС=....#: Invalid number format Press Esc.
(Ошибка времени исполнения 15. Счетчик адреса=...: Неправильный формат числа). Выдаваемое в этом сообщении значение счетчика адреса может быть использовано для поиска оператора, при выполнении которого была обнаружена ошибка (как это сделать, рассказано в документации). Конечно, в нашем случае сразу видно, что мы ошиблись при вводе; но при отладке сложной программы информация о том, при выполнении какого оператора возникла ошибка, может оказаться весьма полезной.
Другой вид ошибок, возникающих в период исполнения программы, - применение операций к недопустимым аргументам. Например, квадратный корень из отрицательного числа извлечь нельзя. Задайте нашей программе коэффициенты, при которых дискриминант будет отрицательным (например, 1 2 3), и будет выдано сообщение об ошибке:
Run-Time Error 16. РС=....#: Invalid argument Press Esc.
(Неправильный аргумент). Это сообщение должно заставить вас проверить введенные коэффициенты. Если они таковы, что квадратное уравнение не имеет корней, то ничего не поделать. Но если вы видите, что корни должны быть (а программа не смогла их найти из-за ошибки в формуле вычисления дискриминанта, но вы этого пока не знаете), то следует искать ошибку в программе. Компилятор поможет найти оператор, при выполнении которого возникла ошибка,- это оператор извлечения квадратного корня из дискриминанта. Раз коэффициенты такие, что корни должны быть, а квадратный корень из дискриминанта не извлекается,- значит, программа неправильно вычисляет дискриминант. После этого рассуждения вы анализируете оператор, вычисляющий дискриминант, и находите ошибку.
Мы рассмотрели здесь только несколько примеров возникновения ошибочных ситуаций. Начав работать с компьютером, вы встретите много других. Полный список сообщений об ошибках есть в документации; там же вы найдете рекомендации по их поиску и устранению (Если же у вас нет документации потому, что вместо того, чтобы купить систему, вы просто скопировали ее у кого-то, то поделом вам.)
Условные операторы. Составной оператор
Вернемся к программе решения квадратного уравнения. Один из ее недостатков - то, что она не проверяет, имеет ли решаемое уравнение вещественные корни (если вещественных корней нет, возникает ошибка времени исполнения).
Программа решения квадратного уравнения с проверкой существования вещественных корней будет иметь следующий вид.
var a, b, c : real; {коэффициенты уравнения}
D, SD : real; {дискриминант и квадратный корень из него}
x1, x2 : real; {корни уравнения}
begin
Write('Введите коэффициенты уравнения a, b и c: ');
ReadLn ( a, b, c );
D := Sqr(b) - (4*a*c);
if D >= 0
then begin
SD := Sqrt(D);
x1 := ( -b + SD ) / ( 2*a );
x2 := ( -b - SD ) / ( 2*a );
WriteLn;
WriteLn('Корни квадратного уравнения:');
WriteLn(' x1 = ', x1 );
WriteLn(' x2 = ', x2 );
end
else WriteLn('Уравнение не имеет вещественных корней');
end.
Чтобы выполнить проверку, нам понадобился условный оператор if. В общем случае оператор if имеет формат
if <логическое-выражение> then <оператор1> else <оператор2>;
Логическое выражение строится способом, очень похожим на способ построения арифметического выражения: из констант и переменных логического типа и элементарных логических выражений с помощью знаков операций not, and, or и xor. Любое логическое выражение принимает одно из двух значений: "истина" или "ложь".
Имеются две константы логического типа: true и false, значениями которых являются "истина" и "ложь" соответственно. (С точки зрения синтаксиса true и false являются предопределенными идентификаторами.)
Переменная логического типа - это переменная, тип которой есть boolean (это тоже предопределенной идентификатор). Например, следующее описание определяет переменную flag как логическую:
var flag : boolean;
В программах логические константы и переменные используются относительно редко; в основном используются логические выражения, построенные из элементарных логических выражений. Элементарное логическое выражение строится из двух логических выражений с помощью операций сравнения:
= | - | равно |
> | - | больше |
< | - | меньше |
>= | - | больше или равно |
<= | - | меньше или равно |
<> | - | не равно |
(Обратите внимание на то, что знаки последних трех операций составлены из двух литер. В их записи нельзя ставить пробел между литерами. Например, запись "<>" обозначает знак операции "не равно", а запись "< >" - идущие подряд знаки операций "меньше" и "больше".)
Значением элементарного логического выражения является "истина", если выполняется соответствующее соотношение между значениями арифметических выражений, и "ложь" в противном случае.
Примеры элементарных логических выражений:
5<7 - имеет значение "истина";
(х+1)<>2 - имеет значение "ложь", если значение переменной х есть 1, и значение "истина", если значение переменной х отлично от 1.
Операции not ("логическое отрицание"), and ("логическое И"), or ("логическое ИЛИ") и xor ("исключающее ИЛИ") задаются следующими значениями из таблицы:
not | and | or | xor | ||||
---|---|---|---|---|---|---|---|
Истина | Ложь | Истина | Ложь | Истина | Ложь | ||
Истина | Ложь | Истина | Ложь | Истина | Истина | Ложь | Истина |
Ложь | Истина | Ложь | Ложь | Истина | Ложь | Истина | Ложь |
Примеры логических выражений, использующих логические операции (мы предполагаем, что х - переменная вещественного типа):
not (х<3) - эквивалентно х>=3;
(х<1.5) ог (х>3) - принимает значение "истина", если х лежит вне отрезка [1.5, 3];
(х>=1.5) and (х<=3) - принимает значение "истина", если х принадлежит отрезку [1.5, 3].
(Последнее условие - "х принадлежит отрезку [1.5, 3]" - нельзя записать иначе; обычная математическая запись "1.5<=х<=3" в Паскале недопустима.)
А теперь мы предлагаем вам новый взгляд на выражения в Паскале. Мы говорили о целых арифметических, вещественных арифметических и логических выражениях. Если мы и дальше будем двигаться таким путем, нам потребуется рассмотреть еще несколько видов выражений. Вместо этого удобнее (и логичнее с точки зрения структуры языка) говорить просто о выражениях.
Выражения строятся из констант, переменных и вызовов функций с помощью знаков операций и круглых скобок. Порядок выполнения операций определяется приоритетами операций и скобками (как обычно в мате магию). Каждая операция требует операндов определенных типов и вырабатывает результат определенного типа (который может отличаться от типов операндов). Некоторые операции допускают несколько вариантов типов операндов; можно считать в таком случае, что имеется несколько операций, которые обозначаются одним и тем же знаком; какая из обозначаемых этим знаком операций должна быть применена, определяется типом операндов. Например, можно считать, что знаком "+" обозначаются четыре операции умножения:
Тип | ||
---|---|---|
Левого оператора | Правого оператора | Результата |
Целый | Целый | Целый |
Целый | Вещественный | Вещественный |
Вещественный | Целый | Вещественный |
Вещественный | Вещественный | Целый |
С этой точки зрения выражение
(x+1 > 1.5) and (y <= 3)
трактуется следующим образом:
- вещественное значение переменной х складывается с целым значением 1; получается вещественный результат;
- вещественное значение подвыражения х+1 сравнивается с вещественным значением 1.5; получается логический результат;
- вещественное значение переменной у сравнивается с целым значением 3; получается логический результат;
- выполняется операция "логическое И" над логическими значениями подвыражений (х+1>=1.5) и (у<=3); получается логический результат.
Приоритеты (1 - высший, 5 - низший) уже известных нам операций задаются следующей таблицей:
Операция | Приоритет | Категория |
---|---|---|
not | 1 | Унарные операции - 1 |
* / div mod and | 2 | Операции типа умножения |
+ - (унарные) | 3 | Унарные операции - 2 |
+ - (бинарные) or хог | 4 | Операции типа сложения |
= <> < > <= >= | 5 | Операции сравнения |
Теперь можно прояснить один момент, который мог показаться загадочным: почему мы в примерах логических выражений вроде
(х+1 >= 1.5) and (у <= 3)
старательно брали операнды логической операции в скобки (в большинстве других языков программирования этого не требуется). Дело в том, что приоритет операции and выше чем приоритет операций сравнения, и поэтому по правилам языка в выражении
х+1 >= 1.5 and у <= 3
подразумевается такая расстановка скобок:
( (х+1) >= (1.5 and у) ) <= 3
- а это совсем не то, что нужно, и даже недопустимо, по правилам языка! Поэтому если бы такое выражение было написано без скобок, компилятор выдал бы сообщение об ошибке "типы операндов не соответствуют операции" (так как операция and неприменима к вещественным операндам). По такой диагностике сразу и не сообразишь в чем дело, не правда ли? Но компилятор прав, ничего более разумного он сообщить не может (выражение
( х >= (true and у) ) <= 3
в котором х и у - переменные типа boolean, вполне допустимо!).
Чтобы избежать таких загадочных сообщений об ошибках, мы и рекомендуем завести себе правило - всегда заключать операнды логических операций в скобки.
А почему приоритет логических операций сделан таким, что приходится ставить скобки? Почему приоритет логических операций не сделан ниже приоритета операций сравнения? Ответ на этот вопрос кроется в деталях языка, которые мы еще не обсуждали,- и не будем обсуждать в настоящем цикле статей. Дело в том, что, кроме операций not, and, or и хоr, применимых к операндам логического типа, есть еще операции not, and, or и хоr, применимые к операндам целых типов (они выполняют побитовые операции). Наличием таких операций и объясняется выбор приоритетов.
Теперь вернемся к оператору if. Как мы уже упоминали, в общем случае он имеет формат
if <логическое-выражение> then <оператор1> else <оператор2>;
Его выполнение происходит следующим образом. Сначала вычисляется логическое выражение. Если его значение есть "истина", то выполняется оператор1, и на этом выполнение оператора if заканчивается. Если же его значение есть "ложь", то выполняется оператор2.
Логическое выражение часто называют условием, а оператор1 и оператор2 - then-альтернативой и else-альтернативой соответственно.
Если в случае ложности условия никаких действий выполнять не нужно, слово else и следующий оператор2 можно опустить. Оператор if в таком случае принимает вид
if <логическое-выражение> then <оператор1>;
Обратите внимание на то, что в случае наличия else-альтернативы ";" после then-альтернативы не ставится. Если вы напишете
if x > 0 then a:=1; else a:=-1;
то компилятор выдаст сообщение об ошибке "Ожидается оператор", и курсор будет установлен на слово else. Действительно, по правилам языка точка с запятой завершает оператор (и компилятор считает, что оператор if не содержит else-альтернативы); после точки должен начинаться новый оператор, а оператор не может начинаться с зарезервированного слова else.
Записывать оператор if мы рекомендуем либо целиком в одной строке программы, либо в несколько строк так, чтобы зарезервированные слова if, then и else начинались в одной и той же позиции (посмотрите, как записан оператор if в примере).
В качестве then- или else-альтернативы оператора может быть указан любой оператор, в том числе и другой оператор if. В такой ситуации может возникнуть неоднозначность. Посмотрите на следующую строку:
if <усл1> then if <усл2> then <оп1> else <оп2>;
К какому оператору if - первому (внешнему) или второму (вложенному) - относится альтернатива else? Для разрешения этой неоднозначности в Паскале принято правило: слово else связывается с ближайшим слева словом if, с которым еще не связано никакое слово else. Таким образом, приведенный выше оператор в соответствии с нашими рекомендациями следует записать так:
if <усл1> then if <усл2> then <оп1> else <оп2>;
А как быть, если при выполнении какого-то условия нужно выполнить не один, а несколько операторов? Тут на помощь приходит составной оператор. Составной оператор позволяет превратить последовательность операторов в один оператор. Для этого перед первым оператором последовательности нужно поставить зарезервированное слово begin, а после последнего - зарезервированное слово end. (Зарезервированные слова begin и end иногда называют "операторными скобками"; заключение последовательности операторов в oneраторные скобки превращает последовательность в один оператор). Последовательность операторов, расположенная между begin и end, может содержать произвольное количество операторов, в том числе один или ни одного. Последнее не гак уж бессмысленно: в период создания большой программы полезно по ходу дела проверять уже написанные куски. Вы можете размещать пустые операторные скобки begin и end в тех местах, где в будущем должна появиться последовательность операторов, получая таким способом возможность скомпилировать без ошибок неоконченную программу.
В рассматриваемом примере программы решения квадратного уравнения в операторе if then-альтернатива является составным оператором, а else-альтернатива - простым.
Использование составного оператора поможет и в том случае, когда слово else нужно связать с не с ближайшим слева словом if, а с более далеким. Этого можно достичь с помощью такой, например, конструкции:
if <усл1> then begin if <усл2> then <оп1> end else <оп2>;
В Паскале имеется и другой условный оператор - case. Мы здесь обсудим только простейший вариант его использования; полное описание смотрите в документации.
Рассмотрим пример. Программа должна ввести с клавиатуры одну из букв А-Д (эта буква понимается как код одной из пяти возможных команд) и выполнить соответствующие действия. Если бы мы использовали оператор if, то у нас получилось бы следующее:
var c : char;
begin
c:= ReadKey;
if c = 'А'
then begin
{ Действия по команде "А" }
end
else if c = 'Б'
then begin
{ Действия по команде "Б" }
end
else if c = 'В'
then begin
{ Действия по команде "В" }
end
else if c = 'Г'
then begin
{ Действия по команде "Г" }
end
else if c = 'Д'
then begin
{ Действия по команде "Д" }
end
else WriteLn('Ошибочная команда');
end.
Использование оператора case значительно упростит нашу программу:
var c : char;
begin
c:= ReadKey;
case c of
'А': begin { Действия по команде "А" } end;
'Б': begin { Действия по команде "Б" } end;
'В': begin { Действия по команде "В" } end;
'Г': begin { Действия по команде "Г" } end;
'Д': begin { Действия по команде "Д" } end;
else WriteLn('Ошибочная команда');
end{case};
end.
Оператор case устроен следующим образом. Он начинается с зарезервированного слова case; после слова case должно стоять выражение (в нашем случае выражение очень простое - оно состоит из одного имени переменной "с"); затем должно стоять зарезервированное слово of; дальше идут case-альтернативы (в нашем примере их пять; они начинаются с 'А', ..., 'Д'); после них может стоять else-альтернатива (ее можно и опустить); заканчивается оператор case зарезервированным словом end. (Обратите внимание на то, что после слова end, завершающего оператор case, мы поставили комментарий {case}. Оператор case часто бывает длинным и может не помещаться целиком на экране. В таком случае бывает трудно понять, к чему относится слово end. Пометка {case} облегчит вам жизнь.) Каждая case-альтернатива состоит из константы, следующего за ней двоеточия и оператора, после которого обязательно должна стоять точка с запятой. Как обычно, если нужно иметь не один оператор, а последовательность, можно воспользоваться составным оператором. Else-альтернатива состоит из зарезервированного слова else и следующего за ним оператора.
Заполняется оператор case следующим образом. Сначала вычисляется выражение, стоящее между словами case и of. Затем полученное значение последовательно сравнивается с константами, указанными в начале case-альтернатив. Если значение выражения совпадает с какой-либо из констант, то выполняется находящийся в этой альтернативе оператор, и на этом выполнение оператора case заканчивается. Если значение выражения не совпало ни с одной из констант, то выполняется оператор, указанный в альтернативе else; если же таковая альтернатива отсутствует, то никаких действий не выполняется.
Если для нескольких значений констант должны быть выполнены одни и те же действия, то эти константы могут быть перечислены через запятую в одной альтернативе. Например если в нашем примере мы хотим, чтобы команды могли вводиться как на верхнем, так и на нижнем регистрах, мы можем напирать программу так:
var c : char;
begin
c:= ReadKey;
case c of
'а','А': begin { Действия по команде "А" } end;
'б','Б': begin { Действия по команде "Б" } end;
'в','В': begin { Действия по команде "В" } end;
'г','Г': begin { Действия по команде "Г" } end;
'д','Д': begin { Действия по команде "Д" } end;
else WriteLn('Ошибочная команда');
end{case};
end.
И наконец, о небольшой новинке, появившейся в примере программы с оператором case.
Мы описали тип переменной "с" как char. Это так называемый литерный тип. Переменная такого типа занимает в памяти один байт и может принимать одно из 256 значений, которые трактуются как литеры; они могут быть отображены на экране или введены с клавиатуры. Константы этого типа могут записываться двумя способами: во-первых, как литеры, заключенные в апострофы; во-вторых, как знак "≠", за которым следует код литеры. Например, на "Корвете" русская буква "А" имеет код 225; поэтому вместо "А" мы могли бы написать ≠225 или ≠$Е1 (Е1 - шестнадцатеричная запись числа 225). Второй способ удобно использовать для записи управляющих символов, не имеющих графического изображения (возврат каретки стрелки и т. п.).
ReadKey - имя стандартной функции. Она ожидает нажатия клавиши на клавиатуре и после нажатия возвращает в качестве результата введенную литеру.
Массивы и строки. Операторы цикла. Оператор перехода
Один из самых распространенных видов работы, выполняемых компьютером - просмотр и обработки больших наборов однотипных данных.
Рассмотрим простейшую задачу такого сорта - ввести с клавиатуры 10 чисел и найти среди них максимальное.
Первое, что нам нужно сделать для этого - завести переменные, в которых мы сможем разместить введенные числа. (Есть, конечно, возможность решить эту задачу без накопления всех чисел в памяти - выбирать максимальное число по мере ввода. Но такое решение невозможно, если нужно не найти максимальное среди введенных чисел, а упорядочить введенные числа или как-нибудь по-другому их обработать. Поэтому мы рассмотрим решение, в котором все числа сначала размещаются в памяти, а потом среди них имеется максимальное.)
Ясно, что заводить 10 переменных плохо. Ну, с десятью мы еще справимся, а как быть, если потребуется 100 или 1000? Вместо этого нужно завести одну переменную, тип которой - массив чисел. Описание нужной нам переменной имеет вид:
var хх : array [1..10] of real;
Здесь впервые после двоеточия появилось не имя типа, а описание типа. Описание состоит из зарезервированного слова array означающего, что переменная этого типа является массивом, диапазона изменения индексов в квадратных скобках (от 1 до 10), зарезервированного слова of и следующего за ним типа элементов массива.
Можно считать, что данное описание определяет сразу десять переменных типа real с именами хх[1], хх[2], ..., хх[10]. Число, указываемое в квадратных скобках после имени переменной-массива, называется индексом. Вместо числа может быть указано произвольное выражение целого типа.
Первое, чего мы добились с помощью типа массива - это одним махом описали сразу 10 переменных (и, при необходимости, с теми же затратами труда могли бы описать и 100, и 1000 переменных). Но, кроме этого количественного достижения, мы получили качественное увеличение возможностей - возможность динамического вычисления имени переменной в процессе работы программы. Ведь если где-то в программе мы напишем "хх[1]", то, в зависимости от текущего значения i, это выражение может ссылаться на любой элемент массива.
Теперь вернемся к решению нашей задачи. Десять переменных мы завели, но теперь в них надо прочитать десять чисел. Это может быть выполнено с помощью оператора:
for i:=1 to 10 do Read(xx[i]);
Оператор будет выполняться так: сначала переменной i будет присвоено значение 1, и будет выполнен оператор Read (xx[i]) (т. е. будет прочитано число в переменную хх[1]); затем значение переменной i будет увеличено на 1 и снова будет выполнен оператор Read (xx[i]); и так будет проделано 10 раз, т. е. оператор Read (xx[i]) будет выполнен 10 раз при значениях Травных 1, 2, ..., 10. Таким образом, во все 10 переменных хх[1], хх[2], ..., хх[10] будут прочитаны числа.
В общем случае оператор for (называемый оператором цикла) имеет вид:
for имя-переменной:=выражение1 to выражение2 do оператор;
Слова , to и do являются зарезервированными. Переменная, задаваемая именем-переменной, называется переменной цикла или параметром цикла. Значение выражения1 называется начальным значением параметра цикла, а значение выражения2 - конечным значением параметра цикла. Оператор, стоящий после зарезервированного слова do, называется телом цикла.
Оператор цикла for выполняется:
Вычисляются значения выражения1 и выражения2.
Вычисленные значения выражения1 и выражения2 сравниваются: если значение выражения1 больше значения выражения2, то выполнение оператора for на этом заканчивается.
Иначе оператор выполняется (выражение2 - выражение 1) + 1 раз; при этом перед первым выполнением тела цикла переменной имя переменной присваивается значение выражения1, а перед каждым последующим выполнением тела цикла значение переменной цикла увеличивается на 1.
Если в теле цикла нужно использовать не один оператор, а несколько, следует использовать составной оператор.
В рассмотренном операторе for переменная цикла в процессе повторений тела цикла увеличивается от начального значения до конечного. Имеется другой вариант оператора for, в котором переменная цикла уменьшается (опять же от начального значения до конечного). Этот вариант оператора for имеет вид:
for имя-переменной:=выражение1 downto выражение2 do оператор;
В этом варианте тело цикла не будет исполняться ни одного раза, если значение выражения1 меньше значения выражения2; иначе тело цикла будет исполнено (выражение2 - выражение1) + 1 раз; перед первым выполнением цикла переменной цикла будет присвоено значение выражения1, а перед каждым последующим оно будет уменьшаться на 1.
Заметьте, что количество выполнений тела цикла определяется до первого выполнения тела цикла, и поэтому если в "выражении2" используются переменные, которые изменяются в теле цикла, это никак не повлияет на количество выполнений тела цикла. Этим Паскаль отличается от других языков программирования. Например, в следующей последовательности операторов:
n := 10;
for i:=1 to n
do begin Write(i); n:= 100; end;
тело цикла будет выполняться 10 раз, хотя уже после первого выполнения тела цикла конечное значение параметра цикла станет равным 100.
Вернемся ко второй части задачи - поиску максимального числа. Для решения ее нам опять понадобится оператор цикла. Действовать мы будем следующим образом. В переменную xmax, в которой в конце концов окажется максимальный элемент, мы занесем первый элемент массива хх - хх[1]. Затем в цикле будем по очереди сравнивать остальные элементы массива - хх[2], ..., хх[10] - с xma[]. Если очередной элемент окажется больше xmax, значение xmax мы заменим на значение этого элемента и продолжим просмотр массива. К концу просмотра в xmax останется максимальный элемент. Таким образом, окончательно программа будет выглядеть так:
var xx : array[1..10] of real;
xmax : real;
i : integer;
begin
for i:=1 to 10 do begin Read(xx[i]);
xmax := xx[1];
for i:=2 to 10 do
if xmax < xx[i] then xmax := xx[i];
Write(xmax);
end.
А теперь настало время поговорить о константах. Наша программа обрабатывает 10 чисел. Если нам потребуется обработать 20 чисел, то нам нужно будет в трех местах заменить число 10 на число 20. Если бы наша программа была больше, то, скорее всего, такую замену пришлось бы делать большее число раз. Мало того, что это неудобно, к тому же есть вероятность, что при замене будет пропущена какая-нибудь запись, и мы получим неправильную программу. Чтобы устранить этот недостаток, мы можем завести в программе константу:
const n = 10;
var xx : array[1..10] of real;
xmax : real;
i : integer;
begin
for i:=1 to n do begin Read(xx[i]);
xmax := xx[1];
for i:=2 to n do
if xmax < xx[i] then xmax := xx[i];
Write(xmax);
end.
Теперь, чтобы изменить количество обрабатываемых программой чисел, достаточно внести изменение только в первую строку.
В общем случае описание константы имеет вид:
const имя-константы=выражение;
const - это зарезервированное слово, которым отмечается начало описания константы. Если нужно описать подряд несколько констант, то слово const достаточно поставить только перед первым описанием константы (но можно ставить и перед каждым). Имя-константы - это имя, которым называют константу. Далее в программе его можно использовать везде, должно было бы стоять выражение. Выражение задает значение константы. Это выражение должно быть константным, т. е. должно вычисляться на этапе компиляции; для этого оно не должно содержать переменных.
Константы уместно использовать для обозначения тех величин, которые являются постоянными для данного варианта программы, но могут меняться от варианта к варианту. Также удобно задавать константами величины, которые вы собираетесь подобрать в период отладки программы - например, позиции сообщений на экране или цвета.
Бывает так, что значения одних констант связаны с другими. Например, нужно задать положение какого-то прямоугольного окна на экране и центральную его позицию. Тогда подойдет описание констант вроде:
const x1 = 10; y1 = 3; { левый верхний угол окна }
x2 = 40; y2 = 10; { правый нижний угол окна }
x_c = x1 + ((x1 - x1) div 2); { центр }
y_c = y1 + ((y1 - y1) div 2); { центр }
Среди массивов выделяется один специальный тип - строки. Переменная строкового типа описывается так:
var s : string ["константное выражение"]
string - зарезервированное слово (в отличие от, например, integer - предопределенного имени; но пока не обращайте внимания на это отличие). Константное-выражение (чаще всего это просто число) задает максимальную длину строки и должно лежать в интервале от 1 до 255. Переменная, описанная как:
var s : string [ n ]
во многих отношениях похожа на переменную, описанную как:
var s : array [ 0 .. n ] of char;
Например, в обоих случаях можно выбрать i-тый элемент с помощью конструкции s[i]. Но есть и отличия. Строка трактуется как массив переменной длины (правда, память резервируется на максимально возможный размер). Первый элемент строки - s[0] - трактуется как текущая длина строки; она может лежать в диапазоне от 0 до максимального размера строки (заданного в ее описании). (Внимательный, но не искушенный читатель заметит здесь противоречие: s[0] имеет литерный тип, как же это может определять длину? Ответ здесь такой: действительно, тип s [0] литерный, но каждая литера имеет свой числовой код (который и хранится в литерной переменной), и этот-то код и определяет текущую длину строки.) Следующие элементы - в количестве s[0] - есть последовательность литер, составляющих строку. Идущие далее (до максимального размера строки) элементы не должны нормально использоваться (там, конечно, что-то лежит, но что - неопределно).
Константы строкового типа записываются как последовательности литер, ограниченные с обоих сторон апострофами. Например:
'абвгде' '1 2 3'
Строки удобно использовать для ввода, вывода и обработки текстов.
По сравнению с обычными массивами, к строкам применима операция конкатенации. Она обозначается знаком "+"; результат ее получается приписыванием к одной строке другой. Например, если значение строковой переменной s1 есть 'Привет', а значение строковой переменной s2 есть 'Вася', то значением выражения:
s1 + ', ' + s2 + '!'
будет строка 'Привет, Вася!'.
Существует также набор процедур и функций, позволяющих выделить в строке подстроку, удалить подстроку из строки и т. п. Полный перечень этих процедур и функций вы найдете в документации.
Мы научились описывать переменные сложных типов - массивы и строки. Но тот способ, которым мы пока располагаем, имеет очевидное неудобство: если нам в разных местах программы потребуется описать несколько переменных одного и того же сложного типа, нам придется многократно повторять одну и ту же длинную запись. Вместо этого мы можем завести новое имя типа, связав с ним длинное описание типа, и использовать далее везде это имя вместо описания. Это достигается с помощью "описания типа". Описание типа имеет вид:
type имя-типа=описание-типа;
type - это зарезервированное слово, отмечающее начало описания типа. Имя типа должно быть новым именем (идентификатором), нигде ранее не использовавшимся. Описание-типа мы можем пока изготовить двумя способами: как описание массива или как описание строки. Если подряд идут несколько описаний типов, то зарезервированное слово type можно ставить только перед первым описанием.
Примеры использования описаний типов:
type numbers = array [1..n] of real;
str5 = string[5];
...
var x : numbers;
...
var s1, s2, s3 : str5;
...
var yy : numbers;
Как мы уже отметили, использование описании типов позволяет сократить программу. Это является удобством, но не необходимостью. Но в ряде случаев в Паскале невозможно обойтись без использования описаний типов. Например, по правилам языка, тип параметра процедуры или функции (о процедурах и функциях - в следующем разделе) может быть задан только именем типа; поэтому, если параметр должен иметь сложный тип, его нужно сначала связать с каким-либо именем типа в описании типа, и затем использовать это имя в заголовке процедуры или функции.
В рассмотренном нами ранее операторе цикла for количество выполнений тела цикла определяется в начале исполнения оператора, до первого выполнения тела цикла. Но часто условие завершения цикла определяется в процессе выполнения тела цикла. В таких случаях следует использовать один из других двух операторов цикла: while или repeat.
Оператор while имеет следующий формат:
while "логическое-выражение" do "оператор"
Здесь while и do - зарезервированные слова; оператор, как и в случае оператора for, Называется телом цикла; логическое выражение называется условием цикла.
Оператор while выполняется следующим образом: вначале вычисляется логическое выражение; если его значение есть "истина", то выполняется оператор; после этого логическое выражение вычисляется снова, и т. д. Когда очередное вычисление логического выражения дает "ложь", выполнение оператора цикла завершается (в частности, если при первом вычислении логического-выражения получится "ложь", тело цикла не будет выполнено ни разу).
Оператор repeat имеет следующий формат:
repeat "последовательность операторов" until "логическое-выражение"
Здесь repeat и until - зарезервированные слова; последовательность операторов называется телом цикла; логическое выражение называется условием цикла.
Оператор repeat выполняется следующим способом: сначала выполняется последовательность операторов; затем вычисляется логическое-выражение; если его значение есть "истина", то на этом выполнение оператора repeat завершается; если же его значение есть "ложь", то снова выполняется последовательность операторов, снова вычисляется логическое выражение и т. д.
Операторы while и repeat очень похожи друг на друга; однако следует обратить внимание на два отличия:
В операторе while условие цикла проверяется перед выполнением тела цикла, а в операторе repeat - после; поэтому тело цикла в операторе repeat всегда будет выполнено по крайней мере один раз.
Оператор while выполняется до тех пор, пока условие цикла остается истинным; как только оно становится ложным, выполнение оператора while завершается. Оператор repeat выполняется до тех пор, пока условие цикла остается ложным; как только оно становится истинным, выполнение оператора repeat завершается.
Обратите внимание на одно существенное отличие оператора for от операторов while и repeat; в операторе for значения выражений, определяющие количество исполнении тела цикла, вычисляются один раз, в начале выполнения оператора цикла; в операторах while и repeat логическое выражение вычисляется снова после каждого выполнения тела цикла.
Вы, вероятно, заметили еще одно отличие: в операторах for и while тело цикла состоит из одного оператора, а в операторе for - из последовательности операторов. Причина этого проста. В операторе repeat есть естественные "скобки" - зарезервированные слова repeat и until - которые ограничивают тело цикла. В операторах for и while таких "скобок" нет; также нет их и в операторе if. В этом, пожалуй, Паскаль отстает от более новых языков, в которые специально добавлены такие "скобки".
else "последовательность операторов" fi;
а оператор while
while "условие" do "последовательность операторов" od;
Новые зарезервированные слова - fi и od - выполняют здесь роль правых скобок, позволяющих определить конец последовательности операторов.
Приведем простые примеры использования операторов while и repeat.
Пусть требуется найти позицию первого пробела в строковой переменной s (Точнее: переменной целого типа i требуется присвоить индекс первого пробела в строковой переменной s, если он там имеется, или длину строки, если строка не содержит пробела.) Это выполняет следующий фрагмент программы:
i := 1;
while (i <= Length(s)) and (s[i] <> ' ')
do i := i + 1;
Функция Length имеет в качестве параметра выражение строкового типа и возвращает в качестве результата текущую длину строки.
Пусть требуется дождаться ввода с клавиатуры возврата каретки (все остальные нажатия клавиш до нажатия возврата каретки должны быть проигнорированы). Это выполняется следующем оператором: (13 -
repeat until ReadKey = #13;
это код возврата каретки). Обратите (13 - это код возврата каретки). Обратите внимание - тело цикла здесь пустое! Действительно, нам ничего не нужно делать - только повторять вызовы функции ReadKey до тех пор, пока она не возвратит 13.
Однако бывают задачи, для решения которых недостаточно рассмотренного набора операторов (по крайней мере, недостаточно для естественного решения задачи). Это задачи, в которых условие окончания цикла определяется внутри тела цикла. В качестве примера такой задачи рассмотрим следующую.
Нам требуется упорядочить по убыванию массив вещественных чисел, т. е. поставить на первое место самое большое число, на второе - следующее по величине и т. д. Мы будем решать эту задачу таким способом: сначала найдем в массиве самый большой элемент и поставим его на первое место; затем найдем самый большой в остатке массива элемент и поставим его на второе место и т. д. (Это не самый эффективный метод решения поставленной задачи; если вам потребуется быстрое упорядочивание, воспользуйтесь другим, более быстрым, алгоритмом. Мы рассматриваем здесь такой алгоритм потому, что он прост и удобен для демонстрации интересующей нас проблемы.) Следующий фрагмент программы реализует сформулированный алгоритм:
const n = 10;
var xx : array[1..10] of real;
tmp : real;
i, j : integer;
...
begin
...
for i := 1 to n - 1 do
for j := i + 1 to n do
if xx[j] > xx[i] then
begin
tmp := xx[i];
xx[i] := xx[j];
xx[j] := tmp;
end;
...
end.
Пока что все в порядке: нам удалось справиться с поставленной задачей известными нам средствами. Но давайте потребуем, чтобы работа была прекращена (и массив оставлен в недоупорядоченном виде) как только будет нажата какая нибудь клавиша на клавиатуре. Тут нам хотелось бы записать циклы как-то вроде:
for i := 1 to n - 1 do
for j := i + 1 to n do
if KeyPressed
then { выйти из циклов }
else if xx[j] > xx[i] then
begin
tmp := xx[i];
xx[i] := xx[j];
xx[j] := tmp;
end;
Для того чтобы так и сделать, нам потребуется оператор перехода goto. Оператор goto имеет формат:
goto метка;
goto - зарезервированное слово. Метка - это либо целое число без знака, либо идентификатор. Любая метка, кроме (возможно, многочисленных) использований в операторах goto, должна появиться еще в двух местах:
В описании меток, располагаемом в разделе описаний (там же, где располагаются описания констант, переменных и типов). Описание меток состоит из зарезервированного слова label и следующего за ним списка меток (в котором метки отделяются друг от друга запятыми). Например:
label 11, StartOfWork, 999;
Перед каким-либо оператором в разделе операторов. Здесь метка отделяется от следующего за ней оператора двоеточием. Перед одним оператором может находиться несколько меток; в этом случае после каждой метки должно стоять двоеточие. В таком случае говорят, что данный оператор помечен меткой.
Действие оператора goto состоит в том, что после него будет выполняться не следующий по порядку в программе оператор, а оператор, помеченный указанной в операторе goto меткой (иначе говорят, что оператор goto передает управление на помеченный меткой оператор).
Окончательно программа нашего примера будет выглядеть так:
const n = 10;
var xx : array[1..10] of real;
tmp : real;
i, j : integer;
lavel EndOfLoops;
...
begin
...
for i := 1 to n - 1 do
for j := i + 1 to n do
if KeyPressed
then goto EndOfLoops
else if xx[j] > xx[i] then
begin
tmp := xx[i];
xx[i] := xx[j];
xx[j] := tmp;
end;
EndOfLoops: ...
...
end.
Оператор goto является мощным инструментом программирования. Машинная команда, являющаяся аналогом оператора goto, является совершенно необходимой: можно отказаться от команды умножения и выполнять умножение с помощью процедур, использующих только команды сложения (кстати, так и происходит в "Корвете": микропроцессор Intel 8080 не имеет команды умножения); можно отказаться практически от каждой из остальных команд и сохранить возможность (пусть и с большим трудом) писать программы - но от команды перехода отказаться нельзя. В старых языках программирования - например, в Фортране - также нельзя обойтись без оператора goto.
Но чрезмерное увлечение оператором goto приводит к тому, что программа становится запутанной и, как следствие, трудно-отлаживаемой и малопонятной.
В моей практике был такой случай. Меня попросили найти ошибку в одной программе. Программа была написана на языке PL/1 и занимала чуть больше печатной страницы. Я бодро взялся за дело. Уже в третьей строке я обнаружил переход на пятую с конца строку. Это несколько смутило меня, но я продолжал расследование. В пятой с конца строке оказался переход на пятую с начала строку, а там - снова переход куда-то в середину программы. Куда был следующий переход, я не стал смотреть: я заявил, что мне легче написать новую программу, чем искать ошибку в имеющейся. (В конечном счете мне и пришлось написать новую программу; в ней я ни разу не использовал goto, и заработала она сразу.)
Один из принципов структурного программирования состоит в отказе от использования goto. Правда, для того чтобы необходимость использования goto полностью отпала, требуется расширение набора управляющих конструкций языка. Паскаль частично удовлетворяет этим требованиям. С одной стороны, набор операторов в нем достаточен для того, чтобы любая программа могла быть написана без использования goto. Однако иногда программа от этого становится только более непонятной. Например, наша программа могла бы быть так переписана без использования goto.
const n = 10;
var xx : array[1..10] of real;
tmp : real;
i, j : integer;
work_flag : boolean;
...
begin
...
work_flag := true;
i := 1;
while work_flag and (i <= n - 1) do
begin
j := i + 1;
while work_flag and (j <= n) do
if KeyPressed
then goto work_flag := false
else
begin
if xx[j] > xx[i] then
begin
tmp := xx[i];
xx[i] := xx[j];
xx[j] := tmp;
end;
j := j + 1;
end;
i := i + 1;
end;
...
end.
Вряд ли этот текст покажется вам более понятным, чем его предыдущий вариант.
Но все-таки использование goto в Паскале требуется относительно редко. И чтобы отбить излишнюю охоту к использованию goto, разработчики языка специально постарались сделать такое использование не очень удобным. Мы, со своей стороны, рекомендуем вам использовать goto только в следующих ситуациях (в них, кстати, использование goto не уменьшает понятность программы):
Для выхода из оператора цикла. В этом случае метка, на которую передается управление, должна находиться непосредственно после оператора, из которого производится выход.
Для выхода из процедуры (о том, что такое процедура, см. следующий раздел). В этом случае метка, на которую передается управление, должна находиться или непосредственно перед завершающим процедуру end, или перед группой операторов, завершающих выполнение процедуры (в этой группе операторов не должно содержаться переходов на начало процедуры).
Для перехода на начало блока в случае, если в процессе выполнения блока обнаруживается необходимость начать все действия сначала (например, при обработке введенной человеком команды: если в команде обнаруживается ошибка, нужно выдать диагностику и повторить ввод команды сначала).
Возможно, вы найдете и другие случаи, в которых уместно использование goto. Мы бы хотели, чтобы приведенные выше правила сделали бы вам более понятным дух, а не букву структурного программирования.
Процедуры и функции
Часто в программах возникает необходимость выполнять в разных местах одни и те же действия - быть может, несколько модифицированные. Например, в программе, выполняющей тригонометрические вычисления, нужно вычислять значения различных тригонометрических функций - синуса, косинуса, тангенса, котангенса и т. д. Или в программе, ведущей диалог с пользователем, нужно выводить на экран сообщения об ошибках (собственно вывод текста сообщения выполняется стандартной процедурой Write, но, кроме вывода текста, его нужно бывает соответствующим образом оформить - заключить в рамочку, добавить какую-либо стандартную часть, дождаться нажатия какой-либо клавиши на клавиатуре и т. п.). Для облегчения решения таких задач предназначены процедуры и функции.
Чем отличаются процедуры от функций? Вы, наверное, заметили различие в двух примерах предыдущего абзаца. То, что вычисляет значение тригонометрической функции, должно выработать результат - вычисленное значение. Такие действия выполняются функциями. При выводе сообщения на экран необходимо выполнить какие-то действия без получения результатов вычисления. Такие действия выполняются процедурами.
В Паскале процедуры и функции оформляются способом, очень похожим на способ оформления программ: они представляют собой как бы маленькие программки, вставленные в большую программу (в некоторых других языках программирования они даже называются подпрограммами).
Описание процедуры или функции помещается в раздел описаний (туда же, где находятся описания констант, переменных, типов, меток). Описание процедуры или функции начинается с заголовка. А то, что идет после заголовка, устроено точно так же, как и программа: сначала идет раздел описаний, а за ним - раздел операторов. Единственное отличие от программы - после зарезервированного слова end, заканчивающего раздел операторов, в процедуре или функции должна стоять точка с запятой (в программе там должна стоять точка).
В разделе описаний процедуры или функции может находиться все то, что может быть в разделе описаний программы: описания констант, переменных, типов, меток, и даже описания процедур и функций. Но важное отличие объектов, описанных внутри процедуры или функции, от объектов, описанных в главной программе, состоит в том, что они видны только внутри текущей процедуры или функции, и не видны в других процедурах и функциях и в главной программе. Однако внутри процедуры видны все объекты, описанные вне ее. Объекты внутри процедуры могут иметь имена, совпадающие с именами объектов вне ее; в таком случае внешние объекты не будут видны внутри процедуры (их имена будут "экранированы" внутренними именами). Объекты, описанные внутри процедуры или функции, называются "локальными" (по отношению к данной процедуре или функции), а описанные вне ее - "глобальными" (по отношению к данной процедуре или функции).
Заголовок процедуры имеет вид:
procedure "имя-процедуры" ("список-параметров");
procedure - это зарезервированное слово, отмечающее начало описания процедуры "имя-процедуры" - это произвольное имя (идентификатор), с помощью которого вы можете в дальнейшем ссылаться на эту процедуру. О "списке-параметров" мы поговорим чуть позже; пока что отметим, что он может отсутствовать, и в таком случае окружающие его скобки должны быть опущены.
Заголовок функции имеет вид:
function "имя-функции" ("список-параметров"): "тип-результата";
function - зарезервированное слово, отмечающее начало описания процедуры. "Имя-функции" - это произвольное имя (идентификатор), с помощью которого можно в дальнейшем ссылаться на эту функцию. "Список-параметров", как и у процедуры, может отсутствовать; в этом случае должны быть опущены и окружающие его скобки. "Тип-результата" есть имя типа (например, integer, real и т. п.) - типа значения, возвращаемого функцией.
Теперь о списке параметров. Для выполнения своих действий процедуре или функции могут быть нужны аргументы. Например, функция вычисления тангенса должна получить значение угла, для которого вычисляется тангенс; процедура выдачи сообщения об ошибке должна получить текст сообщения, которое оно должна вывести. А некоторые процедуры или функции могут не иметь явных аргументов: например, процедура, выполняющая сброс в начальное состояние (очистить экран, присвоить переменным начальные значения и т. д.), может не иметь аргументов.
Список параметров и описывает набор аргументов процедуры или функции. По сути дела он является описанием специальных локальных переменных и оформляется в виде, очень похожем на вид описания переменных. Список параметров состоит из последовательности блоков, отделяемых друг от друга точкой с запятой. Каждый из блоков имеет вид:
"имя-переменной", ..., "имя-переменной": "имя-типа"
или:
var "имя-переменной", ..., "имя-переменной": "имя-типа"
Параметры, описанные первым способом, называются "параметрами, передаваемыми по значению"; описанные вторым способом - "параметрами, передаваемыми по ссылке" (о том, что это значит, мы поговорим несколько позже).
Далее в остальной части процедуры или функции имена переменных, заданные в списке параметров, могут использоваться как обычные переменные.
Мы научились описывать процедуры и функции. Но появление операторов в описании процедуры еще не означает, что они будут выполняться. Для того, чтобы процедура или функция была выполнена, ее нужно "вызвать".
Вызов процедуры выполняется с помощью "оператора вызова процедуры". Оператор вызова процедуры состоит из имени процедуры и идущего вслед за ним в скобках "списка фактических параметров". Список фактических параметров состоит из отдельных фактических параметров, отделенных друг от друга запятой. Количество фактических параметров в списке должно в точности совпадать с количеством параметров, указанным в заголовке процедуры. Если в заголовке процедуры параметр описан как передаваемый по ссылке, то соответствующим фактическим параметром должно быть имя переменной соответствующего типа. Если в заголовке процедуры параметр описан как передаваемый по значению, то соответствующим фактическим параметром может быть любое выражение соответствующего типа (в частности, имя переменной).
Использование параметров в теле процедуры в процессе ее исполнения имеет следующий смысл. Любое использование параметра, передаваемого по ссылке, эквивалентно использованию в этом месте переменной, переданной в качестве фактического параметра. В частности, присваивание такому параметру вызовет изменение значения фактического параметра. Параметры же, передаваемые по значению, нужно рассматривать просто как внутренние переменные процедуры, которым перед началом исполнения процедуры были присвоены значения фактических параметров. Если даже фактический параметр для параметра, передаваемого по значению, был переменной, присваивание нового значения параметру внутри процедуры никак не повлияет на значение переменной, указанной в качестве фактического параметра.
Вызов функции внешне выглядит точно так же, как и вызов процедуры: имя функции и за ним в скобках - фактические параметры. Но вызов функции должен быть не отдельным оператором, а операндом какого-либо выражения (при вычислении такого выражения функция будет вызвана, и возвращенный ею результат будет использован для дальнейшего вычисления выражения). Передача параметров функции выполняется точно так же, как и процедуре.
Для того, чтобы функция могла вернуть результат, внутри ее раздела операторов должен находиться по крайней мере один оператор присваивания вида:
"имя-функции":="выражение";
Значение выражения, вычисленного при исполнении последнего такого оператора, и будет результатом функции. Можно подумать, что в теле функции неявно определена переменная, имя которой совпадает с именем функции, и имя функции можно использовать внутри ее раздела операторов как обычную переменную. Но это не так. Имя функции в указанном смысле можно использовать только в левой части оператора присваивания. Использование его в правой части будет иметь совсем другой смысл - рекурсивный вызов функции (что такое рекурсивный вызов, мы в настоящем цикле статей рассматривать не будем).
Рассмотрим пример:
var x, у, z : integer;
procedure pl(x : integer);
begin
x := 1; { здесь i выполняется присваивание локальной переменной - параметру }
у := 2; { здесь выполняется присваивание глобальной переменной }
end;
procedure р2(var х : integer);
var у : integer;
begin
x := 1; { здесь выполняется присваивание параметру, передаваемому }
{ по ссылке - и, значит, произойдет изменение переменной }
{ - фактического параметра }
у := 2; { здесь выполняется присваивание локальной переменной }
end;
function fl(t : integer) : integer;
var z : integer;
begin
z := t * t; { здесь изменяется локальная переменная }
fl := z + 1; { здесь формируется результат функции }
end;
begin
х := li; у := 12; z := 13; { здесь переменные имеют значения х=11, у=12, z=13 }
pl(5); { здесь переменные имеют значения: х=11, у=2, z=13 }
у := 12; { здесь переменные имеют значения: х=11, у=12, z=13 }
pl(z); { здесь переменные имеют значения: х=11, у=2, z=13 }
у := 12; { здесь переменные имеют значения: х=11, у=12, z=13 }
р2(х); { здесь переменные имеют значения: х=1, у=12, z=13 }
p2(z); { здесь переменные имеют значения: х=1, у=12, z=1 }
у := f1(х + 1);{ здесь переменные имеют значения: х=1, у=5, z=1 }
end.
Более естественный пример использования процедур и функций приведен в примере большой программы, который представлен в одном из следующих разделов.
И в заключение несколько замечаний.
Передача параметров по ссылке уместна тогда, когда вызываемая процедура должна изменить значение передаваемой в качестве параметра переменной. Параметры, которые используются для возврата результатов работы, называются выходными. (Вообще говоря, использование выходных параметров позволяет работать только с процедурами, и не использовать функций. Но использование функций часто делает программу более простой и понятной.) Использование выходных параметров очень полезно тогда, когда функция должна возвратить несколько результатов: например, координаты чего-либо на экране - это два числа.
Другой случай, когда полезно использование параметров, передаваемых по ссылке - это параметры, занимающие много памяти (например, большие массивы). При передаче по их значению потребуется зарезервировать память для массива не только в вызывающей программе, но и в процедуре: кроме того, будет затрачено большое время на копирование параметра. При использовании передачи по ссылке будет скопирован только адрес массива.
Как уже отмечалось, процедуры и функции полезно использовать тогда, когда одни и те же действия необходимо выполнять в разных местах программы. Кроме этого, в процедуры полезно выделять и однократно выполняемые действия - когда они выделены по смыслу. (Например, начальную инициализацию переменных удобно оформлять в виде отдельной процедуры.) Хотя это и удлиняет программу, программа становится понятней. Один из принципов структурного программирования говорит, что каждая процедура (и в том числе главная программа) должна занимать не более одной страницы; если процедура получается длиннее, то стоит ее разбить на несколько процедур. Но при выполнении такого разбиения нужно, разумеется, следить за тем, чтобы каждый из кусков действительно выделялся по смыслу выполняемых им действий.
Работа с текстовым и графическим экраном
Практически любая программа должна как-то общаться с пользователем, получать от него указания и выдавать результаты своей работы. И в первую очередь информация о работе программы выводится на экран.
К сожалению, способы вывода информации на экран различны для различных типов компьютеров. Поэтому процедуры Паскаля, обеспечивающие доступ к экрану, несколько отличаются в реализациях для различных типов компьютеров. Здесь мы подробно опишем процедуры работы с экраном в системе Express Pascal для "Корвета". Знакомство с ними поможет понять принципы построения такой библиотеки процедур, и в будущем вам будет легче разобраться со способами вывода на экран на другом компьютере.
Существуют два (аппаратно реализованных) способа вывода информации на экран: текстовый и графический. Соответственно мы будем говорить о (логических) текстовом и графическом экранах. Отличительной особенностью "Корвета" является возможность одновременного показа на физическом экране содержимого текстового и графического экранов. (Такое невозможно, например, на IBM PC. Если там вам понадобится вывести одновременно какое-либо графическое изображение и текстовое пояснение к нему, то придется рисовать буквы как графические изображения.)
Текстовый экран "Корвета" состоит из 16-ти строк: каждая из строк состоит из 64-х знакомест - позиций, в которых может находиться литера. Всего имеется 256 картинок литер: в "Корвете" они фиксированы (не могут быть изменены программой). Строки нумеруются числами от 0 до 15; знакоместа в строке - числами от 0 до 63.
Вывод на текстовый экран достаточно прост. С основной процедурой вывода информации - Write и ее модификацией WriteLn - мы уже познакомились. Кроме нее, имеются еще две вспомогательные процедуры. Первая из них - ClrScr - не имеет параметров; ее действие состоит в очистке экрана и установке текущей позиции (позиции, начиная с которой будут размещаться литеры при выводе их на экран с помощью процедуры Write) в левый верхний угол экрана.
Вторая процедура позволяет устанавливать текущую позицию в произвольное место экрана. Формат ее вызова таков:
GotoXY(x, y);
Ее параметры (х и у) должны быть выражениями целого типа. Они определяют новое положение текущей позиции на экране. Значение параметра х (горизонтальная координата) должно лежать в интервале от 0 до 63, значение параметра у (вертикальная координата) - в интервале от 0 до 15. Примеры:
GotoXY ( 0, 0); { помещает текущую позицию в левый верхний угол экрана }
GotoXY (15, 0); { помещает текущую позицию в левый нижний угол экрана }
GotoXY ( 0, 63); { помещает текущую позицию в правый верхний угол экрана }
GotoXY (15, 63); { помещает текущую позицию в правый нижний угол экрана }
GotoXY ( 7, 31); { помещает текущую позицию в центр экрана }
В системе Express Pascal текст выводится на экран всегда белым цветом (на черно-белом мониторе самой яркой градацией серого). Можно изменить цвет текста, но это уже выходит за пределы настоящего цикла статей.
Графический экран состоит из отдельных точек (их еще называют пикселями - английское слово pixel). Эти точки занимают 256 строк, по 512 точек в строке. (Каждая литера на текстовом экране тоже строится из таких точек, одно знакомство занимает 8 точек по горизонтали и 16 точек по вертикали.) Строки нумеруются числами от 0 до 255 (строка с номером 0 - верхняя строка экрана, строка с номером 255 - нижняя), точки в строке нумеруются от 0 до 511 (точка с номером 0 - левая точка строки, точка с номером 511 - правая точка строки).
Каждая точка может иметь один из восьми цветов; цвета точек нумеруются числами от 0 до 7. Эти числа кодируют следующие цвета: 0 - черный, 1 - синий, 2 - зеленый, 3 - голубой, 4 - красный, 5 - фиолетовый, 6 - желтый, 7 - белый. На черно-белом мониторе цвета передаются полутонами (градациями серого). Яркость точки возрастает с возрастанием номера цвета от черного (цвет 0) до белого (цвет 7).
Имеется семь процедур работы с графическим экраном. Во всех этих процедурах координаты могут быть заданы выражениями целого типа. Если координаты какой то части фигуры лежат за пределами экрана, соответствующая часть фигуры не будет нарисована.
1. Очистка графического экрана. Формат вызова:
ClrGScr;
Эта процедура вызывает очистку графического экрана и подготовку его к работе. Процедура должна быть обязательно выполнена до любой другой процедуры вывода на графический экран - иначе ничего на экране не появится.
2. Установка текущего цвета. Формат вызова:
SetColor ( clr ) ;
clr должно быть выражением целого типа, принимающим значение в интервале от 0 до 7. Изображения, создаваемые остальными процедурами, будут рисоваться цветом, определенным этой процедурой. Если различные изображения должны рисоваться различными цветами, то перед вызовом каждой процедуры вывода изображения должна вызываться процедура установки цвета.
3. Чтение текущего цвета точки. Формат вызова:
GetPixel ( х, у )
Это функция: ее результат есть цвет (число в диапазоне от 0 до 7) заданной точки, х и у задают координаты точки, цвет которой должен быть прочитан.
4. Рисование точки. Формат вызова:
PutPixel ( х, у );
Эта процедура закрашивает точку с координатами (х, у) цветом, установленным процедурой SetColor.
5. Рисование прямой линии. Формат вызова:
Line ( x1, y1, х2, у2 );
Эта процедура проводит прямую линию от точки с координатами (x1, y1), до точки с координатами (х2, у2). Линия проводится цветом, установленным процедурой SetColor.
6. Рисование прямоугольника. Формат вызова:
Rectangle ( x1, y1, х2, у2 , fill );
Эта процедура рисует прямоугольник, два противоположных угла которого (может быть задана любая пара противоположных углов) имеют координаты (x1, y1) и (х2, у2). Пятый параметр - fill должен быть выражением логического типа. Если его значение 37 есть false, то рисуется контур прямоугольника, если true - рисуется закрашенный прямоугольник. Прямоугольник рисуется цветом, установленным процедурой SetColor.
7. Рисование окружности или круга. Формат вызова:
Circle ( х, у, r, fill );
Эта процедура рисует окружность с центром в точке с координатами (х, у), и радиусом r. Четвертый параметр - fill - должен быть выражением логического типа. Если его значение есть false, то рисуется контур (окружность) , если true - рисуется закрашенный круг. Окружность (или круг) рисуется цветом, установленным процедурой SetColor.
В качестве примера приведем программу, рисующую красивый узор на графическом экране. Она использует только часть описанных процедур.
var i : integer;
begin
ClrScr;
ClrGScr;
for i:=0 to 511 do
begin
SetColor ( (i mod 2) * 7 );
Line ( i, 0, 511 - i, 255 );
end;
for i:=0 to 255 do
begin
SetColor ( (i mod 2) * 7 );
Line ( 0, i, 511, 255 - i );
end;
end.
Маленькое замечание. Выражение, являющееся аргументом процедуры SetColor ((i mod 2)* 7) принимает значение 0 для четных 1 и значение 7 для нечетных. Таким образом, четные линии рисуются черным цветом, а нечетные - белым. Попробуйте поменять способ вычисления цвета - получатся тоже приятные узоры.
8. Пример большой программы
В этом, заключительном, разделе цикла мы приведем пример относительно большой программы - игры "Минер". После того, как вы разберетесь, как устроена эта программа, можно ввести ее в компьютер, скомпилировать, и в награду на проделанные труды немного поразвлечься.
Правила игры "Минер" такие. Игровое поле состоит из клеток, 11 рядов по 14 клеток в каждом. В некоторых клетках находятся мины (при каждой новой игре мины располагаются в новых местах). Задачей игры является определение клеток, в которых находятся мины. Одна из клеток отмечена курсором. Вы можете перемещать курсор по игровому полю с помощью клавиш со стрелками. Вы можете открыть клетку, нажав возврат каретки. Если в открываемой клетке оказалась мина, вы погибли, игра оканчивается. Если же в клетке нет мины, вам выдается число - количество мин в восьми соседствующих с открытой клеткой. Эта информация и помогает вам определять, в каких клетках находятся мины. Если вы установили, что в какой-то клетке находится мина, поставьте на эту клетку курсор и нажмите пробел: в клетке появится звездочка. Ваша пометка: "Здесь мина!". Ошибочную пометку можно убрать, повторно нажав пробел. Игра заканчивается успехом, если вы отметили все клетки с минами и открыли все клетки без мин. Для завершения работы с программой нужно нажать Esc.
А теперь - текст программы:
{============================================================}
{ }
{ И г р а " М И Н Е Р " }
{ }
{============================================================}
const nr_col = 14; nr_ln = 11; { Размеры поля по вертикали }
{ и горизонтали }
nr_mines = 30; { Количество мин }
var х, у : integer; { Координаты текущей клетки }
с : char; { Для ввода литеры с клавиатуры }
n : integer; { Число открытых клеток }
m : integer; { Число отмеченных мин }
mines : array [ 0..nr_col+1, 0..nr_ln+1 ] of boolean;
{ Наличие мин на поле: }
{ true - в клетке есть мина, }
{ false - в клетке мины нет. }
{ Этот массив содержит дополнитель- }
{ ные клетки (отсутствующие в игре- }
{ вом поле - с х-координатой 0 или }
{ nr_col+l или с у-координатой 0 или }
{ nr_1n+1) - для упрощения процедуры }
{ подсчета числа мин на соседних }
{ клетках. }
state : array [ 1..nr_col, 1..nr_ln ] of byte,
{ Текущий статус клетки: }
{ 0..8 - клетка открыта, число по- }
{ называет количество мин в }
{ соседних клетках }
{ 9 - клетка отмечена игроком }
{ как содержащая мину }
{ 10 - клетка не открыта }
{""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""}
{ Инициализация }
procedure Init;
var x, у : integer;
m : integer;
begin
for x:=0 to nr_col+1 { Везде отметить: }
do for y:=0 to nr_ln+1 { "мин нет" }
do mines[x,y] := false;
for x:=1 to nr_col { Везде отметить: }
do for y:=1 to nr_ln { "клетка не открыта" }
do state[x,y] := 10;
Randomize; { Расставить nr_mines мин в }
m := 0; { случайно выбранные клетки }
repeat
х := Random(nr_cоl)+1; у := Random(nr_ln)+1;
if not mines[x,y]
then begin mines[x,y] := true; m:=m+1; end;
until m = nr_mines;
end;
{""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""}
{ Нарисовать пустое поле и сообщения-подсказки. }
{ Игровое поле имеет nr_col клеток по горизонтали и nr_ln }
{ клеток по вертикали Каждая клетка имеет высоту 16 графи- }
{ ческих точек по вертикали (высота одного текстового знако- }
{ места) и 24 точки (ширина трех текстовых знакомест). }
procedure DrawField;
var i : integer;
begin
SetColor(7); { сетка рисуется белым цветом }
for i:=0 to nr_ln { рисуем горизонтальные линии }
do Line ( 0, i*16, nr_col*24, i*16 );
for i:=0 to nr_col { рисуем вертикальные линии }
do Line ( i*24, 0, i*24, nr_ln*16 );
{ Подсказка: }
GotoXY(0,12); Write('Используйте клавиши:');
GotoXY(2,13); Write('стрелки: перемещение текущей позиции');
GotoXY(2,14); Write('пробел: отметить мину / убрать отметку'),
GotoXY(2,15); Write('возврат каретки: открыть клетку');
GotoXY(55,13); Write('Esc -');
GotoXY(55,14); Wr1te('конец');
GotoXY(55,15); Write('игры.');
{ Заготовка для текущей информации: }
GotoXY(49,0); Wr1te('Осталось');
GotoXY(49,1), Write('необследованных');
GotoXY(49,2); Write('клеток:');
GotoXY(49,5); Write('Осталось');
GotoXY(49,6), Write('ненайденных');
GotoXY(49,7), Wr1te('мин.');
end;
{""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""}
{ Функция подсчета числа мин на соседних клетках }
function NearMines ( х, у : integer ) : integer;
var m : integer;
begin
m:=0;
if mines[x-1,y-1] then m:=m+1;
if mines[x ,y-1] then m:=m+1;
if mines[x+1,y-1] then m:=m+1;
if mines[x-1,y ] then m:=m+1;
if mines[x+1,y ] then m:=m+1;
if mines[x-1,y+1] then m:=m+1;
if mines[x ,y+1] then m:=m+1;
if mines[x+1,у+1] then m:=m+1;
NearMines := m;
end;
label Start;
begin
Start:
ClrGScr; ClrScr; { Очистить экран, }
Write(#27';'); { погасить курсор }
Init; { Инициализировать описание минного поля }
DrawField; { Нарисовать пустую сетку на экране }
{ Подготовка главного цикла }
х:=1; у:=1; n:=0; m:=0;
GotoXY(55,3); Write ( nr_col*nr_ln : 4 );
GotoXY(55,8); Write ( nr_nines : 4 );
{ Главный цикл }
repeat
{ Вывести указатель (закрасить текущую клет- }
{ ку), дождаться ввода с клавиатуры, стереть }
{ указатель }
SetColor(1);
Rectangle( (х-1)*24+1, (у-1)*16+1, х*24-1, у*16-1, true );
с := ReadKey;
SetColor(0);
Rectangle( (х-1)*24+1, (у-1)*16+1, х*24-1, у*16-1, true );
{ Разбор случаев ввода: }
case с of
{ Стрелки - изменить текущие координаты }
#$1D {влево} : if х=1 then x:=nr_col else х:=х-1;
#$1С {вправо} : if x=nr_col then х:=1 else х:=х+1;
#$1Е {вверх} : if у=1 then y:=nr_ln else y:=y-1;
#$1F {вниз} : if y=nr_ln then y:=1 else y:=y+1;
{ Пробел: }
{ если клетка закрыта: поставить звездочку }
{ если в клетке звездочка: закрыть }
{ иначе: ничего не делать }
#$20 {пробел} : if (state[x,yj = 10) and (m < nr_mines)
then begin
state[x,у] := 9;
GotoXY ( (x-1)*3+1, y-1 );
Wr1te( '*' );
m := m+1; n := n+1;
end
else if state[x,y]=9
then begin
state[x,у] := 10;
GotoXY ( (x-1)*3+1, y-1 );
Write( ' ' );
m := m-1; n := n-1 ;
end;
{ Возврат каретки: )
{ если клетка открыта или в ней звездочка: }
{ ничего не делать }
{ если клетка закрыта: }
{ если в клетке мина: взорвался - вы- }
{ дать сообщение, дождаться нажа- }
{ тия клавиши, перейти на Start }
{ если в клетке нет мины: вывести в }
{ клетку число мин вокруг }
#$0D {возврат каретки} :
if state[x,y] = 10
then if mines[x,y]
then begin
SetColor(4);
Rectangle ( 43*8, 10*16, 64*8-1, 12*16-1, true );
GotoXY(47,10);
Write( 'Вы взорвались!' );
GotoXY(43,11);
Write('Нажмите любую клавишу');
с := ReadKey;
goto Start;
end
else begin
state[x,y] := NearMines(x,у);
GotoXY( (x-1)*3+1, y-1 );
Write(state[x,y]);
n:=n+1;
end;
end{case};
GotoXY(55,3); Write ( nr_col*nr_ln - n : 4 );
GotoXY(55,8); Write ( nr_mines - m : 4 );
until (n = nr_col*nr_ln) or (c = #$1B{Esc});
{ Теперь нужно разобраться, почему мы вышли }
{ из главного цикла, и либо начать игру сна- }
{ чала, либо закончить работу }
if m = nr_mines
then begin
{ Победа - вывести сообщение, дождаться }
{ нажатия клавиши, перейти на Start }
SetColor(2);
Rectangle ( 43*8, 10*16, 64*8-1, 12*16-1, true );
GotoXY(48,10); Write( 'Вы победили!' );
GotoXY(43,11); Write('Нажмите любую клавишу');
с := ReadKey;
goto Start;
end
else begin
{ Мы попадаем сюда, если нажат Esc. Ничего делать }
{ не нужно - программа заканчивает работу. }
end;
end.
Замечания к программе
Как сделать так, чтобы мины каждый раз располагались в новых местах? Ответ: их нужно располагать случайно. Для этого и используется
процедура Randomize и функция
Процедура Randomize выполняет инициализацию генератора случайных чисел. Если ее не вызывать, то работа генератора случайных чисел будет начинаться из одного и того же состояния, и вы будете получать одно и то же расположение мин.
Функции Random должен быть передан параметр - целое число. Результатом этой функции будет также целое число, случайно выбранное из диапазона от 0 до n-1 (n-значение параметров).
Если вам удалось понять, как работает эта программа, значит наши занятия не прошли даром. Тогда попытайтесь усовершенствовать эту программу. Попробуйте сделать следующее:
- поменяйте размеры игрового поля;
- сделайте так, чтобы размеры игрового поля и количество мин вводились игроком в начале работы программы;
- если открытая клетка содержит 0, то большого ума не требуется, чтобы понять, что в соседних клетках мин нет, и их можно спокойно открыть. Сделайте так, чтобы после открывания клетки с числом 0 все соседние клетки открывались автоматически;
- если, например, открытая клетка содержит 1 и в одной из соседних клеток вы уже заметили мину, то все остальные соседние клетки можно спокойно открывать. Сделайте так, чтобы эти клетки можно было открыть нажатием одной специально отведенной для этого клавиши (например, клавиши табуляции).
Успехов вам!