Программирование в системе 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. Но имейте в виду: практически во всех реализациях Паскаля (в частности, в системе Expres 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.
(Неправильный аргумент). Это сообщение должно заставить вас проверить введенные коээфициенты. Если они таковы, что квадратное уравнение не имеет корней, то ничего не поделать. Но если вы видите, что корни должны быть (а программа не cмогла их найти из-за ошибки в формуле вычисления дискримината, но вы этого пока не знаете), то следует искать ошибку в программе. Компилятор поможет найти оператор, при выполнении которого возникла ошибка,- это оператор извлечения квадратного корня из дискриминанта. Раз коэффициенты такие, что корни должны быть, а квадратный корень из дискриминанта не извлекается,- значит, программа неправиль но вычисляет дискриминант. После этого рассуждения вы анализируете оператор, вычисляющий дискриминант, и находите ошибку.
Мы рассмотрели здесь только несколько примеров возникновения ошибочных ситуаций. Начав работать с компьютером, вы встретите много других. Полный список сообщений об ошибках есть в документации; там же вы найдете рекомендации по их поиску и устранению (Если же у вас нет документации потому, что вместо того, чтобы купить систему, вы просто скопировали ее у кого-то, то поделом вам.)
ПРОДОЛЖЕНИЕ СЛЕДУЕТ В ВЫПУСКЕ 05-1992